main.go 12.4 KB
Newer Older
1 2 3 4 5 6
package main

import (
	"fmt"
	"io"
	"os"
Matt Bell's avatar
Matt Bell committed
7
	"os/signal"
8
	"runtime/pprof"
9
	"syscall"
10

11
	// TODO rm direct reference to go-logging
12
	logging "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/jbenet/go-logging"
13 14 15
	ma "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/jbenet/go-multiaddr"
	manet "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/jbenet/go-multiaddr/net"

16 17 18
	cmds "github.com/jbenet/go-ipfs/commands"
	cmdsCli "github.com/jbenet/go-ipfs/commands/cli"
	cmdsHttp "github.com/jbenet/go-ipfs/commands/http"
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
19 20
	config "github.com/jbenet/go-ipfs/config"
	core "github.com/jbenet/go-ipfs/core"
21
	daemon "github.com/jbenet/go-ipfs/daemon2"
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
22
	updates "github.com/jbenet/go-ipfs/updates"
23
	u "github.com/jbenet/go-ipfs/util"
24
	errors "github.com/jbenet/go-ipfs/util/errors"
Brian Tiger Chow's avatar
Brian Tiger Chow committed
25
	eventlog "github.com/jbenet/go-ipfs/util/eventlog"
26 27 28
)

// log is the command logger
29
var log = eventlog.Logger("cmd/ipfs")
30

31 32 33
// signal to output help
var errHelpRequested = errors.New("Help Requested")

34
const (
35 36
	cpuProfile  = "ipfs.cpuprof"
	heapProfile = "ipfs.memprof"
37 38
	errorFormat = "ERROR: %v\n\n"
)
39

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
40 41 42 43 44 45 46 47 48 49 50 51
type cmdInvocation struct {
	path []string
	cmd  *cmds.Command
	req  cmds.Request
}

// main roadmap:
// - parse the commandline to get a cmdInvocation
// - if user requests, help, print it and exit.
// - run the command invocation
// - output the response
// - if anything fails, print error, maybe with help
52
func main() {
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
53 54 55 56 57 58 59 60 61 62 63
	var invoc cmdInvocation
	var err error

	// we'll call this local helper to output errors.
	// this is so we control how to print errors in one place.
	printErr := func(err error) {
		fmt.Fprintf(os.Stderr, "Error: %s\n", err.Error())
	}

	// this is a local helper to print out help text.
	// there's some considerations that this makes easier.
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
64
	printHelp := func(long bool, w io.Writer) {
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
65 66 67 68 69
		helpFunc := cmdsCli.ShortHelp
		if long {
			helpFunc = cmdsCli.LongHelp
		}

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
70
		helpFunc("ipfs", Root, invoc.path, w)
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
71 72 73
	}

	// parse the commandline into a command invocation
74
	parseErr := invoc.Parse(os.Args[1:])
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
75 76 77

	// BEFORE handling the parse error, if we have enough information
	// AND the user requested help, print it out and exit
78
	if invoc.req != nil {
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
79 80 81 82 83 84
		longH, shortH, err := invoc.requestedHelp()
		if err != nil {
			printErr(err)
			os.Exit(1)
		}
		if longH || shortH {
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
85
			printHelp(longH, os.Stdout)
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
86 87 88 89
			os.Exit(0)
		}
	}

90 91 92 93
	// here we handle the cases where
	// - commands with no Run func are invoked directly.
	// - the main command is invoked.
	if invoc.cmd == nil || invoc.cmd.Run == nil {
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
94
		printHelp(false, os.Stdout)
95 96 97 98
		os.Exit(0)
	}

	// ok now handle parse error (which means cli input was wrong,
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
99
	// e.g. incorrect number of args, or nonexistent subcommand)
100 101
	if parseErr != nil {
		printErr(parseErr)
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
102 103 104 105 106

		// this was a user error, print help.
		if invoc.cmd != nil {
			// we need a newline space.
			fmt.Fprintf(os.Stderr, "\n")
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
107
			printHelp(false, os.Stderr)
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
108
		}
Brian Tiger Chow's avatar
Brian Tiger Chow committed
109 110
		os.Exit(1)
	}
Matt Bell's avatar
Matt Bell committed
111

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
112 113
	// ok, finally, run the command invocation.
	output, err := invoc.Run()
114
	if err != nil {
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
115 116 117 118
		printErr(err)

		// if this error was a client error, print short help too.
		if isClientError(err) {
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
119
			printHelp(false, os.Stderr)
120
		}
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
121
		os.Exit(1)
Brian Tiger Chow's avatar
Brian Tiger Chow committed
122 123
	}

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
124 125 126 127 128
	// everything went better than expected :)
	io.Copy(os.Stdout, output)
}

