From feada9fb149847e7ca1463dc7fd1141d0ce63542 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 20 Oct 2026 21:23:28 +0200 Subject: [PATCH] brother: the admin process becomes the device process, brother-device, with device-asks and device-answers (issue 1453, ADR 0468 clause 1) --- modules/brother/README.md | 14 +++--- ...undary_test.go => device_boundary_test.go} | 18 +++---- ...ion_test.go => device_integration_test.go} | 16 +++---- .../brother/{adminproc.go => deviceproc.go} | 48 +++++++++---------- modules/brother/cmd/brother/main.go | 6 +-- modules/brother/cmd/brother/manifest_test.go | 36 +++++++------- modules/brother/cmd/brother/printer.go | 42 ++++++++-------- modules/brother/cmd/brother/printer_test.go | 22 ++++----- modules/brother/cmd/brother/scan.go | 4 +- modules/brother/module.json | 30 ++++++------ 10 files changed, 118 insertions(+), 118 deletions(-) rename modules/brother/cmd/brother/{admin_boundary_test.go => device_boundary_test.go} (72%) rename modules/brother/cmd/brother/{admin_integration_test.go => device_integration_test.go} (87%) rename modules/brother/cmd/brother/{adminproc.go => deviceproc.go} (70%) diff --git a/modules/brother/README.md b/modules/brother/README.md index 379c893e..c70de68f 100644 --- a/modules/brother/README.md +++ b/modules/brother/README.md @@ -59,7 +59,7 @@ makes it (mode 0755) when it files the first scan. | verb (tool) | does | |---|---| | `printer.status` | without a login: over IPP (`Get-Printer-Attributes`, encoded by hand, on port 631) the state (idle, processing, stopped), the state reasons (such as `toner-low-warning`) and the markers' names, levels (percent; below 0 is unknown) and colours; over eSCL the scanner's state and its feeder's. A part that does not answer is `answers: false` with why. Needs `printer` and `scanner` | -| `printer.settings` | asks the admin process (below) to log in, read one admin page and log out, and waits up to 60 s for its answer. Argument `page`: a path on the admin site, starting with `/` and ending in `.html` (default `/general/status.html`). Answers the page's title, links (href and text) and forms with their fields (name, type, value, label); a password field's value is never given. Read-only: it sends the login and the logout and nothing else | +| `printer.settings` | asks the device process (below) to log in, read one admin page and log out, and waits up to 60 s for its answer. Argument `page`: a path on the admin site, starting with `/` and ending in `.html` (default `/general/status.html`). Answers the page's title, links (href and text) and forms with their fields (name, type, value, label); a password field's value is never given. Read-only: it sends the login and the logout and nothing else | ### Trust and the password @@ -69,18 +69,18 @@ to it; the error names both fingerprints. Get the fingerprint from the device's upper-case hex pairs joined by colons. The admin password is the module's own secret `admin-password` (ADR 0467 clause 4; ADR 0283): the operator gives it -at their desk, and the mesh never generates or rotates it. It is owned by the account `brother-admin`, which the +at their desk, and the mesh never generates or rotates it. It is owned by the account `brother-device`, which the module makes, because the tools run as the operator account and must never read it (ADR 0436 §1, §12; the shape of mesh-mail's sender). The file is `${dir:state}/admin-password.secret`, mode 0600, and `BROTHER_ADMIN_PASSWORD` names -it for the admin process alone; the tools' bundle is given no such name, and `admin_boundary_test.go` checks that +it for the device process alone; the tools' bundle is given no such name, and `admin_boundary_test.go` checks that no tool file imports the package that reads it. While it is not given, `printer.settings` answers that the secret is not given, and the printer is not contacted. -### The admin process +### The device process -`brother admin` runs as `brother-admin` with a bus account of its own (the own secret `broker-admin`), never a -seat. It consumes the work-queue stream `printer-asks` (event `printer.asked`, body `id`, `page`, `asked-at`) and -writes each answer, once, into the state `printer-answers` under the ask's id (ten minutes' life): the page's +`brother device` runs as `brother-device` with a bus account of its own (the own secret `broker-device`), never a +seat. It consumes the work-queue stream `device-asks` (event `device.asked`, body `id`, `page`, `asked-at`) and +writes each answer, once, into the state `device-answers` under the ask's id (ten minutes' life): the page's title, links and form fields, or `error`. It alone reads the password, pins the certificate, logs in, reads the page and logs out. An ask older than two minutes is dropped without contacting the printer; at most three deliveries. It reads `settings.json` (mode 0644, no secret in it) and the state directory is 0711 so that it can diff --git a/modules/brother/cmd/brother/admin_boundary_test.go b/modules/brother/cmd/brother/device_boundary_test.go similarity index 72% rename from modules/brother/cmd/brother/admin_boundary_test.go rename to modules/brother/cmd/brother/device_boundary_test.go index 004f7afb..ad06cc1a 100644 --- a/modules/brother/cmd/brother/admin_boundary_test.go +++ b/modules/brother/cmd/brother/device_boundary_test.go @@ -1,7 +1,7 @@ package main -// The tools never open the admin password (novox/hq ADR 0436 §12, ADR 0467 clause 4): only the admin process -// reads it, as its own account, brother-admin. The build seat runs every test under one account, so no +// The tools never open the admin password (novox/hq ADR 0436 §12, ADR 0467 clause 4): only the device process +// reads it, as its own account, brother-device. The build seat runs every test under one account, so no // permission test can show the split; this is the file-level proxy mesh-mail's secret_boundary_test.go uses // instead: an AST check that the tools' files import no password-reading package and name no password file. @@ -16,7 +16,7 @@ import ( const secretPackage = "brother/cmd/brother/internal/secret" // toolFiles are the files the tools' bundle runs (the scanner's, the printer.status's and the printer.settings' -// ask), as opposed to the admin process's: adminproc.go, admin.go, pin.go and html.go. +// ask), as opposed to the device process's: deviceproc.go, admin.go, pin.go and html.go. var toolFiles = []string{"main.go", "printer.go", "scan.go", "settings.go", "ipp.go", "escl.go", "file.go", "crop.go", "pdf.go"} // 1453: none of the tools' files imports the password-reading package or names the password's file. @@ -29,7 +29,7 @@ func TestTheToolsNeverOpenTheAdminPassword(t *testing.T) { } for _, imp := range f.Imports { if strings.Trim(imp.Path.Value, `"`) == secretPackage { - t.Errorf("%s imports %s, which only the admin process may (ADR 0436 §12)", file, secretPackage) + t.Errorf("%s imports %s, which only the device process may (ADR 0436 §12)", file, secretPackage) } } src, err := os.ReadFile(file) @@ -37,16 +37,16 @@ func TestTheToolsNeverOpenTheAdminPassword(t *testing.T) { t.Fatal(err) } if strings.Contains(string(src), `Getenv("BROTHER_ADMIN_PASSWORD")`) { - t.Errorf("%s reads BROTHER_ADMIN_PASSWORD, the admin process's alone", file) + t.Errorf("%s reads BROTHER_ADMIN_PASSWORD, the device process's alone", file) } } } -// 1453: the admin process does read it through the package, and the package is the one place a password file is +// 1453: the device process does read it through the package, and the package is the one place a password file is // opened. func TestTheAdminProcessReadsThePasswordThroughThePackage(t *testing.T) { fset := token.NewFileSet() - f, err := parser.ParseFile(fset, "adminproc.go", nil, parser.ImportsOnly) + f, err := parser.ParseFile(fset, "deviceproc.go", nil, parser.ImportsOnly) if err != nil { t.Fatal(err) } @@ -57,9 +57,9 @@ func TestTheAdminProcessReadsThePasswordThroughThePackage(t *testing.T) { } } if !found { - t.Errorf("adminproc.go does not import %s", secretPackage) + t.Errorf("deviceproc.go does not import %s", secretPackage) } - for _, file := range append(append([]string{}, toolFiles...), "admin.go", "adminproc.go", "pin.go", "html.go") { + for _, file := range append(append([]string{}, toolFiles...), "admin.go", "deviceproc.go", "pin.go", "html.go") { src, _ := os.ReadFile(file) if strings.Contains(string(src), "os.ReadFile") && file != "settings.go" && file != "file.go" && file != "escl.go" { t.Errorf("%s reads a file: only settings.json (settings.go) and the scan folder's PDFs are read outside the secret package", file) diff --git a/modules/brother/cmd/brother/admin_integration_test.go b/modules/brother/cmd/brother/device_integration_test.go similarity index 87% rename from modules/brother/cmd/brother/admin_integration_test.go rename to modules/brother/cmd/brother/device_integration_test.go index a9706a0a..0237a162 100644 --- a/modules/brother/cmd/brother/admin_integration_test.go +++ b/modules/brother/cmd/brother/device_integration_test.go @@ -2,9 +2,9 @@ package main -// The admin process over a real bus (novox/hq ADR 0467 clause 4; the shape of mesh-mail's +// The device process over a real bus (novox/hq ADR 0467 clause 4; the shape of mesh-mail's // sender_integration_test.go, ADR 0436 §7): a JetStream server of the test's own, the work-queue stream -// printer-asks and its consumer, the state bucket printer-answers, and a fake printer. An ask published on the +// device-asks and its consumer, the state bucket device-answers, and a fake printer. An ask published on the // stream is answered in the state under its id and acknowledged. Compiled by the merge gate's // `go vet -tags integration`; never run on this machine. @@ -46,7 +46,7 @@ func newTestBus(t *testing.T) string { return s.ClientURL() } -// 1453: an ask on printer-asks is answered under its id in printer-answers by the admin process, over the +// 1453: an ask on device-asks is answered under its id in device-answers by the device process, over the // printer's pinned HTTPS and the login, and acknowledged; stopping it leaves nothing in hand. func TestTheAdminProcessAnswersAnAskOverARealBus(t *testing.T) { url := newTestBus(t) @@ -64,7 +64,7 @@ func TestTheAdminProcessAnswersAnAskOverARealBus(t *testing.T) { Retention: jetstream.WorkQueuePolicy, Discard: jetstream.DiscardNew, MaxAge: 24 * time.Hour, MaxBytes: 8 << 20}); err != nil { t.Fatal(err) } - account := process.Account{URL: url, Node: "g14", Module: "brother", User: "g14.brother." + AdminProcessID, Password: "pw"} + account := process.Account{URL: url, Node: "g14", Module: "brother", User: "g14.brother." + DeviceProcessID, Password: "pw"} if _, err := js.CreateConsumer(ctx, AsksStream, jetstream.ConsumerConfig{Durable: account.QueueConsumerName(), AckPolicy: jetstream.AckExplicitPolicy, AckWait: 30 * time.Second, MaxDeliver: 3}); err != nil { t.Fatal(err) @@ -80,14 +80,14 @@ func TestTheAdminProcessAnswersAnAskOverARealBus(t *testing.T) { os.WriteFile(settings, []byte(`{"printer":"`+f.host()+`","printer-certificate":"`+f.print()+`"}`), 0o600) pwFile := filepath.Join(dir, "admin-password.secret") os.WriteFile(pwFile, []byte("pw\n"), 0o600) - w := &adminWorker{ + w := &deviceWorker{ Settings: func() (Settings, error) { return LoadSettings(settings) }, Password: func() (string, error) { return secret.Read(pwFile) }, Now: time.Now, } runCtx, cancel := context.WithCancel(ctx) done := make(chan error, 1) - go func() { done <- runAdmin(runCtx, account, w) }() + go func() { done <- runDevice(runCtx, account, w) }() body, _ := json.Marshal(Ask{ID: "ask-1", Page: "/general/other.html", At: time.Now()}) if _, err := js.Publish(ctx, printerAskedSubject, body, jetstream.WithMsgID("ask-1")); err != nil { @@ -111,10 +111,10 @@ func TestTheAdminProcessAnswersAnAskOverARealBus(t *testing.T) { select { case err := <-done: if err == nil { - t.Fatal("runAdmin returned nil, want ctx.Err() once its context was cancelled") + t.Fatal("runDevice returned nil, want ctx.Err() once its context was cancelled") } case <-time.After(10 * time.Second): - t.Fatal("runAdmin did not stop once its context was cancelled") + t.Fatal("runDevice did not stop once its context was cancelled") } cons, err := js.Consumer(ctx, AsksStream, account.QueueConsumerName()) if err != nil { diff --git a/modules/brother/cmd/brother/adminproc.go b/modules/brother/cmd/brother/deviceproc.go similarity index 70% rename from modules/brother/cmd/brother/adminproc.go rename to modules/brother/cmd/brother/deviceproc.go index e758e9bc..b96d0577 100644 --- a/modules/brother/cmd/brother/adminproc.go +++ b/modules/brother/cmd/brother/deviceproc.go @@ -1,9 +1,9 @@ package main -// mesh brother admin (novox/hq ADR 0467 clause 4, in the shape of ADR 0436 §7): the admin process, a process -// with a bus account of its own (brother-admin), never a seat. It alone of the module's parts reads the admin -// password and logs in to the printer's admin page; the tools queue an ask on the work-queue stream printer-asks -// and read its answer in the state printer-answers. The admin password is never in an answer, an event or a log. +// mesh brother device (novox/hq ADR 0467 clause 4, in the shape of ADR 0436 §7): the device process, a process +// with a bus account of its own (brother-device), never a seat. It alone of the module's parts reads the admin +// password and logs in to the printer's admin page; the tools queue an ask on the work-queue stream device-asks +// and read its answer in the state device-answers. The admin password is never in an answer, an event or a log. import ( "context" @@ -35,9 +35,9 @@ const ( actionTerm ) -// adminWorker answers the asks. Settings and the password are read at each ask, so a changed config value or a +// deviceWorker answers the asks. Settings and the password are read at each ask, so a changed config value or a // password given later is the next ask's. -type adminWorker struct { +type deviceWorker struct { Settings func() (Settings, error) Password func() (string, error) Answers Answers @@ -45,7 +45,7 @@ type adminWorker struct { Logf func(format string, a ...any) } -func (w *adminWorker) logf(format string, a ...any) { +func (w *deviceWorker) logf(format string, a ...any) { if w.Logf != nil { w.Logf(format, a...) } @@ -55,14 +55,14 @@ func (w *adminWorker) logf(format string, a ...any) { // for is acknowledged and the printer not contacted; every other outcome, a failure included, is written as the // answer under the ask's id and the message acknowledged; only an answer that could not be written is // redelivered (the bus's limit ends that). -func (w *adminWorker) Handle(body []byte) (action, time.Duration) { +func (w *deviceWorker) Handle(body []byte) (action, time.Duration) { var ask Ask if err := json.Unmarshal(body, &ask); err != nil || ask.ID == "" { - w.logf("[brother] admin: a queued ask could not be read: %v", err) + w.logf("[brother] device: a queued ask could not be read: %v", err) return actionTerm, 0 } if w.Now().Sub(ask.At) > StaleAfter { - w.logf("[brother] admin: ask %s is %s old; nobody waits for it, the printer is not contacted", ask.ID, w.Now().Sub(ask.At).Round(time.Second)) + w.logf("[brother] device: ask %s is %s old; nobody waits for it, the printer is not contacted", ask.ID, w.Now().Sub(ask.At).Round(time.Second)) return actionAck, 0 } ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) @@ -74,7 +74,7 @@ func (w *adminWorker) Handle(body []byte) (action, time.Duration) { answer = page } if _, err := w.Answers.Create(ask.ID, answer); err != nil && !errors.Is(err, stdio.ErrChanged) { - w.logf("[brother] admin: the answer to %s could not be written: %v", ask.ID, err) + w.logf("[brother] device: the answer to %s could not be written: %v", ask.ID, err) return actionRetry, 5 * time.Second } return actionAck, 0 @@ -83,7 +83,7 @@ func (w *adminWorker) Handle(body []byte) (action, time.Duration) { // read logs in, reads one page of the admin site and logs out. It is read-only: the login (and the logout) are // the only things it sends, and a password field's value is never in the answer. The printer is not contacted // before the settings and the password are known to be given (1453/AC4). -func (w *adminWorker) read(ctx context.Context, page string) (map[string]any, error) { +func (w *deviceWorker) read(ctx context.Context, page string) (map[string]any, error) { if page == "" { page = DefaultPage } @@ -143,40 +143,40 @@ func nonNilForms(f []Form) []Form { return f } -// admin is `brother admin`: it reads its own bus account, connects, and consumes printer-asks until stopped. +// admin is `brother device`: it reads its own bus account, connects, and consumes device-asks until stopped. // It refuses to start naming what is missing when the account cannot be read, and never logs a password. -func admin() int { +func device() int { settings, passwordFile, brokerFile := os.Getenv("BROTHER_SETTINGS"), os.Getenv("BROTHER_ADMIN_PASSWORD"), os.Getenv("BROTHER_BROKER") if brokerFile == "" { - fmt.Fprintln(os.Stderr, "[brother] admin was started without BROTHER_BROKER, its bus account's file") + fmt.Fprintln(os.Stderr, "[brother] device was started without BROTHER_BROKER, its bus account's file") return 2 } account, err := process.ReadAccount(brokerFile) if err != nil { - fmt.Fprintf(os.Stderr, "[brother] admin: %v\n", err) + fmt.Fprintf(os.Stderr, "[brother] device: %v\n", err) return 1 } ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() - w := &adminWorker{ + w := &deviceWorker{ Settings: func() (Settings, error) { return LoadSettings(settings) }, Password: func() (string, error) { return secret.Read(passwordFile) }, Now: time.Now, Logf: func(f string, a ...any) { fmt.Fprintf(os.Stderr, f+"\n", a...) }, } - fmt.Fprintf(os.Stderr, "[brother] admin is consuming %s as %s\n", AsksStream, account.User) - err = runAdmin(ctx, account, w) + fmt.Fprintf(os.Stderr, "[brother] device is consuming %s as %s\n", AsksStream, account.User) + err = runDevice(ctx, account, w) if err != nil && ctx.Err() == nil { - fmt.Fprintf(os.Stderr, "[brother] admin stopped: %v\n", err) + fmt.Fprintf(os.Stderr, "[brother] device stopped: %v\n", err) return 1 } return 0 } -// runAdmin connects as the admin process's own account and consumes printer-asks until ctx ends, answering each -// ask in the state printer-answers on that connection and then settling the message as Handle says. Apart from +// runDevice connects as the device process's own account and consumes device-asks until ctx ends, answering each +// ask in the state device-answers on that connection and then settling the message as Handle says. Apart from // admin so a test can drive it over a bus of its own. -func runAdmin(ctx context.Context, account process.Account, w *adminWorker) error { +func runDevice(ctx context.Context, account process.Account, w *deviceWorker) error { conn, err := process.Connect(account) if err != nil { return fmt.Errorf("the bus cannot be reached: %w", err) @@ -195,7 +195,7 @@ func runAdmin(ctx context.Context, account process.Account, w *adminWorker) erro aerr = msg.Term() } if aerr != nil { - w.logf("[brother] admin: settling %s: %v", msg.Subject, aerr) + w.logf("[brother] device: settling %s: %v", msg.Subject, aerr) } }) } diff --git a/modules/brother/cmd/brother/main.go b/modules/brother/cmd/brother/main.go index af076c4a..22802571 100644 --- a/modules/brother/cmd/brother/main.go +++ b/modules/brother/cmd/brother/main.go @@ -10,8 +10,8 @@ import ( ) func main() { - if len(os.Args) > 1 && os.Args[1] == "admin" { - os.Exit(admin()) + if len(os.Args) > 1 && os.Args[1] == "device" { + os.Exit(device()) } if err := stdio.Serve("", tools(os.Getenv("BROTHER_SETTINGS"), emit, time.Now)); err != nil { fmt.Fprintln(os.Stderr, err) @@ -54,7 +54,7 @@ func tools(settingsFile string, say Emitter, now func() time.Time) []stdio.Tool }, { Name: "printer.settings", - Description: "Read one page of the printer's admin site, read-only: the module's admin process logs in with the " + + Description: "Read one page of the printer's admin site, read-only: the module's device process logs in with the " + "secret admin-password (the operator's; it trusts the printer's pinned certificate alone), gets the page and logs " + "out; this tool asks it and waits up to a minute for its answer. Answers the " + "page's title, its links (href and text) and its forms with their fields (name, type, value, label); a " + diff --git a/modules/brother/cmd/brother/manifest_test.go b/modules/brother/cmd/brother/manifest_test.go index 32f631e2..f98f8342 100644 --- a/modules/brother/cmd/brother/manifest_test.go +++ b/modules/brother/cmd/brother/manifest_test.go @@ -190,9 +190,9 @@ func resource(t *testing.T, m manifest, id string) map[string]any { } // 1453/AC4 (ADR 0467 clause 4, ADR 0436 §1): the admin password is the module's own secret, issued by the -// operator (never generated), and owned by an account the module makes, brother-admin: the tools' runner -// (the operator account) cannot read it, the admin process (that account) can. The process's bus account is -// the own secret broker-admin, owned by the same account. +// operator (never generated), and owned by an account the module makes, brother-device: the tools' runner +// (the operator account) cannot read it, the device process (that account) can. The process's bus account is +// the own secret broker-device, owned by the same account. func TestTheAdminPasswordIsTheOperatorsOwnSecretOwnedByTheAdminAccount(t *testing.T) { m := readManifest(t) s, ok := m.OwnSecrets["admin-password"] @@ -202,35 +202,35 @@ func TestTheAdminPasswordIsTheOperatorsOwnSecretOwnedByTheAdminAccount(t *testin if s["issued-by"] != "outside" { t.Errorf("issued-by = %v, want outside: the mesh never generates the device's password", s["issued-by"]) } - if s["owner"] != "brother-admin" { - t.Errorf("owner = %v, want brother-admin (never root or ${machine:account}: ADR 0436 §1)", s["owner"]) + if s["owner"] != "brother-device" { + t.Errorf("owner = %v, want brother-device (never root or ${machine:account}: ADR 0436 §1)", s["owner"]) } if p, _ := s["prompt"].(string); p != "the admin password of the Brother printer" { t.Errorf("prompt = %q", p) } - if b := m.OwnSecrets["broker-admin"]; b["owner"] != "brother-admin" || b["path"] != "${dir:state}/broker-admin.secret" { - t.Errorf("broker-admin = %v", b) + if b := m.OwnSecrets["broker-device"]; b["owner"] != "brother-device" || b["path"] != "${dir:state}/broker-device.secret" { + t.Errorf("broker-device = %v", b) } acct := resource(t, m, "account") - if acct["type"] != "user" || acct["name"] != "brother-admin" || acct["shell"] != "/usr/bin/nologin" { + if acct["type"] != "user" || acct["name"] != "brother-device" || acct["shell"] != "/usr/bin/nologin" { t.Errorf("account = %v", acct) } } -// 1453 (ADR 0467 clause 4): the admin process runs as brother-admin with the bus access of ADR 0436 §7: it -// consumes the work queue printer-asks and writes the state printer-answers, and is judged by its unit. The +// 1453 (ADR 0467 clause 4): the device process runs as brother-device with the bus access of ADR 0436 §7: it +// consumes the work queue device-asks and writes the state device-answers, and is judged by its unit. The // tools' bundle is given no password file. func TestTheAdminProcessIsDeclaredAsMeshMailsSenderIs(t *testing.T) { m := readManifest(t) - p := resource(t, m, "admin") - if p["type"] != "process" || p["name"] != "brother-admin" || p["user"] != "brother-admin" || p["artifact"] != "tools" { + p := resource(t, m, "device") + if p["type"] != "process" || p["name"] != "brother-device" || p["user"] != "brother-device" || p["artifact"] != "tools" { t.Errorf("process = %v", p) } - if !reflect.DeepEqual(p["run"], []any{"./brother", "admin"}) { + if !reflect.DeepEqual(p["run"], []any{"./brother", "device"}) { t.Errorf("run = %v", p["run"]) } bus, _ := p["bus"].(map[string]any) - if !reflect.DeepEqual(bus["queues"], []any{"printer-asks"}) || !reflect.DeepEqual(bus["state"], []any{"printer-answers"}) { + if !reflect.DeepEqual(bus["queues"], []any{"device-asks"}) || !reflect.DeepEqual(bus["state"], []any{"device-answers"}) { t.Errorf("bus = %v", bus) } if h, _ := p["health"].(map[string]any); h["kind"] != "unit" { @@ -240,7 +240,7 @@ func TestTheAdminProcessIsDeclaredAsMeshMailsSenderIs(t *testing.T) { want := map[string]any{ "BROTHER_SETTINGS": "${dir:state}/settings.json", "BROTHER_ADMIN_PASSWORD": m.OwnSecrets["admin-password"]["path"], - "BROTHER_BROKER": m.OwnSecrets["broker-admin"]["path"], + "BROTHER_BROKER": m.OwnSecrets["broker-device"]["path"], } if !reflect.DeepEqual(env, want) { t.Errorf("env = %v, want %v", env, want) @@ -260,7 +260,7 @@ func TestTheAdminProcessIsDeclaredAsMeshMailsSenderIs(t *testing.T) { t.Errorf("state mode = %v: the admin account must reach its files in it", r["mode"]) } for _, r := range m.Resources { - if r["id"] != "admin" { + if r["id"] != "device" { if r["takes-config"] != nil { for _, k := range r["takes-config"].([]any) { if k == "admin-password" { @@ -272,7 +272,7 @@ func TestTheAdminProcessIsDeclaredAsMeshMailsSenderIs(t *testing.T) { } } -// 1453 (ADR 0467 clause 4): the stream printer-asks is a work queue like mesh-mail's mail-sends, and the +// 1453 (ADR 0467 clause 4): the stream device-asks is a work queue like mesh-mail's mail-sends, and the // answers are a state that expires. func TestThePrinterAsksStreamIsAWorkQueue(t *testing.T) { raw, _ := os.ReadFile(filepath.Join("..", "..", "module.json")) @@ -287,7 +287,7 @@ func TestThePrinterAsksStreamIsAWorkQueue(t *testing.T) { t.Fatalf("streams = %v", m.Streams) } st := m.Streams[0] - if st["name"] != "printer-asks" || st["retention"] != "work-queue" || st["max-deliveries"] != float64(3) || + if st["name"] != "device-asks" || st["retention"] != "work-queue" || st["max-deliveries"] != float64(3) || !reflect.DeepEqual(st["events"], []any{EventAsked}) { t.Errorf("stream = %v", st) } diff --git a/modules/brother/cmd/brother/printer.go b/modules/brother/cmd/brother/printer.go index 071ef18f..29436fcb 100644 --- a/modules/brother/cmd/brother/printer.go +++ b/modules/brother/cmd/brother/printer.go @@ -20,24 +20,24 @@ var pagePattern = regexp.MustCompile(`^/[A-Za-z0-9_./-]*\.html$`) // DefaultPage is the page printer.settings reads when none is asked for. const DefaultPage = "/general/status.html" -// The ask printer.settings puts to the admin process (ADR 0467 clause 4; the shape of ADR 0436's mail-sends): -// the tool never holds the admin password, so it queues the ask on the work-queue stream printer-asks and the -// admin process, an account of its own that alone reads the password, answers under the ask's id in the state -// printer-answers. +// The ask printer.settings puts to the device process (ADR 0467 clause 4; the shape of ADR 0436's mail-sends): +// the tool never holds the admin password, so it queues the ask on the work-queue stream device-asks and the +// device process, an account of its own that alone reads the password, answers under the ask's id in the state +// device-answers. const ( - // EventAsked is the event on the work queue; the stream printer-asks carries it. - EventAsked = "printer.asked" - // AsksStream is the work-queue stream's name on the bus: the manifest's local name printer-asks, capitalised + // EventAsked is the event on the work queue; the stream device-asks carries it. + EventAsked = "device.asked" + // AsksStream is the work-queue stream's name on the bus: the manifest's local name device-asks, capitalised // with its hyphen an underscore, as the controller composes a stream's own bus name. - AsksStream = "PRINTER_ASKS" - // AnswersState is the state the admin process writes its answers in. - AnswersState = "printer-answers" - // AdminProcessID is the admin process's id as module.json's resource "admin" names it: the controller - // delivers its bus account as the own secret broker-admin. - AdminProcessID = "admin" - // AnswerWait is how long printer.settings waits for the admin process. + AsksStream = "DEVICE_ASKS" + // AnswersState is the state the device process writes its answers in. + AnswersState = "device-answers" + // DeviceProcessID is the device process's id as module.json's resource "device" names it: the controller + // delivers its bus account as the own secret broker-device. + DeviceProcessID = "device" + // AnswerWait is how long printer.settings waits for the device process. AnswerWait = 60 * time.Second - // StaleAfter is how old an ask may be when the admin process takes it: older than twice the wait, nobody waits + // StaleAfter is how old an ask may be when the device process takes it: older than twice the wait, nobody waits // for it any more, so the printer is not contacted for it. StaleAfter = 2 * AnswerWait ) @@ -49,7 +49,7 @@ type Ask struct { At time.Time `json:"asked-at"` } -// AskBus is how the tool reaches the admin process: it queues an ask and reads an answer back. +// AskBus is how the tool reaches the device process: it queues an ask and reads an answer back. type AskBus interface { Publish(id string, body any) error Answer(id string) (json.RawMessage, error) @@ -84,7 +84,7 @@ func newAskID() string { return hex.EncodeToString(b) } -// PrinterSettings asks the admin process to log in, read one page of the admin site and log out, and waits for +// PrinterSettings asks the device process to log in, read one page of the admin site and log out, and waits for // its answer. It checks the page and that printer and printer-certificate are set, and reads, opens and sends // nothing of the admin password (ADR 0467 clause 4; ADR 0436 §12): nothing is queued before those checks pass. func (s *Service) PrinterSettings(ctx context.Context, page string) (map[string]any, error) { @@ -106,19 +106,19 @@ func (s *Service) PrinterSettings(ctx context.Context, page string) (map[string] } ask := Ask{ID: newAskID(), Page: page, At: s.Now().UTC()} if err := s.Asks.Publish(ask.ID, ask); err != nil { - return nil, fmt.Errorf("the ask for the admin process could not be queued: %w", err) + return nil, fmt.Errorf("the ask for the device process could not be queued: %w", err) } ctx, cancel := context.WithTimeout(ctx, wait) defer cancel() for { raw, err := s.Asks.Answer(ask.ID) if err != nil { - return nil, fmt.Errorf("the admin process's answers cannot be read: %w", err) + return nil, fmt.Errorf("the device process's answers cannot be read: %w", err) } if raw != nil { var out map[string]any if err := json.Unmarshal(raw, &out); err != nil { - return nil, fmt.Errorf("the admin process's answer is not JSON: %w", err) + return nil, fmt.Errorf("the device process's answer is not JSON: %w", err) } if msg, bad := out["error"].(string); bad { return nil, fmt.Errorf("%s", msg) @@ -127,7 +127,7 @@ func (s *Service) PrinterSettings(ctx context.Context, page string) (map[string] } select { case <-ctx.Done(): - return nil, fmt.Errorf("the admin process did not answer within %s: it may not be running, or the printer is slow (ask %s)", + return nil, fmt.Errorf("the device process did not answer within %s: it may not be running, or the printer is slow (ask %s)", wait, ask.ID) case <-time.After(poll): } diff --git a/modules/brother/cmd/brother/printer_test.go b/modules/brother/cmd/brother/printer_test.go index cb1dabce..679c6f2c 100644 --- a/modules/brother/cmd/brother/printer_test.go +++ b/modules/brother/cmd/brother/printer_test.go @@ -15,7 +15,7 @@ import ( "brother/cmd/brother/internal/secret" ) -// memAnswers is the state printer-answers, in memory. +// memAnswers is the state device-answers, in memory. type memAnswers struct { mu sync.Mutex m map[string]json.RawMessage @@ -46,10 +46,10 @@ func (a *memAnswers) get(key string) json.RawMessage { return a.m[key] } -// fakeAskBus is the bus between the tool and the admin process: an ask published on it is handed to the -// worker, as the work queue hands it to the admin process, and the worker's answers are what the tool reads. +// fakeAskBus is the bus between the tool and the device process: an ask published on it is handed to the +// worker, as the work queue hands it to the device process, and the worker's answers are what the tool reads. type fakeAskBus struct { - worker *adminWorker // nil: nobody consumes the queue + worker *deviceWorker // nil: nobody consumes the queue answers *memAnswers mu sync.Mutex published []Ask @@ -81,7 +81,7 @@ func (b *fakeAskBus) asks() int { type printerRig struct { run func(args map[string]any) (any, error) bus *fakeAskBus - worker *adminWorker + worker *deviceWorker answers *memAnswers } @@ -95,7 +95,7 @@ func newRig(t *testing.T, settings, password string) *printerRig { } file := settingsFile(t, settings) answers := &memAnswers{} - w := &adminWorker{ + w := &deviceWorker{ Settings: func() (Settings, error) { return LoadSettings(file) }, Password: func() (string, error) { return secret.Read(secretFile) }, Answers: answers, @@ -231,7 +231,7 @@ func TestPrinterSettingsSaysWhichConfigIsUnsetAndQueuesNothing(t *testing.T) { } } -// 1453/AC4: the secret not given is said by name, by the admin process, and the printer is not contacted. +// 1453/AC4: the secret not given is said by name, by the device process, and the printer is not contacted. func TestPrinterSettingsSaysTheSecretIsNotGivenAndContactsNothing(t *testing.T) { f := newFakePrinter(t, "B1891", "pw") _, run := printerTools(t, f, "") @@ -263,7 +263,7 @@ func TestPrinterSettingsRefusesAPageOutsideTheSite(t *testing.T) { t.Errorf("page %q: error = %v", p, err) } if _, err := r.worker.read(t.Context(), p); err == nil || !strings.Contains(err.Error(), "not a page") { - t.Errorf("the admin process, page %q: error = %v", p, err) + t.Errorf("the device process, page %q: error = %v", p, err) } } if _, err := r.run(map[string]any{"page": 5}); err == nil { @@ -274,14 +274,14 @@ func TestPrinterSettingsRefusesAPageOutsideTheSite(t *testing.T) { } } -// 1453: nobody consuming the queue is said as the admin process not answering, naming the wait. +// 1453: nobody consuming the queue is said as the device process not answering, naming the wait. func TestPrinterSettingsSaysWhenTheAdminProcessDoesNotAnswer(t *testing.T) { f := newFakePrinter(t, "B1891", "pw") r := newRig(t, printerSettingsJSON(f), "pw") r.bus.worker = nil answerWait = 100 * time.Millisecond _, err := r.run(nil) - if err == nil || !strings.Contains(err.Error(), "admin process did not answer") { + if err == nil || !strings.Contains(err.Error(), "device process did not answer") { t.Fatalf("error = %v", err) } if r.bus.asks() != 1 { @@ -289,7 +289,7 @@ func TestPrinterSettingsSaysWhenTheAdminProcessDoesNotAnswer(t *testing.T) { } } -// 1453: the admin process terminates an ask it cannot read, drops one nobody waits for any more without +// 1453: the device process terminates an ask it cannot read, drops one nobody waits for any more without // contacting the printer, acknowledges one already answered, and asks for redelivery when it cannot write its // answer. func TestTheAdminWorkerSettlesEachAsk(t *testing.T) { diff --git a/modules/brother/cmd/brother/scan.go b/modules/brother/cmd/brother/scan.go index 8c1bbc6b..b10279cf 100644 --- a/modules/brother/cmd/brother/scan.go +++ b/modules/brother/cmd/brother/scan.go @@ -23,9 +23,9 @@ type Service struct { Node string Now func() time.Time Emit Emitter - // Asks is how printer.settings reaches the admin process, which alone logs in to the printer. + // Asks is how printer.settings reaches the device process, which alone logs in to the printer. Asks AskBus - // AnswerWait is how long printer.settings waits for the admin process's answer; AnswerPoll how often it looks. + // AnswerWait is how long printer.settings waits for the device process's answer; AnswerPoll how often it looks. AnswerWait, AnswerPoll time.Duration } diff --git a/modules/brother/module.json b/modules/brother/module.json index 8b35310b..94457484 100644 --- a/modules/brother/module.json +++ b/modules/brother/module.json @@ -67,18 +67,18 @@ "path": "${dir:state}/admin-password.secret", "issued-by": "outside", "prompt": "the admin password of the Brother printer", - "owner": "brother-admin" + "owner": "brother-device" }, - "broker-admin": { - "path": "${dir:state}/broker-admin.secret", - "owner": "brother-admin" + "broker-device": { + "path": "${dir:state}/broker-device.secret", + "owner": "brother-device" } }, "streams": [ { - "name": "printer-asks", + "name": "device-asks", "events": [ - "printer.asked" + "device.asked" ], "max-age-days": 1, "max-mib": 8, @@ -88,7 +88,7 @@ ], "state": [ { - "name": "printer-answers", + "name": "device-answers", "ttl-seconds": 600 } ], @@ -141,7 +141,7 @@ { "id": "account", "type": "user", - "name": "brother-admin", + "name": "brother-device", "shell": "/usr/bin/nologin" }, { @@ -171,29 +171,29 @@ "template": "files/settings.json" }, { - "id": "admin", + "id": "device", "type": "process", - "name": "brother-admin", + "name": "brother-device", "artifact": "tools", "run": [ "./brother", - "admin" + "device" ], - "user": "brother-admin", + "user": "brother-device", "env": { "BROTHER_SETTINGS": "${dir:state}/settings.json", "BROTHER_ADMIN_PASSWORD": "${dir:state}/admin-password.secret", - "BROTHER_BROKER": "${dir:state}/broker-admin.secret" + "BROTHER_BROKER": "${dir:state}/broker-device.secret" }, "restart-on": [ "settings" ], "bus": { "queues": [ - "printer-asks" + "device-asks" ], "state": [ - "printer-answers" + "device-answers" ] }, "health": {