// The nfs-server module's Go bundle (novox/hq ADR 0263): a node shares a folder with the mesh's machines // only, one share per folder, every client mapped to the folder's owner. // // Started with no arguments it is served by the node's runtime as the operator account, over MCP on stdio // (ADR 0188, ADR 0193): the node-nfs-server seat's verbs and the module's health, and beside them it puts // the process's last look in the module's state `exports`, keyed by this machine (ADR 0201). Started as // `nfs-server exports` it is the module's long-running process, as root: it renders the export file from // the assignment's shares and has the kernel read it (process.go). stdout is the MCP channel; everything // else is said on stderr. package main import ( "context" "encoding/json" "fmt" "os" "os/signal" "sort" "syscall" "time" stdio "git.novox.be/novox/mesh-sdk/go" ) // ExportsState is the state the exports are kept in on the bus; MountsState the `mounts` modules' state, // which says what each node wants of which machine. const ( ExportsState = "exports" MountsState = "mounts.shares" ) func main() { if len(os.Args) > 1 { if os.Args[1] != "exports" || len(os.Args) != 2 { fmt.Fprintln(os.Stderr, "usage: nfs-server [exports]") os.Exit(2) } ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, os.Interrupt) defer stop() if err := Here().RunExports(ctx); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } return } m := Here() go ShareExports(m) if err := stdio.Serve("", Tools(m)); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } // ShareExports puts the exports in the module's state every Every, for as long as the bundle runs, so a // consumer's module and a view read them from the bus rather than from this machine. func ShareExports(m *Machine) { machine := os.Getenv("MESH_NODE") if machine == "" { fmt.Fprintln(os.Stderr, "nfs-server: the runtime gave no machine name (MESH_NODE); the exports are not shared") return } last := "" put := func() { m.TakeWants(machine) _, err := stdio.State(ExportsState).Put(machine, m.Exports()) said := "put" if err != nil { said = err.Error() } if said != last { fmt.Fprintln(os.Stderr, "nfs-server: exports state:", said) } last = said } // The SDK reaches the bus once Serve runs. time.Sleep(2 * time.Second) m.TakeBackup(machine) put() backup := time.Tick(BackupEvery) every := time.Tick(Every) for { select { case <-backup: m.TakeBackup(machine) case <-every: put() } } } // askBus calls a seat's verb over the bus as this module, through the runtime. func askBus(key string, body any) (json.RawMessage, error) { return stdio.Ask(key, body) } // TakeWants reads every node's `mounts` state from the bus and writes what is asked of this machine to // the wants file, for the root process. When the bus does not answer, the last wants stay. func (m *Machine) TakeWants(machine string) { state := stdio.State(MountsState) keys, err := state.Keys() if err != nil { fmt.Fprintln(os.Stderr, "nfs-server: the mounts modules' state could not be listed:", err) return } raw := map[string][]byte{} for _, k := range keys { e, err := state.Get(k) if err != nil { fmt.Fprintln(os.Stderr, "nfs-server: the mounts state of", k, "could not be read:", err) return } if e != nil { raw[k] = e.Value } } wants, notes := WantsFromStates(machine, raw) for _, n := range notes { fmt.Fprintln(os.Stderr, "nfs-server:", n) } text, _ := json.MarshalIndent(WantsFromBus{Looked: m.Now().UTC().Format(time.RFC3339), Wants: wants}, "", " ") have, _ := m.Read(WantsFile) if wantsOf(have) == wantsOf(string(text)) { return } if err := m.Write(WantsFile, string(text)+"\n"); err != nil { fmt.Fprintln(os.Stderr, "nfs-server: the wants could not be written:", err) } } // wantsOf is a wants file without its time, to compare two. func wantsOf(text string) string { var w WantsFromBus if json.Unmarshal([]byte(text), &w) != nil { return text } raw, _ := json.Marshal(w.Wants) return string(raw) } // WantsFromStates is what each node's `mounts` state asks of this machine: its wants whose server is this // machine, keyed by the node. A state that cannot be read is said and skipped. func WantsFromStates(machine string, states map[string][]byte) (map[string][]Want, []string) { out := map[string][]Want{} var notes []string keys := make([]string, 0, len(states)) for k := range states { keys = append(keys, k) } sort.Strings(keys) for _, node := range keys { var v struct { Wants []struct { Server string `json:"server"` Share string `json:"share"` Access string `json:"access"` } `json:"wants"` } if err := json.Unmarshal(states[node], &v); err != nil { notes = append(notes, fmt.Sprintf("the mounts state of %s is not one this module reads: %v", node, err)) continue } for _, w := range v.Wants { if w.Server != machine || !validName(w.Share) { continue } access := "rw" if w.Access == "ro" { access = "ro" } out[node] = append(out[node], Want{Share: w.Share, Access: access}) } } return out, notes }