func (i *cmdInvocation) Run() (output io.Reader, err error) {
129 130
	// setup our global interrupt handler.
	i.setupInterruptHandler()
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
131 132 133

	// check if user wants to debug. option OR env var.
	debug, _, err := i.req.Option("debug").Bool()
Brian Tiger Chow's avatar
Brian Tiger Chow committed
134
	if err != nil {
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
135
		return nil, err
Brian Tiger Chow's avatar
Brian Tiger Chow committed
136
	}
137
	if debug || u.GetenvBool("DEBUG") || os.Getenv("IPFS_LOGGING") == "debug" {
Brian Tiger Chow's avatar
Brian Tiger Chow committed
138 139
		u.Debug = true
		u.SetAllLoggers(logging.DEBUG)
140
	}
141

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
142 143
	// if debugging, let's profile.
	// TODO maybe change this to its own option... profiling makes it slower.
144
	if u.Debug {
145
		stopProfilingFunc, err := startProfiling()
146
		if err != nil {
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
147
			return nil, err
148
		}
149
		defer stopProfilingFunc() // to be executed as late as possible
150
	}
151

Matt Bell's avatar
Matt Bell committed
152
	res, err := callCommand(i.req, Root)
Brian Tiger Chow's avatar
Brian Tiger Chow committed
153
	if err != nil {
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
154
		return nil, err
155
	}
156

157 158 159 160
	if err := res.Error(); err != nil {
		return nil, err
	}

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
161
	return res.Reader()
162
}
163

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
164 165
func (i *cmdInvocation) Parse(args []string) error {
	var err error
166

167
	i.req, i.cmd, i.path, err = cmdsCli.Parse(args, os.Stdin, Root)
168
	if err != nil {
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
169
		return err
170 171
	}

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
172
	configPath, err := getConfigRoot(i.req)
173
	if err != nil {
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
174
		return err
175
	}
Brian Tiger Chow's avatar
Brian Tiger Chow committed
176
	log.Debugf("config path is %s", configPath)
177

178
	// this sets up the function that will initialize the config lazily.
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
179
	ctx := i.req.Context()
180
	ctx.ConfigRoot = configPath
181
	ctx.LoadConfig = loadConfig
182

183 184
	// if no encoding was specified by user, default to plaintext encoding
	// (if command doesn't support plaintext, use JSON instead)
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
185
	if !i.req.Option("encoding").Found() {
186
		if i.req.Command().Marshalers != nil && i.req.Command().Marshalers[cmds.Text] != nil {
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
187
			i.req.SetOption("encoding", cmds.Text)
188
		} else {
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
189
			i.req.SetOption("encoding", cmds.JSON)
190 191 192
		}
	}

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
193
	return nil
194 195
}

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
196 197
func (i *cmdInvocation) requestedHelp() (short bool, long bool, err error) {
	longHelp, _, err := i.req.Option("help").Bool()
198
	if err != nil {
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
199
		return false, false, err
200
	}
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
201
	shortHelp, _, err := i.req.Option("h").Bool()
202
	if err != nil {
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
203
		return false, false, err
204
	}
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
205
	return longHelp, shortHelp, nil
206
}
207

208
func callPreCommandHooks(details cmdDetails, req cmds.Request, root *cmds.Command) error {
209 210 211 212

	log.Debug("Calling pre-command hooks...")

	// some hooks only run when the command is executed locally
213
	daemon, err := commandShouldRunOnDaemon(details, req, root)
214 215 216 217 218 219 220
	if err != nil {
		return err
	}

	// check for updates when 1) commands is going to be run locally, 2) the
	// command does not initialize the config, and 3) the command does not
	// pre-empt updates
221
	if !daemon && details.usesConfigAsInput() && details.doesNotPreemptAutoUpdate() {
222 223 224 225 226 227 228 229 230 231 232 233 234

		log.Debug("Calling hook: Check for updates")

		cfg, err := req.Context().GetConfig()
		if err != nil {
			return err
		}
		// Check for updates and potentially install one.
		if err := updates.CliCheckForUpdates(cfg, req.Context().ConfigRoot); err != nil {
			return err
		}
	}

235 236
	// When the upcoming command may use the config and repo, we know it's safe
	// for the log config hook to touch the config/repo
237
	if details.usesConfigAsInput() && details.usesRepo() {
238 239 240 241 242 243 244 245
		log.Debug("Calling hook: Configure Event Logger")
		cfg, err := req.Context().GetConfig()
		if err != nil {
			return err
		}
		configureEventLogger(cfg)
	}

246 247 248
	return nil
}

