github.com/keltia/go-ipfs@v0.3.8-0.20150909044612-210793031c63/cmd/ipfs/main.go (about) 1 // cmd/ipfs implements the primary CLI binary for ipfs 2 package main 3 4 import ( 5 "errors" 6 "fmt" 7 "io" 8 "math/rand" 9 "os" 10 "os/signal" 11 "runtime" 12 "runtime/pprof" 13 "strings" 14 "sync" 15 "syscall" 16 "time" 17 18 ma "github.com/ipfs/go-ipfs/Godeps/_workspace/src/github.com/jbenet/go-multiaddr" 19 manet "github.com/ipfs/go-ipfs/Godeps/_workspace/src/github.com/jbenet/go-multiaddr-net" 20 21 context "github.com/ipfs/go-ipfs/Godeps/_workspace/src/golang.org/x/net/context" 22 cmds "github.com/ipfs/go-ipfs/commands" 23 cmdsCli "github.com/ipfs/go-ipfs/commands/cli" 24 cmdsHttp "github.com/ipfs/go-ipfs/commands/http" 25 core "github.com/ipfs/go-ipfs/core" 26 coreCmds "github.com/ipfs/go-ipfs/core/commands" 27 repo "github.com/ipfs/go-ipfs/repo" 28 config "github.com/ipfs/go-ipfs/repo/config" 29 fsrepo "github.com/ipfs/go-ipfs/repo/fsrepo" 30 eventlog "github.com/ipfs/go-ipfs/thirdparty/eventlog" 31 u "github.com/ipfs/go-ipfs/util" 32 ) 33 34 // log is the command logger 35 var log = eventlog.Logger("cmd/ipfs") 36 37 var ( 38 errUnexpectedApiOutput = errors.New("api returned unexpected output") 39 errApiVersionMismatch = errors.New("api version mismatch") 40 ) 41 42 const ( 43 EnvEnableProfiling = "IPFS_PROF" 44 cpuProfile = "ipfs.cpuprof" 45 heapProfile = "ipfs.memprof" 46 errorFormat = "ERROR: %v\n\n" 47 ) 48 49 type cmdInvocation struct { 50 path []string 51 cmd *cmds.Command 52 req cmds.Request 53 node *core.IpfsNode 54 } 55 56 // main roadmap: 57 // - parse the commandline to get a cmdInvocation 58 // - if user requests, help, print it and exit. 59 // - run the command invocation 60 // - output the response 61 // - if anything fails, print error, maybe with help 62 func main() { 63 rand.Seed(time.Now().UnixNano()) 64 runtime.GOMAXPROCS(3) // FIXME rm arbitrary choice for n 65 ctx := eventlog.ContextWithLoggable(context.Background(), eventlog.Uuid("session")) 66 var err error 67 var invoc cmdInvocation 68 defer invoc.close() 69 70 // we'll call this local helper to output errors. 71 // this is so we control how to print errors in one place. 72 printErr := func(err error) { 73 fmt.Fprintf(os.Stderr, "Error: %s\n", err.Error()) 74 } 75 76 stopFunc, err := profileIfEnabled() 77 if err != nil { 78 printErr(err) 79 os.Exit(1) 80 } 81 defer stopFunc() // to be executed as late as possible 82 83 // this is a local helper to print out help text. 84 // there's some considerations that this makes easier. 85 printHelp := func(long bool, w io.Writer) { 86 helpFunc := cmdsCli.ShortHelp 87 if long { 88 helpFunc = cmdsCli.LongHelp 89 } 90 91 helpFunc("ipfs", Root, invoc.path, w) 92 } 93 94 // this is a message to tell the user how to get the help text 95 printMetaHelp := func(w io.Writer) { 96 cmdPath := strings.Join(invoc.path, " ") 97 fmt.Fprintf(w, "Use 'ipfs %s --help' for information about this command\n", cmdPath) 98 } 99 100 // Handle `ipfs help' 101 if len(os.Args) == 2 && os.Args[1] == "help" { 102 printHelp(false, os.Stdout) 103 os.Exit(0) 104 } 105 106 // parse the commandline into a command invocation 107 parseErr := invoc.Parse(ctx, os.Args[1:]) 108 109 // BEFORE handling the parse error, if we have enough information 110 // AND the user requested help, print it out and exit 111 if invoc.req != nil { 112 longH, shortH, err := invoc.requestedHelp() 113 if err != nil { 114 printErr(err) 115 os.Exit(1) 116 } 117 if longH || shortH { 118 printHelp(longH, os.Stdout) 119 os.Exit(0) 120 } 121 } 122 123 // ok now handle parse error (which means cli input was wrong, 124 // e.g. incorrect number of args, or nonexistent subcommand) 125 if parseErr != nil { 126 printErr(parseErr) 127 128 // this was a user error, print help. 129 if invoc.cmd != nil { 130 // we need a newline space. 131 fmt.Fprintf(os.Stderr, "\n") 132 printMetaHelp(os.Stderr) 133 } 134 os.Exit(1) 135 } 136 137 // here we handle the cases where 138 // - commands with no Run func are invoked directly. 139 // - the main command is invoked. 140 if invoc.cmd == nil || invoc.cmd.Run == nil { 141 printHelp(false, os.Stdout) 142 os.Exit(0) 143 } 144 145 // ok, finally, run the command invocation. 146 intrh, ctx := invoc.SetupInterruptHandler(ctx) 147 defer intrh.Close() 148 149 output, err := invoc.Run(ctx) 150 if err != nil { 151 printErr(err) 152 153 // if this error was a client error, print short help too. 154 if isClientError(err) { 155 printMetaHelp(os.Stderr) 156 } 157 os.Exit(1) 158 } 159 160 // everything went better than expected :) 161 _, err = io.Copy(os.Stdout, output) 162 if err != nil { 163 printErr(err) 164 165 os.Exit(1) 166 } 167 } 168 169 func (i *cmdInvocation) Run(ctx context.Context) (output io.Reader, err error) { 170 171 // check if user wants to debug. option OR env var. 172 debug, _, err := i.req.Option("debug").Bool() 173 if err != nil { 174 return nil, err 175 } 176 if debug || u.GetenvBool("DEBUG") || os.Getenv("IPFS_LOGGING") == "debug" { 177 u.Debug = true 178 u.SetDebugLogging() 179 } 180 181 res, err := callCommand(ctx, i.req, Root, i.cmd) 182 if err != nil { 183 return nil, err 184 } 185 186 if err := res.Error(); err != nil { 187 return nil, err 188 } 189 190 return res.Reader() 191 } 192 193 func (i *cmdInvocation) constructNodeFunc(ctx context.Context) func() (*core.IpfsNode, error) { 194 return func() (*core.IpfsNode, error) { 195 if i.req == nil { 196 return nil, errors.New("constructing node without a request") 197 } 198 199 cmdctx := i.req.InvocContext() 200 if cmdctx == nil { 201 return nil, errors.New("constructing node without a request context") 202 } 203 204 r, err := fsrepo.Open(i.req.InvocContext().ConfigRoot) 205 if err != nil { // repo is owned by the node 206 return nil, err 207 } 208 209 // ok everything is good. set it on the invocation (for ownership) 210 // and return it. 211 n, err := core.NewNode(ctx, &core.BuildCfg{ 212 Online: cmdctx.Online, 213 Repo: r, 214 }) 215 if err != nil { 216 return nil, err 217 } 218 i.node = n 219 return i.node, nil 220 } 221 } 222 223 func (i *cmdInvocation) close() { 224 // let's not forget teardown. If a node was initialized, we must close it. 225 // Note that this means the underlying req.Context().Node variable is exposed. 226 // this is gross, and should be changed when we extract out the exec Context. 227 if i.node != nil { 228 log.Info("Shutting down node...") 229 i.node.Close() 230 } 231 } 232 233 func (i *cmdInvocation) Parse(ctx context.Context, args []string) error { 234 var err error 235 236 i.req, i.cmd, i.path, err = cmdsCli.Parse(args, os.Stdin, Root) 237 if err != nil { 238 return err 239 } 240 241 repoPath, err := getRepoPath(i.req) 242 if err != nil { 243 return err 244 } 245 log.Debugf("config path is %s", repoPath) 246 247 // this sets up the function that will initialize the config lazily. 248 cmdctx := i.req.InvocContext() 249 cmdctx.ConfigRoot = repoPath 250 cmdctx.LoadConfig = loadConfig 251 // this sets up the function that will initialize the node 252 // this is so that we can construct the node lazily. 253 cmdctx.ConstructNode = i.constructNodeFunc(ctx) 254 255 // if no encoding was specified by user, default to plaintext encoding 256 // (if command doesn't support plaintext, use JSON instead) 257 if !i.req.Option("encoding").Found() { 258 if i.req.Command().Marshalers != nil && i.req.Command().Marshalers[cmds.Text] != nil { 259 i.req.SetOption("encoding", cmds.Text) 260 } else { 261 i.req.SetOption("encoding", cmds.JSON) 262 } 263 } 264 265 return nil 266 } 267 268 func (i *cmdInvocation) requestedHelp() (short bool, long bool, err error) { 269 longHelp, _, err := i.req.Option("help").Bool() 270 if err != nil { 271 return false, false, err 272 } 273 shortHelp, _, err := i.req.Option("h").Bool() 274 if err != nil { 275 return false, false, err 276 } 277 return longHelp, shortHelp, nil 278 } 279 280 func callPreCommandHooks(ctx context.Context, details cmdDetails, req cmds.Request, root *cmds.Command) error { 281 282 log.Event(ctx, "callPreCommandHooks", &details) 283 log.Debug("Calling pre-command hooks...") 284 285 return nil 286 } 287 288 func callCommand(ctx context.Context, req cmds.Request, root *cmds.Command, cmd *cmds.Command) (cmds.Response, error) { 289 log.Info(config.EnvDir, " ", req.InvocContext().ConfigRoot) 290 var res cmds.Response 291 292 err := req.SetRootContext(ctx) 293 if err != nil { 294 return nil, err 295 } 296 297 details, err := commandDetails(req.Path(), root) 298 if err != nil { 299 return nil, err 300 } 301 302 client, err := commandShouldRunOnDaemon(*details, req, root) 303 if err != nil { 304 return nil, err 305 } 306 307 err = callPreCommandHooks(ctx, *details, req, root) 308 if err != nil { 309 return nil, err 310 } 311 312 if cmd.PreRun != nil { 313 err = cmd.PreRun(req) 314 if err != nil { 315 return nil, err 316 } 317 } 318 319 if client != nil { 320 log.Debug("Executing command via API") 321 res, err = client.Send(req) 322 if err != nil { 323 if isConnRefused(err) { 324 err = repo.ErrApiNotRunning 325 } 326 return nil, err 327 } 328 329 } else { 330 log.Debug("Executing command locally") 331 332 err := req.SetRootContext(ctx) 333 if err != nil { 334 return nil, err 335 } 336 337 // Okay!!!!! NOW we can call the command. 338 res = root.Call(req) 339 340 } 341 342 if cmd.PostRun != nil { 343 cmd.PostRun(req, res) 344 } 345 346 return res, nil 347 } 348 349 // commandDetails returns a command's details for the command given by |path| 350 // within the |root| command tree. 351 // 352 // Returns an error if the command is not found in the Command tree. 353 func commandDetails(path []string, root *cmds.Command) (*cmdDetails, error) { 354 var details cmdDetails 355 // find the last command in path that has a cmdDetailsMap entry 356 cmd := root 357 for _, cmp := range path { 358 var found bool 359 cmd, found = cmd.Subcommands[cmp] 360 if !found { 361 return nil, fmt.Errorf("subcommand %s should be in root", cmp) 362 } 363 364 if cmdDetails, found := cmdDetailsMap[cmd]; found { 365 details = cmdDetails 366 } 367 } 368 return &details, nil 369 } 370 371 // commandShouldRunOnDaemon determines, from commmand details, whether a 372 // command ought to be executed on an IPFS daemon. 373 // 374 // It returns a client if the command should be executed on a daemon and nil if 375 // it should be executed on a client. It returns an error if the command must 376 // NOT be executed on either. 377 func commandShouldRunOnDaemon(details cmdDetails, req cmds.Request, root *cmds.Command) (cmdsHttp.Client, error) { 378 path := req.Path() 379 // root command. 380 if len(path) < 1 { 381 return nil, nil 382 } 383 384 if details.cannotRunOnClient && details.cannotRunOnDaemon { 385 return nil, fmt.Errorf("command disabled: %s", path[0]) 386 } 387 388 if details.doesNotUseRepo && details.canRunOnClient() { 389 return nil, nil 390 } 391 392 // at this point need to know whether api is running. we defer 393 // to this point so that we dont check unnecessarily 394 395 // did user specify an api to use for this command? 396 apiAddrStr, _, err := req.Option(coreCmds.ApiOption).String() 397 if err != nil { 398 return nil, err 399 } 400 401 client, err := getApiClient(req.InvocContext().ConfigRoot, apiAddrStr) 402 if err == repo.ErrApiNotRunning { 403 if apiAddrStr != "" && req.Command() != daemonCmd { 404 // if user SPECIFIED an api, and this cmd is not daemon 405 // we MUST use it. so error out. 406 return nil, err 407 } 408 409 // ok for api not to be running 410 } else if err != nil { // some other api error 411 return nil, err 412 } 413 414 if client != nil { // daemon is running 415 if details.cannotRunOnDaemon { 416 e := "cannot use API with this command." 417 418 // check if daemon locked. legacy error text, for now. 419 daemonLocked, _ := fsrepo.LockedByOtherProcess(req.InvocContext().ConfigRoot) 420 if daemonLocked { 421 e = "ipfs daemon is running. please stop it to run this command" 422 } 423 424 return nil, cmds.ClientError(e) 425 } 426 427 return client, nil 428 } 429 430 if details.cannotRunOnClient { 431 return nil, cmds.ClientError("must run on the ipfs daemon") 432 } 433 434 return nil, nil 435 } 436 437 func isClientError(err error) bool { 438 439 // Somewhat suprisingly, the pointer cast fails to recognize commands.Error 440 // passed as values, so we check both. 441 442 // cast to cmds.Error 443 switch e := err.(type) { 444 case *cmds.Error: 445 return e.Code == cmds.ErrClient 446 case cmds.Error: 447 return e.Code == cmds.ErrClient 448 } 449 return false 450 } 451 452 func getRepoPath(req cmds.Request) (string, error) { 453 repoOpt, found, err := req.Option("config").String() 454 if err != nil { 455 return "", err 456 } 457 if found && repoOpt != "" { 458 return repoOpt, nil 459 } 460 461 repoPath, err := fsrepo.BestKnownPath() 462 if err != nil { 463 return "", err 464 } 465 return repoPath, nil 466 } 467 468 func loadConfig(path string) (*config.Config, error) { 469 return fsrepo.ConfigAt(path) 470 } 471 472 // startProfiling begins CPU profiling and returns a `stop` function to be 473 // executed as late as possible. The stop function captures the memprofile. 474 func startProfiling() (func(), error) { 475 476 // start CPU profiling as early as possible 477 ofi, err := os.Create(cpuProfile) 478 if err != nil { 479 return nil, err 480 } 481 pprof.StartCPUProfile(ofi) 482 go func() { 483 for _ = range time.NewTicker(time.Second * 30).C { 484 err := writeHeapProfileToFile() 485 if err != nil { 486 log.Error(err) 487 } 488 } 489 }() 490 491 stopProfiling := func() { 492 pprof.StopCPUProfile() 493 defer ofi.Close() // captured by the closure 494 } 495 return stopProfiling, nil 496 } 497 498 func writeHeapProfileToFile() error { 499 mprof, err := os.Create(heapProfile) 500 if err != nil { 501 return err 502 } 503 defer mprof.Close() // _after_ writing the heap profile 504 return pprof.WriteHeapProfile(mprof) 505 } 506 507 // IntrHandler helps set up an interrupt handler that can 508 // be cleanly shut down through the io.Closer interface. 509 type IntrHandler struct { 510 sig chan os.Signal 511 wg sync.WaitGroup 512 } 513 514 func NewIntrHandler() *IntrHandler { 515 ih := &IntrHandler{} 516 ih.sig = make(chan os.Signal, 1) 517 return ih 518 } 519 520 func (ih *IntrHandler) Close() error { 521 close(ih.sig) 522 ih.wg.Wait() 523 return nil 524 } 525 526 // Handle starts handling the given signals, and will call the handler 527 // callback function each time a signal is catched. The function is passed 528 // the number of times the handler has been triggered in total, as 529 // well as the handler itself, so that the handling logic can use the 530 // handler's wait group to ensure clean shutdown when Close() is called. 531 func (ih *IntrHandler) Handle(handler func(count int, ih *IntrHandler), sigs ...os.Signal) { 532 signal.Notify(ih.sig, sigs...) 533 ih.wg.Add(1) 534 go func() { 535 defer ih.wg.Done() 536 count := 0 537 for _ = range ih.sig { 538 count++ 539 handler(count, ih) 540 } 541 signal.Stop(ih.sig) 542 }() 543 } 544 545 func (i *cmdInvocation) SetupInterruptHandler(ctx context.Context) (io.Closer, context.Context) { 546 547 intrh := NewIntrHandler() 548 ctx, cancelFunc := context.WithCancel(ctx) 549 550 handlerFunc := func(count int, ih *IntrHandler) { 551 switch count { 552 case 1: 553 fmt.Println() // Prevent un-terminated ^C character in terminal 554 555 ih.wg.Add(1) 556 go func() { 557 defer ih.wg.Done() 558 cancelFunc() 559 }() 560 561 default: 562 fmt.Println("Received another interrupt before graceful shutdown, terminating...") 563 os.Exit(-1) 564 } 565 } 566 567 intrh.Handle(handlerFunc, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM) 568 569 return intrh, ctx 570 } 571 572 func profileIfEnabled() (func(), error) { 573 // FIXME this is a temporary hack so profiling of asynchronous operations 574 // works as intended. 575 if os.Getenv(EnvEnableProfiling) != "" { 576 stopProfilingFunc, err := startProfiling() // TODO maybe change this to its own option... profiling makes it slower. 577 if err != nil { 578 return nil, err 579 } 580 return stopProfilingFunc, nil 581 } 582 return func() {}, nil 583 } 584 585 // getApiClient checks the repo, and the given options, checking for 586 // a running API service. if there is one, it returns a client. 587 // otherwise, it returns errApiNotRunning, or another error. 588 func getApiClient(repoPath, apiAddrStr string) (cmdsHttp.Client, error) { 589 590 if apiAddrStr == "" { 591 var err error 592 if apiAddrStr, err = fsrepo.APIAddr(repoPath); err != nil { 593 return nil, err 594 } 595 } 596 597 addr, err := ma.NewMultiaddr(apiAddrStr) 598 if err != nil { 599 return nil, err 600 } 601 602 client, err := apiClientForAddr(addr) 603 if err != nil { 604 return nil, err 605 } 606 607 // make sure the api is actually running. 608 // this is slow, as it might mean an RTT to a remote server. 609 // TODO: optimize some way 610 if err := apiVersionMatches(client); err != nil { 611 return nil, err 612 } 613 614 return client, nil 615 } 616 617 // apiVersionMatches checks whether the api server is running the 618 // same version of go-ipfs. for now, only the exact same version of 619 // client + server work. In the future, we should use semver for 620 // proper API versioning! \o/ 621 func apiVersionMatches(client cmdsHttp.Client) (err error) { 622 ver, err := doVersionRequest(client) 623 if err != nil { 624 return err 625 } 626 627 currv := config.CurrentVersionNumber 628 if ver.Version != currv { 629 return fmt.Errorf("%s (%s != %s)", errApiVersionMismatch, ver.Version, currv) 630 } 631 return nil 632 } 633 634 func doVersionRequest(client cmdsHttp.Client) (*coreCmds.VersionOutput, error) { 635 cmd := coreCmds.VersionCmd 636 optDefs, err := cmd.GetOptions([]string{}) 637 if err != nil { 638 return nil, err 639 } 640 641 req, err := cmds.NewRequest([]string{"version"}, nil, nil, nil, cmd, optDefs) 642 if err != nil { 643 return nil, err 644 } 645 646 res, err := client.Send(req) 647 if err != nil { 648 if isConnRefused(err) { 649 err = repo.ErrApiNotRunning 650 } 651 return nil, err 652 } 653 654 ver, ok := res.Output().(*coreCmds.VersionOutput) 655 if !ok { 656 return nil, errUnexpectedApiOutput 657 } 658 return ver, nil 659 } 660 661 func apiClientForAddr(addr ma.Multiaddr) (cmdsHttp.Client, error) { 662 _, host, err := manet.DialArgs(addr) 663 if err != nil { 664 return nil, err 665 } 666 667 return cmdsHttp.NewClient(host), nil 668 } 669 670 func isConnRefused(err error) bool { 671 return strings.Contains(err.Error(), "connection refused") 672 }