brother: the admin process becomes the device process, brother-device, with device-asks and device-answers (issue 1453, ADR 0468 clause 1)

This commit is contained in:
2026-10-20 21:23:28 +02:00
parent adfb61c053
commit feada9fb14
10 changed files with 118 additions and 118 deletions
+7 -7
View File
@@ -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
@@ -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)
@@ -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 {
@@ -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)
}
})
}
+3 -3
View File
@@ -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 " +
+18 -18
View File
@@ -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)
}
+21 -21
View File
@@ -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):
}
+11 -11
View File
@@ -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) {
+2 -2
View File
@@ -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
}
+15 -15
View File
@@ -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": {