249
func callCommand(req cmds.Request, root *cmds.Command) (cmds.Response, error) {
250
	var res cmds.Response
251

252 253 254 255 256 257
	details, err := commandDetails(req.Path(), root)
	if err != nil {
		return nil, err
	}

	useDaemon, err := commandShouldRunOnDaemon(*details, req, root)
258 259 260
	if err != nil {
		return nil, err
	}
261

262
	err = callPreCommandHooks(*details, req, root)
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
263 264 265
	if err != nil {
		return nil, err
	}
266

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
267
	if useDaemon {
268

269 270 271 272 273
		cfg, err := req.Context().GetConfig()
		if err != nil {
			return nil, err
		}

274 275 276 277
		addr, err := ma.NewMultiaddr(cfg.Addresses.API)
		if err != nil {
			return nil, err
		}
278

279
		log.Infof("Executing command on daemon running at %s", addr)
280 281 282 283
		_, host, err := manet.DialArgs(addr)
		if err != nil {
			return nil, err
		}
284

285
		client := cmdsHttp.NewClient(host)
286

287 288 289 290
		res, err = client.Send(req)
		if err != nil {
			return nil, err
		}
291

292
	} else {
293
		log.Info("Executing command locally")
294

295 296 297
		// this sets up the function that will initialize the node
		// this is so that we can construct the node lazily.
		ctx := req.Context()
298

299 300
		ctx.ConstructNode = func() (*core.IpfsNode, error) {
			cfg, err := ctx.GetConfig()
301
			if err != nil {
302
				return nil, err
303
			}
304 305 306 307 308
			return core.NewIpfsNode(cfg, false)
		}

		// Okay!!!!! NOW we can call the command.
		res = root.Call(req)
309

310 311 312 313 314
		// let's not forget teardown. If a node was initialized, we must close it.
		// Note that this means the underlying req.Context().Node variable is exposed.
		// this is gross, and should be changed when we extract out the exec Context.
		node := req.Context().NodeWithoutConstructing()
		if node != nil {
315
			log.Info("Shutting down node...")
316
			node.Close()
317 318
		}
	}
319
	return res, nil
320 321
}

322 323 324 325 326
// commandDetails returns a command's details for the command given by |path|
// within the |root| command tree.
//
// Returns an error if the command is not found in the Command tree.
func commandDetails(path []string, root *cmds.Command) (*cmdDetails, error) {
327 328 329 330 331 332 333
	var details cmdDetails
	// find the last command in path that has a cmdDetailsMap entry
	cmd := root
	for _, cmp := range path {
		var found bool
		cmd, found = cmd.Subcommands[cmp]
		if !found {
334
			return nil, errors.Errorf("subcommand %s should be in root", cmp)
335
		}
336

337 338 339
		if cmdDetails, found := cmdDetailsMap[cmd]; found {
			details = cmdDetails
		}
340
	}
341 342 343 344 345 346 347 348 349 350 351 352 353 354 355
	return &details, nil
}

// commandShouldRunOnDaemon determines, from commmand details, whether a
// command ought to be executed on an IPFS daemon.
//
// It returns true if the command should be executed on a daemon and false if
// it should be executed on a client. It returns an error if the command must
// NOT be executed on either.
func commandShouldRunOnDaemon(details cmdDetails, req cmds.Request, root *cmds.Command) (bool, error) {
	path := req.Path()
	// root command.
	if len(path) < 1 {
		return false, nil
	}
356 357 358 359 360

	if details.cannotRunOnClient && details.cannotRunOnDaemon {
		return false, fmt.Errorf("command disabled: %s", path[0])
	}

361
	if details.doesNotUseRepo && details.canRunOnClient() {
362 363 364
		return false, nil
	}

365
	log.Info("looking for running daemon...")
366 367 368 369 370 371
	// at this point need to know whether daemon is running. we defer
	// to this point so that some commands dont open files unnecessarily.
	daemonLocked := daemon.Locked(req.Context().ConfigRoot)

	if daemonLocked {

372 373
		log.Info("a daemon is running...")

374 375 376 377 378 379 380 381 382 383 384 385 386 387 388
		if details.cannotRunOnDaemon {
			e := "ipfs daemon is running. please stop it to run this command"
			return false, cmds.ClientError(e)
		}

		return true, nil
	}

	if details.cannotRunOnClient {
		return false, cmds.ClientError("must run on the ipfs daemon")
	}

	return false, nil
}

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
389
func isClientError(err error) bool {
Brian Tiger Chow's avatar
Brian Tiger Chow committed
390 391 392 393

	// Somewhat suprisingly, the pointer cast fails to recognize commands.Error
	// passed as values, so we check both.

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
394
	// cast to cmds.Error
Brian Tiger Chow's avatar
Brian Tiger Chow committed
395 396 397 398 399
	switch e := err.(type) {
	case *cmds.Error:
		return e.Code == cmds.ErrClient
	case cmds.Error:
		return e.Code == cmds.ErrClient
400
	}
Brian Tiger Chow's avatar
Brian Tiger Chow committed
401
	return false
402 403 404
}

