main.go 16.6 KB
Newer Older
Jeromy's avatar
Jeromy committed
1
// cmd/ipfs implements the primary CLI binary for ipfs
2 3 4
package main

import (
Brian Tiger Chow's avatar
Brian Tiger Chow committed
5
	"errors"
6 7
	"fmt"
	"io"
8
	"math/rand"
Jeromy's avatar
Jeromy committed
9 10
	"net"
	"net/url"
11
	"os"
Matt Bell's avatar
Matt Bell committed
12
	"os/signal"
13
	"runtime"
14
	"runtime/pprof"
15
	"strings"
16
	"sync"
17
	"syscall"
18
	"time"
19

20 21 22 23 24 25 26 27
	ma "github.com/ipfs/go-ipfs/Godeps/_workspace/src/github.com/jbenet/go-multiaddr"
	manet "github.com/ipfs/go-ipfs/Godeps/_workspace/src/github.com/jbenet/go-multiaddr-net"

	context "github.com/ipfs/go-ipfs/Godeps/_workspace/src/golang.org/x/net/context"
	cmds "github.com/ipfs/go-ipfs/commands"
	cmdsCli "github.com/ipfs/go-ipfs/commands/cli"
	cmdsHttp "github.com/ipfs/go-ipfs/commands/http"
	core "github.com/ipfs/go-ipfs/core"
28 29
	coreCmds "github.com/ipfs/go-ipfs/core/commands"
	repo "github.com/ipfs/go-ipfs/repo"
30 31 32
	config "github.com/ipfs/go-ipfs/repo/config"
	fsrepo "github.com/ipfs/go-ipfs/repo/fsrepo"
	u "github.com/ipfs/go-ipfs/util"
Jeromy's avatar
Jeromy committed
33
	logging "github.com/ipfs/go-ipfs/vendor/QmQg1J6vikuXF9oDvm4wpdeAUvvkVEKW1EYDw9HhTMnP2b/go-log"
34 35
)

36
// log is the command logger
Jeromy's avatar
Jeromy committed
37
var log = logging.Logger("cmd/ipfs")
38

39 40 41 42
var (
	errUnexpectedApiOutput = errors.New("api returned unexpected output")
	errApiVersionMismatch  = errors.New("api version mismatch")
)
43

44
const (
45 46 47 48
	EnvEnableProfiling = "IPFS_PROF"
	cpuProfile         = "ipfs.cpuprof"
	heapProfile        = "ipfs.memprof"
	errorFormat        = "ERROR: %v\n\n"
49
)
50

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
51 52 53 54
type cmdInvocation struct {
	path []string
	cmd  *cmds.Command
	req  cmds.Request
55
	node *core.IpfsNode
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
56 57 58 59 60 61 62 63
}

// 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
64
func main() {
65 66
	rand.Seed(time.Now().UnixNano())
	runtime.GOMAXPROCS(3) // FIXME rm arbitrary choice for n
Jeromy's avatar
Jeromy committed
67
	ctx := logging.ContextWithLoggable(context.Background(), logging.Uuid("session"))
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
68
	var err error
69 70
	var invoc cmdInvocation
	defer invoc.close()
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
71 72 73 74 75 76 77

	// 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())
	}

78 79 80 81 82 83 84
	stopFunc, err := profileIfEnabled()
	if err != nil {
		printErr(err)
		os.Exit(1)
	}
	defer stopFunc() // to be executed as late as possible

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
85 86
	// 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
87
	printHelp := func(long bool, w io.Writer) {
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
88 89 90 91 92
		helpFunc := cmdsCli.ShortHelp
		if long {
			helpFunc = cmdsCli.LongHelp
		}

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
93
		helpFunc("ipfs", Root, invoc.path, w)
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
94 95
	}

96 97 98 99 100 101
	// this is a message to tell the user how to get the help text
	printMetaHelp := func(w io.Writer) {
		cmdPath := strings.Join(invoc.path, " ")
		fmt.Fprintf(w, "Use 'ipfs %s --help' for information about this command\n", cmdPath)
	}

Etienne Laurin's avatar
Etienne Laurin committed
102 103 104 105 106 107
	// Handle `ipfs help'
	if len(os.Args) == 2 && os.Args[1] == "help" {
		printHelp(false, os.Stdout)
		os.Exit(0)
	}

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
108
	// parse the commandline into a command invocation
109
	parseErr := invoc.Parse(ctx, os.Args[1:])
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
110 111 112

	// BEFORE handling the parse error, if we have enough information
	// AND the user requested help, print it out and exit
