| 1 | package commands |
| 2 | |
| 3 | import ( |
| 4 | "encoding/hex" |
| 5 | "errors" |
| 6 | "fmt" |
| 7 | "io" |
| 8 | "time" |
| 9 | |
| 10 | "github.com/ipfs/boxo/path" |
| 11 | cid "github.com/ipfs/go-cid" |
| 12 | "github.com/ipfs/go-datastore" |
| 13 | "github.com/ipfs/go-datastore/mount" |
| 14 | "github.com/ipfs/go-datastore/query" |
| 15 | cmds "github.com/ipfs/go-ipfs-cmds" |
| 16 | oldcmds "github.com/ipfs/kubo/commands" |
| 17 | "github.com/ipfs/kubo/core/commands/cmdenv" |
| 18 | node "github.com/ipfs/kubo/core/node" |
| 19 | "github.com/ipfs/kubo/core/shutdown" |
| 20 | fsrepo "github.com/ipfs/kubo/repo/fsrepo" |
| 21 | ) |
| 22 | |
| 23 | // diagHealthyProbeCIDStr is the well-known empty UnixFS directory, |
| 24 | // built into every kubo node. Fetching it succeeds regardless of peers, |
| 25 | // DHT, or user content, so it isolates the DAG/blockstore pipeline. |
| 26 | const diagHealthyProbeCIDStr = "QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn" |
| 27 | |
| 28 | var DiagCmd = &cmds.Command{ |
| 29 | Helptext: cmds.HelpText{ |
| 30 | Tagline: "Generate diagnostic reports.", |
| 31 | }, |
| 32 | |
| 33 | Subcommands: map[string]*cmds.Command{ |
| 34 | "sys": sysDiagCmd, |
| 35 | "cmds": ActiveReqsCmd, |
| 36 | "profile": sysProfileCmd, |
| 37 | "datastore": diagDatastoreCmd, |
| 38 | "healthy": diagHealthyCmd, |
| 39 | }, |
| 40 | } |
| 41 | |
| 42 | // diagHealthyCmd is a container-healthcheck probe. It fails when shutdown |
| 43 | // has been initiated (even if the RPC API still answers) or when the DAG |
| 44 | // pipeline cannot resolve a built-in CID. |
| 45 | var diagHealthyCmd = &cmds.Command{ |
| 46 | Helptext: cmds.HelpText{ |
| 47 | Tagline: "Report whether the daemon is operational.", |
| 48 | ShortDescription: ` |
| 49 | Exits 0 if the daemon is running and can resolve the well-known empty |
| 50 | UnixFS directory. Exits non-zero if shutdown has started or the DAG |
| 51 | pipeline is broken. Intended for container healthchecks. |
| 52 | `, |
| 53 | }, |
| 54 | Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error { |
| 55 | if t := shutdown.StartedAt(); !t.IsZero() { |
| 56 | return fmt.Errorf("daemon is shutting down (started %s ago)", time.Since(t).Round(time.Second)) |
| 57 | } |
| 58 | api, err := cmdenv.GetApi(env, req) |
| 59 | if err != nil { |
| 60 | return err |
| 61 | } |
| 62 | probeCID, err := cid.Decode(diagHealthyProbeCIDStr) |
| 63 | if err != nil { |
| 64 | return fmt.Errorf("invalid probe CID: %w", err) |
| 65 | } |
| 66 | if _, _, err := api.ResolvePath(req.Context, path.FromCid(probeCID)); err != nil { |
| 67 | return fmt.Errorf("probe resolve: %w", err) |
| 68 | } |
| 69 | if _, err := api.Dag().Get(req.Context, probeCID); err != nil { |
| 70 | return fmt.Errorf("probe fetch: %w", err) |
| 71 | } |
| 72 | return cmds.EmitOnce(res, "ok") |
| 73 | }, |
| 74 | } |
| 75 | |
| 76 | var diagDatastoreCmd = &cmds.Command{ |
| 77 | Status: cmds.Experimental, |
| 78 | Helptext: cmds.HelpText{ |
| 79 | Tagline: "Low-level datastore inspection for debugging and testing.", |
| 80 | ShortDescription: ` |
| 81 | 'ipfs diag datastore' provides low-level access to the datastore for debugging |
| 82 | and testing purposes. |
| 83 | |
| 84 | WARNING: FOR DEBUGGING/TESTING ONLY |
| 85 | |
| 86 | These commands expose internal datastore details and should not be used |
| 87 | in production workflows. The datastore format may change between versions. |
| 88 | |
| 89 | The daemon must not be running when calling these commands. |
| 90 | |
| 91 | When the provider keystore datastores exist on disk (nodes with |
| 92 | Provide.DHT.SweepEnabled=true), they are automatically mounted into the |
| 93 | datastore view under /provider/keystore/0/ and /provider/keystore/1/. |
| 94 | |
| 95 | EXAMPLES |
| 96 | |
| 97 | Inspecting pubsub seqno validator state: |
| 98 | |
| 99 | $ ipfs diag datastore count /pubsub/seqno/ |
| 100 | 2 |
| 101 | $ ipfs diag datastore get --hex /pubsub/seqno/12D3KooW... |
| 102 | Key: /pubsub/seqno/12D3KooW... |
| 103 | Hex Dump: |
| 104 | 00000000 18 81 81 c8 91 c0 ea f6 |........| |
| 105 | |
| 106 | Writing a test key (debugging only): |
| 107 | |
| 108 | $ ipfs diag datastore put /test/mykey "hello" |
| 109 | |
| 110 | Inspecting provider keystore (requires SweepEnabled): |
| 111 | |
| 112 | $ ipfs diag datastore count /provider/keystore/0/ |
| 113 | $ ipfs diag datastore count /provider/keystore/1/ |
| 114 | `, |
| 115 | }, |
| 116 | Subcommands: map[string]*cmds.Command{ |
| 117 | "get": diagDatastoreGetCmd, |
| 118 | "put": diagDatastorePutCmd, |
| 119 | "count": diagDatastoreCountCmd, |
| 120 | }, |
| 121 | } |
| 122 | |
| 123 | const diagDatastoreHexOptionName = "hex" |
| 124 | |
| 125 | type diagDatastoreGetResult struct { |
| 126 | Key string `json:"key"` |
| 127 | Value []byte `json:"value"` |
| 128 | HexDump string `json:"hex_dump,omitempty"` |
| 129 | } |
| 130 | |
| 131 | // openDiagDatastore opens the repo datastore and conditionally mounts any |
| 132 | // provider keystore datastores that exist on disk. It returns the composite |
| 133 | // datastore and a cleanup function that must be called when done. |
| 134 | func openDiagDatastore(env cmds.Environment) (datastore.Datastore, func(), error) { |
| 135 | cctx := env.(*oldcmds.Context) |
| 136 | repo, err := fsrepo.Open(cctx.ConfigRoot) |
| 137 | if err != nil { |
| 138 | return nil, nil, fmt.Errorf("failed to open repo: %w", err) |
| 139 | } |
| 140 | |
| 141 | extraMounts, extraCloser, err := node.MountKeystoreDatastores(repo) |
| 142 | if err != nil { |
| 143 | repo.Close() |
| 144 | return nil, nil, err |
| 145 | } |
| 146 | |
| 147 | closer := func() { |
| 148 | extraCloser() |
| 149 | repo.Close() |
| 150 | } |
| 151 | |
| 152 | if len(extraMounts) == 0 { |
| 153 | return repo.Datastore(), closer, nil |
| 154 | } |
| 155 | |
| 156 | mounts := []mount.Mount{{Prefix: datastore.NewKey("/"), Datastore: repo.Datastore()}} |
| 157 | mounts = append(mounts, extraMounts...) |
| 158 | return mount.New(mounts), closer, nil |
| 159 | } |
| 160 | |
| 161 | var diagDatastoreGetCmd = &cmds.Command{ |
| 162 | Status: cmds.Experimental, |
| 163 | Helptext: cmds.HelpText{ |
| 164 | Tagline: "Read a raw key from the datastore.", |
| 165 | ShortDescription: ` |
| 166 | Returns the value stored at the given datastore key. |
| 167 | Default output is raw bytes. Use --hex for human-readable hex dump. |
| 168 | |
| 169 | The daemon must not be running when using this command. |
| 170 | |
| 171 | WARNING: FOR DEBUGGING/TESTING ONLY |
| 172 | `, |
| 173 | }, |
| 174 | Arguments: []cmds.Argument{ |
| 175 | cmds.StringArg("key", true, false, "Datastore key to read (e.g., /pubsub/seqno/<peerid>)"), |
| 176 | }, |
| 177 | Options: []cmds.Option{ |
| 178 | cmds.BoolOption(diagDatastoreHexOptionName, "Output hex dump instead of raw bytes"), |
| 179 | }, |
| 180 | NoRemote: true, |
| 181 | PreRun: DaemonNotRunning, |
| 182 | Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error { |
| 183 | ds, closer, err := openDiagDatastore(env) |
| 184 | if err != nil { |
| 185 | return err |
| 186 | } |
| 187 | defer closer() |
| 188 | |
| 189 | keyStr := req.Arguments[0] |
| 190 | key := datastore.NewKey(keyStr) |
| 191 | |
| 192 | val, err := ds.Get(req.Context, key) |
| 193 | if err != nil { |
| 194 | if errors.Is(err, datastore.ErrNotFound) { |
| 195 | return fmt.Errorf("key not found: %s", keyStr) |
| 196 | } |
| 197 | return fmt.Errorf("failed to read key: %w", err) |
| 198 | } |
| 199 | |
| 200 | result := &diagDatastoreGetResult{ |
| 201 | Key: keyStr, |
| 202 | Value: val, |
| 203 | } |
| 204 | |
| 205 | if hexDump, _ := req.Options[diagDatastoreHexOptionName].(bool); hexDump { |
| 206 | result.HexDump = hex.Dump(val) |
| 207 | } |
| 208 | |
| 209 | return cmds.EmitOnce(res, result) |
| 210 | }, |
| 211 | Type: diagDatastoreGetResult{}, |
| 212 | Encoders: cmds.EncoderMap{ |
| 213 | cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, result *diagDatastoreGetResult) error { |
| 214 | if result.HexDump != "" { |
| 215 | fmt.Fprintf(w, "Key: %s\nHex Dump:\n%s", result.Key, result.HexDump) |
| 216 | return nil |
| 217 | } |
| 218 | // Raw bytes output |
| 219 | _, err := w.Write(result.Value) |
| 220 | return err |
| 221 | }), |
| 222 | }, |
| 223 | } |
| 224 | |
| 225 | var diagDatastorePutCmd = &cmds.Command{ |
| 226 | Status: cmds.Experimental, |
| 227 | Helptext: cmds.HelpText{ |
| 228 | Tagline: "Write a raw key-value pair to the datastore.", |
| 229 | ShortDescription: ` |
| 230 | Stores the given value at the specified datastore key. |
| 231 | |
| 232 | The daemon must not be running when using this command. |
| 233 | |
| 234 | WARNING: FOR DEBUGGING/TESTING ONLY |
| 235 | `, |
| 236 | }, |
| 237 | Arguments: []cmds.Argument{ |
| 238 | cmds.StringArg("key", true, false, "Datastore key (e.g., /test/mykey)"), |
| 239 | cmds.StringArg("value", true, false, "Value to store (as a string)"), |
| 240 | }, |
| 241 | NoRemote: true, |
| 242 | PreRun: DaemonNotRunning, |
| 243 | Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error { |
| 244 | ds, closer, err := openDiagDatastore(env) |
| 245 | if err != nil { |
| 246 | return err |
| 247 | } |
| 248 | defer closer() |
| 249 | |
| 250 | key := datastore.NewKey(req.Arguments[0]) |
| 251 | if err := ds.Put(req.Context, key, []byte(req.Arguments[1])); err != nil { |
| 252 | return fmt.Errorf("failed to put key: %w", err) |
| 253 | } |
| 254 | if err := ds.Sync(req.Context, key); err != nil { |
| 255 | return fmt.Errorf("failed to sync: %w", err) |
| 256 | } |
| 257 | return nil |
| 258 | }, |
| 259 | } |
| 260 | |
| 261 | type diagDatastoreCountResult struct { |
| 262 | Prefix string `json:"prefix"` |
| 263 | Count int64 `json:"count"` |
| 264 | } |
| 265 | |
| 266 | var diagDatastoreCountCmd = &cmds.Command{ |
| 267 | Status: cmds.Experimental, |
| 268 | Helptext: cmds.HelpText{ |
| 269 | Tagline: "Count entries matching a datastore prefix.", |
| 270 | ShortDescription: ` |
| 271 | Counts the number of datastore entries whose keys start with the given prefix. |
| 272 | |
| 273 | The daemon must not be running when using this command. |
| 274 | |
| 275 | WARNING: FOR DEBUGGING/TESTING ONLY |
| 276 | `, |
| 277 | }, |
| 278 | Arguments: []cmds.Argument{ |
| 279 | cmds.StringArg("prefix", true, false, "Datastore key prefix (e.g., /pubsub/seqno/)"), |
| 280 | }, |
| 281 | NoRemote: true, |
| 282 | PreRun: DaemonNotRunning, |
| 283 | Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error { |
| 284 | ds, closer, err := openDiagDatastore(env) |
| 285 | if err != nil { |
| 286 | return err |
| 287 | } |
| 288 | defer closer() |
| 289 | |
| 290 | prefix := req.Arguments[0] |
| 291 | |
| 292 | q := query.Query{ |
| 293 | Prefix: prefix, |
| 294 | KeysOnly: true, |
| 295 | } |
| 296 | |
| 297 | results, err := ds.Query(req.Context, q) |
| 298 | if err != nil { |
| 299 | return fmt.Errorf("failed to query datastore: %w", err) |
| 300 | } |
| 301 | defer results.Close() |
| 302 | |
| 303 | var count int64 |
| 304 | for result := range results.Next() { |
| 305 | if result.Error != nil { |
| 306 | return fmt.Errorf("query error: %w", result.Error) |
| 307 | } |
| 308 | count++ |
| 309 | } |
| 310 | |
| 311 | return cmds.EmitOnce(res, &diagDatastoreCountResult{ |
| 312 | Prefix: prefix, |
| 313 | Count: count, |
| 314 | }) |
| 315 | }, |
| 316 | Type: diagDatastoreCountResult{}, |
| 317 | Encoders: cmds.EncoderMap{ |
| 318 | cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, result *diagDatastoreCountResult) error { |
| 319 | _, err := fmt.Fprintf(w, "%d\n", result.Count) |
| 320 | return err |
| 321 | }), |
| 322 | }, |
| 323 | } |