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

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

21
	oldcmds "github.com/ipfs/go-ipfs/commands"
22
	core "github.com/ipfs/go-ipfs/core"
23
	coreCmds "github.com/ipfs/go-ipfs/core/commands"
keks's avatar
keks committed
24
	corehttp "github.com/ipfs/go-ipfs/core/corehttp"
keks's avatar
keks committed
25
	loader "github.com/ipfs/go-ipfs/plugin/loader"
26
	repo "github.com/ipfs/go-ipfs/repo"
27 28
	config "github.com/ipfs/go-ipfs/repo/config"
	fsrepo "github.com/ipfs/go-ipfs/repo/fsrepo"
29

Steven Allen's avatar
Steven Allen committed
30 31
	u "gx/ipfs/QmPsAfmDBnZN3kZGSuNwvCNDZiHneERSKmRcFyG3UkvcT3/go-ipfs-util"
	manet "gx/ipfs/QmSGL5Uoa6gKHgBBwQG8u1CWKUC8ZnwaZiLgFVTFBR2bxr/go-multiaddr-net"
32
	logging "gx/ipfs/QmSpJByNKFX1sCsHBEp3R73FL4NF6FnQTEGyNAXHm2GS52/go-log"
Steven Allen's avatar
Steven Allen committed
33 34
	loggables "gx/ipfs/QmSvcDkiRwB8LuMhUtnvhum2C851Mproo75ZDD19jx43tD/go-libp2p-loggables"
	ma "gx/ipfs/QmW8s4zTsUoX1Q6CeYxVKPyqSKbF7H1YDUyTostBtZ8DaG/go-multiaddr"
keks's avatar
keks committed
35 36 37
	"gx/ipfs/QmXPgUkyFLMN3c79WrGM2VbjWynSPnmaHjF2AviBVQE2i7/go-ipfs-cmds"
	"gx/ipfs/QmXPgUkyFLMN3c79WrGM2VbjWynSPnmaHjF2AviBVQE2i7/go-ipfs-cmds/cli"
	"gx/ipfs/QmXPgUkyFLMN3c79WrGM2VbjWynSPnmaHjF2AviBVQE2i7/go-ipfs-cmds/http"
38
	osh "gx/ipfs/QmXuBJ7DR6k3rmUEKtvVMhwjmXDuJgXXPUt4LQXKBMsU93/go-os-helper"
39 40
)

41
// log is the command logger
Jeromy's avatar
Jeromy committed
42
var log = logging.Logger("cmd/ipfs")
43

44
var errRequestCanceled = errors.New("request canceled")
45

46
const (
47 48 49
	EnvEnableProfiling = "IPFS_PROF"
	cpuProfile         = "ipfs.cpuprof"
	heapProfile        = "ipfs.memprof"
50
)
51

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
52
type cmdInvocation struct {
53
	req  *cmds.Request
54
	node *core.IpfsNode
55
	ctx  *oldcmds.Context
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
56 57
}

Jan Winkelmann's avatar
Jan Winkelmann committed
58 59 60 61 62 63
type exitErr int

func (e exitErr) Error() string {
	return fmt.Sprint("exit code", int(e))
}

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
64 65
// main roadmap:
// - parse the commandline to get a cmdInvocation
66
// - if user requests help, print it and exit.
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
67 68 69
// - run the command invocation
// - output the response
// - if anything fails, print error, maybe with help
70
func main() {
71 72 73 74
	os.Exit(mainRet())
}

func mainRet() int {
75
	rand.Seed(time.Now().UnixNano())
76
	ctx := logging.ContextWithLoggable(context.Background(), loggables.Uuid("session"))
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
77 78 79 80 81 82 83 84
	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())
	}

85 86 87
	stopFunc, err := profileIfEnabled()
	if err != nil {
		printErr(err)
88
		return 1
89 90 91
	}
	defer stopFunc() // to be executed as late as possible

keks's avatar
keks committed
92 93
	intrh, ctx := setupInterruptHandler(ctx)
	defer intrh.Close()
94

Etienne Laurin's avatar
Etienne Laurin committed
95
	// Handle `ipfs help'
