webui: the unit script takes --send (#1576)
Row 83. scripts/webui-service.sh render and install take --send, only
with the full preview set; otherwise the script refuses before writing
anything. With --send the rendered ExecStart ends in --send; renders
without it are byte-identical to the base. status says whether the
installed unit sends. The script header, install message, README and
TOOLS.md carry the decision 102 start wording: Jason or Mos starts and
restarts the Console, no other agent.
Dewey's round 1, candidate manifest f764cd9e, base d13b9300. Approved
by Darkwing (27721) and Filbert (27722). Sage's gate green 04:07:30Z to
04:10:47Z.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
+14
-7
@@ -246,17 +246,22 @@ scripts/webui-service.sh install [--dir DIR] [--no-reload] [FLAGS]
|
|||||||
scripts/webui-service.sh uninstall [--dir DIR] [--no-reload]
|
scripts/webui-service.sh uninstall [--dir DIR] [--no-reload]
|
||||||
scripts/webui-service.sh status [--dir DIR]
|
scripts/webui-service.sh status [--dir DIR]
|
||||||
# FLAGS: [--port N] [--business ID] [--board URL]
|
# FLAGS: [--port N] [--business ID] [--board URL]
|
||||||
# [--public-host NAME --auth-header HEADER --allow-user ID]
|
# [--public-host NAME --auth-header HEADER --allow-user ID [--send]]
|
||||||
```
|
```
|
||||||
|
|
||||||
The systemd user unit `mosaic-webui.service` runs the Console,
|
The systemd user unit `mosaic-webui.service` runs the Console,
|
||||||
`node packages/webui/src/cli.mjs serve`, with serve's flags as given:
|
`node packages/webui/src/cli.mjs serve`, with serve's flags as given:
|
||||||
`--port` defaults to 7332 (7330 is the operator's own Console), and the
|
`--port` defaults to 7332 (7330 is the operator's own Console), and the
|
||||||
preview set goes all three or none. The template is
|
preview set goes all three or none. `--send` (row 83) goes only with the
|
||||||
|
preview set and is refused otherwise (exit 1, nothing written); ExecStart
|
||||||
|
then ends with `--send`, and the Console needs the system config at start
|
||||||
|
(`--send needs the bus configured`, exit 2). Without `--send` the unit is
|
||||||
|
read-only. The template is
|
||||||
`packages/webui/systemd/mosaic-webui.service.in`. `install` writes a temp
|
`packages/webui/systemd/mosaic-webui.service.in`. `install` writes a temp
|
||||||
file, renames it and runs `systemctl --user daemon-reload`; `uninstall`
|
file, renames it and runs `systemctl --user daemon-reload`; `uninstall`
|
||||||
refuses while the unit is active; `status` prints the unit state and
|
refuses while the unit is active; `status` prints the unit state, whether
|
||||||
`GET /healthz` on the installed port. Each value is held to a plain
|
the installed unit sends, and `GET /healthz` on the installed port. Each
|
||||||
|
value is held to a plain
|
||||||
character set the unit can carry (no space, `%`, `$`, quote, backslash,
|
character set the unit can carry (no space, `%`, `$`, quote, backslash,
|
||||||
`&` or `|`) and refused otherwise. The unit sets no `MOSAIC_*`, `CLAUDE*`,
|
`&` or `|`) and refused otherwise. The unit sets no `MOSAIC_*`, `CLAUDE*`,
|
||||||
`CODEX*` or `PI_*` variable. Serve's exit 2 (usage, config, or a port
|
`CODEX*` or `PI_*` variable. Serve's exit 2 (usage, config, or a port
|
||||||
@@ -264,9 +269,11 @@ already in use) is not restarted.
|
|||||||
|
|
||||||
There is no start, stop, enable or restart verb. The Console reads the bus
|
There is no start, stop, enable or restart verb. The Console reads the bus
|
||||||
through the human transport, so whoever starts it holds the human's
|
through the human transport, so whoever starts it holds the human's
|
||||||
capability; an agent starting it, even through systemd, works around the
|
capability. Jason starts and restarts it
|
||||||
human proof (lead decisions 62 and 94). Jason starts it from his own shell:
|
(`systemctl --user enable --now mosaic-webui`), and so may Mos under lead
|
||||||
`systemctl --user enable --now mosaic-webui`.
|
decision 102, which accepts that sends from a Console Mos started enter the
|
||||||
|
trail as Jason (`human-cli`). Any other agent starting it, even through
|
||||||
|
systemd, works around the human proof (lead decisions 62 and 94).
|
||||||
Exit codes: `0` ok · `1` failed or refused · `4` usage.
|
Exit codes: `0` ok · `1` failed or refused · `4` usage.
|
||||||
|
|
||||||
## Discord connector (`scripts/discord.sh`)
|
## Discord connector (`scripts/discord.sh`)
|
||||||
|
|||||||
@@ -100,8 +100,15 @@ Under `--send` the first of those can also send (next section).
|
|||||||
|
|
||||||
To run the Console as a systemd user unit, `scripts/webui-service.sh`
|
To run the Console as a systemd user unit, `scripts/webui-service.sh`
|
||||||
renders and installs one with these flags (row 75; `docs/TOOLS.md`
|
renders and installs one with these flags (row 75; `docs/TOOLS.md`
|
||||||
"Console service"). Jason starts it from his own shell (lead decision 94).
|
"Console service"). Jason starts and restarts it, and so may Mos under lead
|
||||||
The script takes no `--send`, so a unit it installs is read-only.
|
decision 102, which accepts that sends from a Console Mos started enter the
|
||||||
|
trail as Jason. Any other agent starting it works around the human proof
|
||||||
|
(lead decisions 62 and 94). Without `--send` the unit is read-only. The script takes `--send` (row 83)
|
||||||
|
only with the three preview flags; otherwise it exits 1 and writes nothing.
|
||||||
|
With it, the unit's ExecStart ends with `--send`, and the Console needs the
|
||||||
|
system config at start: without it serve refuses with `--send needs the bus
|
||||||
|
configured` and exits 2, which the unit doesn't restart. `status` says
|
||||||
|
whether the installed unit sends.
|
||||||
|
|
||||||
## Message a role (row 76, #1570)
|
## Message a role (row 76, #1570)
|
||||||
|
|
||||||
@@ -113,9 +120,11 @@ node packages/webui/src/cli.mjs serve --port 7332 \
|
|||||||
`--send` is off by default. It goes only with the three preview flags and
|
`--send` is off by default. It goes only with the three preview flags and
|
||||||
refuses at start without them. It also needs the system config: without it
|
refuses at start without them. It also needs the system config: without it
|
||||||
the start refuses with `--send needs the bus configured`, where a read-only
|
the start refuses with `--send needs the bus configured`, where a read-only
|
||||||
Console would still serve the board. With `--send` the Inbox has a "Message
|
Console would still serve the board. A unit that sends comes from
|
||||||
a role" box: a role picker filled from the agents read, a text box and
|
`scripts/webui-service.sh install` with `--send` (above). With `--send` the
|
||||||
Send. The message reaches the role as a message from the human, class INFO,
|
Inbox has a "Message a role" box: a role picker filled from the agents read,
|
||||||
|
a text box and Send. The message reaches the role as a message from the
|
||||||
|
human, class INFO,
|
||||||
through `message.send` on the human transport. It goes the same way as one
|
through `message.send` on the human transport. It goes the same way as one
|
||||||
sent from the CLI, with class INFO where `mosaic talk` uses REQUEST (lead
|
sent from the CLI, with class INFO where `mosaic talk` uses REQUEST (lead
|
||||||
decision 95). Decisions are still answered in a
|
decision 95). Decisions are still answered in a
|
||||||
|
|||||||
@@ -11,10 +11,35 @@ import { fileURLToPath } from 'node:url';
|
|||||||
// scripts/webui-service.sh (row 75, #1569). Nothing here starts a unit or
|
// scripts/webui-service.sh (row 75, #1569). Nothing here starts a unit or
|
||||||
// reaches the real systemd: install and uninstall run with --no-reload or a
|
// reaches the real systemd: install and uninstall run with --no-reload or a
|
||||||
// stand-in systemctl on PATH, and XDG_CONFIG_HOME points into a temp dir.
|
// stand-in systemctl on PATH, and XDG_CONFIG_HOME points into a temp dir.
|
||||||
|
// Row 83 (#1576) adds --send, taken only with the preview set.
|
||||||
const REPO = fileURLToPath(new URL('../../..', import.meta.url)).replace(/\/$/, '');
|
const REPO = fileURLToPath(new URL('../../..', import.meta.url)).replace(/\/$/, '');
|
||||||
const SCRIPT = join(REPO, 'scripts', 'webui-service.sh');
|
const SCRIPT = join(REPO, 'scripts', 'webui-service.sh');
|
||||||
const NODE_DIR = dirname(process.execPath);
|
const NODE_DIR = dirname(process.execPath);
|
||||||
const PREVIEW = ['--public-host', 'console.example.com', '--auth-header', 'X-authentik-uid', '--allow-user', 'jason'];
|
const PREVIEW = ['--public-host', 'console.example.com', '--auth-header', 'X-authentik-uid', '--allow-user', 'jason'];
|
||||||
|
const PREVIEW_FLAGS = '--public-host console.example.com --auth-header X-authentik-uid --allow-user jason';
|
||||||
|
const SEND_REFUSAL = /^webui-service: --send needs the preview mode \(--public-host, --auth-header and --allow-user\)$/m;
|
||||||
|
const TEMPLATE = readFileSync(join(REPO, 'packages', 'webui', 'systemd', 'mosaic-webui.service.in'), 'utf8');
|
||||||
|
|
||||||
|
// The unit as the base script renders it (row 75): the template with its four
|
||||||
|
// placeholders filled in, nothing else. Row 83 adds --send and must leave
|
||||||
|
// every unit rendered without it byte for byte as this.
|
||||||
|
function filled(flags) {
|
||||||
|
const base = '/usr/local/bin:/usr/bin:/bin';
|
||||||
|
const path = `:${base}:`.includes(`:${NODE_DIR}:`) ? base : `${NODE_DIR}:${base}`;
|
||||||
|
return TEMPLATE.replaceAll('@REPO@', REPO).replaceAll('@PATH@', path).replaceAll('@NODE@', process.execPath).replaceAll('@FLAGS@', flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A loopback port with nothing listening, so status's /healthz request
|
||||||
|
// reaches no real Console.
|
||||||
|
async function closedPort() {
|
||||||
|
const server = createServer();
|
||||||
|
server.listen(0, '127.0.0.1');
|
||||||
|
await once(server, 'listening');
|
||||||
|
const { port } = server.address();
|
||||||
|
server.close();
|
||||||
|
await once(server, 'close');
|
||||||
|
return port;
|
||||||
|
}
|
||||||
|
|
||||||
function tmp(t) {
|
function tmp(t) {
|
||||||
const dir = mkdtempSync(join(tmpdir(), 'webui-service-'));
|
const dir = mkdtempSync(join(tmpdir(), 'webui-service-'));
|
||||||
@@ -114,7 +139,7 @@ test('the rendered unit names none of the human proof agent markers and sets no
|
|||||||
const source = readFileSync(join(REPO, 'packages', 'bus', 'src', 'human.mjs'), 'utf8');
|
const source = readFileSync(join(REPO, 'packages', 'bus', 'src', 'human.mjs'), 'utf8');
|
||||||
const markers = [...source.match(/const MARKERS = \[([^\]]*)\]/)[1].matchAll(/'([A-Z_]+)'/g)].map((m) => m[1]);
|
const markers = [...source.match(/const MARKERS = \[([^\]]*)\]/)[1].matchAll(/'([A-Z_]+)'/g)].map((m) => m[1]);
|
||||||
assert.ok(markers.length >= 7 && markers.includes('MOSAIC_BUS_CAP') && markers.includes('CLAUDECODE'), markers.join(' '));
|
assert.ok(markers.length >= 7 && markers.includes('MOSAIC_BUS_CAP') && markers.includes('CLAUDECODE'), markers.join(' '));
|
||||||
for (const args of [[], [...PREVIEW, '--business', 'mosaic']]) {
|
for (const args of [[], [...PREVIEW, '--business', 'mosaic'], [...PREVIEW, '--send']]) {
|
||||||
const r = await run(dir, ['render', ...args]);
|
const r = await run(dir, ['render', ...args]);
|
||||||
assert.equal(r.status, 0, r.stderr);
|
assert.equal(r.status, 0, r.stderr);
|
||||||
for (const marker of markers) assert.ok(!r.stdout.includes(marker), marker);
|
for (const marker of markers) assert.ok(!r.stdout.includes(marker), marker);
|
||||||
@@ -212,9 +237,129 @@ test('status prints the unit state and GET /healthz on the installed port', asyn
|
|||||||
|
|
||||||
test('usage: an unknown verb or flag, and start or enable, exit 4', async (t) => {
|
test('usage: an unknown verb or flag, and start or enable, exit 4', async (t) => {
|
||||||
const dir = tmp(t);
|
const dir = tmp(t);
|
||||||
for (const args of [[], ['start'], ['stop'], ['enable'], ['restart'], ['render', '--bogus'], ['render', '--port'], ['render', '--dir', dir], ['render', '--no-reload'], ['uninstall', '--port', '7332'], ['status', '--business', 'mosaic'], ['status', '--no-reload']]) {
|
for (const args of [[], ['start'], ['stop'], ['enable'], ['restart'], ['render', '--bogus'], ['render', '--port'], ['render', '--dir', dir], ['render', '--no-reload'], ['uninstall', '--port', '7332'], ['status', '--business', 'mosaic'], ['status', '--no-reload'], ['render', '--send', 'yes', ...PREVIEW]]) {
|
||||||
const r = await run(dir, args);
|
const r = await run(dir, args);
|
||||||
assert.equal(r.status, 4, args.join(' '));
|
assert.equal(r.status, 4, args.join(' '));
|
||||||
assert.match(r.stderr, /^usage: scripts\/webui-service\.sh/);
|
assert.match(r.stderr, /^usage: scripts\/webui-service\.sh/);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('without --send, render is the template with its placeholders filled, byte for byte', async (t) => {
|
||||||
|
const dir = tmp(t);
|
||||||
|
for (const [args, flags] of [
|
||||||
|
[[], '--port 7332'],
|
||||||
|
[['--port', '7340'], '--port 7340'],
|
||||||
|
[['--business', 'mosaic'], '--port 7332 --business mosaic'],
|
||||||
|
[PREVIEW, `--port 7332 ${PREVIEW_FLAGS}`],
|
||||||
|
[['--port', '7340', '--business', 'mosaic.dev_1', '--board', 'http://127.0.0.1:7331', ...PREVIEW], `--port 7340 --business mosaic.dev_1 --board http://127.0.0.1:7331 ${PREVIEW_FLAGS}`],
|
||||||
|
]) {
|
||||||
|
const r = await run(dir, ['render', ...args]);
|
||||||
|
assert.equal(r.status, 0, r.stderr);
|
||||||
|
assert.equal(r.stdout, filled(flags), args.join(' '));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('render --send with the preview set ends ExecStart with --send and changes nothing else', async (t) => {
|
||||||
|
const dir = tmp(t);
|
||||||
|
const extra = ['--port', '7340', '--business', 'mosaic'];
|
||||||
|
for (const args of [[...PREVIEW, '--send'], ['--send', ...PREVIEW], [...PREVIEW.slice(0, 2), '--send', ...PREVIEW.slice(2)], ['--send', ...extra, ...PREVIEW]]) {
|
||||||
|
const plain = args.filter((a) => a !== '--send');
|
||||||
|
const flags = `${plain.includes('--port') ? '--port 7340 --business mosaic' : '--port 7332'} ${PREVIEW_FLAGS}`;
|
||||||
|
const r = await run(dir, ['render', ...args]);
|
||||||
|
assert.equal(r.status, 0, r.stderr);
|
||||||
|
assert.equal(r.stdout, filled(`${flags} --send`), args.join(' '));
|
||||||
|
assert.ok(execStart(r.stdout).endsWith(' --allow-user jason --send'), execStart(r.stdout));
|
||||||
|
const without = await run(dir, ['render', ...plain]);
|
||||||
|
assert.equal(without.status, 0, without.stderr);
|
||||||
|
const a = r.stdout.split('\n'), b = without.stdout.split('\n');
|
||||||
|
assert.equal(a.length, b.length);
|
||||||
|
assert.deepEqual(a.map((l, i) => (l === b[i] ? null : [l, b[i]])).filter(Boolean), [[`${execStart(without.stdout)} --send`, execStart(without.stdout)]]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('--send without the whole preview set is refused and writes nothing', async (t) => {
|
||||||
|
const dir = tmp(t);
|
||||||
|
const pairs = [PREVIEW.slice(0, 2), PREVIEW.slice(2, 4), PREVIEW.slice(4, 6)];
|
||||||
|
const partial = [[], ['--port', '7340'], ['--business', 'mosaic'], ...pairs, [...pairs[0], ...pairs[1]], [...pairs[0], ...pairs[2]], [...pairs[1], ...pairs[2]]];
|
||||||
|
for (const args of partial) {
|
||||||
|
const r = await run(dir, ['render', '--send', ...args]);
|
||||||
|
assert.equal(r.status, 1, args.join(' '));
|
||||||
|
assert.equal(r.stdout, '', args.join(' '));
|
||||||
|
assert.match(r.stderr, SEND_REFUSAL, args.join(' '));
|
||||||
|
}
|
||||||
|
const bin = standIn(dir);
|
||||||
|
const fresh = join(dir, 'never');
|
||||||
|
for (const args of partial) {
|
||||||
|
const r = await run(dir, ['install', '--dir', fresh, '--no-reload', ...args, '--send'], bin);
|
||||||
|
assert.equal(r.status, 1, args.join(' '));
|
||||||
|
assert.match(r.stderr, SEND_REFUSAL, args.join(' '));
|
||||||
|
}
|
||||||
|
assert.ok(!existsSync(fresh), 'a refusal creates no directory');
|
||||||
|
const unitDir = join(dir, 'units');
|
||||||
|
const target = join(unitDir, 'mosaic-webui.service');
|
||||||
|
assert.equal((await run(dir, ['install', '--dir', unitDir, '--no-reload', ...PREVIEW], bin)).status, 0);
|
||||||
|
const before = readFileSync(target, 'utf8');
|
||||||
|
const r = await run(dir, ['install', '--dir', unitDir, '--no-reload', '--send', ...PREVIEW.slice(0, 4)], bin);
|
||||||
|
assert.equal(r.status, 1);
|
||||||
|
assert.equal(readFileSync(target, 'utf8'), before, 'the installed unit is left alone');
|
||||||
|
assert.deepEqual(readdirSync(unitDir), ['mosaic-webui.service']);
|
||||||
|
assert.ok(!existsSync(join(dir, 'calls.txt')), 'no systemctl call');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('install --send into --dir with --no-reload writes the sending unit, and status says whether it sends', async (t) => {
|
||||||
|
const dir = tmp(t);
|
||||||
|
const unitDir = join(dir, 'units');
|
||||||
|
const target = join(unitDir, 'mosaic-webui.service');
|
||||||
|
const port = await closedPort();
|
||||||
|
const flags = `--port ${port} ${PREVIEW_FLAGS}`;
|
||||||
|
const r = await run(dir, ['install', '--dir', unitDir, '--no-reload', '--port', String(port), ...PREVIEW, '--send']);
|
||||||
|
assert.equal(r.status, 0, r.stderr);
|
||||||
|
assert.match(r.stdout, new RegExp(`^written: ${target}$`, 'm'));
|
||||||
|
// Lead decision 102: Jason or Mos starts it, no other agent.
|
||||||
|
assert.match(r.stdout, /^this script starts nothing: .* Jason\nstarts and restarts it, or Mos under lead decision 102 .*\ntrail as Jason\); no other agent does:$/m);
|
||||||
|
assert.match(r.stdout, new RegExp(`^serve: \\S+ ${REPO.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/packages/webui/src/cli\\.mjs serve ${flags} --send$`, 'm'));
|
||||||
|
assert.equal(readFileSync(target, 'utf8'), filled(`${flags} --send`));
|
||||||
|
assert.equal(statSync(target).mode & 0o777, 0o644);
|
||||||
|
assert.deepEqual(readdirSync(unitDir), ['mosaic-webui.service']);
|
||||||
|
const bin = standIn(dir);
|
||||||
|
const sending = await run(dir, ['status', '--dir', unitDir], bin);
|
||||||
|
assert.equal(sending.status, 0, sending.stderr);
|
||||||
|
assert.match(sending.stdout, /^send: on \(--send; human messages to roles\)$/m);
|
||||||
|
assert.match(sending.stdout, /^healthz: \S+ unreachable \(ECONNREFUSED\)$/m);
|
||||||
|
const again = await run(dir, ['install', '--dir', unitDir, '--no-reload', '--port', String(port), '--send', ...PREVIEW], bin);
|
||||||
|
assert.match(again.stdout, new RegExp(`^unchanged: ${target}$`, 'm'));
|
||||||
|
const readOnly = await run(dir, ['install', '--dir', unitDir, '--no-reload', '--port', String(port), ...PREVIEW], bin);
|
||||||
|
assert.match(readOnly.stdout, new RegExp(`^written: ${target}$`, 'm'));
|
||||||
|
assert.equal(readFileSync(target, 'utf8'), filled(flags));
|
||||||
|
const off = await run(dir, ['status', '--dir', unitDir], bin);
|
||||||
|
assert.match(off.stdout, /^send: off \(read-only\)$/m);
|
||||||
|
assert.doesNotMatch(off.stdout, /^send: on/m);
|
||||||
|
// The only systemctl calls are status's reads; install ran none.
|
||||||
|
assert.deepEqual(readFileSync(join(dir, 'calls.txt'), 'utf8').split('\n').filter(Boolean).filter((c) => !/^--user (is-enabled|is-active|show) mosaic-webui\.service/.test(c)), []);
|
||||||
|
const none = await run(dir, ['status', '--dir', join(dir, 'empty')], bin);
|
||||||
|
assert.doesNotMatch(none.stdout, /^send:/m, 'no unit, no send line');
|
||||||
|
// serve takes --send as an allowed user, so ExecStart can end with
|
||||||
|
// `--allow-user --send` on a unit that doesn't send.
|
||||||
|
const oddUser = [...PREVIEW.slice(0, 4), '--allow-user', '--send'];
|
||||||
|
for (const [args, ending, state] of [[oddUser, '--allow-user --send', 'off'], [[...oddUser, '--send'], '--allow-user --send --send', 'on']]) {
|
||||||
|
const r = await run(dir, ['install', '--dir', unitDir, '--no-reload', '--port', String(port), ...args]);
|
||||||
|
assert.equal(r.status, 0, r.stderr);
|
||||||
|
assert.ok(execStart(readFileSync(target, 'utf8')).endsWith(` ${ending}`), args.join(' '));
|
||||||
|
assert.match((await run(dir, ['status', '--dir', unitDir], bin)).stdout, new RegExp(`^send: ${state} `, 'm'), args.join(' '));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('uninstall and status take no --send', async (t) => {
|
||||||
|
const dir = tmp(t);
|
||||||
|
const bin = standIn(dir);
|
||||||
|
const unitDir = join(dir, 'units');
|
||||||
|
assert.equal((await run(dir, ['install', '--dir', unitDir, '--no-reload', ...PREVIEW, '--send'])).status, 0);
|
||||||
|
for (const args of [['uninstall', '--dir', unitDir, '--send'], ['uninstall', '--dir', unitDir, '--no-reload', '--send'], ['status', '--dir', unitDir, '--send']]) {
|
||||||
|
const r = await run(dir, args, bin);
|
||||||
|
assert.equal(r.status, 4, args.join(' '));
|
||||||
|
assert.match(r.stderr, /^usage: scripts\/webui-service\.sh/);
|
||||||
|
assert.equal(r.stdout, '', args.join(' '));
|
||||||
|
}
|
||||||
|
assert.ok(existsSync(join(unitDir, 'mosaic-webui.service')), 'still installed');
|
||||||
|
assert.ok(!existsSync(join(dir, 'calls.txt')), 'no systemctl call');
|
||||||
|
});
|
||||||
|
|||||||
@@ -11,19 +11,24 @@
|
|||||||
# rename) and run `systemctl --user daemon-reload`
|
# rename) and run `systemctl --user daemon-reload`
|
||||||
# uninstall [--dir DIR] [--no-reload]
|
# uninstall [--dir DIR] [--no-reload]
|
||||||
# remove the unit; refuses while it is active
|
# remove the unit; refuses while it is active
|
||||||
# status [--dir DIR] unit state, then GET /healthz on its port
|
# status [--dir DIR] unit state, whether it sends, then GET
|
||||||
|
# /healthz on its port
|
||||||
#
|
#
|
||||||
# FLAGS are serve's: --port N (default 7332; 7330 is the operator's own
|
# FLAGS are serve's: --port N (default 7332; 7330 is the operator's own
|
||||||
# Console), --business ID, --board URL, and the preview set --public-host
|
# Console), --business ID, --board URL, and the preview set --public-host
|
||||||
# NAME --auth-header HEADER --allow-user ID, all three or none. systemd
|
# NAME --auth-header HEADER --allow-user ID, all three or none, and --send
|
||||||
|
# (row 83), taken only with the preview set: the unit's Console then sends
|
||||||
|
# human messages to roles and needs the system config at start. systemd
|
||||||
# expands % and $ and splits on spaces, so each value is held to a plain
|
# expands % and $ and splits on spaces, so each value is held to a plain
|
||||||
# character set and refused otherwise. None is a secret.
|
# character set and refused otherwise. None is a secret.
|
||||||
#
|
#
|
||||||
# There is no start, stop, enable or restart verb. The Console reads the
|
# There is no start, stop, enable or restart verb. The Console reads the
|
||||||
# bus through the human transport, so whoever starts it holds the human's
|
# bus through the human transport, so whoever starts it holds the human's
|
||||||
# capability. An agent starting it, even detached through systemd, works
|
# capability. Jason starts and restarts it
|
||||||
# around the human proof (lead decisions 62 and 94). Jason starts it from
|
# (`systemctl --user enable --now mosaic-webui`), and so may Mos under lead
|
||||||
# his own shell: `systemctl --user enable --now mosaic-webui`.
|
# decision 102, which accepts that sends from a Console Mos started enter
|
||||||
|
# the trail as Jason (human-cli). Any other agent starting it, even detached
|
||||||
|
# through systemd, works around the human proof (lead decisions 62 and 94).
|
||||||
#
|
#
|
||||||
# Exit codes: 0 ok, 1 operation failed, 4 usage.
|
# Exit codes: 0 ok, 1 operation failed, 4 usage.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
@@ -31,7 +36,7 @@ REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|||||||
TEMPLATE="$REPO/packages/webui/systemd/mosaic-webui.service.in"
|
TEMPLATE="$REPO/packages/webui/systemd/mosaic-webui.service.in"
|
||||||
UNIT_NAME="mosaic-webui.service"
|
UNIT_NAME="mosaic-webui.service"
|
||||||
USAGE="usage: scripts/webui-service.sh render [FLAGS] | install [--dir DIR] [--no-reload] [FLAGS] | uninstall [--dir DIR] [--no-reload] | status [--dir DIR]
|
USAGE="usage: scripts/webui-service.sh render [FLAGS] | install [--dir DIR] [--no-reload] [FLAGS] | uninstall [--dir DIR] [--no-reload] | status [--dir DIR]
|
||||||
FLAGS: [--port N] [--business ID] [--board URL] [--public-host NAME --auth-header HEADER --allow-user ID]"
|
FLAGS: [--port N] [--business ID] [--board URL] [--public-host NAME --auth-header HEADER --allow-user ID [--send]]"
|
||||||
|
|
||||||
die() { echo "webui-service: $*" >&2; exit 1; }
|
die() { echo "webui-service: $*" >&2; exit 1; }
|
||||||
usage() { echo "$USAGE" >&2; exit 4; }
|
usage() { echo "$USAGE" >&2; exit 4; }
|
||||||
@@ -45,6 +50,7 @@ BOARD=""
|
|||||||
PUBLIC_HOST=""
|
PUBLIC_HOST=""
|
||||||
AUTH_HEADER=""
|
AUTH_HEADER=""
|
||||||
ALLOW_USER=""
|
ALLOW_USER=""
|
||||||
|
SEND=0
|
||||||
SERVE_SET=0
|
SERVE_SET=0
|
||||||
|
|
||||||
parse_flags() {
|
parse_flags() {
|
||||||
@@ -58,6 +64,7 @@ parse_flags() {
|
|||||||
--public-host) [ $# -ge 2 ] || usage; PUBLIC_HOST="$2"; SERVE_SET=1; shift 2 ;;
|
--public-host) [ $# -ge 2 ] || usage; PUBLIC_HOST="$2"; SERVE_SET=1; shift 2 ;;
|
||||||
--auth-header) [ $# -ge 2 ] || usage; AUTH_HEADER="$2"; SERVE_SET=1; shift 2 ;;
|
--auth-header) [ $# -ge 2 ] || usage; AUTH_HEADER="$2"; SERVE_SET=1; shift 2 ;;
|
||||||
--allow-user) [ $# -ge 2 ] || usage; ALLOW_USER="$2"; SERVE_SET=1; shift 2 ;;
|
--allow-user) [ $# -ge 2 ] || usage; ALLOW_USER="$2"; SERVE_SET=1; shift 2 ;;
|
||||||
|
--send) SEND=1; SERVE_SET=1; shift ;;
|
||||||
*) usage ;;
|
*) usage ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
@@ -86,6 +93,9 @@ serve_flags() {
|
|||||||
[ -z "$PUBLIC_HOST" ] || preview=$((preview + 1))
|
[ -z "$PUBLIC_HOST" ] || preview=$((preview + 1))
|
||||||
[ -z "$AUTH_HEADER" ] || preview=$((preview + 1))
|
[ -z "$AUTH_HEADER" ] || preview=$((preview + 1))
|
||||||
[ -z "$ALLOW_USER" ] || preview=$((preview + 1))
|
[ -z "$ALLOW_USER" ] || preview=$((preview + 1))
|
||||||
|
# serve refuses --send without the preview set; refuse it here first,
|
||||||
|
# so no unit is written that would only exit 2 at start.
|
||||||
|
[ "$SEND" = 0 ] || [ "$preview" -eq 3 ] || die "--send needs the preview mode (--public-host, --auth-header and --allow-user)"
|
||||||
if [ "$preview" -ne 0 ]; then
|
if [ "$preview" -ne 0 ]; then
|
||||||
[ "$preview" -eq 3 ] || die "--public-host, --auth-header and --allow-user go together: all three or none"
|
[ "$preview" -eq 3 ] || die "--public-host, --auth-header and --allow-user go together: all three or none"
|
||||||
case "$PUBLIC_HOST" in [A-Za-z0-9]*) ;; *) die "public host must be a hostname, no port or scheme: $PUBLIC_HOST" ;; esac
|
case "$PUBLIC_HOST" in [A-Za-z0-9]*) ;; *) die "public host must be a hostname, no port or scheme: $PUBLIC_HOST" ;; esac
|
||||||
@@ -95,6 +105,7 @@ serve_flags() {
|
|||||||
case "$ALLOW_USER" in *[!A-Za-z0-9._@+-]*) die "allowed user contains a character the unit cannot carry: $ALLOW_USER" ;; esac
|
case "$ALLOW_USER" in *[!A-Za-z0-9._@+-]*) die "allowed user contains a character the unit cannot carry: $ALLOW_USER" ;; esac
|
||||||
flags="$flags --public-host $PUBLIC_HOST --auth-header $AUTH_HEADER --allow-user $ALLOW_USER"
|
flags="$flags --public-host $PUBLIC_HOST --auth-header $AUTH_HEADER --allow-user $ALLOW_USER"
|
||||||
fi
|
fi
|
||||||
|
[ "$SEND" = 0 ] || flags="$flags --send"
|
||||||
echo "$flags"
|
echo "$flags"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,8 +149,9 @@ install_unit() {
|
|||||||
echo "daemon-reload done"
|
echo "daemon-reload done"
|
||||||
fi
|
fi
|
||||||
cat <<MSG
|
cat <<MSG
|
||||||
this script starts nothing: the Console holds the human's capability, so the
|
this script starts nothing: the Console holds the human's capability. Jason
|
||||||
human starts it from their own shell:
|
starts and restarts it, or Mos under lead decision 102 (sends then enter the
|
||||||
|
trail as Jason); no other agent does:
|
||||||
systemctl --user enable --now mosaic-webui start now and at login
|
systemctl --user enable --now mosaic-webui start now and at login
|
||||||
systemctl --user status mosaic-webui
|
systemctl --user status mosaic-webui
|
||||||
journalctl --user -u mosaic-webui -f the Console's log
|
journalctl --user -u mosaic-webui -f the Console's log
|
||||||
@@ -182,6 +194,13 @@ status_unit() {
|
|||||||
echo "healthz: not checked; no unit at $target"
|
echo "healthz: not checked; no unit at $target"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
# render puts --send right after the allowed user, which may itself be
|
||||||
|
# the value --send; a unit that sends ends with both.
|
||||||
|
if grep -q '^ExecStart=.* --allow-user [^ ][^ ]* --send$' "$target"; then
|
||||||
|
echo "send: on (--send; human messages to roles)"
|
||||||
|
else
|
||||||
|
echo "send: off (read-only)"
|
||||||
|
fi
|
||||||
local port
|
local port
|
||||||
port="$(sed -n 's/^ExecStart=.* --port \([0-9][0-9]*\).*$/\1/p' "$target")"
|
port="$(sed -n 's/^ExecStart=.* --port \([0-9][0-9]*\).*$/\1/p' "$target")"
|
||||||
[ -n "$port" ] || { echo "healthz: not checked; no --port in $target"; return 0; }
|
[ -n "$port" ] || { echo "healthz: not checked; no --port in $target"; return 0; }
|
||||||
|
|||||||
Reference in New Issue
Block a user