main.go 12.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 (
5
	"context"
Brian Tiger Chow's avatar
Brian Tiger Chow committed
6
	"errors"
7 8
	"fmt"
	"io"
9
	"math/rand"
10
	"os"
Matt Bell's avatar
Matt Bell committed
11
	"os/signal"
12
	"path/filepath"
13
	"runtime/pprof"
14
	"strings"
15
	"sync"
16
	"syscall"
17
	"time"
18

19
	oldcmds "github.com/ipfs/go-ipfs/commands"
20
	core "github.com/ipfs/go-ipfs/core"
Spartucus's avatar
Spartucus committed
21
	corecmds "github.com/ipfs/go-ipfs/core/commands"
keks's avatar
keks committed
22
	corehttp "github.com/ipfs/go-ipfs/core/corehttp"
keks's avatar
keks committed
23
	loader "github.com/ipfs/go-ipfs/plugin/loader"
24
	repo "github.com/ipfs/go-ipfs/repo"
25
	fsrepo "github.com/ipfs/go-ipfs/repo/fsrepo"
26

Steven Allen's avatar
Steven Allen committed
27
	"gx/ipfs/QmPEpj17FDRpc7K1aArKZp3RsHtzRMKykeK9GVgn4WQGPR/go-ipfs-config"
28
	u "gx/ipfs/QmPdKqUcHGFdeSpvjVoaTRPPstGif9GBZb5Q56RVw9o69A/go-ipfs-util"
29 30 31
	"gx/ipfs/QmSXUokcP4TJpFfqozT69AVAYRtzXVMUjzQVkYX41R9Svs/go-ipfs-cmds"
	"gx/ipfs/QmSXUokcP4TJpFfqozT69AVAYRtzXVMUjzQVkYX41R9Svs/go-ipfs-cmds/cli"
	"gx/ipfs/QmSXUokcP4TJpFfqozT69AVAYRtzXVMUjzQVkYX41R9Svs/go-ipfs-cmds/http"
Steven Allen's avatar
Steven Allen committed
32 33
	ma "gx/ipfs/QmT4U94DnD8FRfqr21obWY32HLM5VExccPKMjQHofeYqr9/go-multiaddr"
	loggables "gx/ipfs/QmVrDtvvQCUeMZaY9UFkae6c85kdQ1GvVEhPrjPTdjxRLv/go-libp2p-loggables"
34
	osh "gx/ipfs/QmXuBJ7DR6k3rmUEKtvVMhwjmXDuJgXXPUt4LQXKBMsU93/go-os-helper"
Steven Allen's avatar
Steven Allen committed
35
	logging "gx/ipfs/QmZChCsSt8DctjceaL56Eibc29CVQq4dGKRXC5JRZ6Ppae/go-log"
Steven Allen's avatar
Steven Allen committed
36 37
	manet "gx/ipfs/Qmaabb1tJZ2CX5cp6MuuiGgns71NYoxdgQP6Xdid1dVceC/go-multiaddr-net"
	madns "gx/ipfs/QmeHJXPqCNzSFbVkYM1uQLuM2L5FyJB9zukQ7EeqRP8ZC9/go-multiaddr-dns"
38 39
)

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

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

45 46 47
// declared as a var for testing purposes
var dnsResolver = madns.DefaultResolver

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

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

func mainRet() int {
65
	rand.Seed(time.Now().UnixNano())
66
	ctx := logging.ContextWithLoggable(context.Background(), loggables.Uuid("session"))
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
67 68 69 70 71 72 73 74
	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())
	}

75 76 77
	stopFunc, err := profileIfEnabled()
	if err != nil {
		printErr(err)
78
		return 1
79 80 81
	}
	defer stopFunc() // to be executed as late as possible

keks's avatar
keks committed
82 83
	intrh, ctx := setupInterruptHandler(ctx)
	defer intrh.Close()
84

Etienne Laurin's avatar
Etienne Laurin committed
85
	// Handle `ipfs help'