96 97
	if len(os.Args) == 2 {
		if os.Args[1] == "help" {
keks's avatar
keks committed
98
			os.Args[1] = "-h"
99 100 101
		} else if os.Args[1] == "--version" {
			os.Args[1] = "version"
		}
Etienne Laurin's avatar
Etienne Laurin committed
102 103
	}

keks's avatar
keks committed
104 105
	buildEnv := func(ctx context.Context, req *cmds.Request) (interface{}, error) {
		repoPath, err := getRepoPath(req)
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
106
		if err != nil {
keks's avatar
keks committed
107
			return nil, err
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
108
		}
keks's avatar
keks committed
109
		log.Debugf("config path is %s", repoPath)
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
110

keks's avatar
keks committed
111
		// this sets up the function that will initialize the config lazily.
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
112

keks's avatar
keks committed
113 114
		// this sets up the function that will initialize the node
		// this is so that we can construct the node lazily.
Matt Bell's avatar
Matt Bell committed
115

keks's avatar
keks committed
116 117 118 119 120 121 122 123
		return &oldcmds.Context{
			ConfigRoot: repoPath,
			LoadConfig: loadConfig,
			ReqLog:     &oldcmds.ReqLog{},
			ConstructNode: func() (n *core.IpfsNode, err error) {
				if req == nil {
					return nil, errors.New("constructing node without a request")
				}
Etienne Laurin's avatar
Etienne Laurin committed
124

keks's avatar
keks committed
125 126 127 128
				r, err := fsrepo.Open(repoPath)
				if err != nil { // repo is owned by the node
					return nil, err
				}
Jan Winkelmann's avatar
Jan Winkelmann committed
129

keks's avatar
keks committed
130 131 132 133 134 135 136 137 138 139 140
				// ok everything is good. set it on the invocation (for ownership)
				// and return it.
				n, err = core.NewNode(ctx, &core.BuildCfg{
					// TODO(keks) figure out how Online was set before. I think it was set to
					// a value that always is the zero value so we can just drop it, but
					// I'll have to check that.
					Repo: r,
				})
				if err != nil {
					return nil, err
				}
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
141

keks's avatar
keks committed
142 143 144 145 146 147 148 149
				n.SetLocal(true)
				return n, nil
			},
		}, nil
	}

	err = cli.Run(ctx, Root, os.Args, os.Stdin, os.Stdout, os.Stderr, buildEnv, makeExecutor)
	if err != nil {
150
		return 1
Brian Tiger Chow's avatar
Brian Tiger Chow committed
151 152
	}

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
153
	// everything went better than expected :)
154
	return 0
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
155 156
}

keks's avatar
keks committed
157
func checkDebug(req *cmds.Request) {
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
158
	// check if user wants to debug. option OR env var.
keks's avatar
keks committed
159
	debug, _ := req.Options["debug"].(bool)
160
	if debug || os.Getenv("IPFS_LOGGING") == "debug" {
Brian Tiger Chow's avatar
Brian Tiger Chow committed
161
		u.Debug = true
Jeromy's avatar
Jeromy committed
162
		logging.SetDebugLogging()
163
	}
164 165 166
	if u.GetenvBool("DEBUG") {
		u.Debug = true
	}
167 168
}

keks's avatar
keks committed
169 170 171
func makeExecutor(req *cmds.Request, env interface{}) (cmds.Executor, error) {
	checkDebug(req)
	details, err := commandDetails(req.Path, Root)
172
	if err != nil {
keks's avatar
keks committed
173
		return nil, err
174
	}
175

keks's avatar
keks committed
176
	client, err := commandShouldRunOnDaemon(*details, req, Root, env.(*oldcmds.Context))
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
177
	if err != nil {
keks's avatar
keks committed
178
		return nil, err
Jan Winkelmann's avatar
Jan Winkelmann committed
179 180
	}

keks's avatar
keks committed
181 182 183
	var exctr cmds.Executor
	if client != nil && !req.Command.External {
		exctr = client.(cmds.Executor)
184
	} else {
keks's avatar
keks committed
185
		pluginpath := filepath.Join(env.(*oldcmds.Context).ConfigRoot, "plugins")
186
		if _, err := loader.LoadPlugins(pluginpath); err != nil {
keks's avatar
keks committed
187
			log.Warning("error loading plugins: ", err)
188 189
		}

keks's avatar
keks committed
190
		exctr = cmds.NewExecutor(req.Root)
191 192
	}

keks's avatar
keks committed
193
	return exctr, nil
194 195
}