113
	if invoc.req != nil {
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
114 115 116 117 118 119
		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
120
			printHelp(longH, os.Stdout)
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
121 122 123 124
			os.Exit(0)
		}
	}

125
	// ok now handle parse error (which means cli input was wrong,
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
126
	// e.g. incorrect number of args, or nonexistent subcommand)
127 128
	if parseErr != nil {
		printErr(parseErr)
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
129 130 131 132 133

		// this was a user error, print help.
		if invoc.cmd != nil {
			// we need a newline space.
			fmt.Fprintf(os.Stderr, "\n")
134
			printMetaHelp(os.Stderr)
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
135
		}
Brian Tiger Chow's avatar
Brian Tiger Chow committed
136 137
		os.Exit(1)
	}
Matt Bell's avatar
Matt Bell committed
138

Etienne Laurin's avatar
Etienne Laurin committed
139 140 141 142 143 144 145 146
	// 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 {
		printHelp(false, os.Stdout)
		os.Exit(0)
	}

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
147
	// ok, finally, run the command invocation.
148
	intrh, ctx := invoc.SetupInterruptHandler(ctx)
149
	defer intrh.Close()
150

151
	output, err := invoc.Run(ctx)
152
	if err != nil {
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
153 154 155 156
		printErr(err)

		// if this error was a client error, print short help too.
		if isClientError(err) {
157
			printMetaHelp(os.Stderr)
158
		}
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
159
		os.Exit(1)
Brian Tiger Chow's avatar
Brian Tiger Chow committed
160 161
	}

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
162
	// everything went better than expected :)
Jeromy's avatar
Jeromy committed
163 164 165 166 167 168
	_, err = io.Copy(os.Stdout, output)
	if err != nil {
		printErr(err)

		os.Exit(1)
	}
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
169 170
}

171
func (i *cmdInvocation) Run(ctx context.Context) (output io.Reader, err error) {
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
172 173 174

	// 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
175
	if err != nil {
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
176
		return nil, err
Brian Tiger Chow's avatar
Brian Tiger Chow committed
177
	}
178
	if debug || os.Getenv("IPFS_LOGGING") == "debug" {
Brian Tiger Chow's avatar
Brian Tiger Chow committed
179
		u.Debug = true
Jeromy's avatar
Jeromy committed
180
		logging.SetDebugLogging()
181
	}
182 183 184
	if u.GetenvBool("DEBUG") {
		u.Debug = true
	}
185

186
	res, err := callCommand(ctx, i.req, Root, i.cmd)
Brian Tiger Chow's avatar
Brian Tiger Chow committed
187
	if err != nil {
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
188
		return nil, err
189
	}
190

191 192 193 194
	if err := res.Error(); err != nil {
		return nil, err
	}

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
195
	return res.Reader()
196
}
197

Brian Tiger Chow's avatar
Brian Tiger Chow committed
198
func (i *cmdInvocation) constructNodeFunc(ctx context.Context) func() (*core.IpfsNode, error) {
199 200 201 202
	return func() (*core.IpfsNode, error) {
		if i.req == nil {
			return nil, errors.New("constructing node without a request")
		}
203

Jeromy's avatar
Jeromy committed
204
		cmdctx := i.req.InvocContext()
205 206 207
		if cmdctx == nil {
			return nil, errors.New("constructing node without a request context")
		}
208

Jeromy's avatar
Jeromy committed
209
		r, err := fsrepo.Open(i.req.InvocContext().ConfigRoot)
210
		if err != nil { // repo is owned by the node
211
			return nil, err
212
		}
213

214 215
		// ok everything is good. set it on the invocation (for ownership)
		// and return it.
216 217 218 219
		n, err := core.NewNode(ctx, &core.BuildCfg{
			Online: cmdctx.Online,
			Repo:   r,
		})
220 221 222 223 224
		if err != nil {
			return nil, err
		}
		i.node = n
		return i.node, nil
225
	}
226 227 228 229 230 231 232 233 234 235 236 237
}

func (i *cmdInvocation) close() {
	// 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.
	if i.node != nil {
		log.Info("Shutting down node...")
		i.node.Close()
	}
}

