start-command 0.37.9 → 0.38.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/CHANGELOG.md +6 -0
- package/README.md +3 -0
- package/package.json +1 -1
- package/src/bin/cli.js +62 -26
- package/src/lib/args-parser-queries.js +45 -1
- package/src/lib/args-parser.js +10 -0
- package/src/lib/docker-command-handoff.js +34 -6
- package/src/lib/docker-runtime-args.js +7 -0
- package/src/lib/execution-attempt.js +19 -9
- package/src/lib/execution-chain.js +182 -0
- package/src/lib/execution-env.js +120 -0
- package/src/lib/execution-identity.js +180 -0
- package/src/lib/execution-recovery.js +17 -0
- package/src/lib/execution-resume.js +17 -0
- package/src/lib/execution-store.js +11 -22
- package/src/lib/isolation.js +21 -3
- package/src/lib/launch-identity-options.js +81 -0
- package/src/lib/launch-identity.js +411 -0
- package/src/lib/log-uploader.js +15 -1
- package/src/lib/query-commands.js +18 -3
- package/src/lib/screen-isolation.js +11 -2
- package/src/lib/spawn-helpers.js +33 -16
- package/src/lib/status-formatter.js +5 -1
- package/src/lib/usage.js +8 -1
- package/test/args-parser-attach-resume.js +1 -1
- package/test/args-parser-control.js +2 -2
- package/test/regression-229.js +267 -0
- package/test/regression-230.js +234 -0
- package/test/regression-231.js +298 -0
- package/test/shared-golden.js +15 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# start-command
|
|
2
2
|
|
|
3
|
+
## 0.38.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 8e62079: Executions now carry a stable identity a supervisor can rely on. A launch with an explicit `--session-id` that already exists is refused with exit 1 (`execution <uuid> already exists; use $ --resume <uuid> -- <command> to continue it, or pass --append-attempt`) instead of replacing the record and truncating its log; `--append-attempt` keeps the record, archives the previous attempt in `attemptHistory` and appends the new attempt to the same log (#229). Every backend (direct, `--isolated-user`, screen, tmux, docker, ssh, and docker resumes of stopped containers) exports `START_EXECUTION_ID`, `START_SESSION_NAME` and `START_ATTEMPT` to the command it runs (#230). `$ --alias <id> <name>` adds a name and `$ --rename <id> <name>` sets the display name of an execution, `--continues <id>` records the execution a fresh launch continues, `--status` reports `aliases` and the continuation `chain`, and `--upload-log` uploads the logs of the whole chain, oldest first (#231).
|
|
8
|
+
|
|
3
9
|
## 0.37.9
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -29,6 +29,9 @@ $ -i docker -d --label task.tool=codex -- my-task
|
|
|
29
29
|
$ --attach <id>
|
|
30
30
|
$ --resume <id> -- <command>
|
|
31
31
|
$ --resume-all
|
|
32
|
+
$ --session-id <uuid> --append-attempt -- <command>
|
|
33
|
+
$ --continues <id> -- <command>
|
|
34
|
+
$ --rename <id> <name>
|
|
32
35
|
```
|
|
33
36
|
|
|
34
37
|
See the project-wide [README](../README.md), [docs/USAGE.md](../docs/USAGE.md),
|
package/package.json
CHANGED
package/src/bin/cli.js
CHANGED
|
@@ -18,7 +18,6 @@ const {
|
|
|
18
18
|
getTimestamp,
|
|
19
19
|
createLogHeader,
|
|
20
20
|
createLogFooter,
|
|
21
|
-
writeLogFile,
|
|
22
21
|
appendLogFile,
|
|
23
22
|
createLogPath,
|
|
24
23
|
getDefaultDockerImage,
|
|
@@ -41,6 +40,7 @@ const { runWithBunSpawn, runWithNodeSpawn } = require('../lib/spawn-helpers');
|
|
|
41
40
|
const { resolveChildExitCode } = require('../lib/exit-reason');
|
|
42
41
|
const { printUsage } = require('../lib/usage');
|
|
43
42
|
const { buildDisplayCommand, getCommandName } = require('../lib/shell-utils');
|
|
43
|
+
const launchIdentity = require('../lib/launch-identity');
|
|
44
44
|
|
|
45
45
|
// A closed stdout/stderr pipe (`$ --version | head -1`) must neither crash
|
|
46
46
|
// start nor replace the command's exit code: drop the failed output and keep
|
|
@@ -245,6 +245,8 @@ const useCommandStream =
|
|
|
245
245
|
|
|
246
246
|
// Generate session ID if not provided (auto-generate UUID)
|
|
247
247
|
const sessionId = wrapperOptions.sessionId || generateUUID();
|
|
248
|
+
// How this launch uses its execution id (issue #229), set before dispatch
|
|
249
|
+
let launch = null;
|
|
248
250
|
|
|
249
251
|
// Main execution
|
|
250
252
|
(async () => {
|
|
@@ -256,6 +258,19 @@ const sessionId = wrapperOptions.sessionId || generateUUID();
|
|
|
256
258
|
process.exit(queryExitCode === null ? 0 : queryExitCode);
|
|
257
259
|
}
|
|
258
260
|
|
|
261
|
+
// An explicit --session-id must not overwrite a tracked execution (#229).
|
|
262
|
+
try {
|
|
263
|
+
launch = launchIdentity.prepareLaunch(
|
|
264
|
+
getExecutionStore(),
|
|
265
|
+
wrapperOptions,
|
|
266
|
+
sessionId,
|
|
267
|
+
createLogPath(wrapperOptions.isolated || 'direct', sessionId)
|
|
268
|
+
);
|
|
269
|
+
} catch (error) {
|
|
270
|
+
console.error(`Error: ${error.message}`);
|
|
271
|
+
process.exit(1);
|
|
272
|
+
}
|
|
273
|
+
|
|
259
274
|
if (wrapperOptions.isolated === 'docker') {
|
|
260
275
|
const {
|
|
261
276
|
resolveResourceOptions,
|
|
@@ -314,8 +329,8 @@ async function runWithIsolation(
|
|
|
314
329
|
effectiveImage = getDefaultDockerImage();
|
|
315
330
|
}
|
|
316
331
|
|
|
317
|
-
// Create log file path
|
|
318
|
-
const logFilePath =
|
|
332
|
+
// Create log file path (an appended attempt keeps the execution's log)
|
|
333
|
+
const logFilePath = launch.logPath;
|
|
319
334
|
|
|
320
335
|
// Get session name (will be generated by runIsolated if not provided)
|
|
321
336
|
const sessionName =
|
|
@@ -424,6 +439,7 @@ async function runWithIsolation(
|
|
|
424
439
|
useCommandStream,
|
|
425
440
|
},
|
|
426
441
|
});
|
|
442
|
+
launchIdentity.applyLaunch(executionRecord, launch);
|
|
427
443
|
}
|
|
428
444
|
|
|
429
445
|
// Print start block with session ID and isolation info
|
|
@@ -448,8 +464,13 @@ async function runWithIsolation(
|
|
|
448
464
|
currentExecutionRecord = executionRecord;
|
|
449
465
|
require('../lib/launch-owner').markLaunch(executionRecord);
|
|
450
466
|
try {
|
|
451
|
-
|
|
467
|
+
launchIdentity.claimLaunch(store, executionRecord, launch);
|
|
452
468
|
} catch (err) {
|
|
469
|
+
if (err instanceof launchIdentity.LaunchRefusedError) {
|
|
470
|
+
console.error(`Error: ${err.message}`);
|
|
471
|
+
currentExecutionRecord = null;
|
|
472
|
+
process.exit(1);
|
|
473
|
+
}
|
|
453
474
|
console.error(
|
|
454
475
|
JSON.stringify({
|
|
455
476
|
code: 'LAUNCH_RESERVATION_FAILED',
|
|
@@ -483,7 +504,12 @@ async function runWithIsolation(
|
|
|
483
504
|
);
|
|
484
505
|
}
|
|
485
506
|
|
|
486
|
-
|
|
507
|
+
launchIdentity.writeLaunchLog(
|
|
508
|
+
logFilePath,
|
|
509
|
+
logContent,
|
|
510
|
+
launch,
|
|
511
|
+
executionRecord
|
|
512
|
+
);
|
|
487
513
|
|
|
488
514
|
let result;
|
|
489
515
|
|
|
@@ -495,6 +521,7 @@ async function runWithIsolation(
|
|
|
495
521
|
...options,
|
|
496
522
|
session: sessionName,
|
|
497
523
|
sessionId,
|
|
524
|
+
attemptNumber: launch.attemptNumber,
|
|
498
525
|
image: effectiveImage,
|
|
499
526
|
endpoint: options.endpoint,
|
|
500
527
|
detached: mode === 'detached',
|
|
@@ -513,7 +540,10 @@ async function runWithIsolation(
|
|
|
513
540
|
});
|
|
514
541
|
} else if (createdUser) {
|
|
515
542
|
// Run directly as the created user (no isolation environment)
|
|
516
|
-
result = await runAsIsolatedUser(
|
|
543
|
+
result = await runAsIsolatedUser(
|
|
544
|
+
launchIdentity.isolatedUserCommand(cmd, sessionId, launch),
|
|
545
|
+
createdUser
|
|
546
|
+
);
|
|
517
547
|
} else {
|
|
518
548
|
// This shouldn't happen in isolation mode, but handle gracefully
|
|
519
549
|
result = { success: false, message: 'No isolation configuration provided' };
|
|
@@ -637,8 +667,8 @@ async function runDirect(cmd, sessionId) {
|
|
|
637
667
|
const shell = isWindows ? 'powershell.exe' : process.env.SHELL || '/bin/sh';
|
|
638
668
|
const shellArgs = isWindows ? ['-Command', cmd] : ['-c', cmd];
|
|
639
669
|
|
|
640
|
-
// Setup logging
|
|
641
|
-
const logFilePath =
|
|
670
|
+
// Setup logging (an appended attempt keeps the execution's log)
|
|
671
|
+
const logFilePath = launch.logPath;
|
|
642
672
|
|
|
643
673
|
let logContent = '';
|
|
644
674
|
const startTime = getTimestamp();
|
|
@@ -666,6 +696,8 @@ async function runDirect(cmd, sessionId) {
|
|
|
666
696
|
runtimeVersion,
|
|
667
697
|
},
|
|
668
698
|
});
|
|
699
|
+
launchIdentity.applyLaunch(executionRecord, launch);
|
|
700
|
+
launchIdentity.claimDirectLaunch(store, executionRecord, launch, config);
|
|
669
701
|
// Set global reference for signal cleanup
|
|
670
702
|
currentExecutionRecord = executionRecord;
|
|
671
703
|
}
|
|
@@ -746,7 +778,13 @@ async function runDirect(cmd, sessionId) {
|
|
|
746
778
|
shell,
|
|
747
779
|
shellArgs,
|
|
748
780
|
logFilePath,
|
|
749
|
-
logContent
|
|
781
|
+
logContent: launchIdentity.launchLogContent(
|
|
782
|
+
logContent,
|
|
783
|
+
launch,
|
|
784
|
+
executionRecord
|
|
785
|
+
),
|
|
786
|
+
appendLog: launch.append,
|
|
787
|
+
env: launchIdentity.directLaunchEnv(sessionId, launch),
|
|
750
788
|
startTimeMs,
|
|
751
789
|
executionRecord,
|
|
752
790
|
store,
|
|
@@ -786,8 +824,8 @@ async function runDirectWithCommandStream(
|
|
|
786
824
|
const isWindows = process.platform === 'win32';
|
|
787
825
|
const shell = isWindows ? 'powershell.exe' : process.env.SHELL || '/bin/sh';
|
|
788
826
|
|
|
789
|
-
// Setup logging
|
|
790
|
-
const logFilePath =
|
|
827
|
+
// Setup logging (an appended attempt keeps the execution's log)
|
|
828
|
+
const logFilePath = launch.logPath;
|
|
791
829
|
|
|
792
830
|
let logContent = '';
|
|
793
831
|
const startTime = getTimestamp();
|
|
@@ -816,6 +854,8 @@ async function runDirectWithCommandStream(
|
|
|
816
854
|
executionMode: 'command-stream',
|
|
817
855
|
},
|
|
818
856
|
});
|
|
857
|
+
launchIdentity.applyLaunch(executionRecord, launch);
|
|
858
|
+
launchIdentity.claimDirectLaunch(store, executionRecord, launch, config);
|
|
819
859
|
// Set global reference for signal cleanup
|
|
820
860
|
currentExecutionRecord = executionRecord;
|
|
821
861
|
}
|
|
@@ -853,27 +893,23 @@ async function runDirectWithCommandStream(
|
|
|
853
893
|
console.log('[command-stream] Using command-stream library');
|
|
854
894
|
console.log('');
|
|
855
895
|
|
|
856
|
-
// Save initial execution record (PID will be updated later if available)
|
|
857
|
-
if (executionRecord && store) {
|
|
858
|
-
try {
|
|
859
|
-
store.save(executionRecord);
|
|
860
|
-
} catch (err) {
|
|
861
|
-
if (config.verbose) {
|
|
862
|
-
console.error(
|
|
863
|
-
`[Tracking] Warning: Could not save execution record: ${err.message}`
|
|
864
|
-
);
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
}
|
|
868
|
-
|
|
869
896
|
// Execute the command using command-stream with real-time output
|
|
870
897
|
// Using mirror: true to show output in real-time, capture: true to collect it
|
|
871
898
|
// Using raw() to avoid auto-escaping that might interfere with complex shell commands
|
|
872
|
-
const $cmd = $({
|
|
899
|
+
const $cmd = $({
|
|
900
|
+
mirror: true,
|
|
901
|
+
capture: true,
|
|
902
|
+
env: launchIdentity.directLaunchEnv(sessionId, launch),
|
|
903
|
+
});
|
|
873
904
|
|
|
874
905
|
let exitCode;
|
|
875
906
|
try {
|
|
876
|
-
|
|
907
|
+
launchIdentity.writeLaunchLog(
|
|
908
|
+
logFilePath,
|
|
909
|
+
logContent,
|
|
910
|
+
launch,
|
|
911
|
+
executionRecord
|
|
912
|
+
);
|
|
877
913
|
// Use raw() to pass the command without auto-escaping
|
|
878
914
|
// This is important for complex commands with pipes, redirects, etc.
|
|
879
915
|
const result = await $cmd`${raw(cmd)}`;
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* Control modes: --upload-log, --stop, --terminate, --attach [--read-only],
|
|
10
10
|
* --resume [-- <command>], --resume-all,
|
|
11
11
|
* --cleanup [<uuid-or-session-name>]
|
|
12
|
+
* Naming modes: --alias <id> <name>, --rename <id> <name> (issue #231)
|
|
12
13
|
*/
|
|
13
14
|
|
|
14
15
|
/**
|
|
@@ -39,6 +40,9 @@ function createQueryOptionDefaults() {
|
|
|
39
40
|
cleanup: false, // Clean up stale "executing" records
|
|
40
41
|
cleanupDryRun: false, // Show what would be cleaned without cleaning
|
|
41
42
|
cleanupTarget: null, // UUID/session name whose containers and images --cleanup removes
|
|
43
|
+
alias: null, // Execution that --alias adds a name to
|
|
44
|
+
rename: null, // Execution that --rename renames
|
|
45
|
+
newName: null, // Name given by --alias/--rename
|
|
42
46
|
};
|
|
43
47
|
}
|
|
44
48
|
|
|
@@ -120,6 +124,35 @@ function hasValue(value) {
|
|
|
120
124
|
return value !== null && value !== undefined;
|
|
121
125
|
}
|
|
122
126
|
|
|
127
|
+
/**
|
|
128
|
+
* `--alias <id> <name>` and `--rename <id> <name>` take two arguments.
|
|
129
|
+
*/
|
|
130
|
+
const NAMING_OPTIONS = {
|
|
131
|
+
'--alias': 'alias',
|
|
132
|
+
'--rename': 'rename',
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
function parseNamingOption(args, index, options) {
|
|
136
|
+
const key = NAMING_OPTIONS[args[index]];
|
|
137
|
+
if (!key) {
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
const [identifier, name] = args.slice(index + 1, index + 3);
|
|
141
|
+
if (
|
|
142
|
+
identifier === undefined ||
|
|
143
|
+
name === undefined ||
|
|
144
|
+
identifier.startsWith('-') ||
|
|
145
|
+
name.startsWith('-')
|
|
146
|
+
) {
|
|
147
|
+
throw new Error(
|
|
148
|
+
`Option ${args[index]} requires a UUID or session name and a new name`
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
options[key] = identifier;
|
|
152
|
+
options.newName = name;
|
|
153
|
+
return 3;
|
|
154
|
+
}
|
|
155
|
+
|
|
123
156
|
/**
|
|
124
157
|
* Parse a single query/control option.
|
|
125
158
|
* @param {string[]} args - Arguments array
|
|
@@ -135,6 +168,11 @@ function parseQueryOption(args, index, options) {
|
|
|
135
168
|
return cleanupConsumed;
|
|
136
169
|
}
|
|
137
170
|
|
|
171
|
+
const namingConsumed = parseNamingOption(args, index, options);
|
|
172
|
+
if (namingConsumed > 0) {
|
|
173
|
+
return namingConsumed;
|
|
174
|
+
}
|
|
175
|
+
|
|
138
176
|
const flag = FLAG_OPTIONS[arg];
|
|
139
177
|
if (flag) {
|
|
140
178
|
flag(options);
|
|
@@ -216,6 +254,12 @@ function activeQueryModes(options) {
|
|
|
216
254
|
if (options.cleanup) {
|
|
217
255
|
modes.push('--cleanup');
|
|
218
256
|
}
|
|
257
|
+
if (hasValue(options.alias)) {
|
|
258
|
+
modes.push('--alias');
|
|
259
|
+
}
|
|
260
|
+
if (hasValue(options.rename)) {
|
|
261
|
+
modes.push('--rename');
|
|
262
|
+
}
|
|
219
263
|
return modes;
|
|
220
264
|
}
|
|
221
265
|
|
|
@@ -248,7 +292,7 @@ function validateQueryOptions(options) {
|
|
|
248
292
|
|
|
249
293
|
if (modes.length > 1) {
|
|
250
294
|
throw new Error(
|
|
251
|
-
'Cannot combine --status, --list, --upload-log, --stop, --terminate, --attach, --resume, --resume-all, or --
|
|
295
|
+
'Cannot combine --status, --list, --upload-log, --stop, --terminate, --attach, --resume, --resume-all, --cleanup, --alias, or --rename in the same invocation'
|
|
252
296
|
);
|
|
253
297
|
}
|
|
254
298
|
|
package/src/lib/args-parser.js
CHANGED
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
* --resume-all Re-attach or reconcile every execution still marked running
|
|
40
40
|
* --cleanup [<uuid-or-session>] Clean up stale "executing" records, or one execution's containers and snapshot images
|
|
41
41
|
* --cleanup-dry-run Show stale records that would be cleaned up (without actually cleaning)
|
|
42
|
+
* --alias <id> <name>, --rename <id> <name>, --append-attempt, --continues <id>: see args-parser-queries.js, launch-identity-options.js
|
|
42
43
|
*/
|
|
43
44
|
|
|
44
45
|
const { getDefaultDockerImage } = require('./docker-utils');
|
|
@@ -47,6 +48,7 @@ const dockerRecoveryOptions = require('./docker-recovery-options');
|
|
|
47
48
|
const cpuPenalty = require('./cpu-penalty');
|
|
48
49
|
const dockerResourceOptions = require('./docker-resource-options');
|
|
49
50
|
const snapshotResumeOptions = require('./snapshot-resume-options');
|
|
51
|
+
const launchIdentityOptions = require('./launch-identity-options');
|
|
50
52
|
const { parseSequence, isSequence } = require('./sequence-parser');
|
|
51
53
|
const { buildCommandString } = require('./shell-utils');
|
|
52
54
|
const {
|
|
@@ -217,6 +219,7 @@ function parseArgs(args) {
|
|
|
217
219
|
useCommandStream: false, // Use command-stream library for command execution
|
|
218
220
|
...createQueryOptionDefaults(),
|
|
219
221
|
...snapshotResumeOptions.createSnapshotOptionDefaults(),
|
|
222
|
+
...launchIdentityOptions.createLaunchIdentityOptionDefaults(),
|
|
220
223
|
};
|
|
221
224
|
|
|
222
225
|
let commandArgs = [];
|
|
@@ -580,6 +583,12 @@ function parseOption(args, index, options) {
|
|
|
580
583
|
return queryConsumed;
|
|
581
584
|
}
|
|
582
585
|
|
|
586
|
+
// --append-attempt, --continues <id> (issues #229, #231)
|
|
587
|
+
const identityConsumed = launchIdentityOptions.parse(args, index, options);
|
|
588
|
+
if (identityConsumed > 0) {
|
|
589
|
+
return identityConsumed;
|
|
590
|
+
}
|
|
591
|
+
|
|
583
592
|
// Not a recognized wrapper option
|
|
584
593
|
return 0;
|
|
585
594
|
}
|
|
@@ -830,6 +839,7 @@ function validateOptions(options) {
|
|
|
830
839
|
|
|
831
840
|
snapshotResumeOptions.validate(options);
|
|
832
841
|
validateQueryOptions(options);
|
|
842
|
+
launchIdentityOptions.validate(options);
|
|
833
843
|
|
|
834
844
|
// Validate shell option
|
|
835
845
|
if (options.shell !== null && options.shell !== undefined) {
|
|
@@ -12,7 +12,9 @@ function buildCommandHandoffArgs(mainArgs, markerPath) {
|
|
|
12
12
|
return [
|
|
13
13
|
'sh',
|
|
14
14
|
'-c',
|
|
15
|
-
|
|
15
|
+
// `$p.env` refreshes the START_* variables a `docker start` cannot
|
|
16
|
+
// change (issue #230) before either command runs.
|
|
17
|
+
'p=$1; shift; if [ -f "$p.env" ]; then . "$p.env"; fi; if [ -f "$p" ]; then exec sh "$p"; fi; exec "$@"',
|
|
16
18
|
'start-command-handoff',
|
|
17
19
|
markerPath,
|
|
18
20
|
...mainArgs,
|
|
@@ -49,27 +51,53 @@ function buildCommandHandoffScript(command, options = {}) {
|
|
|
49
51
|
return `exec ${shellQuote(shell)}${interactive} -c ${shellQuote(effective)}\n`;
|
|
50
52
|
}
|
|
51
53
|
|
|
52
|
-
function
|
|
54
|
+
function copyIntoContainer(containerName, target, content, runner) {
|
|
53
55
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'start-handoff-'));
|
|
54
56
|
const file = path.join(dir, 'command');
|
|
55
57
|
try {
|
|
56
|
-
fs.writeFileSync(file,
|
|
57
|
-
mode: 0o644,
|
|
58
|
-
});
|
|
58
|
+
fs.writeFileSync(file, content, { mode: 0o644 });
|
|
59
59
|
fs.chmodSync(file, 0o644);
|
|
60
60
|
return runner(require('./docker-cleanup').getDockerCommand(), [
|
|
61
61
|
'cp',
|
|
62
62
|
file,
|
|
63
|
-
`${containerName}:${
|
|
63
|
+
`${containerName}:${target}`,
|
|
64
64
|
]);
|
|
65
65
|
} finally {
|
|
66
66
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
function writeCommandHandoff(containerName, command, runner, options = {}) {
|
|
71
|
+
return copyIntoContainer(
|
|
72
|
+
containerName,
|
|
73
|
+
commandHandoffPath(containerName),
|
|
74
|
+
buildCommandHandoffScript(command, options),
|
|
75
|
+
runner
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Copy the START_* variables of the next attempt into a stopped container
|
|
81
|
+
* that was created with the handoff selector (issue #230).
|
|
82
|
+
* @param {string} containerName - Container name
|
|
83
|
+
* @param {object} env - Variables from `buildExecutionEnv`
|
|
84
|
+
* @param {Function} runner - Command runner
|
|
85
|
+
* @returns {object} Runner result
|
|
86
|
+
*/
|
|
87
|
+
function writeExecutionEnvHandoff(containerName, env, runner) {
|
|
88
|
+
const statement = require('./execution-env').buildExecutionEnvExport(env);
|
|
89
|
+
return copyIntoContainer(
|
|
90
|
+
containerName,
|
|
91
|
+
`${commandHandoffPath(containerName)}.env`,
|
|
92
|
+
statement ? `${statement}\n` : '',
|
|
93
|
+
runner
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
70
97
|
module.exports = {
|
|
71
98
|
commandHandoffPath,
|
|
72
99
|
buildCommandHandoffArgs,
|
|
73
100
|
buildCommandHandoffScript,
|
|
74
101
|
writeCommandHandoff,
|
|
102
|
+
writeExecutionEnvHandoff,
|
|
75
103
|
};
|
|
@@ -42,6 +42,13 @@ function buildDockerRuntimeArgs(options = {}) {
|
|
|
42
42
|
for (const envVar of options.env || []) {
|
|
43
43
|
args.push('-e', envVar);
|
|
44
44
|
}
|
|
45
|
+
// Execution identity (issue #230); last, so a user `--env` cannot spoof it.
|
|
46
|
+
const executionEnv = require('./execution-env');
|
|
47
|
+
for (const envVar of executionEnv.executionEnvAssignments(
|
|
48
|
+
executionEnv.executionEnvFromOptions(options)
|
|
49
|
+
)) {
|
|
50
|
+
args.push('-e', envVar);
|
|
51
|
+
}
|
|
45
52
|
for (const label of require('./docker-labels').dockerLabels(options)) {
|
|
46
53
|
args.push('--label', label);
|
|
47
54
|
}
|
|
@@ -46,6 +46,21 @@ function readAttemptActivity(record) {
|
|
|
46
46
|
return record;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/** Number of the record's current attempt: 1 until it is resumed. */
|
|
50
|
+
function currentAttemptNumber(record) {
|
|
51
|
+
return (
|
|
52
|
+
record?.attempt?.number ||
|
|
53
|
+
(Number(record?.options?.resumeCount) || 0) +
|
|
54
|
+
(Number(record?.options?.recoveryAttempts) || 0) +
|
|
55
|
+
1
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Number the next attempt of `record` gets (`START_ATTEMPT`, issue #230). */
|
|
60
|
+
function nextAttemptNumber(record) {
|
|
61
|
+
return currentAttemptNumber(record) + 1;
|
|
62
|
+
}
|
|
63
|
+
|
|
49
64
|
function createAttempt(record, plan) {
|
|
50
65
|
let logOffset = null;
|
|
51
66
|
if (record.logPath) {
|
|
@@ -58,11 +73,7 @@ function createAttempt(record, plan) {
|
|
|
58
73
|
}
|
|
59
74
|
}
|
|
60
75
|
return {
|
|
61
|
-
number:
|
|
62
|
-
(record.attempt?.number ||
|
|
63
|
-
(Number(record.options?.resumeCount) || 0) +
|
|
64
|
-
(Number(record.options?.recoveryAttempts) || 0) +
|
|
65
|
-
1) + 1,
|
|
76
|
+
number: nextAttemptNumber(record),
|
|
66
77
|
startedAt: new Date().toISOString(),
|
|
67
78
|
logOffset,
|
|
68
79
|
lastOutputAt: null,
|
|
@@ -78,10 +89,7 @@ function archiveAttempt(record) {
|
|
|
78
89
|
readAttemptActivity(record);
|
|
79
90
|
const snapshot = {
|
|
80
91
|
...(record.attempt || {
|
|
81
|
-
number:
|
|
82
|
-
(Number(record.options?.resumeCount) || 0) +
|
|
83
|
-
(Number(record.options?.recoveryAttempts) || 0) +
|
|
84
|
-
1,
|
|
92
|
+
number: currentAttemptNumber(record),
|
|
85
93
|
startedAt: record.options?.resumedAt || record.startTime,
|
|
86
94
|
logOffset: null,
|
|
87
95
|
}),
|
|
@@ -176,6 +184,8 @@ module.exports = {
|
|
|
176
184
|
appendLifecycle,
|
|
177
185
|
archiveAttempt,
|
|
178
186
|
createAttempt,
|
|
187
|
+
currentAttemptNumber,
|
|
188
|
+
nextAttemptNumber,
|
|
179
189
|
patchAttempt,
|
|
180
190
|
readAttemptActivity,
|
|
181
191
|
readAttemptLogTail,
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Continuation chains (issue #231): a launch with `--continues <id>` stores
|
|
3
|
+
* the UUID it continues, so a supervisor that relaunches fresh instead of
|
|
4
|
+
* `--resume`-ing in place can still treat every execution of one task as a
|
|
5
|
+
* whole. `--status` prints the chain and `--upload-log` uploads one file with
|
|
6
|
+
* every execution of it, oldest first.
|
|
7
|
+
*/
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { executionAliases, hasUserNames } = require('./execution-identity');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* UUIDs of the chain `record` belongs to, oldest first. Backward links come
|
|
15
|
+
* from `continues`; forward, the most recently started record that continues
|
|
16
|
+
* the current one is followed. Cycles stop the walk.
|
|
17
|
+
* @param {object[]} records - Stored records
|
|
18
|
+
* @param {object} record - Any member of the chain
|
|
19
|
+
* @returns {object[]} Records of the chain, oldest first
|
|
20
|
+
*/
|
|
21
|
+
function buildChain(records, record) {
|
|
22
|
+
const seen = new Set([record.uuid]);
|
|
23
|
+
const chain = [record];
|
|
24
|
+
for (let cur = record; cur.continues; ) {
|
|
25
|
+
const prev = records.find((r) => r.uuid === cur.continues);
|
|
26
|
+
if (!prev || seen.has(prev.uuid)) {
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
seen.add(prev.uuid);
|
|
30
|
+
chain.unshift(prev);
|
|
31
|
+
cur = prev;
|
|
32
|
+
}
|
|
33
|
+
for (let cur = record; ; ) {
|
|
34
|
+
const next = records
|
|
35
|
+
.filter((r) => r.continues === cur.uuid && !seen.has(r.uuid))
|
|
36
|
+
.sort((a, b) => Date.parse(b.startTime) - Date.parse(a.startTime))[0];
|
|
37
|
+
if (!next) {
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
seen.add(next.uuid);
|
|
41
|
+
chain.push(next);
|
|
42
|
+
cur = next;
|
|
43
|
+
}
|
|
44
|
+
return chain;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Resolve `--continues <id>` for a new launch to the UUID it stores.
|
|
49
|
+
* @param {object|null} store - ExecutionStore
|
|
50
|
+
* @param {string} identifier - Any name of the earlier execution
|
|
51
|
+
* @param {string} sessionId - UUID of the new launch
|
|
52
|
+
* @returns {string} UUID of the continued execution
|
|
53
|
+
* @throws {Error} When it cannot be resolved
|
|
54
|
+
*/
|
|
55
|
+
function resolveContinues(store, identifier, sessionId) {
|
|
56
|
+
if (!store) {
|
|
57
|
+
throw new Error('--continues requires execution tracking.');
|
|
58
|
+
}
|
|
59
|
+
const target = store.get(identifier);
|
|
60
|
+
if (!target) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
`No execution found with UUID or session name: ${identifier}`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
if (target.uuid === sessionId) {
|
|
66
|
+
throw new Error(`Execution ${sessionId} cannot continue itself`);
|
|
67
|
+
}
|
|
68
|
+
return target.uuid;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Wrap a status record so `toObject()` also reports the names it answers to
|
|
73
|
+
* (`aliases`, once `--alias`/`--rename` gave it one) and its continuation
|
|
74
|
+
* `chain`, right after `uuid`.
|
|
75
|
+
* @param {object} record - Record-like object with toObject()
|
|
76
|
+
* @param {object|null} store - ExecutionStore
|
|
77
|
+
* @returns {object} The record, or a wrapper with the extra fields
|
|
78
|
+
*/
|
|
79
|
+
function attachIdentity(record, store) {
|
|
80
|
+
const aliases = hasUserNames(record) ? executionAliases(record) : null;
|
|
81
|
+
const chain =
|
|
82
|
+
store &&
|
|
83
|
+
(record.continues ||
|
|
84
|
+
store.getAll().some((r) => r.continues === record.uuid))
|
|
85
|
+
? buildChain(store.getAll(), record).map((r) => r.uuid)
|
|
86
|
+
: null;
|
|
87
|
+
if (!aliases && !(chain && chain.length > 1)) {
|
|
88
|
+
return record;
|
|
89
|
+
}
|
|
90
|
+
const wrapped = Object.create(Object.getPrototypeOf(record));
|
|
91
|
+
Object.assign(wrapped, record);
|
|
92
|
+
const source = record.toObject.bind(record);
|
|
93
|
+
wrapped.toObject = function () {
|
|
94
|
+
const ordered = {};
|
|
95
|
+
for (const [key, value] of Object.entries(source())) {
|
|
96
|
+
ordered[key] = value;
|
|
97
|
+
if (key === 'uuid') {
|
|
98
|
+
if (aliases) {
|
|
99
|
+
ordered.aliases = aliases;
|
|
100
|
+
}
|
|
101
|
+
if (chain && chain.length > 1) {
|
|
102
|
+
ordered.chain = chain;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return ordered;
|
|
107
|
+
};
|
|
108
|
+
return wrapped;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Text-format lines for the identity fields of a status object.
|
|
113
|
+
* @param {object} obj - Output of toObject()
|
|
114
|
+
* @returns {string[]} Lines
|
|
115
|
+
*/
|
|
116
|
+
function identityTextLines(obj) {
|
|
117
|
+
return [
|
|
118
|
+
...(obj.aliases ? [`Aliases: ${obj.aliases.join(', ')}`] : []),
|
|
119
|
+
...(obj.continues ? [`Continues: ${obj.continues}`] : []),
|
|
120
|
+
...(obj.chain ? [`Chain: ${obj.chain.join(' -> ')}`] : []),
|
|
121
|
+
];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Concatenate the logs of a chain into one temporary file, oldest first,
|
|
126
|
+
* each preceded by a lifecycle header in the format `--resume` writes.
|
|
127
|
+
* @param {object[]} chain - Records, oldest first
|
|
128
|
+
* @returns {{path: string, cleanup: Function}} Combined log
|
|
129
|
+
*/
|
|
130
|
+
function writeChainLog(chain) {
|
|
131
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'start-chain-log-'));
|
|
132
|
+
const file = path.join(dir, `${chain[chain.length - 1].uuid}.chain.log`);
|
|
133
|
+
chain.forEach((record, index) => {
|
|
134
|
+
const header = {
|
|
135
|
+
event: 'continuation',
|
|
136
|
+
uuid: record.uuid,
|
|
137
|
+
chainIndex: index + 1,
|
|
138
|
+
chainLength: chain.length,
|
|
139
|
+
continues: record.continues || null,
|
|
140
|
+
command: record.command,
|
|
141
|
+
startTime: record.startTime,
|
|
142
|
+
logPath: record.logPath || null,
|
|
143
|
+
};
|
|
144
|
+
fs.appendFileSync(
|
|
145
|
+
file,
|
|
146
|
+
`\n[Start Command Lifecycle] ${JSON.stringify(header)}\n`
|
|
147
|
+
);
|
|
148
|
+
if (record.logPath && fs.existsSync(record.logPath)) {
|
|
149
|
+
fs.appendFileSync(file, fs.readFileSync(record.logPath));
|
|
150
|
+
} else {
|
|
151
|
+
fs.appendFileSync(file, `Log file not found: ${record.logPath}\n`);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
return {
|
|
155
|
+
path: file,
|
|
156
|
+
cleanup: () => fs.rmSync(dir, { recursive: true, force: true }),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Log path `--upload-log` should upload for `record`: its own log, or a
|
|
162
|
+
* combined log when it belongs to a chain of more than one execution.
|
|
163
|
+
* @param {object} store - ExecutionStore
|
|
164
|
+
* @param {object} record - Resolved record
|
|
165
|
+
* @returns {{path: string, cleanup: Function}} Log to upload
|
|
166
|
+
*/
|
|
167
|
+
function prepareUploadLog(store, record) {
|
|
168
|
+
const chain = buildChain(store.getAll(), record);
|
|
169
|
+
if (chain.length < 2) {
|
|
170
|
+
return { path: record.logPath, cleanup: () => {} };
|
|
171
|
+
}
|
|
172
|
+
return writeChainLog(chain);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
module.exports = {
|
|
176
|
+
attachIdentity,
|
|
177
|
+
buildChain,
|
|
178
|
+
identityTextLines,
|
|
179
|
+
prepareUploadLog,
|
|
180
|
+
resolveContinues,
|
|
181
|
+
writeChainLog,
|
|
182
|
+
};
|