func getConfigRoot(req cmds.Request) (string, error) {
405
	configOpt, found, err := req.Option("config").String()
406 407 408
	if err != nil {
		return "", err
	}
409
	if found && configOpt != "" {
410
		return configOpt, nil
411 412 413 414 415 416 417 418 419
	}

	configPath, err := config.PathRoot()
	if err != nil {
		return "", err
	}
	return configPath, nil
}

420
func loadConfig(path string) (*config.Config, error) {
421 422 423 424 425 426 427
	configFile, err := config.Filename(path)
	if err != nil {
		return nil, err
	}

	return config.Load(configFile)
}
428

429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450
// startProfiling begins CPU profiling and returns a `stop` function to be
// executed as late as possible. The stop function captures the memprofile.
func startProfiling() (func(), error) {

	// start CPU profiling as early as possible
	ofi, err := os.Create(cpuProfile)
	if err != nil {
		return nil, err
	}
	pprof.StartCPUProfile(ofi)

	stopProfiling := func() {
		pprof.StopCPUProfile()
		defer ofi.Close() // captured by the closure
		err := writeHeapProfileToFile()
		if err != nil {
			log.Critical(err)
		}
	}
	return stopProfiling, nil
}

451 452 453
func writeHeapProfileToFile() error {
	mprof, err := os.Create(heapProfile)
	if err != nil {
454
		return err
455
	}
456
	defer mprof.Close() // _after_ writing the heap profile
457 458
	return pprof.WriteHeapProfile(mprof)
}
459

Matt Bell's avatar
Matt Bell committed
460
// listen for and handle SIGTERM
461 462 463 464
func (i *cmdInvocation) setupInterruptHandler() {

	ctx := i.req.Context()
	sig := allInterruptSignals()
Matt Bell's avatar
Matt Bell committed
465 466

	go func() {
467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482

		for {
			// first time, try to shut down.
			<-sig
			log.Critical("Received interrupt signal, shutting down...")

			n, err := ctx.GetNode()
			if err == nil {
				go n.Close()
				select {
				case <-n.Closed():
				case <-sig:
					log.Critical("Received another interrupt signal, terminating...")
				}
			}

483
			os.Exit(0)
Matt Bell's avatar
Matt Bell committed
484
		}
485

Matt Bell's avatar
Matt Bell committed
486 487
	}()
}
488 489 490 491 492 493 494

func allInterruptSignals() chan os.Signal {
	sigc := make(chan os.Signal, 1)
	signal.Notify(sigc, syscall.SIGHUP, syscall.SIGINT,
		syscall.SIGTERM, syscall.SIGQUIT)
	return sigc
}
495 496 497 498

func configureEventLogger(config *config.Config) error {

	if u.Debug {
499
		eventlog.Configure(eventlog.LevelDebug)
500
	} else {
501
		eventlog.Configure(eventlog.LevelInfo)
502 503
	}

504
	eventlog.Configure(eventlog.LdJSONFormatter)
505

506
	rotateConf := eventlog.LogRotatorConfig{
507 508 509 510 511 512
		Filename:   config.Logs.Filename,
		MaxSizeMB:  config.Logs.MaxSizeMB,
		MaxBackups: config.Logs.MaxBackups,
		MaxAgeDays: config.Logs.MaxAgeDays,
	}

513
	eventlog.Configure(eventlog.OutputRotatingLogFile(rotateConf))
514 515
	return nil
}