feat: cmd/ipfs: Make it possible to depend on cmd/ipfs
Łukasz Magiera committed
Mar 30, 2023 at 19:25 UTC
b5dddf67a23d597c63863e3401b31b0f8f31a047
13 files changed
+511
-497
cmd/ipfs/kubo/add_migrations.go
renamed
+1
-1
@@ -1,4 +1,4 @@
1
-package main
1
+package kubo
2
3
import (
4
"context"
cmd/ipfs/kubo/daemon.go
renamed
+1
-1
@@ -1,4 +1,4 @@
1
-package main
1
+package kubo
2
3
import (
4
"errors"
cmd/ipfs/kubo/daemon_linux.go
renamed
+1
-1
@@ -1,7 +1,7 @@
1
//go:build linux
2
// +build linux
3
4
-package main
4
+package kubo
5
6
import (
7
daemon "github.com/coreos/go-systemd/v22/daemon"
cmd/ipfs/kubo/daemon_other.go
renamed
+1
-1
@@ -1,7 +1,7 @@
1
//go:build !linux
2
// +build !linux
3
4
-package main
4
+package kubo
5
6
func notifyReady() {}
7
cmd/ipfs/kubo/debug.go
renamed
+1
-1
@@ -1,4 +1,4 @@
1
-package main
1
+package kubo
2
3
import (
4
"net/http"
cmd/ipfs/kubo/dnsresolve_test.go
renamed
+1
-1
@@ -1,4 +1,4 @@
1
-package main
1
+package kubo
2
3
import (
4
"context"
cmd/ipfs/kubo/init.go
renamed
+1
-1
@@ -1,4 +1,4 @@
1
-package main
1
+package kubo
2
3
import (
4
"context"
cmd/ipfs/kubo/ipfs.go
renamed
+1
-1
@@ -1,4 +1,4 @@
1
-package main
1
+package kubo
2
3
import (
4
commands "github.com/ipfs/kubo/core/commands"
cmd/ipfs/kubo/pinmfs.go
renamed
+1
-1
@@ -1,4 +1,4 @@
1
-package main
1
+package kubo
2
3
import (
4
"context"
cmd/ipfs/kubo/pinmfs_test.go
renamed
+1
-1
@@ -1,4 +1,4 @@
1
-package main
1
+package kubo
2
3
import (
4
"context"
cmd/ipfs/kubo/start.go
new
+494
@@ -0,0 +1,494 @@
1
+// cmd/ipfs implements the primary CLI binary for ipfs
2
+package kubo
3
+
4
+import (
5
+ "bytes"
6
+ "context"
7
+ "encoding/json"
8
+ "errors"
9
+ "fmt"
10
+ "io"
11
+ "net"
12
+ "net/http"
13
+ "os"
14
+ "runtime/pprof"
15
+ "strings"
16
+ "time"
17
+
18
+ "github.com/blang/semver/v4"
19
+ "github.com/google/uuid"
20
+ u "github.com/ipfs/boxo/util"
21
+ cmds "github.com/ipfs/go-ipfs-cmds"
22
+ "github.com/ipfs/go-ipfs-cmds/cli"
23
+ cmdhttp "github.com/ipfs/go-ipfs-cmds/http"
24
+ logging "github.com/ipfs/go-log"
25
+ ipfs "github.com/ipfs/kubo"
26
+ "github.com/ipfs/kubo/client/rpc/auth"
27
+ "github.com/ipfs/kubo/cmd/ipfs/util"
28
+ oldcmds "github.com/ipfs/kubo/commands"
29
+ config "github.com/ipfs/kubo/config"
30
+ "github.com/ipfs/kubo/core"
31
+ corecmds "github.com/ipfs/kubo/core/commands"
32
+ "github.com/ipfs/kubo/core/corehttp"
33
+ "github.com/ipfs/kubo/plugin/loader"
34
+ "github.com/ipfs/kubo/repo"
35
+ "github.com/ipfs/kubo/repo/fsrepo"
36
+ "github.com/ipfs/kubo/tracing"
37
+ ma "github.com/multiformats/go-multiaddr"
38
+ madns "github.com/multiformats/go-multiaddr-dns"
39
+ manet "github.com/multiformats/go-multiaddr/net"
40
+ "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
41
+ "go.opentelemetry.io/contrib/propagators/autoprop"
42
+ "go.opentelemetry.io/otel"
43
+ "go.opentelemetry.io/otel/attribute"
44
+ "go.opentelemetry.io/otel/codes"
45
+ "go.opentelemetry.io/otel/trace"
46
+)
47
+
48
+// log is the command logger.
49
+var (
50
+ log = logging.Logger("cmd/ipfs")
51
+ tracer trace.Tracer
52
+)
53
+
54
+// declared as a var for testing purposes.
55
+var dnsResolver = madns.DefaultResolver
56
+
57
+const (
58
+ EnvEnableProfiling = "IPFS_PROF"
59
+ cpuProfile = "ipfs.cpuprof"
60
+ heapProfile = "ipfs.memprof"
61
+)
62
+
63
+type PluginPreloader func(*loader.PluginLoader) error
64
+
65
+func LoadPlugins(repoPath string, preload PluginPreloader) (*loader.PluginLoader, error) {
66
+ plugins, err := loader.NewPluginLoader(repoPath)
67
+ if err != nil {
68
+ return nil, fmt.Errorf("error loading plugins: %s", err)
69
+ }
70
+
71
+ if preload != nil {
72
+ if err := preload(plugins); err != nil {
73
+ return nil, fmt.Errorf("error loading plugins (preload): %s", err)
74
+ }
75
+ }
76
+
77
+ if err := plugins.Initialize(); err != nil {
78
+ return nil, fmt.Errorf("error initializing plugins: %s", err)
79
+ }
80
+
81
+ if err := plugins.Inject(); err != nil {
82
+ return nil, fmt.Errorf("error initializing plugins: %s", err)
83
+ }
84
+ return plugins, nil
85
+}
86
+
87
+// main roadmap:
88
+// - parse the commandline to get a cmdInvocation
89
+// - if user requests help, print it and exit.
90
+// - run the command invocation
91
+// - output the response
92
+// - if anything fails, print error, maybe with help.
93
+func main() {
94
+ os.Exit(Start(BuildDefaultEnv))
95
+}
96
+
97
+func printErr(err error) int {
98
+ fmt.Fprintf(os.Stderr, "Error: %s\n", err.Error())
99
+ return 1
100
+}
101
+
102
+func newUUID(key string) logging.Metadata {
103
+ ids := "#UUID-ERROR#"
104
+ if id, err := uuid.NewRandom(); err == nil {
105
+ ids = id.String()
106
+ }
107
+ return logging.Metadata{
108
+ key: ids,
109
+ }
110
+}
111
+
112
+func BuildDefaultEnv(ctx context.Context, req *cmds.Request) (cmds.Environment, error) {
113
+ return BuildEnv(ctx, req, nil)
114
+}
115
+
116
+func BuildEnv(ctx context.Context, req *cmds.Request, pl PluginPreloader) (cmds.Environment, error) {
117
+ checkDebug(req)
118
+ repoPath, err := GetRepoPath(req)
119
+ if err != nil {
120
+ return nil, err
121
+ }
122
+ log.Debugf("config path is %s", repoPath)
123
+
124
+ plugins, err := LoadPlugins(repoPath, pl)
125
+ if err != nil {
126
+ return nil, err
127
+ }
128
+
129
+ // this sets up the function that will initialize the node
130
+ // this is so that we can construct the node lazily.
131
+ return &oldcmds.Context{
132
+ ConfigRoot: repoPath,
133
+ ReqLog: &oldcmds.ReqLog{},
134
+ Plugins: plugins,
135
+ ConstructNode: func() (n *core.IpfsNode, err error) {
136
+ if req == nil {
137
+ return nil, errors.New("constructing node without a request")
138
+ }
139
+
140
+ r, err := fsrepo.Open(repoPath)
141
+ if err != nil { // repo is owned by the node
142
+ return nil, err
143
+ }
144
+
145
+ // ok everything is good. set it on the invocation (for ownership)
146
+ // and return it.
147
+ n, err = core.NewNode(ctx, &core.BuildCfg{
148
+ Repo: r,
149
+ })
150
+ if err != nil {
151
+ return nil, err
152
+ }
153
+
154
+ return n, nil
155
+ },
156
+ }, nil
157
+}
158
+
159
+func Start(buildEnv func(ctx context.Context, req *cmds.Request) (cmds.Environment, error)) (exitCode int) {
160
+ ctx := logging.ContextWithLoggable(context.Background(), newUUID("session"))
161
+
162
+ tp, err := tracing.NewTracerProvider(ctx)
163
+ if err != nil {
164
+ return printErr(err)
165
+ }
166
+ defer func() {
167
+ if err := tp.Shutdown(ctx); err != nil {
168
+ exitCode = printErr(err)
169
+ }
170
+ }()
171
+ otel.SetTracerProvider(tp)
172
+ otel.SetTextMapPropagator(autoprop.NewTextMapPropagator())
173
+ tracer = tp.Tracer("Kubo-cli")
174
+
175
+ stopFunc, err := profileIfEnabled()
176
+ if err != nil {
177
+ return printErr(err)
178
+ }
179
+ defer stopFunc() // to be executed as late as possible
180
+
181
+ intrh, ctx := util.SetupInterruptHandler(ctx)
182
+ defer intrh.Close()
183
+
184
+ // Handle `ipfs version` or `ipfs help`
185
+ if len(os.Args) > 1 {
186
+ // Handle `ipfs --version'
187
+ if os.Args[1] == "--version" {
188
+ os.Args[1] = "version"
189
+ }
190
+
191
+ // Handle `ipfs help` and `ipfs help <sub-command>`
192
+ if os.Args[1] == "help" {
193
+ if len(os.Args) > 2 {
194
+ os.Args = append(os.Args[:1], os.Args[2:]...)
195
+ // Handle `ipfs help --help`
196
+ // append `--help`,when the command is not `ipfs help --help`
197
+ if os.Args[1] != "--help" {
198
+ os.Args = append(os.Args, "--help")
199
+ }
200
+ } else {
201
+ os.Args[1] = "--help"
202
+ }
203
+ }
204
+ } else if insideGUI() { // if no args were passed, and we're in a GUI environment
205
+ // launch the daemon instead of launching a ghost window
206
+ os.Args = append(os.Args, "daemon", "--init")
207
+ }
208
+
209
+ // output depends on executable name passed in os.Args
210
+ // so we need to make sure it's stable
211
+ os.Args[0] = "ipfs"
212
+
213
+ err = cli.Run(ctx, Root, os.Args, os.Stdin, os.Stdout, os.Stderr, buildEnv, makeExecutor)
214
+ if err != nil {
215
+ return 1
216
+ }
217
+
218
+ // everything went better than expected :)
219
+ return 0
220
+}
221
+
222
+func insideGUI() bool {
223
+ return util.InsideGUI()
224
+}
225
+
226
+func checkDebug(req *cmds.Request) {
227
+ // check if user wants to debug. option OR env var.
228
+ debug, _ := req.Options["debug"].(bool)
229
+ if debug || os.Getenv("IPFS_LOGGING") == "debug" {
230
+ u.Debug = true
231
+ logging.SetDebugLogging()
232
+ }
233
+ if u.GetenvBool("DEBUG") {
234
+ u.Debug = true
235
+ }
236
+}
237
+
238
+func apiAddrOption(req *cmds.Request) (ma.Multiaddr, error) {
239
+ apiAddrStr, apiSpecified := req.Options[corecmds.ApiOption].(string)
240
+ if !apiSpecified {
241
+ return nil, nil
242
+ }
243
+ return ma.NewMultiaddr(apiAddrStr)
244
+}
245
+
246
+// encodedAbsolutePathVersion is the version from which the absolute path header in
247
+// multipart requests is %-encoded. Before this version, its sent raw.
248
+var encodedAbsolutePathVersion = semver.MustParse("0.23.0-dev")
249
+
250
+func makeExecutor(req *cmds.Request, env interface{}) (cmds.Executor, error) {
251
+ exe := tracingWrappedExecutor{cmds.NewExecutor(req.Root)}
252
+ cctx := env.(*oldcmds.Context)
253
+
254
+ // Check if the command is disabled.
255
+ if req.Command.NoLocal && req.Command.NoRemote {
256
+ return nil, fmt.Errorf("command disabled: %v", req.Path)
257
+ }
258
+
259
+ // Can we just run this locally?
260
+ if !req.Command.NoLocal {
261
+ if doesNotUseRepo, ok := corecmds.GetDoesNotUseRepo(req.Command.Extra); doesNotUseRepo && ok {
262
+ return exe, nil
263
+ }
264
+ }
265
+
266
+ // Get the API option from the commandline.
267
+ apiAddr, err := apiAddrOption(req)
268
+ if err != nil {
269
+ return nil, err
270
+ }
271
+
272
+ // Require that the command be run on the daemon when the API flag is
273
+ // passed (unless we're trying to _run_ the daemon).
274
+ daemonRequested := apiAddr != nil && req.Command != daemonCmd
275
+
276
+ // Run this on the client if required.
277
+ if req.Command.NoRemote {
278
+ if daemonRequested {
279
+ // User requested that the command be run on the daemon but we can't.
280
+ // NOTE: We drop this check for the `ipfs daemon` command.
281
+ return nil, errors.New("api flag specified but command cannot be run on the daemon")
282
+ }
283
+ return exe, nil
284
+ }
285
+
286
+ // Finally, look in the repo for an API file.
287
+ if apiAddr == nil {
288
+ var err error
289
+ apiAddr, err = fsrepo.APIAddr(cctx.ConfigRoot)
290
+ switch err {
291
+ case nil, repo.ErrApiNotRunning:
292
+ default:
293
+ return nil, err
294
+ }
295
+ }
296
+
297
+ // Still no api specified? Run it on the client or fail.
298
+ if apiAddr == nil {
299
+ if req.Command.NoLocal {
300
+ return nil, fmt.Errorf("command must be run on the daemon: %v", req.Path)
301
+ }
302
+ return exe, nil
303
+ }
304
+
305
+ // Resolve the API addr.
306
+ apiAddr, err = resolveAddr(req.Context, apiAddr)
307
+ if err != nil {
308
+ return nil, err
309
+ }
310
+ network, host, err := manet.DialArgs(apiAddr)
311
+ if err != nil {
312
+ return nil, err
313
+ }
314
+
315
+ // Construct the executor.
316
+ opts := []cmdhttp.ClientOpt{
317
+ cmdhttp.ClientWithAPIPrefix(corehttp.APIPath),
318
+ }
319
+
320
+ // Fallback on a local executor if we (a) have a repo and (b) aren't
321
+ // forcing a daemon.
322
+ if !daemonRequested && fsrepo.IsInitialized(cctx.ConfigRoot) {
323
+ opts = append(opts, cmdhttp.ClientWithFallback(exe))
324
+ }
325
+
326
+ var tpt http.RoundTripper
327
+ switch network {
328
+ case "tcp", "tcp4", "tcp6":
329
+ tpt = http.DefaultTransport
330
+ case "unix":
331
+ path := host
332
+ host = "unix"
333
+ tpt = &http.Transport{
334
+ DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
335
+ return net.Dial("unix", path)
336
+ },
337
+ }
338
+ default:
339
+ return nil, fmt.Errorf("unsupported API address: %s", apiAddr)
340
+ }
341
+
342
+ apiAuth, specified := req.Options[corecmds.ApiAuthOption].(string)
343
+ if specified {
344
+ authorization := config.ConvertAuthSecret(apiAuth)
345
+ tpt = auth.NewAuthorizedRoundTripper(authorization, tpt)
346
+ }
347
+
348
+ httpClient := &http.Client{
349
+ Transport: otelhttp.NewTransport(tpt),
350
+ }
351
+ opts = append(opts, cmdhttp.ClientWithHTTPClient(httpClient))
352
+
353
+ // Fetch remove version, as some feature compatibility might change depending on it.
354
+ remoteVersion, err := getRemoteVersion(tracingWrappedExecutor{cmdhttp.NewClient(host, opts...)})
355
+ if err != nil {
356
+ return nil, err
357
+ }
358
+ opts = append(opts, cmdhttp.ClientWithRawAbsPath(remoteVersion.LT(encodedAbsolutePathVersion)))
359
+
360
+ return tracingWrappedExecutor{cmdhttp.NewClient(host, opts...)}, nil
361
+}
362
+
363
+type tracingWrappedExecutor struct {
364
+ exec cmds.Executor
365
+}
366
+
367
+func (twe tracingWrappedExecutor) Execute(req *cmds.Request, re cmds.ResponseEmitter, env cmds.Environment) error {
368
+ ctx, span := tracer.Start(req.Context, "cmds."+strings.Join(req.Path, "."), trace.WithAttributes(attribute.StringSlice("Arguments", req.Arguments)))
369
+ defer span.End()
370
+ req.Context = ctx
371
+
372
+ err := twe.exec.Execute(req, re, env)
373
+ if err != nil {
374
+ span.SetStatus(codes.Error, err.Error())
375
+ }
376
+ return err
377
+}
378
+
379
+func GetRepoPath(req *cmds.Request) (string, error) {
380
+ repoOpt, found := req.Options[corecmds.RepoDirOption].(string)
381
+ if found && repoOpt != "" {
382
+ return repoOpt, nil
383
+ }
384
+
385
+ repoPath, err := fsrepo.BestKnownPath()
386
+ if err != nil {
387
+ return "", err
388
+ }
389
+ return repoPath, nil
390
+}
391
+
392
+// startProfiling begins CPU profiling and returns a `stop` function to be
393
+// executed as late as possible. The stop function captures the memprofile.
394
+func startProfiling() (func(), error) {
395
+ // start CPU profiling as early as possible
396
+ ofi, err := os.Create(cpuProfile)
397
+ if err != nil {
398
+ return nil, err
399
+ }
400
+ err = pprof.StartCPUProfile(ofi)
401
+ if err != nil {
402
+ ofi.Close()
403
+ return nil, err
404
+ }
405
+ go func() {
406
+ for range time.NewTicker(time.Second * 30).C {
407
+ err := writeHeapProfileToFile()
408
+ if err != nil {
409
+ log.Error(err)
410
+ }
411
+ }
412
+ }()
413
+
414
+ stopProfiling := func() {
415
+ pprof.StopCPUProfile()
416
+ ofi.Close() // captured by the closure
417
+ }
418
+ return stopProfiling, nil
419
+}
420
+
421
+func writeHeapProfileToFile() error {
422
+ mprof, err := os.Create(heapProfile)
423
+ if err != nil {
424
+ return err
425
+ }
426
+ defer mprof.Close() // _after_ writing the heap profile
427
+ return pprof.WriteHeapProfile(mprof)
428
+}
429
+
430
+func profileIfEnabled() (func(), error) {
431
+ // FIXME this is a temporary hack so profiling of asynchronous operations
432
+ // works as intended.
433
+ if os.Getenv(EnvEnableProfiling) != "" {
434
+ stopProfilingFunc, err := startProfiling() // TODO maybe change this to its own option... profiling makes it slower.
435
+ if err != nil {
436
+ return nil, err
437
+ }
438
+ return stopProfilingFunc, nil
439
+ }
440
+ return func() {}, nil
441
+}
442
+
443
+func resolveAddr(ctx context.Context, addr ma.Multiaddr) (ma.Multiaddr, error) {
444
+ ctx, cancelFunc := context.WithTimeout(ctx, 10*time.Second)
445
+ defer cancelFunc()
446
+
447
+ addrs, err := dnsResolver.Resolve(ctx, addr)
448
+ if err != nil {
449
+ return nil, err
450
+ }
451
+
452
+ if len(addrs) == 0 {
453
+ return nil, errors.New("non-resolvable API endpoint")
454
+ }
455
+
456
+ return addrs[0], nil
457
+}
458
+
459
+type nopWriter struct {
460
+ io.Writer
461
+}
462
+
463
+func (nw nopWriter) Close() error {
464
+ return nil
465
+}
466
+
467
+func getRemoteVersion(exe cmds.Executor) (*semver.Version, error) {
468
+ ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Second*30))
469
+ defer cancel()
470
+
471
+ req, err := cmds.NewRequest(ctx, []string{"version"}, nil, nil, nil, Root)
472
+ if err != nil {
473
+ return nil, err
474
+ }
475
+
476
+ var buf bytes.Buffer
477
+ re, err := cmds.NewWriterResponseEmitter(nopWriter{&buf}, req)
478
+ if err != nil {
479
+ return nil, err
480
+ }
481
+
482
+ err = exe.Execute(req, re, nil)
483
+ if err != nil {
484
+ return nil, err
485
+ }
486
+
487
+ var out ipfs.VersionInfo
488
+ dec := json.NewDecoder(&buf)
489
+ if err := dec.Decode(&out); err != nil {
490
+ return nil, err
491
+ }
492
+
493
+ return semver.New(out.Version)
494
+}
cmd/ipfs/main.go
+2
-485
@@ -1,494 +1,11 @@
1
-// cmd/ipfs implements the primary CLI binary for ipfs
1
package main
2
3
import (
5
- "bytes"
6
- "context"
7
- "encoding/json"
8
- "errors"
9
- "fmt"
10
- "io"
11
- "net"
12
- "net/http"
4
"os"
14
- "runtime/pprof"
15
- "strings"
16
- "time"
5
18
- "github.com/blang/semver/v4"
19
- "github.com/google/uuid"
20
- u "github.com/ipfs/boxo/util"
21
- cmds "github.com/ipfs/go-ipfs-cmds"
22
- "github.com/ipfs/go-ipfs-cmds/cli"
23
- cmdhttp "github.com/ipfs/go-ipfs-cmds/http"
24
- logging "github.com/ipfs/go-log"
25
- ipfs "github.com/ipfs/kubo"
26
- "github.com/ipfs/kubo/client/rpc/auth"
27
- "github.com/ipfs/kubo/cmd/ipfs/util"
28
- oldcmds "github.com/ipfs/kubo/commands"
29
- config "github.com/ipfs/kubo/config"
30
- "github.com/ipfs/kubo/core"
31
- corecmds "github.com/ipfs/kubo/core/commands"
32
- "github.com/ipfs/kubo/core/corehttp"
33
- "github.com/ipfs/kubo/plugin/loader"
34
- "github.com/ipfs/kubo/repo"
35
- "github.com/ipfs/kubo/repo/fsrepo"
36
- "github.com/ipfs/kubo/tracing"
37
- ma "github.com/multiformats/go-multiaddr"
38
- madns "github.com/multiformats/go-multiaddr-dns"
39
- manet "github.com/multiformats/go-multiaddr/net"
40
- "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
41
- "go.opentelemetry.io/contrib/propagators/autoprop"
42
- "go.opentelemetry.io/otel"
43
- "go.opentelemetry.io/otel/attribute"
44
- "go.opentelemetry.io/otel/codes"
45
- "go.opentelemetry.io/otel/trace"
6
+ "github.com/ipfs/kubo/cmd/ipfs/kubo"
7
)
8
48
-// log is the command logger.
49
-var (
50
- log = logging.Logger("cmd/ipfs")
51
- tracer trace.Tracer
52
-)
53
-
54
-// declared as a var for testing purposes.
55
-var dnsResolver = madns.DefaultResolver
56
-
57
-const (
58
- EnvEnableProfiling = "IPFS_PROF"
59
- cpuProfile = "ipfs.cpuprof"
60
- heapProfile = "ipfs.memprof"
61
-)
62
-
63
-type PluginPreloader func(*loader.PluginLoader) error
64
-
65
-func LoadPlugins(repoPath string, preload PluginPreloader) (*loader.PluginLoader, error) {
66
- plugins, err := loader.NewPluginLoader(repoPath)
67
- if err != nil {
68
- return nil, fmt.Errorf("error loading plugins: %s", err)
69
- }
70
-
71
- if preload != nil {
72
- if err := preload(plugins); err != nil {
73
- return nil, fmt.Errorf("error loading plugins (preload): %s", err)
74
- }
75
- }
76
-
77
- if err := plugins.Initialize(); err != nil {
78
- return nil, fmt.Errorf("error initializing plugins: %s", err)
79
- }
80
-
81
- if err := plugins.Inject(); err != nil {
82
- return nil, fmt.Errorf("error initializing plugins: %s", err)
83
- }
84
- return plugins, nil
85
-}
86
-
87
-// main roadmap:
88
-// - parse the commandline to get a cmdInvocation
89
-// - if user requests help, print it and exit.
90
-// - run the command invocation
91
-// - output the response
92
-// - if anything fails, print error, maybe with help.
9
func main() {
94
- os.Exit(Start(BuildDefaultEnv))
95
-}
96
-
97
-func printErr(err error) int {
98
- fmt.Fprintf(os.Stderr, "Error: %s\n", err.Error())
99
- return 1
100
-}
101
-
102
-func newUUID(key string) logging.Metadata {
103
- ids := "#UUID-ERROR#"
104
- if id, err := uuid.NewRandom(); err == nil {
105
- ids = id.String()
106
- }
107
- return logging.Metadata{
108
- key: ids,
109
- }
110
-}
111
-
112
-func BuildDefaultEnv(ctx context.Context, req *cmds.Request) (cmds.Environment, error) {
113
- return BuildEnv(ctx, req, nil)
114
-}
115
-
116
-func BuildEnv(ctx context.Context, req *cmds.Request, pl PluginPreloader) (cmds.Environment, error) {
117
- checkDebug(req)
118
- repoPath, err := GetRepoPath(req)
119
- if err != nil {
120
- return nil, err
121
- }
122
- log.Debugf("config path is %s", repoPath)
123
-
124
- plugins, err := LoadPlugins(repoPath, pl)
125
- if err != nil {
126
- return nil, err
127
- }
128
-
129
- // this sets up the function that will initialize the node
130
- // this is so that we can construct the node lazily.
131
- return &oldcmds.Context{
132
- ConfigRoot: repoPath,
133
- ReqLog: &oldcmds.ReqLog{},
134
- Plugins: plugins,
135
- ConstructNode: func() (n *core.IpfsNode, err error) {
136
- if req == nil {
137
- return nil, errors.New("constructing node without a request")
138
- }
139
-
140
- r, err := fsrepo.Open(repoPath)
141
- if err != nil { // repo is owned by the node
142
- return nil, err
143
- }
144
-
145
- // ok everything is good. set it on the invocation (for ownership)
146
- // and return it.
147
- n, err = core.NewNode(ctx, &core.BuildCfg{
148
- Repo: r,
149
- })
150
- if err != nil {
151
- return nil, err
152
- }
153
-
154
- return n, nil
155
- },
156
- }, nil
157
-}
158
-
159
-func Start(buildEnv func(ctx context.Context, req *cmds.Request) (cmds.Environment, error)) (exitCode int) {
160
- ctx := logging.ContextWithLoggable(context.Background(), newUUID("session"))
161
-
162
- tp, err := tracing.NewTracerProvider(ctx)
163
- if err != nil {
164
- return printErr(err)
165
- }
166
- defer func() {
167
- if err := tp.Shutdown(ctx); err != nil {
168
- exitCode = printErr(err)
169
- }
170
- }()
171
- otel.SetTracerProvider(tp)
172
- otel.SetTextMapPropagator(autoprop.NewTextMapPropagator())
173
- tracer = tp.Tracer("Kubo-cli")
174
-
175
- stopFunc, err := profileIfEnabled()
176
- if err != nil {
177
- return printErr(err)
178
- }
179
- defer stopFunc() // to be executed as late as possible
180
-
181
- intrh, ctx := util.SetupInterruptHandler(ctx)
182
- defer intrh.Close()
183
-
184
- // Handle `ipfs version` or `ipfs help`
185
- if len(os.Args) > 1 {
186
- // Handle `ipfs --version'
187
- if os.Args[1] == "--version" {
188
- os.Args[1] = "version"
189
- }
190
-
191
- // Handle `ipfs help` and `ipfs help <sub-command>`
192
- if os.Args[1] == "help" {
193
- if len(os.Args) > 2 {
194
- os.Args = append(os.Args[:1], os.Args[2:]...)
195
- // Handle `ipfs help --help`
196
- // append `--help`,when the command is not `ipfs help --help`
197
- if os.Args[1] != "--help" {
198
- os.Args = append(os.Args, "--help")
199
- }
200
- } else {
201
- os.Args[1] = "--help"
202
- }
203
- }
204
- } else if insideGUI() { // if no args were passed, and we're in a GUI environment
205
- // launch the daemon instead of launching a ghost window
206
- os.Args = append(os.Args, "daemon", "--init")
207
- }
208
-
209
- // output depends on executable name passed in os.Args
210
- // so we need to make sure it's stable
211
- os.Args[0] = "ipfs"
212
-
213
- err = cli.Run(ctx, Root, os.Args, os.Stdin, os.Stdout, os.Stderr, buildEnv, makeExecutor)
214
- if err != nil {
215
- return 1
216
- }
217
-
218
- // everything went better than expected :)
219
- return 0
220
-}
221
-
222
-func insideGUI() bool {
223
- return util.InsideGUI()
224
-}
225
-
226
-func checkDebug(req *cmds.Request) {
227
- // check if user wants to debug. option OR env var.
228
- debug, _ := req.Options["debug"].(bool)
229
- if debug || os.Getenv("IPFS_LOGGING") == "debug" {
230
- u.Debug = true
231
- logging.SetDebugLogging()
232
- }
233
- if u.GetenvBool("DEBUG") {
234
- u.Debug = true
235
- }
236
-}
237
-
238
-func apiAddrOption(req *cmds.Request) (ma.Multiaddr, error) {
239
- apiAddrStr, apiSpecified := req.Options[corecmds.ApiOption].(string)
240
- if !apiSpecified {
241
- return nil, nil
242
- }
243
- return ma.NewMultiaddr(apiAddrStr)
244
-}
245
-
246
-// encodedAbsolutePathVersion is the version from which the absolute path header in
247
-// multipart requests is %-encoded. Before this version, its sent raw.
248
-var encodedAbsolutePathVersion = semver.MustParse("0.23.0-dev")
249
-
250
-func makeExecutor(req *cmds.Request, env interface{}) (cmds.Executor, error) {
251
- exe := tracingWrappedExecutor{cmds.NewExecutor(req.Root)}
252
- cctx := env.(*oldcmds.Context)
253
-
254
- // Check if the command is disabled.
255
- if req.Command.NoLocal && req.Command.NoRemote {
256
- return nil, fmt.Errorf("command disabled: %v", req.Path)
257
- }
258
-
259
- // Can we just run this locally?
260
- if !req.Command.NoLocal {
261
- if doesNotUseRepo, ok := corecmds.GetDoesNotUseRepo(req.Command.Extra); doesNotUseRepo && ok {
262
- return exe, nil
263
- }
264
- }
265
-
266
- // Get the API option from the commandline.
267
- apiAddr, err := apiAddrOption(req)
268
- if err != nil {
269
- return nil, err
270
- }
271
-
272
- // Require that the command be run on the daemon when the API flag is
273
- // passed (unless we're trying to _run_ the daemon).
274
- daemonRequested := apiAddr != nil && req.Command != daemonCmd
275
-
276
- // Run this on the client if required.
277
- if req.Command.NoRemote {
278
- if daemonRequested {
279
- // User requested that the command be run on the daemon but we can't.
280
- // NOTE: We drop this check for the `ipfs daemon` command.
281
- return nil, errors.New("api flag specified but command cannot be run on the daemon")
282
- }
283
- return exe, nil
284
- }
285
-
286
- // Finally, look in the repo for an API file.
287
- if apiAddr == nil {
288
- var err error
289
- apiAddr, err = fsrepo.APIAddr(cctx.ConfigRoot)
290
- switch err {
291
- case nil, repo.ErrApiNotRunning:
292
- default:
293
- return nil, err
294
- }
295
- }
296
-
297
- // Still no api specified? Run it on the client or fail.
298
- if apiAddr == nil {
299
- if req.Command.NoLocal {
300
- return nil, fmt.Errorf("command must be run on the daemon: %v", req.Path)
301
- }
302
- return exe, nil
303
- }
304
-
305
- // Resolve the API addr.
306
- apiAddr, err = resolveAddr(req.Context, apiAddr)
307
- if err != nil {
308
- return nil, err
309
- }
310
- network, host, err := manet.DialArgs(apiAddr)
311
- if err != nil {
312
- return nil, err
313
- }
314
-
315
- // Construct the executor.
316
- opts := []cmdhttp.ClientOpt{
317
- cmdhttp.ClientWithAPIPrefix(corehttp.APIPath),
318
- }
319
-
320
- // Fallback on a local executor if we (a) have a repo and (b) aren't
321
- // forcing a daemon.
322
- if !daemonRequested && fsrepo.IsInitialized(cctx.ConfigRoot) {
323
- opts = append(opts, cmdhttp.ClientWithFallback(exe))
324
- }
325
-
326
- var tpt http.RoundTripper
327
- switch network {
328
- case "tcp", "tcp4", "tcp6":
329
- tpt = http.DefaultTransport
330
- case "unix":
331
- path := host
332
- host = "unix"
333
- tpt = &http.Transport{
334
- DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
335
- return net.Dial("unix", path)
336
- },
337
- }
338
- default:
339
- return nil, fmt.Errorf("unsupported API address: %s", apiAddr)
340
- }
341
-
342
- apiAuth, specified := req.Options[corecmds.ApiAuthOption].(string)
343
- if specified {
344
- authorization := config.ConvertAuthSecret(apiAuth)
345
- tpt = auth.NewAuthorizedRoundTripper(authorization, tpt)
346
- }
347
-
348
- httpClient := &http.Client{
349
- Transport: otelhttp.NewTransport(tpt),
350
- }
351
- opts = append(opts, cmdhttp.ClientWithHTTPClient(httpClient))
352
-
353
- // Fetch remove version, as some feature compatibility might change depending on it.
354
- remoteVersion, err := getRemoteVersion(tracingWrappedExecutor{cmdhttp.NewClient(host, opts...)})
355
- if err != nil {
356
- return nil, err
357
- }
358
- opts = append(opts, cmdhttp.ClientWithRawAbsPath(remoteVersion.LT(encodedAbsolutePathVersion)))
359
-
360
- return tracingWrappedExecutor{cmdhttp.NewClient(host, opts...)}, nil
361
-}
362
-
363
-type tracingWrappedExecutor struct {
364
- exec cmds.Executor
365
-}
366
-
367
-func (twe tracingWrappedExecutor) Execute(req *cmds.Request, re cmds.ResponseEmitter, env cmds.Environment) error {
368
- ctx, span := tracer.Start(req.Context, "cmds."+strings.Join(req.Path, "."), trace.WithAttributes(attribute.StringSlice("Arguments", req.Arguments)))
369
- defer span.End()
370
- req.Context = ctx
371
-
372
- err := twe.exec.Execute(req, re, env)
373
- if err != nil {
374
- span.SetStatus(codes.Error, err.Error())
375
- }
376
- return err
377
-}
378
-
379
-func GetRepoPath(req *cmds.Request) (string, error) {
380
- repoOpt, found := req.Options[corecmds.RepoDirOption].(string)
381
- if found && repoOpt != "" {
382
- return repoOpt, nil
383
- }
384
-
385
- repoPath, err := fsrepo.BestKnownPath()
386
- if err != nil {
387
- return "", err
388
- }
389
- return repoPath, nil
390
-}
391
-
392
-// startProfiling begins CPU profiling and returns a `stop` function to be
393
-// executed as late as possible. The stop function captures the memprofile.
394
-func startProfiling() (func(), error) {
395
- // start CPU profiling as early as possible
396
- ofi, err := os.Create(cpuProfile)
397
- if err != nil {
398
- return nil, err
399
- }
400
- err = pprof.StartCPUProfile(ofi)
401
- if err != nil {
402
- ofi.Close()
403
- return nil, err
404
- }
405
- go func() {
406
- for range time.NewTicker(time.Second * 30).C {
407
- err := writeHeapProfileToFile()
408
- if err != nil {
409
- log.Error(err)
410
- }
411
- }
412
- }()
413
-
414
- stopProfiling := func() {
415
- pprof.StopCPUProfile()
416
- ofi.Close() // captured by the closure
417
- }
418
- return stopProfiling, nil
419
-}
420
-
421
-func writeHeapProfileToFile() error {
422
- mprof, err := os.Create(heapProfile)
423
- if err != nil {
424
- return err
425
- }
426
- defer mprof.Close() // _after_ writing the heap profile
427
- return pprof.WriteHeapProfile(mprof)
428
-}
429
-
430
-func profileIfEnabled() (func(), error) {
431
- // FIXME this is a temporary hack so profiling of asynchronous operations
432
- // works as intended.
433
- if os.Getenv(EnvEnableProfiling) != "" {
434
- stopProfilingFunc, err := startProfiling() // TODO maybe change this to its own option... profiling makes it slower.
435
- if err != nil {
436
- return nil, err
437
- }
438
- return stopProfilingFunc, nil
439
- }
440
- return func() {}, nil
441
-}
442
-
443
-func resolveAddr(ctx context.Context, addr ma.Multiaddr) (ma.Multiaddr, error) {
444
- ctx, cancelFunc := context.WithTimeout(ctx, 10*time.Second)
445
- defer cancelFunc()
446
-
447
- addrs, err := dnsResolver.Resolve(ctx, addr)
448
- if err != nil {
449
- return nil, err
450
- }
451
-
452
- if len(addrs) == 0 {
453
- return nil, errors.New("non-resolvable API endpoint")
454
- }
455
-
456
- return addrs[0], nil
457
-}
458
-
459
-type nopWriter struct {
460
- io.Writer
461
-}
462
-
463
-func (nw nopWriter) Close() error {
464
- return nil
465
-}
466
-
467
-func getRemoteVersion(exe cmds.Executor) (*semver.Version, error) {
468
- ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Second*30))
469
- defer cancel()
470
-
471
- req, err := cmds.NewRequest(ctx, []string{"version"}, nil, nil, nil, Root)
472
- if err != nil {
473
- return nil, err
474
- }
475
-
476
- var buf bytes.Buffer
477
- re, err := cmds.NewWriterResponseEmitter(nopWriter{&buf}, req)
478
- if err != nil {
479
- return nil, err
480
- }
481
-
482
- err = exe.Execute(req, re, nil)
483
- if err != nil {
484
- return nil, err
485
- }
486
-
487
- var out ipfs.VersionInfo
488
- dec := json.NewDecoder(&buf)
489
- if err := dec.Decode(&out); err != nil {
490
- return nil, err
491
- }
492
-
493
- return semver.New(out.Version)
10
+ os.Exit(kubo.Start(kubo.BuildDefaultEnv))
11
}
cmd/ipfs/runmain_test.go
+5
-2
@@ -1,13 +1,15 @@
1
//go:build testrunmain
2
// +build testrunmain
3
4
-package main
4
+package main_test
5
6
import (
7
"flag"
8
"fmt"
9
"os"
10
"testing"
11
+
12
+ "github.com/ipfs/kubo/cmd/ipfs/kubo"
13
)
14
15
// this abuses go so much that I felt dirty writing this code
@@ -16,7 +18,8 @@ import (
18
func TestRunMain(t *testing.T) {
19
args := flag.Args()
20
os.Args = append([]string{os.Args[0]}, args...)
19
- ret := Start()
21
+
22
+ ret := kubo.Start(kubo.BuildDefaultEnv)
23
24
p := os.Getenv("IPFS_COVER_RET_FILE")
25
if len(p) != 0 {