|
| 1 | +# Architecture |
| 2 | + |
| 3 | + |
| 4 | + |
| 5 | +## Package documentation |
| 6 | + |
| 7 | +The Go project hosts a documentation server that can be accessed here: |
| 8 | +https://pkg.go.dev/github.com/djdv/go-filesystem-utils/internal |
| 9 | +The public `pkgsite` service hides a lot of information by default, so developers are encouraged |
| 10 | +to run their own local [`godoc`](https://pkg.go.dev/golang.org/x/tools/cmd/godoc) server (or any |
| 11 | +equivalent documentation server such as `golds`, etc.). |
| 12 | +`godoc` allows an `m=all` query string, which will render internal packages and unexported |
| 13 | +identifiers. |
| 14 | +E.g. `godoc -http:6060` then navigate to "http://127.0.0.1:6060/pkg/github.com/djdv/go-filesystem-utils/?m=all". |
| 15 | + |
| 16 | +This project separates platform differences at compile time via build constraints, so other |
| 17 | +useful query strings are `GOOS` and `GOARCH` which work with both `pkgsite` and `godoc`. |
| 18 | +By default, the documentation servers will render documentation for the `$GOOS` and `$GOARCH` of |
| 19 | +the requester, but these can be specified explicitly to request documentation sets for other |
| 20 | +combinations. |
| 21 | +E.g. "https://pkg.go.dev/os?GOOS=windows", "https://pkg.go.dev/os?GOOS=darwin&GOARCH=amd64", etc. |
| 22 | + |
| 23 | +As APIs stabilize, packages may be moved out of `/internal`, potentially into separate repos. |
| 24 | + |
| 25 | +## `fs` command |
| 26 | + |
| 27 | +### Command line interface |
| 28 | + |
| 29 | +The `fs` command serves multiple roles, acting as both a client and server process. |
| 30 | +The package `command` facilitates multiplexing and dispatching between our subcommands. |
| 31 | + |
| 32 | +### Command implementation |
| 33 | + |
| 34 | +(Sub)commands are defined as Go functions that have a signature compatible with |
| 35 | +one of the `command.Make*Command` constructors. |
| 36 | +E.g. `func execute(ctx context.Context, arbitrary ExecuteType...)` |
| 37 | +which is compatible with the `command.MakeVariadicCommand` constructor. |
| 38 | + |
| 39 | +The `ExecuteType` constraint may be any Go type which satisfies its interface. |
| 40 | +Specifically, this includes a `BindFlags(*flag.FlagSet)` method which registers the type with |
| 41 | +Go's standard `flag.FlagSet` type. |
| 42 | + |
| 43 | +Package `command` will handle parsing the command line and resolving subcommands, before passing |
| 44 | +the expected parameters to the execute function. |
| 45 | + |
| 46 | +For more details, you can read the examples in the `command` package, or read the |
| 47 | +implementations used by the `fs` commands. |
| 48 | + |
| 49 | +### `fs` execute function implementations |
| 50 | + |
| 51 | +The execute functions used by the `fs` commands typically accept a variadic list of "option" |
| 52 | +functions, which are applied to a "settings" structure if provided. |
| 53 | +This is the most complicated function type supported by the `command` package, but carries some |
| 54 | +benefits in regards to lazy evaluation, handling of default values, and chaining shared options |
| 55 | +across commands. |
| 56 | + |
| 57 | +*Note: the actual implementations are more abstracted than these examples, but the principles |
| 58 | +remain the same.* |
| 59 | + |
| 60 | +```go |
| 61 | +type ( |
| 62 | + fooSettings struct { |
| 63 | + value int |
| 64 | + } |
| 65 | + fooOption func(*fooSettings) |
| 66 | +) |
| 67 | + |
| 68 | +func fooExecute(ctx context.Context, options ...fooOption) error { |
| 69 | + settings := fooSettings{ |
| 70 | + value: 1, // Default. |
| 71 | + } |
| 72 | + for _, apply := range options { |
| 73 | + apply(&settings) // Override defaults. |
| 74 | + } |
| 75 | + // Execute foo command with settings... |
| 76 | + return nil |
| 77 | +} |
| 78 | +``` |
| 79 | + |
| 80 | +This function signature can satisfy the `command.ExecuteVariadic` constraint, which allows the |
| 81 | +`ExecuteType` to be a pointer to any underlying slice type. |
| 82 | +To do this, we implement a method which binds to a `flag.FlagSet` on a pointer receiver that |
| 83 | +matches the variadic type. |
| 84 | + |
| 85 | +```go |
| 86 | +type fooOptions []fooOption // Same underlying type as `...fooOption`. |
| 87 | + |
| 88 | +// Satisfy the [command.ExecuteType] constraint. |
| 89 | +func (fo *fooOptions) BindFlags(flagSet *flag.FlagSet) { |
| 90 | + flagSet.Func("flag", "help text", func(parameter string) error { |
| 91 | + value, err := parse(parameter) |
| 92 | + // Handle err... |
| 93 | + *fo = append(*fo, func(settings *fooSettings) { |
| 94 | + settings.value = value |
| 95 | + }) |
| 96 | + return nil |
| 97 | + }) |
| 98 | +} |
| 99 | +``` |
| 100 | + |
| 101 | +We can then call the command constructor to create a formal command. |
| 102 | + |
| 103 | +```go |
| 104 | +func Foo() command.Command { |
| 105 | + const ( |
| 106 | + name = "foo" |
| 107 | + synopsis = "Frobnicates a bar." |
| 108 | + usage = "Foo command long help text..." |
| 109 | + ) |
| 110 | + return command.MakeVariadicCommand[fooOptions](name, synopsis, usage, execute) |
| 111 | +} |
| 112 | +``` |
| 113 | + |
| 114 | +## Client <-> Server <-> OS interactions |
| 115 | + |
| 116 | + |
| 117 | + |
| 118 | +### Client commands |
| 119 | + |
| 120 | +Client commands are primarily only responsible for parsing the command line vector, |
| 121 | +instantiating a connection to a server, and passing parameters to Go interfaces which give names |
| 122 | +to common 9P operation sequences. |
| 123 | + |
| 124 | +If an address is not specified via the `-api-server` flag, client commands will try to connect |
| 125 | +to a default local address. If the default server cannot initially be dialed, the client command |
| 126 | +will automatically spawn a daemon process that will exit when it becomes idle for some time. |
| 127 | +(See: help text for `-api-exit-after` flag) |
| 128 | + |
| 129 | +### Server command |
| 130 | + |
| 131 | +`fs daemon` is the server component which responds to requests made by client commands. |
| 132 | +The daemon is typically run in the background and persists to facilitate longstanding operations |
| 133 | +such as engaging with the host's file system APIs. |
| 134 | + |
| 135 | +When the daemon starts, it first initializes its 9P file system, then tries to serve this system |
| 136 | +on an address. Afterwards, it launches a number of event monitors which handle tasks |
| 137 | +like IPC communication, shutdown requests, idle detection, and more. |
| 138 | + |
| 139 | + |
| 140 | +### Daemon IPC |
| 141 | + |
| 142 | +The daemon can communicate with a parent process using the 9P2000.L protocol over standard |
| 143 | +input and output, as well as text over standard error. |
| 144 | +When the parent process is finished with the service, it must write the `EOT` byte to the file |
| 145 | +`/control/release`. |
| 146 | + |
| 147 | +<!-- vi: set textwidth=96: --> |
0 commit comments