196 197 198 199 200
// 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) {
201 202 203 204
	var details cmdDetails
	// find the last command in path that has a cmdDetailsMap entry
	cmd := root
	for _, cmp := range path {
Jan Winkelmann's avatar
Jan Winkelmann committed
205 206
		cmd = cmd.Subcommand(cmp)
		if cmd == nil {
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
207
			return nil, fmt.Errorf("subcommand %s should be in root", cmp)
208
		}
209

Jan Winkelmann's avatar
Jan Winkelmann committed
210
		if cmdDetails, found := cmdDetailsMap[strings.Join(path, "/")]; found {
211 212
			details = cmdDetails
		}
213
	}
214 215 216 217
	return &details, nil
}

// commandShouldRunOnDaemon determines, from commmand details, whether a
218
// command ought to be executed on an ipfs daemon.
219
//
220
// It returns a client if the command should be executed on a daemon and nil if
221 222
// it should be executed on a client. It returns an error if the command must
// NOT be executed on either.
223 224
func commandShouldRunOnDaemon(details cmdDetails, req *cmds.Request, root *cmds.Command, cctx *oldcmds.Context) (http.Client, error) {
	path := req.Path
225 226
	// root command.
	if len(path) < 1 {
227
		return nil, nil
228
	}
229 230

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

234
	if details.doesNotUseRepo && details.canRunOnClient() {
235
		return nil, nil
236 237
	}

238 239 240 241
	// 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?
242
	apiAddrStr, _ := req.Options[coreCmds.ApiOption].(string)
243

244
	client, err := getApiClient(cctx.ConfigRoot, apiAddrStr)
245
	if err == repo.ErrApiNotRunning {
246
		if apiAddrStr != "" && req.Command != daemonCmd {
247 248 249 250
			// if user SPECIFIED an api, and this cmd is not daemon
			// we MUST use it. so error out.
			return nil, err
		}
251

252 253 254 255
		// ok for api not to be running
	} else if err != nil { // some other api error
		return nil, err
	}
256

michael's avatar
michael committed
257
	if client != nil {
258
		if details.cannotRunOnDaemon {
259
			// check if daemon locked. legacy error text, for now.
michael's avatar
michael committed
260
			log.Debugf("Command cannot run on daemon. Checking if daemon is locked")
261
			if daemonLocked, _ := fsrepo.LockedByOtherProcess(cctx.ConfigRoot); daemonLocked {
rht's avatar
rht committed
262
				return nil, cmds.ClientError("ipfs daemon is running. please stop it to run this command")
263
			}
rht's avatar
rht committed
264
			return nil, nil
265 266
		}

267
		return client, nil
268 269 270
	}

	if details.cannotRunOnClient {
271
		return nil, cmds.ClientError("must run on the ipfs daemon")
272 273
	}

274
	return nil, nil
275 276
}

277 278
func getRepoPath(req *cmds.Request) (string, error) {
	repoOpt, found := req.Options["config"].(string)
Brian Tiger Chow's avatar
Brian Tiger Chow committed
279 280
	if found && repoOpt != "" {
		return repoOpt, nil
281 282
	}

Brian Tiger Chow's avatar
Brian Tiger Chow committed
283
	repoPath, err := fsrepo.BestKnownPath()
284 285 286
	if err != nil {
		return "", err
	}
Brian Tiger Chow's avatar
Brian Tiger Chow committed
287
	return repoPath, nil
288 289
}

290
func loadConfig(path string) (*config.Config, error) {
Brian Tiger Chow's avatar
huh  
Brian Tiger Chow committed
291
	return fsrepo.ConfigAt(path)
292
}
293

294 295 296 297 298 299 300 301 302
// 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)
303
	go func() {
304
		for range time.NewTicker(time.Second * 30).C {
305 306
			err := writeHeapProfileToFile()
			if err != nil {
rht's avatar
rht committed
307
				log.Error(err)
308 309 310
			}
		}
	}()
311 312 313 314 315 316 317 318

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

319 320 321
func writeHeapProfileToFile() error {
	mprof, err := os.Create(heapProfile)
	if err != nil {
322
		return err
323
	}
324
	defer mprof.Close() // _after_ writing the heap profile
325 326
	return pprof.WriteHeapProfile(mprof)
}
327

