| 1 | package core |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "fmt" |
| 6 | "reflect" |
| 7 | "sync" |
| 8 | "time" |
| 9 | |
| 10 | "github.com/ipfs/boxo/bootstrap" |
| 11 | "github.com/ipfs/kubo/core/node" |
| 12 | |
| 13 | "github.com/ipfs/go-metrics-interface" |
| 14 | "go.uber.org/dig" |
| 15 | "go.uber.org/fx" |
| 16 | ) |
| 17 | |
| 18 | // FXNodeInfo contains information useful for adding fx options. |
| 19 | // This is the extension point for providing more info/context to fx plugins |
| 20 | // to make decisions about what options to include. |
| 21 | type FXNodeInfo struct { |
| 22 | FXOptions []fx.Option |
| 23 | } |
| 24 | |
| 25 | // fxOptFunc takes in some info about the IPFS node and returns the full set of fx opts to use. |
| 26 | type fxOptFunc func(FXNodeInfo) ([]fx.Option, error) |
| 27 | |
| 28 | var fxOptionFuncs []fxOptFunc |
| 29 | |
| 30 | // RegisterFXOptionFunc registers a function that is run before the fx app is initialized. |
| 31 | // Functions are invoked in the order they are registered, |
| 32 | // and the resulting options are passed into the next function's FXNodeInfo. |
| 33 | // |
| 34 | // Note that these are applied globally, by all invocations of NewNode. |
| 35 | // There are multiple places in Kubo that construct nodes, such as: |
| 36 | // - Repo initialization |
| 37 | // - Daemon initialization |
| 38 | // - When running migrations |
| 39 | // - etc. |
| 40 | // |
| 41 | // If your fx options are doing anything sophisticated, you should keep this in mind. |
| 42 | // |
| 43 | // For example, if you plug in a blockservice that disallows non-allowlisted CIDs, |
| 44 | // this may break migrations that fetch migration code over IPFS. |
| 45 | func RegisterFXOptionFunc(optFunc fxOptFunc) { |
| 46 | fxOptionFuncs = append(fxOptionFuncs, optFunc) |
| 47 | } |
| 48 | |
| 49 | // from https://stackoverflow.com/a/59348871 |
| 50 | type valueContext struct { |
| 51 | context.Context |
| 52 | } |
| 53 | |
| 54 | func (valueContext) Deadline() (deadline time.Time, ok bool) { return } |
| 55 | func (valueContext) Done() <-chan struct{} { return nil } |
| 56 | func (valueContext) Err() error { return nil } |
| 57 | |
| 58 | type BuildCfg = node.BuildCfg // Alias for compatibility until we properly refactor the constructor interface |
| 59 | |
| 60 | // NewNode constructs and returns an IpfsNode using the given cfg. |
| 61 | func NewNode(ctx context.Context, cfg *BuildCfg) (*IpfsNode, error) { |
| 62 | // save this context as the "lifetime" ctx. |
| 63 | lctx := ctx |
| 64 | |
| 65 | // derive a new context that ignores cancellations from the lifetime ctx. |
| 66 | ctx, cancel := context.WithCancel(valueContext{ctx}) |
| 67 | |
| 68 | // add a metrics scope. |
| 69 | ctx = metrics.CtxScope(ctx, "ipfs") |
| 70 | |
| 71 | n := &IpfsNode{ |
| 72 | ctx: ctx, |
| 73 | } |
| 74 | |
| 75 | opts := []fx.Option{ |
| 76 | node.IPFS(ctx, cfg), |
| 77 | fx.NopLogger, |
| 78 | } |
| 79 | for _, optFunc := range fxOptionFuncs { |
| 80 | var err error |
| 81 | opts, err = optFunc(FXNodeInfo{FXOptions: opts}) |
| 82 | if err != nil { |
| 83 | cancel() |
| 84 | return nil, fmt.Errorf("building fx opts: %w", err) |
| 85 | } |
| 86 | } |
| 87 | //nolint:staticcheck // https://github.com/ipfs/kubo/pull/9423#issuecomment-1341038770 |
| 88 | opts = append(opts, fx.Extract(n)) |
| 89 | |
| 90 | app := fx.New(opts...) |
| 91 | |
| 92 | var once sync.Once |
| 93 | var stopErr error |
| 94 | n.stop = func() error { |
| 95 | once.Do(func() { |
| 96 | // Bound app.Stop with a deadline so an FX OnStop hook that |
| 97 | // never returns cannot hang the daemon. ShutdownTimeout==0 |
| 98 | // opts out of the cap entirely and restores the legacy |
| 99 | // behavior of waiting forever for hooks to complete. The |
| 100 | // daemon's watchdog in cmd/ipfs/kubo/daemon.go fires at the |
| 101 | // same deadline and is the unconditional os.Exit fallback. |
| 102 | stopCtx := context.Background() |
| 103 | if cfg.ShutdownTimeout > 0 { |
| 104 | var stopCancel context.CancelFunc |
| 105 | stopCtx, stopCancel = context.WithTimeout(stopCtx, cfg.ShutdownTimeout) |
| 106 | defer stopCancel() |
| 107 | } |
| 108 | stopErr = app.Stop(stopCtx) |
| 109 | if stopErr != nil { |
| 110 | log.Errorf("failure on stop: %v", stopErr) |
| 111 | } |
| 112 | // Cancel the context _after_ the app has stopped. |
| 113 | cancel() |
| 114 | }) |
| 115 | return stopErr |
| 116 | } |
| 117 | n.IsOnline = cfg.Online |
| 118 | |
| 119 | go func() { |
| 120 | // Shut down the application if the lifetime context is canceled. |
| 121 | // NOTE: we _should_ stop the application by calling `Close()` |
| 122 | // on the process. But we currently manage everything with contexts. |
| 123 | select { |
| 124 | case <-lctx.Done(): |
| 125 | err := n.stop() |
| 126 | if err != nil { |
| 127 | log.Error("failure on stop: ", err) |
| 128 | } |
| 129 | case <-ctx.Done(): |
| 130 | } |
| 131 | }() |
| 132 | |
| 133 | if app.Err() != nil { |
| 134 | return nil, logAndUnwrapFxError(app.Err()) |
| 135 | } |
| 136 | |
| 137 | if err := app.Start(ctx); err != nil { |
| 138 | return nil, logAndUnwrapFxError(err) |
| 139 | } |
| 140 | |
| 141 | // TODO: How soon will bootstrap move to libp2p? |
| 142 | if !cfg.Online { |
| 143 | return n, nil |
| 144 | } |
| 145 | |
| 146 | return n, n.Bootstrap(bootstrap.DefaultBootstrapConfig) |
| 147 | } |
| 148 | |
| 149 | // Log the entire `app.Err()` but return only the innermost one to the user |
| 150 | // given the full error can be very long (as it can expose the entire build |
| 151 | // graph in a single string). |
| 152 | // |
| 153 | // The fx.App error exposed through `app.Err()` normally contains un-exported |
| 154 | // errors from its low-level `dig` package: |
| 155 | // * https://github.com/uber-go/dig/blob/5e5a20d/error.go#L82 |
| 156 | // These usually wrap themselves in many layers to expose where in the build |
| 157 | // chain did the error happen. Although useful for a developer that needs to |
| 158 | // debug it, it can be very confusing for a user that just wants the IPFS error |
| 159 | // that he can probably fix without being aware of the entire chain. |
| 160 | // Unwrapping everything is not the best solution as there can be useful |
| 161 | // information in the intermediate errors, mainly in the next to last error |
| 162 | // that locates which component is the build error coming from, but it's the |
| 163 | // best we can do at the moment given all errors in dig are private and we |
| 164 | // just have the generic `RootCause` API. |
| 165 | func logAndUnwrapFxError(fxAppErr error) error { |
| 166 | if fxAppErr == nil { |
| 167 | return nil |
| 168 | } |
| 169 | |
| 170 | log.Error("constructing the node: ", fxAppErr) |
| 171 | |
| 172 | err := fxAppErr |
| 173 | for { |
| 174 | extractedErr := dig.RootCause(err) |
| 175 | // Note that the `RootCause` name is misleading as it just unwraps only |
| 176 | // *one* error layer at a time, so we need to continuously call it. |
| 177 | if !reflect.TypeOf(extractedErr).Comparable() { |
| 178 | // Some internal errors are not comparable (e.g., `dig.errMissingTypes` |
| 179 | // which is a slice) and we can't go further. |
| 180 | break |
| 181 | } |
| 182 | if extractedErr == err { |
| 183 | // We didn't unwrap any new error in the last call, reached the innermost one. |
| 184 | break |
| 185 | } |
| 186 | err = extractedErr |
| 187 | } |
| 188 | |
| 189 | return fmt.Errorf("constructing the node (see log for full detail): %w", err) |
| 190 | } |