stack-release: activate --pm-after-harness, pm-on and the pending pointer, so a re-pin past c216f47db starts the PM after the harness kind is set (row 169, #1666)
activate ID BUSINESS --pm-after-harness installs the bus without --pm and writes a pointer with pm false and pmPending true. It refuses pm-after-harness-target when the target has no harness kind, and pm-after-harness-set when the business file already sets plugins.harness. pm-on BUSINESS refuses not-pending and harness-unset, then reinstalls the bus with --pm on the pointer's copy and runs the full gate. A failure puts the phase-1 unit back (pm-on-failed, exit 1), or stops both units (pm-on-down, exit 2). A plain activate refuses pm-pending. status prints the pending state and the next step. activate and rollback refuse business-unreadable when the target's code can't read the business file's plug-ins. Built by Darkwing on4b738e2da(patch-id 17a90c4ad795, manifest c6aaac1f...54af), approved by Filbert (#1666 comment 29263), Sage's gate green one7d47c346plus the patch.
This commit is contained in:
+41
-7
@@ -366,7 +366,8 @@ reports checkout or copy identity and refactor drift, and writes nothing.
|
||||
|
||||
```
|
||||
scripts/stack-release.sh adopt BUSINESS
|
||||
scripts/stack-release.sh activate ID BUSINESS [--pm] [--migrate] [--timeout S] [--plan]
|
||||
scripts/stack-release.sh activate ID BUSINESS [--pm | --pm-after-harness] [--migrate] [--timeout S] [--plan]
|
||||
scripts/stack-release.sh pm-on BUSINESS [--timeout S]
|
||||
scripts/stack-release.sh rollback BUSINESS [--restore ACTIVATION [--events N]] [--timeout S] [--plan]
|
||||
scripts/stack-release.sh status BUSINESS
|
||||
common options: --copies DIR, --bin DIR
|
||||
@@ -380,9 +381,9 @@ running sessions, so run it only when no PM turn is in flight. `--plan` and
|
||||
The script keeps `<dataRoot>/state/stack-active.json` (the live copy, business,
|
||||
PM flag, claude pin, Console flags, schema and activation) and appends every
|
||||
run to `<dataRoot>/state/stack-activation-log.jsonl`: a `started` line, then
|
||||
one final line with `result` `ok`, `refused`, `gate-failed` or
|
||||
`rollback-failed`. A refusal exits 3, a gate failure that rolled back exits 1,
|
||||
a failed rollback exits 2, usage exits 4. Only one run at a time holds
|
||||
one final line with `result` `ok`, `refused`, `gate-failed`,
|
||||
`rollback-failed`, `pm-on-failed` or `pm-on-down`. A refusal exits 3, a gate
|
||||
failure that rolled back exits 1, a failed rollback exits 2, usage exits 4. Only one run at a time holds
|
||||
`state/stack-release.lock` (`run-locked`).
|
||||
|
||||
1. Once per host, `adopt BUSINESS` records the running stack as the pointer:
|
||||
@@ -401,8 +402,9 @@ a failed rollback exits 2, usage exits 4. Only one run at a time holds
|
||||
`activate` refuses `drop-in-override` and names the file.
|
||||
5. With Mos's go, `activate ID BUSINESS`, plus `--migrate` when the plan
|
||||
says so. Before it stops anything, it refuses a missing, unpushed or
|
||||
unreadable copy, a busy `mosaic-bus@` instance, and unit flags that differ
|
||||
from the pointer. Then it saves the unit files, drop-ins and wrapper under
|
||||
unreadable copy, a busy `mosaic-bus@` instance, unit flags that differ
|
||||
from the pointer, and a business file that names a plug-in kind the
|
||||
target's `KINDS` lacks (`unknown-kind-for-target`, naming each kind). Then it saves the unit files, drop-ins and wrapper under
|
||||
`state/stack-rollback/<activation>/`, stops the Console then the bus, and
|
||||
reads the stopped store. A `-wal` or `-shm` beside it refuses
|
||||
`store-unclean` and leaves the store alone. A migration takes an
|
||||
@@ -420,10 +422,42 @@ a failed rollback exits 2, usage exits 4. Only one run at a time holds
|
||||
7. `rollback BUSINESS` returns to the copy before the pointer's, taken from
|
||||
the log. When the schemas differ it needs `--restore ACTIVATION`; events
|
||||
written after that backup need `--events N` with the count it prints, and
|
||||
Jason decides first.
|
||||
Jason decides first. It refuses `unknown-kind-for-target` like `activate`;
|
||||
set each kind it names to `null` with `plugins set` through the live bus
|
||||
first.
|
||||
8. Jason's plain `mosaic talk pm "..."` must print sent and a reply. Keep the
|
||||
`status` output with the receipts.
|
||||
|
||||
#### A target past c216f47db: the PM after the harness kind
|
||||
|
||||
From c216f47db on, the bus launches the PM only with a `harness` kind in the
|
||||
business file, and no copy before it can set one. So `activate --pm` to such a
|
||||
copy fails its gate and rolls back. Use two phases instead (row 169, Gitea
|
||||
#1666, mos-0848):
|
||||
|
||||
1. `activate ID BUSINESS --pm-after-harness`, in place of `--pm`. It needs a
|
||||
pointer with `pm` true. Before anything stops, it refuses
|
||||
`pm-after-harness-target` when the target's `KINDS` has no `harness`, and
|
||||
`pm-after-harness-set` when the business file already sets `harness`;
|
||||
use `--pm` in both cases. It installs the bus without `--pm`, runs every
|
||||
gate check but the PM's, and writes the pointer with `pm` false and
|
||||
`pmPending` true. The log's final line is `result ok` with `pm pending`.
|
||||
`status` and `--plan` print the pending state and the next step.
|
||||
2. Jason sets `harness` with `plugins set` through the live bus.
|
||||
3. `pm-on BUSINESS` refuses `not-pending` without a pending pointer and
|
||||
`harness-unset` unless `harness` is a non-empty list the pointer's copy
|
||||
loads. It restarts the bus alone, with `--pm` on the same copy, and runs
|
||||
the whole gate. A pass writes `pm` true and drops `pmPending`. A failure
|
||||
puts the phase-1 bus unit back and reruns the phase-1 gate. If that
|
||||
passes, the log says `pm-on-failed`, the pointer stays pending and the run
|
||||
exits 1; fix `harness` and run `pm-on` again. If it fails too, both units
|
||||
stop, the log says `pm-on-down`, and the run exits 2. Neither touches the
|
||||
copy, the Console or the store.
|
||||
|
||||
While the pointer is pending, `activate` refuses `pm-pending`. Run `pm-on`
|
||||
or `rollback`; a rollback from pending installs the old copy with `--pm` and
|
||||
gates its launch.
|
||||
|
||||
A `started` line with no final line is an interrupted run. `status` shows
|
||||
it, and `adopt`, `activate` and `rollback` refuse until Jason or Mos settles
|
||||
it. Nothing prunes copies, rollback sets, backups or post-attempt stores.
|
||||
|
||||
@@ -240,11 +240,18 @@ const M=git(repo,'rev-parse','HEAD');git(repo,'update-ref','refs/remotes/origin/
|
||||
// `to` false leaves out the `to:` lines (C1); a string puts that value in each one (A). The --plan
|
||||
// steps line can't be read from either (T27).
|
||||
const storeText=(v,froms,to=true)=>`export const SCHEMA_VERSION = '${v}';\nexport const MIGRATIONS = [\n${froms.map((f,i)=>` {\n from: '${f}',\n${to===false?'':` to: '${to===true?froms[i+1]??v:to}',\n`} run() {},\n },\n`).join('')}];\n`;
|
||||
// Plug-in kinds (row 169): C1 and C know the harness kind, as a copy at or past c216f47db does. The
|
||||
// rest have it under another name, as ca15d73e9 has no harness kind. Every copy has the real business
|
||||
// loader and roles, which the tool imports to read the business file.
|
||||
const KINDS_NEW=readFileSync(join(repo,'packages','plugins','src','kinds.mjs'),'utf8');
|
||||
const KINDS_OLD=KINDS_NEW.replace('\n harness: Object.freeze({','\n "harness-before-c216f47db": Object.freeze({');
|
||||
assert.notEqual(KINDS_OLD,KINDS_NEW);
|
||||
for(const [id,store] of [[F,'// a copy from before SCHEMA_VERSION\n'],[A,storeText('3g',['3f'],'3x')],[B,storeText('3g',['3f'])],[C1,storeText('3g',['3f'],false)],[C,storeText('3h',['3f','3g'])],[S,storeText('3g',['3f'])],[U,storeText('3g',['3f'])]]){
|
||||
const d=join(COPIES,id);
|
||||
for(const [f,text,mode] of [['scripts/mosaic',script(mosaicStub),0o755],['packages/webui/src/cli.mjs','// fixture Console, never run\n',0o644],['packages/bus/src/store.mjs',store,0o644],['packages/bus/src/migrate-schema.mjs',`(${migrateStub})();\n`,0o644]]){
|
||||
for(const [f,text,mode] of [['scripts/mosaic',script(mosaicStub),0o755],['packages/webui/src/cli.mjs','// fixture Console, never run\n',0o644],['packages/bus/src/store.mjs',store,0o644],['packages/bus/src/migrate-schema.mjs',`(${migrateStub})();\n`,0o644],['packages/plugins/src/kinds.mjs',[C1,C].includes(id)?KINDS_NEW:KINDS_OLD,0o644]]){
|
||||
mkdirSync(dirname(join(d,f)),{recursive:true});writeFileSync(join(d,f),text,{mode});
|
||||
}
|
||||
for(const p of ['packages/business/src','roles'])cpSync(join(repo,p),join(d,p),{recursive:true});
|
||||
execFileSync('chmod',['-R','a-w',d]);
|
||||
}
|
||||
for(const [name,fn] of [['systemctl',systemctl],['journalctl',journalctl],['systemd-analyze',systemdAnalyze]])writeFileSync(join(SBIN,name),script(fn),{mode:0o755});
|
||||
@@ -283,6 +290,16 @@ function tree(p){
|
||||
// T17: what a run may never remove.
|
||||
const kept=(e)=>[...tree(join(e.data,'backups','stack')),...tree(join(e.state,'stack-rollback')),...posts(e).flatMap((n)=>tree(join(e.data,n))),...tree(COPIES)].map((l)=>l.replace(/ .*/,''));
|
||||
|
||||
// The business file (row 169): the committed example made loadable, as acme, with `plugins` when given.
|
||||
const EXAMPLE=readFileSync(join(repo,'packages','business','examples','mosaic-stack.example.json'),'utf8');
|
||||
function biz(e,plugins){
|
||||
const b=JSON.parse(EXAMPLE);let n=0;
|
||||
const walk=(o)=>{for(const [k,v] of Object.entries(o)){if(k==='botId')o[k]=++n;else if(v==='YYYY-MM-DD')o[k]='2027-01-01';else if(v&&typeof v==='object')walk(v)}};
|
||||
walk(b);b.id='acme';if(plugins!==undefined)b.plugins=plugins;
|
||||
const f=join(dirname(e.cfg),'businesses','acme.json');mkdirSync(dirname(f),{recursive:true});
|
||||
writeFileSync(f,JSON.stringify(b,null,2)+'\n',{mode:0o600});chmodSync(f,0o600);
|
||||
}
|
||||
|
||||
async function mk(t){
|
||||
const root=realpathSync(mkdtempSync(join(tmpdir(),'sr-')));
|
||||
const e={root,data:join(root,'data'),bin:join(root,'bin'),sd:join(root,'sd'),units:join(root,'xcfg','systemd','user'),cfg:join(root,'cfg','config.json'),claude:join(root,'claude-bin','claude')};
|
||||
@@ -290,6 +307,7 @@ async function mk(t){
|
||||
for(const d of [join(root,'home'),dirname(e.cfg),join(e.data,'bus'),e.bin,dirname(e.claude),join(e.sd,'faults'),e.units,join(root,'xdata')])mkdirSync(d,{recursive:true});
|
||||
chmodSync(join(e.data,'bus'),0o700);
|
||||
writeFileSync(e.cfg,JSON.stringify({configVersion:1,environment:'development',dataRoot:e.data,execution:{backend:'docker',provider:'zai',model:'glm-5.3-flash',adapter:'mock'}}));
|
||||
biz(e);
|
||||
writeFileSync(join(e.bin,'mosaic'),`#!/usr/bin/env bash\n${MARKER}\n[ "\${1:-}" = __lease-capability ] && exec "${e.bin}/mosaic-old" "$@"\nexec "/old/scripts/mosaic" "$@"\n`,{mode:0o755});
|
||||
writeFileSync(join(e.bin,'mosaic-old'),'#!/bin/sh\nexit 0\n',{mode:0o755});
|
||||
writeFileSync(e.claude,'#!/bin/sh\necho "9.9.9 (Claude Code fixture)"\n',{mode:0o755});
|
||||
@@ -827,3 +845,181 @@ test('T31 a quoted or escaped Environment= or ExecStart= is unreadable: refusals
|
||||
for(const x of [r,d])assert.ok(!x.err.includes('/a'),x.err);
|
||||
});
|
||||
});
|
||||
|
||||
// --- Row 169 (#1666): docs/plans/2026-10-19_pm-after-harness.md items 1 to 4 ---
|
||||
|
||||
const HARNESS={harness:['claude-code']};
|
||||
const busUnit=(e)=>readFileSync(join(e.units,'[email protected]'),'utf8');
|
||||
const execLine=(to,pm)=>`ExecStart=${COPIES}/${to}/scripts/mosaic bus start %i${pm?' --pm':''}\n`;
|
||||
const sinceLines=(e)=>lines(join(e.sd,'journal-since.log')).length;
|
||||
const PENDING_MSG='the pointer is pending (activate --pm-after-harness); run pm-on or rollback first';
|
||||
// The stack after phase 1: seeded on B with --pm, then activate C1 --pm-after-harness.
|
||||
async function pending(e){
|
||||
await seed(e,{pm:true});
|
||||
const r=await run(e,'activate',C1,'acme','--pm-after-harness');assert.equal(r.code,0,r.out+r.err);
|
||||
}
|
||||
// A refusal before anything stops: exit 3, the message, no file changed, no start or stop, one log line.
|
||||
async function refused(e,args,code,msg){
|
||||
const before=fp(e),n=calls(e).length,logs=logLines(e).length;
|
||||
const r=await run(e,...args);assert.equal(r.code,3,r.out+r.err);
|
||||
assert.equal(r.err,`stack-release: refusing ${code}: ${msg}\n`);
|
||||
assert.deepEqual(fp(e),before);assert.deepEqual(startsStops(e,n),[]);
|
||||
assert.equal(logLines(e).length,logs+1);assert.deepEqual([logLines(e).at(-1).result,logLines(e).at(-1).code],['refused',code]);
|
||||
}
|
||||
|
||||
test('T32 item 1: --pm-after-harness refuses a target without harness or a file that sets it, and passes without the PM',async(t)=>{
|
||||
await t.test('usage: not with --pm, and only for activate',async(t)=>{
|
||||
const e=await mk(t);await seed(e,{pm:true});const logs=logLines(e).length;
|
||||
for(const args of [['activate',C1,'acme','--pm','--pm-after-harness'],['activate',C1,'acme','--pm-after-harness','--pm'],['rollback','acme','--pm-after-harness'],['pm-on','acme','--pm-after-harness']]){
|
||||
const r=await run(e,...args);assert.equal(r.code,4,args.join(' '));assert.match(r.err,/^usage: stack-release.sh adopt BUSINESS\n/);
|
||||
}
|
||||
assert.equal(logLines(e).length,logs);
|
||||
});
|
||||
await t.test('pm-after-harness-target',async(t)=>{
|
||||
const e=await mk(t);await seed(e,{pm:true});
|
||||
await refused(e,['activate',A,'acme','--pm-after-harness'],'pm-after-harness-target',`${A} has no harness plug-in kind, so there is nothing to wait for; use --pm`);
|
||||
});
|
||||
await t.test('pm-after-harness-set',async(t)=>{
|
||||
const e=await mk(t);await seed(e,{pm:true});
|
||||
for(const [h,shown] of [[['claude-code'],'["claude-code"]'],[[],'[]']]){
|
||||
biz(e,{harness:h});
|
||||
await refused(e,['activate',C1,'acme','--pm-after-harness'],'pm-after-harness-set',`the business file already sets harness ${shown}; use --pm`);
|
||||
}
|
||||
});
|
||||
await t.test('a pointer without pm',async(t)=>{
|
||||
const e=await mk(t);await seed(e);
|
||||
await refused(e,['activate',C1,'acme','--pm-after-harness'],'unit-flags-drift','the units differ from the pointer; settle it first:\n pm: pointer 0, --pm-after-harness needs a pointer with pm 1');
|
||||
});
|
||||
await t.test('phase 1 installs without --pm, skips pm_launched and writes the pending pointer',async(t)=>{
|
||||
const e=await mk(t);await seed(e,{pm:true});const since=sinceLines(e),n=calls(e).length;
|
||||
const r=await run(e,'activate',C1,'acme','--pm-after-harness');assert.equal(r.code,0,r.out+r.err);
|
||||
assert.ok(r.out.endsWith(`stack-release: activate ok: ${C1} is live for acme without its PM; the PM is pending: Jason's plugins set of harness, then pm-on\n`),r.out);
|
||||
assert.ok(busUnit(e).includes(execLine(C1,false)),busUnit(e));
|
||||
assert.equal(sinceLines(e),since);
|
||||
assert.deepEqual(startsStops(e,n),[`stop ${CON}`,`stop ${BUS}`,`start ${BUS}`,`start ${CON}`]);
|
||||
const {at,activation,...p}=pointer(e);
|
||||
assert.deepEqual(p,{copy:C1,business:'acme',pm:false,pmPending:true,claude:e.claude,console:['--port',e.port,'--business','acme'],schema:'3g'});
|
||||
const l=logLines(e).at(-1);assert.deepEqual([l.op,l.from,l.to,l.result,l.pm],['activate',B,C1,'ok','pending']);
|
||||
});
|
||||
});
|
||||
|
||||
test('T33 item 2: read_pointer takes pmPending only as true beside pm false; a pending pointer refuses activate and shows in status',async(t)=>{
|
||||
await t.test('read_pointer',async(t)=>{
|
||||
const e=await mk(t);await seed(e);const good=pointer(e);
|
||||
for(const bad of [{pmPending:false},{pmPending:'true'},{pmPending:1},{pmPending:null},{pm:true,pmPending:true}]){
|
||||
writeFileSync(e.pointer,JSON.stringify({...good,...bad})+'\n');
|
||||
const before=fp(e),n=calls(e).length,logs=logLines(e).length;
|
||||
for(const args of [['activate',C1,'acme'],['status','acme']]){
|
||||
const r=await run(e,...args);assert.equal(r.code,1,`${JSON.stringify(bad)} ${args[0]}`);
|
||||
assert.ok(r.err.endsWith(`stack-release: ${e.pointer} is unreadable; Jason or Mos settles it\n`),r.err);
|
||||
}
|
||||
assert.deepEqual(fp(e),before);assert.deepEqual(startsStops(e,n),[]);assert.equal(logLines(e).length,logs);
|
||||
}
|
||||
});
|
||||
await t.test('pm-pending, already-adopted and the status line',async(t)=>{
|
||||
const e=await mk(t);await pending(e);
|
||||
for(const args of [['activate',C,'acme','--migrate'],['activate',C,'acme','--pm','--migrate'],['activate',C1,'acme','--pm-after-harness']])await refused(e,args,'pm-pending',PENDING_MSG);
|
||||
await refused(e,['adopt','acme'],'already-adopted',`${e.pointer} exists (copy ${C1})`);
|
||||
const before=tree(e.data),r=await run(e,'status','acme');assert.equal(r.code,0,r.err);
|
||||
const out=r.out.split('\n');
|
||||
assert.deepEqual(out.slice(0,3),[`pointer: ${e.pointer}`,` ${readFileSync(e.pointer,'utf8').trimEnd()}`,'pm: pending (activate --pm-after-harness); next: Jason\'s plugins set of harness, then stack-release.sh pm-on acme']);
|
||||
assert.ok(out.includes('unit flags: match the pointer'),r.out);
|
||||
assert.deepEqual(tree(e.data),before);
|
||||
});
|
||||
});
|
||||
|
||||
test('T34 item 3: pm-on refuses before any stop, passes with the PM, or puts the phase-1 bus back',async(t)=>{
|
||||
await t.test('not-pending',async(t)=>{
|
||||
const e=await mk(t);await seed(e,{pm:true});biz(e,HARNESS);
|
||||
await refused(e,['pm-on','acme'],'not-pending','the pointer is not pending; pm-on follows activate --pm-after-harness');
|
||||
});
|
||||
await t.test('harness-unset',async(t)=>{
|
||||
const e=await mk(t);await pending(e);
|
||||
await refused(e,['pm-on','acme'],'harness-unset','the business file sets no harness; Jason sets it with plugins set first');
|
||||
biz(e,{harness:[]});
|
||||
await refused(e,['pm-on','acme'],'harness-unset','the business file\'s harness is [], not a non-empty list');
|
||||
});
|
||||
await t.test('a launched line passes: the bus alone restarts with --pm',async(t)=>{
|
||||
const e=await mk(t);await pending(e);biz(e,HARNESS);
|
||||
const since=sinceLines(e),n=calls(e).length,con=sha(join(e.units,CON)),act=pointer(e).activation;
|
||||
const r=await run(e,'pm-on','acme');assert.equal(r.code,0,r.out+r.err);
|
||||
assert.ok(r.out.endsWith(`stack-release: pm-on ok: the PM is live on ${C1} for acme\n`),r.out);
|
||||
assert.deepEqual(startsStops(e,n),[`stop ${BUS}`,`start ${BUS}`]);
|
||||
assert.ok(busUnit(e).includes(execLine(C1,true)),busUnit(e));assert.equal(sha(join(e.units,CON)),con);
|
||||
assert.equal(sinceLines(e),since+1);assert.match(lines(join(e.sd,'journal-since.log')).at(-1),/^@\d+$/);
|
||||
const {at,activation,...p}=pointer(e);assert.notEqual(activation,act);
|
||||
assert.deepEqual(p,{copy:C1,business:'acme',pm:true,claude:e.claude,console:['--port',e.port,'--business','acme'],schema:'3g'});
|
||||
const l=logLines(e).at(-1);assert.deepEqual([l.op,l.to,l.result,l.pm,l.activation],['pm-on',C1,'ok','on',activation]);
|
||||
const s=await run(e,'status','acme');assert.equal(s.code,0,s.err);assert.ok(!s.out.includes('pm: pending'),s.out);
|
||||
});
|
||||
await t.test('a failed pm_launched puts the phase-1 unit back: exit 1, still pending',async(t)=>{
|
||||
const e=await mk(t);await pending(e);biz(e,HARNESS);fault(e,`pm-${C1}`,'none');
|
||||
const before=fp(e),p0=readFileSync(e.pointer,'utf8'),n=calls(e).length;
|
||||
const r=await run(e,'pm-on','acme','--timeout','3');assert.equal(r.code,1,r.out+r.err);
|
||||
const l=logLines(e).at(-1),rs=join(e.state,'stack-rollback',l.activation);
|
||||
assert.equal(r.err,`stack-release: pm-on failed with pm-launch-unconfirmed (no launched line since the start); the bus runs ${C1} without its PM again and passed the phase-1 gate, and the pointer stays pending. Fix harness with plugins set, then run pm-on again. Rollback set: ${rs}\n`);
|
||||
assert.deepEqual([l.op,l.result,l.code,l.why],['pm-on','pm-on-failed','pm-launch-unconfirmed','no launched line since the start']);
|
||||
assert.deepEqual(fp(e),before);assert.ok(busUnit(e).includes(execLine(C1,false)));
|
||||
assert.equal(readFileSync(e.pointer,'utf8'),p0);
|
||||
assert.deepEqual([units(e)[BUS].state,units(e)[BUS].wd,units(e)[CON].state],['active',join(COPIES,C1),'active']);
|
||||
assert.deepEqual(startsStops(e,n),[`stop ${BUS}`,`start ${BUS}`,`stop ${BUS}`,`start ${BUS}`]);
|
||||
});
|
||||
await t.test('a failed phase-1 gate too: pm-on-down, exit 2, both units stopped',async(t)=>{
|
||||
const e=await mk(t);await pending(e);biz(e,HARNESS);fault(e,`healthz-${C1}`);
|
||||
const p0=readFileSync(e.pointer,'utf8'),store=sha(e.store);
|
||||
const r=await run(e,'pm-on','acme','--timeout','3');assert.equal(r.code,2,r.out+r.err);
|
||||
const l=logLines(e).at(-1),rs=join(e.state,'stack-rollback',l.activation);
|
||||
assert.equal(r.err,`stack-release: pm-on-down: pm-on failed with healthz and the phase-1 bus with healthz; both units are stopped. Rollback set: ${rs}\n`);
|
||||
assert.deepEqual([l.result,l.code,l.phase1],['pm-on-down','healthz','healthz']);
|
||||
assert.deepEqual([units(e)[BUS].state,units(e)[CON].state],['inactive','inactive']);
|
||||
assert.equal(readFileSync(e.pointer,'utf8'),p0);assert.equal(sha(e.store),store);
|
||||
assert.ok(busUnit(e).includes(execLine(C1,false)));
|
||||
});
|
||||
});
|
||||
|
||||
test('T35 item 4: rollback from pending installs --pm; a plug-in kind the target lacks refuses by name',async(t)=>{
|
||||
await t.test('rollback from pending gates pm_launched on the old copy',async(t)=>{
|
||||
const e=await mk(t);await pending(e);const since=sinceLines(e);
|
||||
const r=await run(e,'rollback','acme');assert.equal(r.code,0,r.out+r.err);
|
||||
assert.ok(busUnit(e).includes(execLine(B,true)),busUnit(e));
|
||||
assert.equal(sinceLines(e),since+1);
|
||||
const {at,activation,...p}=pointer(e);
|
||||
assert.deepEqual(p,{copy:B,business:'acme',pm:true,claude:e.claude,console:['--port',e.port,'--business','acme'],schema:'3g'});
|
||||
});
|
||||
await t.test('after the harness set, rollback refuses until harness is gone',async(t)=>{
|
||||
const e=await mk(t);await pending(e);biz(e,HARNESS);
|
||||
assert.equal((await run(e,'pm-on','acme')).code,0);
|
||||
await refused(e,['rollback','acme'],'unknown-kind-for-target',`the business file names plug-in kind(s) harness, which ${B} does not know; set each to null with plugins set through the live bus first`);
|
||||
biz(e);
|
||||
const r=await run(e,'rollback','acme');assert.equal(r.code,0,r.out+r.err);
|
||||
assert.ok(busUnit(e).includes(execLine(B,true)));assert.deepEqual([pointer(e).copy,pointer(e).pm],[B,true]);
|
||||
});
|
||||
await t.test('activate names every kind the target lacks',async(t)=>{
|
||||
const e=await mk(t);await seed(e);
|
||||
biz(e,{...HARNESS,widget:{}});
|
||||
await refused(e,['activate',A,'acme'],'unknown-kind-for-target',`the business file names plug-in kind(s) harness widget, which ${A} does not know`);
|
||||
await refused(e,['activate',C1,'acme'],'unknown-kind-for-target',`the business file names plug-in kind(s) widget, which ${C1} does not know`);
|
||||
});
|
||||
});
|
||||
|
||||
test('T36 --plan shows the pending state and lists the row 169 refusals it can see',async(t)=>{
|
||||
const e=await mk(t);await seed(e,{pm:true});
|
||||
const quiet=async(code,...args)=>{
|
||||
const before=fp(e),n=calls(e).length,logs=logLines(e).length,r=await run(e,...args);
|
||||
assert.equal(r.code,code,r.out+r.err);assert.equal(r.err,'');
|
||||
assert.deepEqual(fp(e),before);assert.deepEqual(startsStops(e,n),[]);assert.equal(logLines(e).length,logs);
|
||||
return r.out.split('\n');
|
||||
};
|
||||
let out=await quiet(0,'activate',C1,'acme','--pm-after-harness','--plan');
|
||||
assert.deepEqual(out.slice(-5),['pm: the bus starts without --pm and the PM stays pending; next: Jason\'s plugins set of harness, then pm-on',...planTail(e,C1),'']);
|
||||
biz(e,HARNESS);
|
||||
out=await quiet(3,'activate',A,'acme','--pm-after-harness','--plan');
|
||||
assert.deepEqual(out.slice(-6),['pm: the bus starts without --pm and the PM stays pending; next: Jason\'s plugins set of harness, then pm-on',...planTail(e,A),`refuses unknown-kind-for-target: the business file names plug-in kind(s) harness, which ${A} does not know`,'']);
|
||||
out=await quiet(3,'activate',C1,'acme','--pm-after-harness','--plan');
|
||||
assert.equal(out.at(-2),'refuses pm-after-harness-set: the business file already sets harness ["claude-code"]; use --pm');
|
||||
biz(e);assert.equal((await run(e,'activate',C1,'acme','--pm-after-harness')).code,0);
|
||||
out=await quiet(3,'activate',C,'acme','--plan');
|
||||
assert.deepEqual(out.slice(-6),['pm: the pointer is pending (activate --pm-after-harness); the bus runs without its PM',...planTail(e,C,true),`refuses pm-pending: ${PENDING_MSG}`,'']);
|
||||
out=await quiet(0,'rollback','acme','--plan');
|
||||
assert.deepEqual(out.slice(-5),['pm: the pointer is pending (activate --pm-after-harness); the bus runs without its PM',...planTail(e,B,true),'']);
|
||||
});
|
||||
|
||||
+192
-24
@@ -25,12 +25,13 @@ COPIES="${XDG_DATA_HOME:-$HOME/.local/share}/mosaic-stack/copies"
|
||||
BIN="$HOME/.npm-global/bin"
|
||||
UNIT_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
|
||||
CONSOLE_UNIT=mosaic-webui.service
|
||||
TIMEOUT=60 WANT_PM=0 MIGRATE=0 PLAN=0 RESTORE="" EVENTS=""
|
||||
TIMEOUT=60 WANT_PM=0 AFTER_HARNESS=0 MIGRATE=0 PLAN=0 RESTORE="" EVENTS=""
|
||||
|
||||
usage() {
|
||||
cat >&2 <<'USAGE'
|
||||
usage: stack-release.sh adopt BUSINESS
|
||||
stack-release.sh activate ID BUSINESS [--pm] [--migrate] [--timeout S] [--plan]
|
||||
stack-release.sh activate ID BUSINESS [--pm | --pm-after-harness] [--migrate] [--timeout S] [--plan]
|
||||
stack-release.sh pm-on BUSINESS [--timeout S]
|
||||
stack-release.sh rollback BUSINESS [--restore ACTIVATION [--events N]] [--timeout S] [--plan]
|
||||
stack-release.sh status BUSINESS
|
||||
common options: --copies DIR, --bin DIR
|
||||
@@ -64,6 +65,7 @@ parse_opts() {
|
||||
--timeout) [ "$OP" != status ] && [ "$OP" != adopt ] && [ $# -ge 2 ] && [[ "$2" =~ ^[1-9][0-9]{0,4}$ ]] || usage; TIMEOUT="$2"; shift 2 ;;
|
||||
--plan) [ "$OP" = activate ] || [ "$OP" = rollback ] || usage; PLAN=1; shift ;;
|
||||
--pm) [ "$OP" = activate ] || usage; WANT_PM=1; shift ;;
|
||||
--pm-after-harness) [ "$OP" = activate ] || usage; AFTER_HARNESS=1; shift ;;
|
||||
--migrate) [ "$OP" = activate ] || usage; MIGRATE=1; shift ;;
|
||||
--restore) [ "$OP" = rollback ] && [ $# -ge 2 ] && [[ "$2" =~ ^[0-9]{8}T[0-9]{6}Z-[0-9a-f]{12}$ ]] || usage; RESTORE="$2"; shift 2 ;;
|
||||
--events) [ "$OP" = rollback ] && [ $# -ge 2 ] && [[ "$2" =~ ^(0|[1-9][0-9]*)$ ]] || usage; EVENTS="$2"; shift 2 ;;
|
||||
@@ -71,6 +73,8 @@ parse_opts() {
|
||||
esac
|
||||
done
|
||||
[ -z "$EVENTS" ] || [ -n "$RESTORE" ] || usage
|
||||
# --pm-after-harness replaces --pm (row 169 item 1), so the two never go together.
|
||||
[ "$WANT_PM" = 0 ] || [ "$AFTER_HARNESS" = 0 ] || usage
|
||||
local d
|
||||
for d in "$COPIES" "$BIN"; do
|
||||
case "$d" in /*) ;; *) usage ;; esac
|
||||
@@ -80,30 +84,32 @@ parse_opts() {
|
||||
|
||||
# --- The pointer and the log (item 1) ---
|
||||
|
||||
# Sets P_COPY P_BUSINESS P_PM P_CLAUDE P_SCHEMA P_ACT and the array P_CONSOLE; returns 1 with no
|
||||
# pointer. A pointer that is there but malformed stops the script: Jason or Mos settles it.
|
||||
# Sets P_COPY P_BUSINESS P_PM P_PENDING P_CLAUDE P_SCHEMA P_ACT and the array P_CONSOLE; returns 1
|
||||
# with no pointer. A pointer that is there but malformed stops the script: Jason or Mos settles it.
|
||||
# pmPending (row 169) is missing or true, and true only beside pm false.
|
||||
read_pointer() {
|
||||
[ -f "$POINTER" ] || return 1
|
||||
local fields
|
||||
fields="$(node -e '
|
||||
const p=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8"));
|
||||
const ok=/^[0-9a-f]{40}$/.test(p.copy)&&typeof p.business==="string"&&typeof p.pm==="boolean"&&typeof p.claude==="string"&&typeof p.schema==="string"&&typeof p.activation==="string"&&Array.isArray(p.console)&&p.console.every((t)=>typeof t==="string"&&t.length>0&&!/[\n\r]/.test(t));
|
||||
const ok=/^[0-9a-f]{40}$/.test(p.copy)&&typeof p.business==="string"&&typeof p.pm==="boolean"&&(p.pmPending===undefined||(p.pmPending===true&&p.pm===false))&&typeof p.claude==="string"&&typeof p.schema==="string"&&typeof p.activation==="string"&&Array.isArray(p.console)&&p.console.every((t)=>typeof t==="string"&&t.length>0&&!/[\n\r]/.test(t));
|
||||
if(!ok)throw new Error("pointer fields are missing or malformed");
|
||||
process.stdout.write([p.copy,p.business,p.pm?1:0,p.claude,p.schema,p.activation,...p.console].join("\n")+"\n");' -- "$POINTER")" || { echo "stack-release: $POINTER is unreadable; Jason or Mos settles it" >&2; exit 1; }
|
||||
process.stdout.write([p.copy,p.business,p.pm?1:0,p.pmPending?1:0,p.claude,p.schema,p.activation,...p.console].join("\n")+"\n");' -- "$POINTER")" || { echo "stack-release: $POINTER is unreadable; Jason or Mos settles it" >&2; exit 1; }
|
||||
local -a f
|
||||
mapfile -t f <<< "$fields"
|
||||
P_COPY="${f[0]}" P_BUSINESS="${f[1]}" P_PM="${f[2]}" P_CLAUDE="${f[3]}" P_SCHEMA="${f[4]}" P_ACT="${f[5]}"
|
||||
P_CONSOLE=("${f[@]:6}")
|
||||
P_COPY="${f[0]}" P_BUSINESS="${f[1]}" P_PM="${f[2]}" P_PENDING="${f[3]}" P_CLAUDE="${f[4]}" P_SCHEMA="${f[5]}" P_ACT="${f[6]}"
|
||||
P_CONSOLE=("${f[@]:7}")
|
||||
}
|
||||
|
||||
# Writes the pointer through a temp file in state/ and a rename. Args: copy pm claude schema console...
|
||||
# Writes the pointer through a temp file in state/ and a rename. Args: copy pm pending claude schema
|
||||
# console... A pending of 1 writes pmPending true (row 169); 0 leaves the key out.
|
||||
write_pointer() {
|
||||
local tmp
|
||||
tmp="$(mktemp "$STATE/.stack-active.XXXXXX")"
|
||||
node -e '
|
||||
const [copy,business,pm,claude,schema,activation,at,...console]=process.argv.slice(1);
|
||||
process.stdout.write(JSON.stringify({copy,business,pm:pm==="1",claude,console,schema,activation,at})+"\n");' -- \
|
||||
"$1" "$BUSINESS" "$2" "$3" "$4" "$ACT" "$(now)" "${@:5}" > "$tmp"
|
||||
const [copy,business,pm,pending,claude,schema,activation,at,...console]=process.argv.slice(1);
|
||||
process.stdout.write(JSON.stringify({copy,business,pm:pm==="1",...(pending==="1"?{pmPending:true}:{}),claude,console,schema,activation,at})+"\n");' -- \
|
||||
"$1" "$BUSINESS" "$2" "$3" "$4" "$5" "$ACT" "$(now)" "${@:6}" > "$tmp"
|
||||
chmod 0644 "$tmp"
|
||||
mv -f "$tmp" "$POINTER"
|
||||
}
|
||||
@@ -218,6 +224,58 @@ check_target() {
|
||||
[ "$TO" != "$P_COPY" ] || refuse already-active "$TO is the pointer's copy"
|
||||
}
|
||||
|
||||
# --- Plug-in kinds (row 169) ---
|
||||
|
||||
# Unlike copy_schema, these import the copy's own code, as a business-check does: its kinds.mjs
|
||||
# (no imports) and, for `harness`, its business.mjs loader. Modes, for copy $2:
|
||||
# has KIND exits 0 when the copy's KINDS has KIND
|
||||
# unknown prints the business file's plugins keys the copy's KINDS lacks, from a plain JSON read
|
||||
# harness prints plugins.harness as JSON, or `none`, from the copy's loader
|
||||
# A failure exits 1 and prints its first line.
|
||||
PLUGINS_JS='
|
||||
const {readFileSync}=require("node:fs");const {pathToFileURL}=require("node:url");
|
||||
const [mode,copy,id,kind]=process.argv.slice(1);
|
||||
const imp=(p)=>import(pathToFileURL(`${copy}/packages/${p}`).href);
|
||||
(async()=>{
|
||||
const {KINDS}=await imp("plugins/src/kinds.mjs");
|
||||
if(mode==="has")process.exit(Object.hasOwn(KINDS,kind)?0:1);
|
||||
const b=await imp("business/src/business.mjs");
|
||||
if(mode==="unknown"){
|
||||
const p=JSON.parse(readFileSync(b.businessFilePath(id),"utf8")).plugins;
|
||||
if(p===undefined)return;
|
||||
if(p===null||typeof p!=="object"||Array.isArray(p))throw new Error("its plugins is not an object");
|
||||
process.stdout.write(Object.keys(p).filter((k)=>!Object.hasOwn(KINDS,k)).join(" "));
|
||||
}else{
|
||||
const h=b.loadBusiness(id,{rolesDir:`${copy}/roles`}).plugins.harness;
|
||||
process.stdout.write(h===undefined?"none":JSON.stringify(h));
|
||||
}
|
||||
})().catch((e)=>{process.stdout.write(String(e?.message??e).split("\n")[0]);process.exit(1)});'
|
||||
plugins() { node -e "$PLUGINS_JS" -- "$1" "$COPIES/$2" "$BUSINESS" "${3:-}"; }
|
||||
|
||||
# A refusal --plan lists and goes on from; outside --plan it is refuse, which exits. A caller
|
||||
# returns after it, so the checks that depend on this one are skipped.
|
||||
check_refuse() {
|
||||
[ "$PLAN" = 1 ] || refuse "$@"
|
||||
local code="$1"; shift
|
||||
PLAN_REFUSE+=("$code: $*")
|
||||
}
|
||||
|
||||
# Item 1 and item 4, before anything stops, for target $TO: no plug-in kind the target lacks, then
|
||||
# for --pm-after-harness the target's harness kind and no harness entry yet.
|
||||
plugin_checks() {
|
||||
local out fix=""
|
||||
out="$(plugins unknown "$TO" 2>&1)" || { check_refuse business-unreadable "cannot read the business file's plugins with $TO's code: $out"; return 0; }
|
||||
if [ -n "$out" ]; then
|
||||
[ "$OP" != rollback ] || fix="; set each to null with plugins set through the live bus first"
|
||||
check_refuse unknown-kind-for-target "the business file names plug-in kind(s) $out, which $TO does not know$fix"
|
||||
return 0
|
||||
fi
|
||||
[ "$AFTER_HARNESS" = 1 ] || return 0
|
||||
plugins has "$TO" harness 2>/dev/null || { check_refuse pm-after-harness-target "$TO has no harness plug-in kind, so there is nothing to wait for; use --pm"; return 0; }
|
||||
out="$(plugins harness "$TO" 2>&1)" || { check_refuse business-unreadable "$TO's loader refuses the business file: $out"; return 0; }
|
||||
[ "$out" = none ] || check_refuse pm-after-harness-set "the business file already sets harness $out; use --pm"
|
||||
}
|
||||
|
||||
# --- The store ---
|
||||
|
||||
STORE_JS='
|
||||
@@ -533,12 +591,19 @@ drop_in_override() {
|
||||
}
|
||||
|
||||
# unit-flags-drift: the effective units against the pointer, and --pm against the pointer's pm.
|
||||
# --pm-after-harness stands in for --pm (row 169 item 1). A pending pointer (pm false, unit
|
||||
# without --pm) matches its unit; only pm-on and rollback get this far with one, since activate
|
||||
# refuses pm-pending first and adopt refuses already-adopted.
|
||||
flags_drift() {
|
||||
local d=""
|
||||
[ "$U_PM" = "$P_PM" ] || d+=$'\n'" pm: pointer $P_PM, unit $U_PM"
|
||||
[ "$U_CLAUDE" = "$P_CLAUDE" ] || d+=$'\n'" claude: pointer $P_CLAUDE, unit $U_CLAUDE"
|
||||
[ "${P_CONSOLE[*]}" = "${U_CONSOLE[*]}" ] || d+=$'\n'" console: pointer ${P_CONSOLE[*]}, unit ${U_CONSOLE[*]}"
|
||||
[ "$OP" != activate ] || [ "$WANT_PM" = "$P_PM" ] || d+=$'\n'" pm: pointer $P_PM, --pm given $WANT_PM"
|
||||
if [ "$AFTER_HARNESS" = 1 ]; then
|
||||
[ "$P_PM" = 1 ] || d+=$'\n'" pm: pointer $P_PM, --pm-after-harness needs a pointer with pm 1"
|
||||
else
|
||||
[ "$OP" != activate ] || [ "$WANT_PM" = "$P_PM" ] || d+=$'\n'" pm: pointer $P_PM, --pm given $WANT_PM"
|
||||
fi
|
||||
[ -z "$d" ] || refuse unit-flags-drift "the units differ from the pointer; settle it first:$d"
|
||||
}
|
||||
|
||||
@@ -577,7 +642,7 @@ do_adopt() {
|
||||
[ -n "$U_CLAUDE" ] || refuse no-claude-pin "$BUS_UNIT has no MOSAIC_CLAUDE_BIN"
|
||||
target_schema "$TO" || refuse copy-schema-unknown "cannot read SCHEMA_VERSION from $COPIES/$TO/packages/bus/src/store.mjs"
|
||||
ACT="$(date -u +%Y%m%dT%H%M%SZ)-${TO:0:12}"
|
||||
write_pointer "$TO" "$U_PM" "$U_CLAUDE" "$T_SCHEMA" "${U_CONSOLE[@]}"
|
||||
write_pointer "$TO" "$U_PM" 0 "$U_CLAUDE" "$T_SCHEMA" "${U_CONSOLE[@]}"
|
||||
log_line result ok
|
||||
echo "adopted $TO for $BUSINESS (pm $U_PM, schema $T_SCHEMA)"
|
||||
}
|
||||
@@ -666,9 +731,14 @@ plan() {
|
||||
echo "restore: the store must go back to $T_SCHEMA; needs --restore $(from_log backup || echo ACTIVATION)"
|
||||
fi
|
||||
plan_commits
|
||||
echo "bus: scripts/bus-service.sh install --copy $TO --copies $COPIES$([ "$P_PM" = 0 ] || echo ' --pm') --claude $P_CLAUDE"
|
||||
[ "$P_PENDING" = 0 ] || echo "pm: the pointer is pending (activate --pm-after-harness); the bus runs without its PM"
|
||||
[ "$RUN_PENDING" = 0 ] || echo "pm: the bus starts without --pm and the PM stays pending; next: Jason's plugins set of harness, then pm-on"
|
||||
echo "bus: scripts/bus-service.sh install --copy $TO --copies $COPIES$([ "$RUN_PM" = 0 ] || echo ' --pm') --claude $P_CLAUDE"
|
||||
echo "Console: scripts/webui-service.sh install --copy $TO --copies $COPIES${P_CONSOLE[*]:+ ${P_CONSOLE[*]}}"
|
||||
echo "CLI: scripts/stack-copy.sh wrapper $TO --copies $COPIES --bin $BIN"
|
||||
local r
|
||||
for r in "${PLAN_REFUSE[@]}"; do echo "refuses $r"; done
|
||||
[ "${#PLAN_REFUSE[@]}" = 0 ] || exit 3
|
||||
}
|
||||
|
||||
# Steps 4.1 to 4.5 and the gate for target $TO. $1 is the store step: migrate or restore.
|
||||
@@ -701,21 +771,39 @@ run_steps() {
|
||||
elif [ -n "$RESTORE" ]; then
|
||||
rollback_in_run store-differs
|
||||
fi
|
||||
install_stack "$TO" "$P_PM" "$P_CLAUDE" "${P_CONSOLE[@]}" || after_fail "$FAIL"
|
||||
install_stack "$TO" "$RUN_PM" "$P_CLAUDE" "${P_CONSOLE[@]}" || after_fail "$FAIL"
|
||||
start_both || after_fail start
|
||||
gate "$TO" "$P_PM" || after_fail "$GATE"
|
||||
gate "$TO" "$RUN_PM" || after_fail "$GATE"
|
||||
trap - INT TERM HUP
|
||||
write_pointer "$TO" "$P_PM" "$P_CLAUDE" "$T_SCHEMA" "${P_CONSOLE[@]}"
|
||||
log_line result ok
|
||||
echo "stack-release: $OP ok: $TO is live for $BUSINESS"
|
||||
write_pointer "$TO" "$RUN_PM" "$RUN_PENDING" "$P_CLAUDE" "$T_SCHEMA" "${P_CONSOLE[@]}"
|
||||
if [ "$RUN_PENDING" = 1 ]; then
|
||||
log_line result ok pm pending
|
||||
echo "stack-release: $OP ok: $TO is live for $BUSINESS without its PM; the PM is pending: Jason's plugins set of harness, then pm-on"
|
||||
else
|
||||
log_line result ok
|
||||
echo "stack-release: $OP ok: $TO is live for $BUSINESS"
|
||||
fi
|
||||
}
|
||||
|
||||
# The pm the run installs and gates, and whether the pointer it writes is pending (row 169).
|
||||
# --pm-after-harness installs without --pm and leaves the PM pending; a rollback from a pending
|
||||
# pointer installs the --pm the activate replaced.
|
||||
run_pm() {
|
||||
RUN_PM="$P_PM" RUN_PENDING=0
|
||||
if [ "$AFTER_HARNESS" = 1 ]; then RUN_PM=0 RUN_PENDING=1
|
||||
elif [ "$P_PENDING" = 1 ]; then RUN_PM=1
|
||||
fi
|
||||
}
|
||||
|
||||
do_activate() {
|
||||
TO="$ID"
|
||||
[ "$PLAN" = 1 ] || lock
|
||||
pointer_for_business
|
||||
if [ "$PLAN" = 1 ]; then check_target; plan; exit 0; fi
|
||||
run_pm
|
||||
[ "$P_PENDING" = 0 ] || check_refuse pm-pending "the pointer is pending (activate --pm-after-harness); run pm-on or rollback first"
|
||||
if [ "$PLAN" = 1 ]; then check_target; plugin_checks; plan; exit 0; fi
|
||||
precheck
|
||||
plugin_checks
|
||||
new_activation
|
||||
run_steps migrate
|
||||
}
|
||||
@@ -724,8 +812,10 @@ do_rollback() {
|
||||
[ "$PLAN" = 1 ] || lock
|
||||
pointer_for_business
|
||||
TO="$(from_log previous)" || refuse no-previous "the log names no earlier copy than $P_COPY"
|
||||
if [ "$PLAN" = 1 ]; then check_target; plan; exit 0; fi
|
||||
run_pm
|
||||
if [ "$PLAN" = 1 ]; then check_target; plugin_checks; plan; exit 0; fi
|
||||
precheck
|
||||
plugin_checks
|
||||
[ "$P_SCHEMA" = "$T_SCHEMA" ] || [ -n "$RESTORE" ] || refuse schema-differs "the store is at $P_SCHEMA and $TO expects $T_SCHEMA; pass --restore ACTIVATION"
|
||||
new_activation
|
||||
if [ -n "$RESTORE" ]; then
|
||||
@@ -750,9 +840,86 @@ do_rollback() {
|
||||
run_steps restore
|
||||
}
|
||||
|
||||
# --- pm-on (row 169 item 3) ---
|
||||
|
||||
# Stops the bus alone and waits for it and the stopped check, within --timeout. The Console runs on.
|
||||
stop_bus() {
|
||||
systemctl --user stop "$BUS_UNIT" || return 1
|
||||
local deadline=$((SECONDS + TIMEOUT)) s
|
||||
while :; do
|
||||
s="$(systemctl --user is-active "$BUS_UNIT" 2>/dev/null || true)"
|
||||
case "$s" in inactive|failed) if stopped; then say "stopped $BUS_UNIT"; return 0; fi ;; esac
|
||||
[ "$SECONDS" -lt "$deadline" ] || return 1
|
||||
sleep 1
|
||||
done
|
||||
}
|
||||
|
||||
start_bus() {
|
||||
START_EPOCH="$(date +%s)" && START_MS="$(date +%s%3N)" || return 1
|
||||
systemctl --user start "$BUS_UNIT"
|
||||
}
|
||||
|
||||
# mos-0848 point 3: put the phase-1 bus unit back (no --pm), restart it and rerun the phase-1 gate.
|
||||
# Passing leaves the bus live without its PM and the pointer pending (exit 1); failing stops both
|
||||
# units (exit 2). Neither touches the copy or the store. $1 is the code pm-on failed with.
|
||||
pm_on_fail() {
|
||||
trap - INT TERM HUP
|
||||
local code="$1" why="$GATE_WHY" pcode=""
|
||||
say "pm-on failed: $code${why:+ ($why)}; putting the phase-1 bus unit back"
|
||||
GATE_WHY=""
|
||||
if ! stop_bus; then pcode=stop-timeout
|
||||
elif ! put_back; then pcode=put-back
|
||||
elif ! start_bus; then pcode=start
|
||||
elif ! gate "$P_COPY" 0; then pcode="$GATE"
|
||||
fi
|
||||
if [ -z "$pcode" ]; then
|
||||
log_line result pm-on-failed code "$code" why "${why:-@null}"
|
||||
echo "stack-release: pm-on failed with $code${why:+ ($why)}; the bus runs $P_COPY without its PM again and passed the phase-1 gate, and the pointer stays pending. Fix harness with plugins set, then run pm-on again. Rollback set: $RS" >&2
|
||||
exit 1
|
||||
fi
|
||||
local pwhy="$GATE_WHY"
|
||||
systemctl --user stop "$CONSOLE_UNIT" || true
|
||||
systemctl --user stop "$BUS_UNIT" || true
|
||||
say "pm-on-down: pm-on $code, phase 1 $pcode"
|
||||
log_line result pm-on-down code "$code" why "${why:-@null}" phase1 "$pcode" phase1Why "${pwhy:-@null}"
|
||||
echo "stack-release: pm-on-down: pm-on failed with $code${why:+ ($why)} and the phase-1 bus with $pcode${pwhy:+ ($pwhy)}; both units are stopped. Rollback set: $RS" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
# Phase 2: the bus unit alone, on the pointer's copy with --pm, gated with pm_launched.
|
||||
do_pm_on() {
|
||||
lock
|
||||
pointer_for_business
|
||||
[ "$P_PENDING" = 1 ] || refuse not-pending "the pointer is not pending; pm-on follows activate --pm-after-harness"
|
||||
local out
|
||||
out="$(plugins harness "$P_COPY" 2>&1)" || refuse harness-unset "$P_COPY's loader refuses the business file: $out"
|
||||
[ "$out" != none ] || refuse harness-unset "the business file sets no harness; Jason sets it with plugins set first"
|
||||
node -e 'const h=JSON.parse(process.argv[1]);process.exit(Array.isArray(h)&&h.length>0?0:1)' -- "$out" 2>/dev/null || refuse harness-unset "the business file's harness is $out, not a non-empty list"
|
||||
instance_busy
|
||||
drop_in_override
|
||||
read_units || refuse unit-flags-drift "cannot read $BUS_UNIT or $CONSOLE_UNIT"
|
||||
[ -n "$U_CLAUDE" ] || refuse no-claude-pin "$BUS_UNIT has no MOSAIC_CLAUDE_BIN"
|
||||
flags_drift
|
||||
TO="$P_COPY"
|
||||
new_activation
|
||||
log_line result started
|
||||
capture || refuse capture-failed "could not write the rollback set $RS; nothing was stopped"
|
||||
trap 'pm_on_fail signal' INT TERM HUP
|
||||
stop_bus || pm_on_fail stop-timeout
|
||||
scripts/bus-service.sh install --copy "$P_COPY" --copies "$COPIES" --pm --claude "$P_CLAUDE" >> "$STEPS" 2>&1 || pm_on_fail install-bus
|
||||
say "installed $BUS_UNIT for $P_COPY with --pm"
|
||||
start_bus || pm_on_fail start
|
||||
gate "$P_COPY" 1 || pm_on_fail "$GATE"
|
||||
trap - INT TERM HUP
|
||||
write_pointer "$P_COPY" 1 0 "$P_CLAUDE" "$P_SCHEMA" "${P_CONSOLE[@]}"
|
||||
log_line result ok pm on
|
||||
echo "stack-release: pm-on ok: the PM is live on $P_COPY for $BUSINESS"
|
||||
}
|
||||
|
||||
do_status() {
|
||||
echo "pointer: $POINTER"
|
||||
if read_pointer; then sed 's/^/ /' "$POINTER"; else echo " none"; fi
|
||||
[ "$P_PENDING" = 0 ] || echo "pm: pending (activate --pm-after-harness); next: Jason's plugins set of harness, then stack-release.sh pm-on $BUSINESS"
|
||||
echo "log (last five lines):"
|
||||
if [ -f "$LOG" ]; then tail -n 5 "$LOG" | sed 's/^/ /'; else echo " none"; fi
|
||||
local open
|
||||
@@ -786,7 +953,7 @@ OP="$1"; shift
|
||||
ID=""
|
||||
case "$OP" in
|
||||
activate) [ $# -ge 2 ] || usage; ID="$1"; BUSINESS="$2"; shift 2; [[ "$ID" =~ ^[0-9a-f]{40}$ ]] || usage ;;
|
||||
adopt|rollback|status) [ $# -ge 1 ] || usage; BUSINESS="$1"; shift ;;
|
||||
adopt|pm-on|rollback|status) [ $# -ge 1 ] || usage; BUSINESS="$1"; shift ;;
|
||||
-h|--help) usage 0 ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
@@ -802,10 +969,11 @@ STORE="$DATA/bus/bus.sqlite"
|
||||
BUS_UNIT="mosaic-bus@$BUSINESS.service"
|
||||
FROM="" TO="" ACT="" RS="" POST="" BACKUP="" TAKEN="" RESTORED="" STEPS="" COUNT="" UNCLEAN="" FAIL=""
|
||||
STORE_SCHEMA="" START_EPOCH="" START_MS=0 GATE="" GATE_WHY="" S_CHANGED=""
|
||||
P_COPY="" P_PM=0 P_SCHEMA="" P_CLAUDE="" P_CONSOLE=()
|
||||
P_COPY="" P_PM=0 P_PENDING=0 P_SCHEMA="" P_CLAUDE="" P_CONSOLE=() RUN_PM=0 RUN_PENDING=0 PLAN_REFUSE=()
|
||||
case "$OP" in
|
||||
adopt) do_adopt ;;
|
||||
activate) do_activate ;;
|
||||
pm-on) do_pm_on ;;
|
||||
rollback) do_rollback ;;
|
||||
status) do_status ;;
|
||||
esac
|
||||
|
||||
Reference in New Issue
Block a user