c8ctl-plugin-nano 1.22.0 → 1.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/c8ctl-plugin.js +114 -4
- package/nanobpmn-binary.json +4 -4
- package/package.json +8 -8
package/c8ctl-plugin.js
CHANGED
|
@@ -212,6 +212,34 @@ function getSupervisorLogDir() {
|
|
|
212
212
|
return join(getLogDir(), 'supervisor');
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
+
/**
|
|
216
|
+
* Per-worker activity directory + file. A supervised `nano work` child writes a
|
|
217
|
+
* small JSON marker here reporting which job(s) it is currently servicing (or
|
|
218
|
+
* that it is idle); the daemon reads it for `supervisor status`. Worker ids are
|
|
219
|
+
* validated (`isValidWorkerName`: letters, digits, . _ -) so they are safe as a
|
|
220
|
+
* filename with no traversal risk.
|
|
221
|
+
*/
|
|
222
|
+
function getSupervisorActivityDir() {
|
|
223
|
+
return join(getStateHome(), 'supervisor-activity');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function supervisorWorkerActivityFile(id) {
|
|
227
|
+
return join(getSupervisorActivityDir(), `${id}.json`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Read a worker's activity marker. Returns the parsed object, or `null` when the
|
|
232
|
+
* file is absent (worker not reporting yet, or a standalone/older worker) or
|
|
233
|
+
* unreadable. Pure enough for status rendering (best-effort IO).
|
|
234
|
+
*/
|
|
235
|
+
function readWorkerActivity(id) {
|
|
236
|
+
try {
|
|
237
|
+
return JSON.parse(readFileSync(supervisorWorkerActivityFile(id), 'utf-8'));
|
|
238
|
+
} catch {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
215
243
|
/**
|
|
216
244
|
* Deterministic control-socket path shared by the daemon and every client.
|
|
217
245
|
* Derived from a hash of the (possibly overridden) state home so distinct
|
|
@@ -3071,6 +3099,40 @@ async function workAgent(req, flags) {
|
|
|
3071
3099
|
logger.info(` max parallel: ${maxParallelJobs}; job timeout: ${jobKillMs}ms; activation lock: ${jobLockMs}ms; poll timeout: ${pollTimeoutMs}ms`);
|
|
3072
3100
|
logger.info('Polling for work — press Ctrl-C to stop.');
|
|
3073
3101
|
|
|
3102
|
+
// When launched under the supervisor, report per-job activity — which job(s)
|
|
3103
|
+
// this worker is currently servicing, or that it is idle — to a small marker
|
|
3104
|
+
// file the supervisor reads for `supervisor status`. The daemon passes the
|
|
3105
|
+
// path via NANO_SUPERVISOR_ACTIVITY_FILE; a standalone `nano work` has no such
|
|
3106
|
+
// env var and writes nothing (this is entirely advisory).
|
|
3107
|
+
const activityFile = process.env.NANO_SUPERVISOR_ACTIVITY_FILE || null;
|
|
3108
|
+
const activeJobs = new Map(); // jobKey -> { type, since (ms epoch) }
|
|
3109
|
+
const writeActivity = () => {
|
|
3110
|
+
if (!activityFile) return;
|
|
3111
|
+
const jobs = [...activeJobs.entries()].map(([key, v]) => ({ key, type: v.type, since: v.since }));
|
|
3112
|
+
const payload = { pid: process.pid, updatedAt: Date.now(), busy: jobs.length > 0, jobs };
|
|
3113
|
+
const tmp = `${activityFile}.${process.pid}.tmp`;
|
|
3114
|
+
try {
|
|
3115
|
+
mkdirSync(dirname(activityFile), { recursive: true });
|
|
3116
|
+
writeFileSync(tmp, JSON.stringify(payload), { mode: 0o600 });
|
|
3117
|
+
renameSync(tmp, activityFile); // atomic swap so a reader never sees a half-write
|
|
3118
|
+
} catch {
|
|
3119
|
+
try { rmSync(tmp, { force: true }); } catch { /* best effort */ }
|
|
3120
|
+
/* best effort — activity is advisory, never fail a job over it */
|
|
3121
|
+
}
|
|
3122
|
+
};
|
|
3123
|
+
const recordJobStart = (job, jobType) => {
|
|
3124
|
+
if (!activityFile) return;
|
|
3125
|
+
activeJobs.set(String(job.jobKey), { type: jobType, since: Date.now() });
|
|
3126
|
+
writeActivity();
|
|
3127
|
+
};
|
|
3128
|
+
const recordJobEnd = (job) => {
|
|
3129
|
+
if (!activityFile) return;
|
|
3130
|
+
activeJobs.delete(String(job.jobKey));
|
|
3131
|
+
writeActivity();
|
|
3132
|
+
};
|
|
3133
|
+
// Seed an initial idle marker so status reports 'idle' immediately after spawn.
|
|
3134
|
+
writeActivity();
|
|
3135
|
+
|
|
3074
3136
|
// A per-job-type worker factory. Captures all the CLI-local + profile context
|
|
3075
3137
|
// in closure scope so the profile watcher below can (re)spawn a poller for any
|
|
3076
3138
|
// job type on demand without re-reading the flags.
|
|
@@ -3082,6 +3144,8 @@ async function workAgent(req, flags) {
|
|
|
3082
3144
|
jobTimeoutMs: jobLockMs,
|
|
3083
3145
|
pollTimeoutMs,
|
|
3084
3146
|
jobHandler: async (job) => {
|
|
3147
|
+
recordJobStart(job, jobType);
|
|
3148
|
+
try {
|
|
3085
3149
|
logger.info(`[${jobType}] job ${job.jobKey} (instance ${job.processInstanceKey ?? '-'}) → ${buildAgentCommandLine(profile.command, effectiveArgs)}`);
|
|
3086
3150
|
|
|
3087
3151
|
// Disk-budget admission shed: if the engine data root is below the free
|
|
@@ -3254,6 +3318,9 @@ async function workAgent(req, flags) {
|
|
|
3254
3318
|
retries,
|
|
3255
3319
|
variables: { [AGENT_RESULT_KEY]: resultEnvelope },
|
|
3256
3320
|
});
|
|
3321
|
+
} finally {
|
|
3322
|
+
recordJobEnd(job);
|
|
3323
|
+
}
|
|
3257
3324
|
},
|
|
3258
3325
|
});
|
|
3259
3326
|
|
|
@@ -3659,6 +3726,23 @@ function formatDuration(ms) {
|
|
|
3659
3726
|
function summarizeSupervisorWorker(w, now = Date.now()) {
|
|
3660
3727
|
const alive = isPidAlive(w.pid);
|
|
3661
3728
|
const uptimeMs = alive && w.startedAt ? Math.max(0, now - new Date(w.startedAt).getTime()) : 0;
|
|
3729
|
+
// Per-job activity (supervised workers only). Guard on pid so a stale marker
|
|
3730
|
+
// left by a previous incarnation can't show a dead job as in-flight.
|
|
3731
|
+
let activity = null; // { state: 'busy'|'idle', jobs: [{ key, type, sinceMs }] }
|
|
3732
|
+
if (alive) {
|
|
3733
|
+
const act = readWorkerActivity(w.id);
|
|
3734
|
+
if (act && act.pid === w.pid) {
|
|
3735
|
+
const jobs = Array.isArray(act.jobs)
|
|
3736
|
+
? act.jobs.map((j) => ({
|
|
3737
|
+
key: String(j.key),
|
|
3738
|
+
type: j.type ?? null,
|
|
3739
|
+
sinceMs: Number.isFinite(j.since) ? Math.max(0, now - j.since) : null,
|
|
3740
|
+
}))
|
|
3741
|
+
: [];
|
|
3742
|
+
activity = { state: jobs.length > 0 ? 'busy' : 'idle', jobs };
|
|
3743
|
+
}
|
|
3744
|
+
// No marker (or a stale-pid one): leave activity null → rendered as unknown.
|
|
3745
|
+
}
|
|
3662
3746
|
return {
|
|
3663
3747
|
id: w.id,
|
|
3664
3748
|
profile: w.profile,
|
|
@@ -3668,9 +3752,22 @@ function summarizeSupervisorWorker(w, now = Date.now()) {
|
|
|
3668
3752
|
uptimeMs,
|
|
3669
3753
|
lastExit: w.lastExit ?? null,
|
|
3670
3754
|
args: Array.isArray(w.args) ? w.args : [],
|
|
3755
|
+
activity,
|
|
3671
3756
|
};
|
|
3672
3757
|
}
|
|
3673
3758
|
|
|
3759
|
+
/** One-line JOB cell for a status row: the serviced job key, `idle`, or `-`. */
|
|
3760
|
+
function supervisorJobCell(w) {
|
|
3761
|
+
if (w.state !== 'running') return '-';
|
|
3762
|
+
const a = w.activity;
|
|
3763
|
+
if (!a) return '?'; // alive but not reporting (older worker / marker not yet written)
|
|
3764
|
+
if (a.state !== 'busy' || a.jobs.length === 0) return 'idle';
|
|
3765
|
+
const [first, ...rest] = a.jobs;
|
|
3766
|
+
const dur = first.sinceMs != null ? ` (${formatDuration(first.sinceMs)})` : '';
|
|
3767
|
+
const more = rest.length > 0 ? ` +${rest.length}` : '';
|
|
3768
|
+
return `${first.key}${more}${dur}`;
|
|
3769
|
+
}
|
|
3770
|
+
|
|
3674
3771
|
/** Render a supervisor status object as an aligned text table. */
|
|
3675
3772
|
function formatSupervisorStatus(status) {
|
|
3676
3773
|
const lines = [];
|
|
@@ -3690,13 +3787,14 @@ function formatSupervisorStatus(status) {
|
|
|
3690
3787
|
id: String(w.id),
|
|
3691
3788
|
profile: String(w.profile),
|
|
3692
3789
|
state: String(w.state),
|
|
3790
|
+
job: supervisorJobCell(w),
|
|
3693
3791
|
pid: w.pid ? String(w.pid) : '-',
|
|
3694
3792
|
restarts: String(w.restarts),
|
|
3695
3793
|
uptime: w.state === 'running' ? formatDuration(w.uptimeMs) : '-',
|
|
3696
3794
|
last: w.lastExit ? String(w.lastExit) : '-',
|
|
3697
3795
|
}));
|
|
3698
|
-
const head = { id: 'ID', profile: 'PROFILE', state: 'STATE', pid: 'PID', restarts: 'RESTARTS', uptime: 'UPTIME', last: 'LAST EXIT' };
|
|
3699
|
-
const cols = ['id', 'profile', 'state', 'pid', 'restarts', 'uptime', 'last'];
|
|
3796
|
+
const head = { id: 'ID', profile: 'PROFILE', state: 'STATE', job: 'JOB', pid: 'PID', restarts: 'RESTARTS', uptime: 'UPTIME', last: 'LAST EXIT' };
|
|
3797
|
+
const cols = ['id', 'profile', 'state', 'job', 'pid', 'restarts', 'uptime', 'last'];
|
|
3700
3798
|
const width = {};
|
|
3701
3799
|
for (const c of cols) width[c] = Math.max(head[c].length, ...rows.map((r) => r[c].length));
|
|
3702
3800
|
const fmt = (r) => ' ' + cols.map((c) => r[c].padEnd(width[c])).join(' ');
|
|
@@ -3855,10 +3953,17 @@ async function runSupervisorDaemon() {
|
|
|
3855
3953
|
const startWorker = (w) => {
|
|
3856
3954
|
let fd;
|
|
3857
3955
|
try { fd = openSync(w.logFile, 'a'); } catch { fd = 'ignore'; }
|
|
3956
|
+
// Clear any stale activity marker from a previous incarnation so a freshly
|
|
3957
|
+
// (re)started worker never briefly shows a dead job as in-flight.
|
|
3958
|
+
const activityFile = supervisorWorkerActivityFile(w.id);
|
|
3959
|
+
w.activityFile = activityFile;
|
|
3960
|
+
try { rmSync(activityFile, { force: true }); } catch { /* best effort */ }
|
|
3858
3961
|
// `--name w.id` makes the child's broker workerName match this worker's
|
|
3859
3962
|
// supervisor id, so the same profile launched twice is distinct end-to-end.
|
|
3963
|
+
// NANO_SUPERVISOR_ACTIVITY_FILE tells the child where to report per-job
|
|
3964
|
+
// activity for `supervisor status` (idle vs the job key it is servicing).
|
|
3860
3965
|
const child = spawn(exec, [entry, 'nano', 'work', w.profile, '--name', w.id, ...w.args], {
|
|
3861
|
-
env: process.env,
|
|
3966
|
+
env: { ...process.env, NANO_SUPERVISOR_ACTIVITY_FILE: activityFile },
|
|
3862
3967
|
stdio: ['ignore', fd, fd],
|
|
3863
3968
|
});
|
|
3864
3969
|
if (typeof fd === 'number') { try { closeSync(fd); } catch { /* dup'd into child */ } }
|
|
@@ -3880,6 +3985,8 @@ async function runSupervisorDaemon() {
|
|
|
3880
3985
|
settled = true;
|
|
3881
3986
|
w.pid = null;
|
|
3882
3987
|
w.lastExit = reason;
|
|
3988
|
+
// Drop the activity marker — a dead worker services no job.
|
|
3989
|
+
try { rmSync(w.activityFile || supervisorWorkerActivityFile(w.id), { force: true }); } catch { /* best effort */ }
|
|
3883
3990
|
const ranMs = Date.now() - (w.spawnedAt || Date.now());
|
|
3884
3991
|
if (ranMs >= SUPERVISOR_HEALTHY_UPTIME_MS) w.restarts = 0;
|
|
3885
3992
|
if (w.stopping || shuttingDown || !workers.has(w.id)) { persist(); return; }
|
|
@@ -3939,6 +4046,7 @@ async function runSupervisorDaemon() {
|
|
|
3939
4046
|
if (!workers.has(id)) return false;
|
|
3940
4047
|
await stopWorker(id);
|
|
3941
4048
|
workers.delete(id);
|
|
4049
|
+
try { rmSync(supervisorWorkerActivityFile(id), { force: true }); } catch { /* best effort */ }
|
|
3942
4050
|
dlog(`worker '${id}' removed`);
|
|
3943
4051
|
broadcast({ type: 'event', event: 'worker-remove', id });
|
|
3944
4052
|
persist();
|
|
@@ -5952,6 +6060,8 @@ export {
|
|
|
5952
6060
|
formatDuration,
|
|
5953
6061
|
summarizeSupervisorWorker,
|
|
5954
6062
|
formatSupervisorStatus,
|
|
6063
|
+
supervisorJobCell,
|
|
6064
|
+
supervisorWorkerActivityFile,
|
|
5955
6065
|
WORK_FORWARD_FLAGS,
|
|
5956
6066
|
runSupervisorDaemon,
|
|
5957
6067
|
startSupervisorDaemon,
|
|
@@ -6003,7 +6113,7 @@ export const metadata = {
|
|
|
6003
6113
|
{ command: 'c8ctl nano work coder --sandbox docker --image ghcr.io/acme/agent:1', description: 'Run jobs in isolated containers with disk-hygiene reaping' },
|
|
6004
6114
|
{ command: 'c8ctl nano supervisor start --worker reviewer --worker coder', description: 'Start a detached supervisor managing several workers from one terminal' },
|
|
6005
6115
|
{ command: 'c8ctl nano supervisor', description: 'Attach an interactive console to the supervisor (detach with Ctrl-D, leaving it running)' },
|
|
6006
|
-
{ command: 'c8ctl nano supervisor status', description: 'List supervised workers (pid, state, restarts, uptime) without the console' },
|
|
6116
|
+
{ command: 'c8ctl nano supervisor status', description: 'List supervised workers (pid, state, serviced job / idle, restarts, uptime) without the console' },
|
|
6007
6117
|
{ command: 'c8ctl nano supervisor add decider --max-parallel 2', description: 'Add a supervised worker (forwarding work flags) to the running supervisor' },
|
|
6008
6118
|
{ command: 'c8ctl nano supervisor restart reviewer', description: 'Restart a supervised worker by id or profile' },
|
|
6009
6119
|
{ command: 'c8ctl nano supervisor stop', description: 'Stop the supervisor daemon and all its workers' },
|
package/nanobpmn-binary.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.0.
|
|
3
|
-
"commit": "
|
|
4
|
-
"updated": "2026-
|
|
5
|
-
}
|
|
2
|
+
"version": "0.0.12",
|
|
3
|
+
"commit": "39f4a33",
|
|
4
|
+
"updated": "2026-08-07T05:19:36Z"
|
|
5
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "c8ctl-plugin-nano",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.23.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "c8ctl plugin to start, inspect, and stop a local Nano BPM (nanobpmn) cluster",
|
|
6
6
|
"main": "c8ctl-plugin.js",
|
|
@@ -47,12 +47,12 @@
|
|
|
47
47
|
"semantic-release": "^25.0.3"
|
|
48
48
|
},
|
|
49
49
|
"optionalDependencies": {
|
|
50
|
-
"@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.
|
|
51
|
-
"@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.
|
|
52
|
-
"@nanobpm/c8ctl-plugin-nano-linux-x64": "1.
|
|
53
|
-
"@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.
|
|
54
|
-
"@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.
|
|
55
|
-
"@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.
|
|
56
|
-
"@nanobpm/c8ctl-plugin-nano-win32-x64": "1.
|
|
50
|
+
"@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.23.0",
|
|
51
|
+
"@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.23.0",
|
|
52
|
+
"@nanobpm/c8ctl-plugin-nano-linux-x64": "1.23.0",
|
|
53
|
+
"@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.23.0",
|
|
54
|
+
"@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.23.0",
|
|
55
|
+
"@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.23.0",
|
|
56
|
+
"@nanobpm/c8ctl-plugin-nano-win32-x64": "1.23.0"
|
|
57
57
|
}
|
|
58
58
|
}
|