86 87
	if len(os.Args) == 2 {
		if os.Args[1] == "help" {
keks's avatar
keks committed
88
			os.Args[1] = "-h"
89 90 91
		} else if os.Args[1] == "--version" {
			os.Args[1] = "version"
		}
Etienne Laurin's avatar
Etienne Laurin committed
92 93
	}

Łukasz Magiera's avatar
Łukasz Magiera committed
94
	// output depends on executable name passed in os.Args
95 96 97
	// so we need to make sure it's stable
	os.Args[0] = "ipfs"

Jeromy's avatar
Jeromy committed
98
	buildEnv := func(ctx context.Context, req *cmds.Request) (cmds.Environment, error) {
99
		checkDebug(req)
keks's avatar
keks committed
100
		repoPath, err := getRepoPath(req)
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
101
		if err != nil {
keks's avatar
keks committed
102
			return nil, err
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
103
		}
keks's avatar
keks committed
104
		log.Debugf("config path is %s", repoPath)
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
105

keks's avatar
keks committed
106 107 108 109 110 111 112 113 114 115
		// this sets up the function that will initialize the node
		// this is so that we can construct the node lazily.
		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
116

keks's avatar
keks committed
117 118 119 120
				r, err := fsrepo.Open(repoPath)
				if err != nil { // repo is owned by the node
					return nil, err
				}
Jan Winkelmann's avatar
Jan Winkelmann committed
121

keks's avatar
keks committed
122 123 124 125 126 127 128 129
				// ok everything is good. set it on the invocation (for ownership)
				// and return it.
				n, err = core.NewNode(ctx, &core.BuildCfg{
					Repo: r,
				})
				if err != nil {
					return nil, err
				}
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
130

keks's avatar
keks committed
131 132 133 134 135 136 137 138
				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 {
139
		return 1
Brian Tiger Chow's avatar
Brian Tiger Chow committed
140 141
	}

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
142
	// everything went better than expected :)
143
	return 0
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
144 145
}

keks's avatar
keks committed
146
func checkDebug(req *cmds.Request) {
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
147
	// check if user wants to debug. option OR env var.
keks's avatar
keks committed
148
	debug, _ := req.Options["debug"].(bool)
149
	if debug || os.Getenv("IPFS_LOGGING") == "debug" {
Brian Tiger Chow's avatar
Brian Tiger Chow committed
150
		u.Debug = true
Jeromy's avatar
Jeromy committed
151
		logging.SetDebugLogging()
152
	}
153 154 155
	if u.GetenvBool("DEBUG") {
		u.Debug = true
	}
156 157
}

keks's avatar
keks committed
158
func makeExecutor(req *cmds.Request, env interface{}) (cmds.Executor, error) {
159
	details := commandDetails(req.Path)
Łukasz Magiera's avatar
Łukasz Magiera committed
160
	client, err := commandShouldRunOnDaemon(*details, req, env.(*oldcmds.Context))
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
161
	if err != nil {
keks's avatar
keks committed
162
		return nil, err
Jan Winkelmann's avatar
Jan Winkelmann committed
163 164
	}

keks's avatar
keks committed
165 166 167
	var exctr cmds.Executor
	if client != nil && !req.Command.External {
		exctr = client.(cmds.Executor)
168
	} else {
169 170 171 172 173 174 175 176 177 178
		cctx := env.(*oldcmds.Context)
		pluginpath := filepath.Join(cctx.ConfigRoot, "plugins")

		// check if repo is accessible before loading plugins
		ok, err := checkPermissions(cctx.ConfigRoot)
		if err != nil {
			return nil, err
		}
		if ok {
			if _, err := loader.LoadPlugins(pluginpath); err != nil {
179
				log.Error("error loading plugins: ", err)
180
			}
181 182
		}

keks's avatar
keks committed
183
		exctr = cmds.NewExecutor(req.Root)
184 185
	}

keks's avatar
keks committed
186
	return exctr, nil
187 188
}

189 190 191 192 193 194 195 196 197 198 199 200 201 202
func checkPermissions(path string) (bool, error) {
	_, err := os.Open(path)
	if os.IsNotExist(err) {
		// repo does not exist yet - don't load plugins, but also don't fail
		return false, nil
	}
	if os.IsPermission(err) {
		// repo is not accessible. error out.
		return false, fmt.Errorf("error opening repository at %s: permission denied", path)
	}

	return true, nil
}