238
func (i *cmdInvocation) Parse(ctx context.Context, args []string) error {
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
239
	var err error
240

241
	i.req, i.cmd, i.path, err = cmdsCli.Parse(args, os.Stdin, Root)
242
	if err != nil {
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
243
		return err
244 245
	}

Brian Tiger Chow's avatar
Brian Tiger Chow committed
246
	repoPath, err := getRepoPath(i.req)
247
	if err != nil {
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
248
		return err
249
	}
Brian Tiger Chow's avatar
Brian Tiger Chow committed
250
	log.Debugf("config path is %s", repoPath)
251

252
	// this sets up the function that will initialize the config lazily.
Jeromy's avatar
Jeromy committed
253
	cmdctx := i.req.InvocContext()
Brian Tiger Chow's avatar
Brian Tiger Chow committed
254
	cmdctx.ConfigRoot = repoPath
255
	cmdctx.LoadConfig = loadConfig
256 257
	// this sets up the function that will initialize the node
	// this is so that we can construct the node lazily.
Brian Tiger Chow's avatar
Brian Tiger Chow committed
258
	cmdctx.ConstructNode = i.constructNodeFunc(ctx)
259

260 261
	// 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
262
	if !i.req.Option("encoding").Found() {
263
		if i.req.Command().Marshalers != nil && i.req.Command().Marshalers[cmds.Text] != nil {
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
264
			i.req.SetOption("encoding", cmds.Text)
265
		} else {
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
266
			i.req.SetOption("encoding", cmds.JSON)
267 268 269
		}
	}

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
270
	return nil
271 272
}

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
273 274
func (i *cmdInvocation) requestedHelp() (short bool, long bool, err error) {
	longHelp, _, err := i.req.Option("help").Bool()
275
	if err != nil {
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
276
		return false, false, err
277
	}
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
278
	shortHelp, _, err := i.req.Option("h").Bool()
279
	if err != nil {
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
280
		return false, false, err
281
	}
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
282
	return longHelp, shortHelp, nil
283
}
284

285
func callPreCommandHooks(ctx context.Context, details cmdDetails, req cmds.Request, root *cmds.Command) error {
286

287
	log.Event(ctx, "callPreCommandHooks", &details)
288 289 290 291 292
	log.Debug("Calling pre-command hooks...")

	return nil
}

293
func callCommand(ctx context.Context, req cmds.Request, root *cmds.Command, cmd *cmds.Command) (cmds.Response, error) {
Jeromy's avatar
Jeromy committed
294
	log.Info(config.EnvDir, " ", req.InvocContext().ConfigRoot)
295
	var res cmds.Response
296

Jeromy's avatar
Jeromy committed
297 298 299 300
	err := req.SetRootContext(ctx)
	if err != nil {
		return nil, err
	}
301

302 303 304 305 306
	details, err := commandDetails(req.Path(), root)
	if err != nil {
		return nil, err
	}

307
	client, err := commandShouldRunOnDaemon(*details, req, root)
308 309 310
	if err != nil {
		return nil, err
	}
311

312
	err = callPreCommandHooks(ctx, *details, req, root)
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
313 314 315
	if err != nil {
		return nil, err
	}
316

317 318 319 320 321 322 323
	if cmd.PreRun != nil {
		err = cmd.PreRun(req)
		if err != nil {
			return nil, err
		}
	}

324 325
	if client != nil {
		log.Debug("Executing command via API")
326 327
		res, err = client.Send(req)
		if err != nil {
328 329 330
			if isConnRefused(err) {
				err = repo.ErrApiNotRunning
			}
331 332
			return nil, err
		}
333

334
	} else {
335
		log.Debug("Executing command locally")
336

Jeromy's avatar
Jeromy committed
337
		err := req.SetRootContext(ctx)
Jeromy's avatar
Jeromy committed
338 339 340 341
		if err != nil {
			return nil, err
		}

342 343
		// Okay!!!!! NOW we can call the command.
		res = root.Call(req)
344 345

	}
346 347

	if cmd.PostRun != nil {
348
		cmd.PostRun(req, res)
349 350
	}

351
	return res, nil
352 353
}

354 355 356 357 358
// 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) {
359 360 361 362 363 364 365
	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 {
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
366
			return nil, fmt.Errorf("subcommand %s should be in root", cmp)
367
		}
368

369 370 371
		if cmdDetails, found := cmdDetailsMap[cmd]; found {
			details = cmdDetails
		}
372
	}
373 374 375 376 377 378
	return &details, nil
}