328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345
// 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
}
346

347 348 349 350 351 352 353 354
// 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
355
	go func() {
356 357
		defer ih.wg.Done()
		count := 0
358
		for range ih.sig {
359 360 361 362 363 364 365
			count++
			handler(count, ih)
		}
		signal.Stop(ih.sig)
	}()
}

keks's avatar
keks committed
366
func setupInterruptHandler(ctx context.Context) (io.Closer, context.Context) {
367
	intrh := NewIntrHandler()
368 369
	ctx, cancelFunc := context.WithCancel(ctx)

370 371 372
	handlerFunc := func(count int, ih *IntrHandler) {
		switch count {
		case 1:
373
			fmt.Println() // Prevent un-terminated ^C character in terminal
374 375 376 377

			ih.wg.Add(1)
			go func() {
				defer ih.wg.Done()
378
				cancelFunc()
379
			}()
380

381 382 383
		default:
			fmt.Println("Received another interrupt before graceful shutdown, terminating...")
			os.Exit(-1)
Matt Bell's avatar
Matt Bell committed
384
		}
385 386 387
	}

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

389
	return intrh, ctx
390
}
391 392 393 394

func profileIfEnabled() (func(), error) {
	// FIXME this is a temporary hack so profiling of asynchronous operations
	// works as intended.
395
	if os.Getenv(EnvEnableProfiling) != "" {
396 397 398 399 400 401 402 403
		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
}
404

405 406 407 408
var apiFileErrorFmt string = `Failed to parse '%[1]s/api' file.
	error: %[2]s
If you're sure go-ipfs isn't running, you can just delete it.
`
409 410
var checkIPFSUnixFmt = "Otherwise check:\n\tps aux | grep ipfs"
var checkIPFSWinFmt = "Otherwise check:\n\ttasklist | findstr ipfs"
411

412 413 414
// 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.
Jan Winkelmann's avatar
Jan Winkelmann committed
415
func getApiClient(repoPath, apiAddrStr string) (http.Client, error) {
416 417 418 419 420 421 422 423 424
	var apiErrorFmt string
	switch {
	case osh.IsUnix():
		apiErrorFmt = apiFileErrorFmt + checkIPFSUnixFmt
	case osh.IsWindows():
		apiErrorFmt = apiFileErrorFmt + checkIPFSWinFmt
	default:
		apiErrorFmt = apiFileErrorFmt
	}
425

426 427 428 429 430 431 432 433 434 435 436 437 438
	var addr ma.Multiaddr
	var err error
	if len(apiAddrStr) != 0 {
		addr, err = ma.NewMultiaddr(apiAddrStr)
		if err != nil {
			return nil, err
		}
		if len(addr.Protocols()) == 0 {
			return nil, fmt.Errorf("mulitaddr doesn't provide any protocols")
		}
	} else {
		addr, err = fsrepo.APIAddr(repoPath)
		if err == repo.ErrApiNotRunning {
439 440 441
			return nil, err
		}

442 443 444 445 446 447
		if err != nil {
			return nil, fmt.Errorf(apiErrorFmt, repoPath, err.Error())
		}
	}
	if len(addr.Protocols()) == 0 {
		return nil, fmt.Errorf(apiErrorFmt, repoPath, "multiaddr doesn't provide any protocols")
448
	}
rht's avatar
rht committed
449
	return apiClientForAddr(addr)
450 451
}

Jan Winkelmann's avatar
Jan Winkelmann committed
452
func apiClientForAddr(addr ma.Multiaddr) (http.Client, error) {
453 454 455 456 457
	_, host, err := manet.DialArgs(addr)
	if err != nil {
		return nil, err
	}

keks's avatar
keks committed
458
	return http.NewClient(host, http.ClientWithAPIPrefix(corehttp.APIPath)), nil
459 460 461
}

func isConnRefused(err error) bool {
Jeromy's avatar
Jeromy committed
462 463 464 465 466 467 468 469 470 471 472
	// 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"
473
}
rht's avatar
rht committed
474 475 476 477 478 479 480

func wrapContextCanceled(err error) error {
	if strings.Contains(err.Error(), "request canceled") {
		err = errRequestCanceled
	}
	return err
}