203 204
// commandDetails returns a command's details for the command given by |path|.
func commandDetails(path []string) *cmdDetails {
205 206
	var details cmdDetails
	// find the last command in path that has a cmdDetailsMap entry
207 208
	for i := range path {
		if cmdDetails, found := cmdDetailsMap[strings.Join(path[:i+1], "/")]; found {
209 210
			details = cmdDetails
		}
211
	}
212
	return &details
213 214
}

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

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

232
	if details.doesNotUseRepo && details.canRunOnClient() {
233
		return nil, nil
234 235
	}

236
	// at this point need to know whether api is running. we defer
Łukasz Magiera's avatar
Łukasz Magiera committed
237
	// to this point so that we don't check unnecessarily
238 239

	// did user specify an api to use for this command?
Spartucus's avatar
Spartucus committed
240
	apiAddrStr, _ := req.Options[corecmds.ApiOption].(string)
241

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

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

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

265
		return client, nil
266 267 268
	}

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

272
	return nil, nil
273 274
}

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

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

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

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

	stopProfiling := func() {
		pprof.StopCPUProfile()
312
		ofi.Close() // captured by the closure
313 314 315 316
	}
	return stopProfiling, nil
}

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

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

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

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

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

			ih.wg.Add(1)
			go func() {
				defer ih.wg.Done()
376
				cancelFunc()
377
			}()
378

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

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

387
	return intrh, ctx
388
}
389 390 391 392

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

403 404 405 406
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.
`
407 408
var checkIPFSUnixFmt = "Otherwise check:\n\tps aux | grep ipfs"
var checkIPFSWinFmt = "Otherwise check:\n\ttasklist | findstr ipfs"
409

410
// getAPIClient checks the repo, and the given options, checking for
411 412
// a running API service. if there is one, it returns a client.
// otherwise, it returns errApiNotRunning, or another error.
413
func getAPIClient(ctx context.Context, repoPath, apiAddrStr string) (http.Client, error) {
414 415 416 417 418 419 420 421 422
	var apiErrorFmt string
	switch {
	case osh.IsUnix():
		apiErrorFmt = apiFileErrorFmt + checkIPFSUnixFmt
	case osh.IsWindows():
		apiErrorFmt = apiFileErrorFmt + checkIPFSWinFmt
	default:
		apiErrorFmt = apiFileErrorFmt
	}
423

424 425 426 427 428 429 430 431
	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 {
Lars Gierth's avatar
Lars Gierth committed
432
			return nil, fmt.Errorf("multiaddr doesn't provide any protocols")
433 434 435 436
		}
	} else {
		addr, err = fsrepo.APIAddr(repoPath)
		if err == repo.ErrApiNotRunning {
437 438 439
			return nil, err
		}

440 441 442 443 444 445
		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")
446
	}
447
	return apiClientForAddr(ctx, addr)
448 449
}

450
func apiClientForAddr(ctx context.Context, addr ma.Multiaddr) (http.Client, error) {
451
	addr, err := resolveAddr(ctx, addr)
452 453 454 455
	if err != nil {
		return nil, err
	}

456 457 458 459
	_, host, err := manet.DialArgs(addr)
	if err != nil {
		return nil, err
	}
460

461 462
	return http.NewClient(host, http.ClientWithAPIPrefix(corehttp.APIPath)), nil
}
463

464 465 466 467 468 469 470 471
func resolveAddr(ctx context.Context, addr ma.Multiaddr) (ma.Multiaddr, error) {
	ctx, cancelFunc := context.WithTimeout(ctx, 10*time.Second)
	defer cancelFunc()

	addrs, err := dnsResolver.Resolve(ctx, addr)
	if err != nil {
		return nil, err
	}
472

473 474
	if len(addrs) == 0 {
		return nil, errors.New("non-resolvable API endpoint")
475 476
	}

477
	return addrs[0], nil
478
}