// commandShouldRunOnDaemon determines, from commmand details, whether a
// command ought to be executed on an IPFS daemon.
//
379
// It returns a client if the command should be executed on a daemon and nil if
380 381
// it should be executed on a client. It returns an error if the command must
// NOT be executed on either.
382
func commandShouldRunOnDaemon(details cmdDetails, req cmds.Request, root *cmds.Command) (cmdsHttp.Client, error) {
383 384 385
	path := req.Path()
	// root command.
	if len(path) < 1 {
386
		return nil, nil
387
	}
388 389

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

393
	if details.doesNotUseRepo && details.canRunOnClient() {
394
		return nil, nil
395 396
	}

397 398 399 400 401
	// at this point need to know whether api is running. we defer
	// to this point so that we dont check unnecessarily

	// did user specify an api to use for this command?
	apiAddrStr, _, err := req.Option(coreCmds.ApiOption).String()
402
	if err != nil {
403
		return nil, err
404
	}
405

406 407 408 409 410 411 412
	client, err := getApiClient(req.InvocContext().ConfigRoot, apiAddrStr)
	if err == repo.ErrApiNotRunning {
		if apiAddrStr != "" && req.Command() != daemonCmd {
			// if user SPECIFIED an api, and this cmd is not daemon
			// we MUST use it. so error out.
			return nil, err
		}
413

414 415 416 417
		// ok for api not to be running
	} else if err != nil { // some other api error
		return nil, err
	}
418

419
	if client != nil { // daemon is running
420
		if details.cannotRunOnDaemon {
421 422 423 424 425 426 427 428 429
			e := "cannot use API with this command."

			// check if daemon locked. legacy error text, for now.
			daemonLocked, _ := fsrepo.LockedByOtherProcess(req.InvocContext().ConfigRoot)
			if daemonLocked {
				e = "ipfs daemon is running. please stop it to run this command"
			}

			return nil, cmds.ClientError(e)
430 431
		}

432
		return client, nil
433 434 435
	}

	if details.cannotRunOnClient {
436
		return nil, cmds.ClientError("must run on the ipfs daemon")
437 438
	}

439
	return nil, nil
440 441
}

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
442
func isClientError(err error) bool {
Brian Tiger Chow's avatar
Brian Tiger Chow committed
443 444 445 446

	// 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
447
	// cast to cmds.Error
Brian Tiger Chow's avatar
Brian Tiger Chow committed
448 449 450 451 452
	switch e := err.(type) {
	case *cmds.Error:
		return e.Code == cmds.ErrClient
	case cmds.Error:
		return e.Code == cmds.ErrClient
453
	}
Brian Tiger Chow's avatar
Brian Tiger Chow committed
454
	return false
455 456
}

Brian Tiger Chow's avatar
Brian Tiger Chow committed
457 458
func getRepoPath(req cmds.Request) (string, error) {
	repoOpt, found, err := req.Option("config").String()
459 460 461
	if err != nil {
		return "", err
	}
Brian Tiger Chow's avatar
Brian Tiger Chow committed
462 463
	if found && repoOpt != "" {
		return repoOpt, nil
464 465
	}

Brian Tiger Chow's avatar
Brian Tiger Chow committed
466
	repoPath, err := fsrepo.BestKnownPath()
467 468 469
	if err != nil {
		return "", err
	}
Brian Tiger Chow's avatar
Brian Tiger Chow committed
470
	return repoPath, nil
471 472
}

473
func loadConfig(path string) (*config.Config, error) {
Brian Tiger Chow's avatar
huh  
Brian Tiger Chow committed
474
	return fsrepo.ConfigAt(path)
475
}
476

477 478 479 480 481 482 483 484 485 486
// 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)
487 488 489 490
	go func() {
		for _ = range time.NewTicker(time.Second * 30).C {
			err := writeHeapProfileToFile()
			if err != nil {
rht's avatar
rht committed
491
				log.Error(err)
492 493 494
			}
		}
	}()
495 496 497 498 499 500 501 502

	stopProfiling := func() {
		pprof.StopCPUProfile()
		defer ofi.Close() // captured by the closure
	}
	return stopProfiling, nil
}

503 504 505
func writeHeapProfileToFile() error {
	mprof, err := os.Create(heapProfile)
	if err != nil {
506
		return err
507
	}
508
	defer mprof.Close() // _after_ writing the heap profile
509 510
	return pprof.WriteHeapProfile(mprof)
}
511

512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529
// IntrHandler helps set up an interrupt handler that can
// be cleanly shut down through the io.Closer interface.
type IntrHandler struct {
	sig chan os.Signal
	wg  sync.WaitGroup
}

func NewIntrHandler() *IntrHandler {
	ih := &IntrHandler{}
	ih.sig = make(chan os.Signal, 1)
	return ih
}

func (ih *IntrHandler) Close() error {
	close(ih.sig)
	ih.wg.Wait()
	return nil
}
530

531 532 533 534 535 536 537 538
// Handle starts handling the given signals, and will call the handler
// callback function each time a signal is catched. The function is passed
// the number of times the handler has been triggered in total, as
// well as the handler itself, so that the handling logic can use the
// handler's wait group to ensure clean shutdown when Close() is called.
func (ih *IntrHandler) Handle(handler func(count int, ih *IntrHandler), sigs ...os.Signal) {
	signal.Notify(ih.sig, sigs...)
	ih.wg.Add(1)
Matt Bell's avatar
Matt Bell committed
539
	go func() {
540 541 542 543 544 545 546 547 548 549
		defer ih.wg.Done()
		count := 0
		for _ = range ih.sig {
			count++
			handler(count, ih)
		}
		signal.Stop(ih.sig)
	}()
}

550
func (i *cmdInvocation) SetupInterruptHandler(ctx context.Context) (io.Closer, context.Context) {
551

552
	intrh := NewIntrHandler()
553 554
	ctx, cancelFunc := context.WithCancel(ctx)

555 556 557
	handlerFunc := func(count int, ih *IntrHandler) {
		switch count {
		case 1:
558
			fmt.Println() // Prevent un-terminated ^C character in terminal
559 560 561 562

			ih.wg.Add(1)
			go func() {
				defer ih.wg.Done()
563
				cancelFunc()
564
			}()
565

566 567 568
		default:
			fmt.Println("Received another interrupt before graceful shutdown, terminating...")
			os.Exit(-1)
Matt Bell's avatar
Matt Bell committed
569
		}
570 571 572
	}

	intrh.Handle(handlerFunc, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM)
573

574
	return intrh, ctx
575
}
576 577 578 579

func profileIfEnabled() (func(), error) {
	// FIXME this is a temporary hack so profiling of asynchronous operations
	// works as intended.
580
	if os.Getenv(EnvEnableProfiling) != "" {
581 582 583 584 585 586 587 588
		stopProfilingFunc, err := startProfiling() // TODO maybe change this to its own option... profiling makes it slower.
		if err != nil {
			return nil, err
		}
		return stopProfilingFunc, nil
	}
	return func() {}, nil
}
589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675

// getApiClient checks the repo, and the given options, checking for
// a running API service. if there is one, it returns a client.
// otherwise, it returns errApiNotRunning, or another error.
func getApiClient(repoPath, apiAddrStr string) (cmdsHttp.Client, error) {

	if apiAddrStr == "" {
		var err error
		if apiAddrStr, err = fsrepo.APIAddr(repoPath); err != nil {
			return nil, err
		}
	}

	addr, err := ma.NewMultiaddr(apiAddrStr)
	if err != nil {
		return nil, err
	}

	client, err := apiClientForAddr(addr)
	if err != nil {
		return nil, err
	}

	// make sure the api is actually running.
	// this is slow, as it might mean an RTT to a remote server.
	// TODO: optimize some way
	if err := apiVersionMatches(client); err != nil {
		return nil, err
	}

	return client, nil
}

// apiVersionMatches checks whether the api server is running the
// same version of go-ipfs. for now, only the exact same version of
// client + server work. In the future, we should use semver for
// proper API versioning! \o/
func apiVersionMatches(client cmdsHttp.Client) (err error) {
	ver, err := doVersionRequest(client)
	if err != nil {
		return err
	}

	currv := config.CurrentVersionNumber
	if ver.Version != currv {
		return fmt.Errorf("%s (%s != %s)", errApiVersionMismatch, ver.Version, currv)
	}
	return nil
}

func doVersionRequest(client cmdsHttp.Client) (*coreCmds.VersionOutput, error) {
	cmd := coreCmds.VersionCmd
	optDefs, err := cmd.GetOptions([]string{})
	if err != nil {
		return nil, err
	}

	req, err := cmds.NewRequest([]string{"version"}, nil, nil, nil, cmd, optDefs)
	if err != nil {
		return nil, err
	}

	res, err := client.Send(req)
	if err != nil {
		if isConnRefused(err) {
			err = repo.ErrApiNotRunning
		}
		return nil, err
	}

	ver, ok := res.Output().(*coreCmds.VersionOutput)
	if !ok {
		return nil, errUnexpectedApiOutput
	}
	return ver, nil
}

func apiClientForAddr(addr ma.Multiaddr) (cmdsHttp.Client, error) {
	_, host, err := manet.DialArgs(addr)
	if err != nil {
		return nil, err
	}

	return cmdsHttp.NewClient(host), nil
}

func isConnRefused(err error) bool {
Jeromy's avatar
Jeromy committed
676 677 678 679 680 681 682 683 684 685 686
	// unwrap url errors from http calls
	if urlerr, ok := err.(*url.Error); ok {
		err = urlerr.Err
	}

	netoperr, ok := err.(*net.OpError)
	if !ok {
		return false
	}

	return netoperr.Op == "dial"
687
}