start-command 0.37.9 → 0.39.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 +26 -0
- package/README.md +7 -2
- package/bunfig.toml +4 -0
- package/eslint.config.mjs +1 -0
- package/package.json +4 -4
- package/src/bin/cli.js +62 -26
- package/src/lib/args-parser-queries.js +45 -1
- package/src/lib/args-parser.js +19 -0
- package/src/lib/cgroup-memory.js +95 -23
- package/src/lib/command-builder.js +4 -0
- package/src/lib/container-removal.js +4 -5
- package/src/lib/detached-finalize.js +1 -0
- package/src/lib/docker-command-handoff.js +34 -6
- package/src/lib/docker-init-option.js +65 -0
- package/src/lib/docker-inspect-batch.js +176 -0
- package/src/lib/docker-resource-options.js +21 -2
- package/src/lib/docker-runtime-args.js +19 -3
- package/src/lib/execution-attempt.js +19 -9
- package/src/lib/execution-chain.js +182 -0
- package/src/lib/execution-control.js +8 -6
- package/src/lib/execution-env.js +120 -0
- package/src/lib/execution-identity.js +180 -0
- package/src/lib/execution-recovery.js +21 -1
- package/src/lib/execution-resume.js +21 -1
- package/src/lib/execution-store.js +30 -23
- package/src/lib/exit-evidence.js +139 -7
- 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/output-blocks.js +11 -0
- package/src/lib/query-commands.js +18 -3
- package/src/lib/resume-resources.js +2 -2
- package/src/lib/screen-isolation.js +11 -2
- package/src/lib/spawn-helpers.js +33 -16
- package/src/lib/status-formatter.js +39 -131
- package/src/lib/status-probe.js +149 -0
- package/src/lib/store-decode-cache.js +141 -0
- package/src/lib/usage.js +11 -1
- package/test/args-parser-attach-resume.js +1 -1
- package/test/args-parser-control.js +2 -2
- package/test/cargo-lock-version.mjs +121 -0
- package/test/check-rust-coverage.mjs +71 -0
- package/test/check-web-archive.mjs +154 -0
- package/test/ci-workflow-invariants.js +204 -0
- package/test/command-builder-docker-cleanup.js +24 -0
- package/test/command-exit-codes.mjs +91 -0
- package/test/docker-runtime-options.js +2 -0
- package/test/docker-snapshot-safety.js +39 -0
- package/test/execution-resume.js +2 -0
- package/test/fixtures/fake-npm.sh +64 -0
- package/test/fixtures/lychee-report.md +39 -0
- package/test/javascript-first-ci.js +69 -0
- package/test/links-workflow.mjs +127 -0
- package/test/lint-gate.mjs +56 -0
- package/test/npm-publish.mjs +181 -0
- package/test/recheck-broken-links.mjs +452 -0
- package/test/regression-176.js +2 -0
- package/test/regression-187.js +2 -0
- package/test/regression-191.js +4 -0
- package/test/regression-193.js +2 -0
- package/test/regression-198.js +2 -0
- package/test/regression-207.js +8 -2
- package/test/regression-224.js +2 -0
- package/test/regression-229.js +267 -0
- package/test/regression-230.js +234 -0
- package/test/regression-231.js +298 -0
- package/test/regression-236.js +250 -0
- package/test/regression-238.js +129 -0
- package/test/regression-241.js +254 -0
- package/test/regression-242.js +241 -0
- package/test/regression-243.js +493 -0
- package/test/regression-244.js +471 -0
- package/test/release-push-retry.mjs +172 -0
- package/test/run-js-tests.mjs +15 -3
- package/test/setup/private-snapshot-lock.js +17 -0
- package/test/shared-golden.js +44 -2
- package/test/transient-links.mjs +139 -0
|
@@ -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
|
+
};
|
|
@@ -11,6 +11,7 @@ const {
|
|
|
11
11
|
formatAsNestedLinksNotation,
|
|
12
12
|
} = require('./output-blocks');
|
|
13
13
|
const { getDockerCommand, getDockerSpawnOptions } = require('./docker-cleanup');
|
|
14
|
+
const { prefetchedInspect } = require('./docker-inspect-batch');
|
|
14
15
|
|
|
15
16
|
const ControlAction = {
|
|
16
17
|
STOP: 'stop',
|
|
@@ -148,14 +149,15 @@ function collectProcessIds(record, runner = runCommand) {
|
|
|
148
149
|
),
|
|
149
150
|
];
|
|
150
151
|
addIfPresent(processIds, 'commandPids', commandPids);
|
|
152
|
+
} else if (isolated === 'docker' && record.containerState === 'removed') {
|
|
153
|
+
// The container is gone, so there is nothing to ask Docker (#243).
|
|
154
|
+
addIfPresent(processIds, 'containerId', opts.containerId);
|
|
151
155
|
} else if (isolated === 'docker') {
|
|
152
156
|
addIfPresent(processIds, 'containerId', opts.containerId);
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
'
|
|
157
|
-
sessionName,
|
|
158
|
-
]);
|
|
157
|
+
const format = '{{.Id}} {{.State.Pid}}';
|
|
158
|
+
const result =
|
|
159
|
+
prefetchedInspect(format, sessionName) ||
|
|
160
|
+
runner(getDockerCommand(), ['inspect', '-f', format, sessionName]);
|
|
159
161
|
if (result.success && result.stdout.trim()) {
|
|
160
162
|
const [containerId, pidValue] = result.stdout.trim().split(/\s+/);
|
|
161
163
|
addIfPresent(processIds, 'containerId', containerId);
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* START_* variables that tell a launched command which execution it belongs
|
|
3
|
+
* to (issue #230). Every backend exports the same three variables, next to
|
|
4
|
+
* the kill-recovery `START_COMMAND_RECOVERY_ATTEMPT`:
|
|
5
|
+
*
|
|
6
|
+
* - START_EXECUTION_ID: the execution UUID (`--session-id`)
|
|
7
|
+
* - START_SESSION_NAME: the isolation session/container name (`--session`);
|
|
8
|
+
* empty for direct runs, which have no session
|
|
9
|
+
* - START_ATTEMPT: 1 for the first run, n for the n-th attempt (`--resume`,
|
|
10
|
+
* kill recovery or `--append-attempt`), the same number `--status` shows
|
|
11
|
+
*/
|
|
12
|
+
const { shellQuote } = require('./isolation-log-utils');
|
|
13
|
+
|
|
14
|
+
const EXECUTION_ID_ENV = 'START_EXECUTION_ID';
|
|
15
|
+
const SESSION_NAME_ENV = 'START_SESSION_NAME';
|
|
16
|
+
const ATTEMPT_ENV = 'START_ATTEMPT';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Build the START_* variables for one attempt.
|
|
20
|
+
* @param {object} params - {executionId, sessionName, attempt}
|
|
21
|
+
* @returns {object} Variables in a stable order, or {} without an execution id
|
|
22
|
+
*/
|
|
23
|
+
function buildExecutionEnv({ executionId, sessionName, attempt } = {}) {
|
|
24
|
+
if (!executionId) {
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
const number = Number(attempt);
|
|
28
|
+
return {
|
|
29
|
+
[EXECUTION_ID_ENV]: String(executionId),
|
|
30
|
+
[SESSION_NAME_ENV]: sessionName ? String(sessionName) : '',
|
|
31
|
+
[ATTEMPT_ENV]: String(
|
|
32
|
+
Number.isSafeInteger(number) && number > 0 ? number : 1
|
|
33
|
+
),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Build the variables from isolation options (`runIsolated`, `docker run`).
|
|
39
|
+
* @param {object} options - {sessionId|executionId, containerName|session, attemptNumber}
|
|
40
|
+
* @returns {object} Variables, or {}
|
|
41
|
+
*/
|
|
42
|
+
function executionEnvFromOptions(options = {}) {
|
|
43
|
+
return buildExecutionEnv({
|
|
44
|
+
executionId: options.sessionId || options.executionId,
|
|
45
|
+
sessionName: options.containerName || options.session,
|
|
46
|
+
attempt: options.attemptNumber,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* `KEY=value` assignments, e.g. for `docker run -e`.
|
|
52
|
+
* @param {object} env - Variables
|
|
53
|
+
* @returns {string[]} Assignments
|
|
54
|
+
*/
|
|
55
|
+
function executionEnvAssignments(env = {}) {
|
|
56
|
+
return Object.entries(env).map(([key, value]) => `${key}=${value}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* POSIX `export` statement for the variables, without a trailing separator.
|
|
61
|
+
* @param {object} env - Variables
|
|
62
|
+
* @returns {string} Statement, or '' when there is nothing to export
|
|
63
|
+
*/
|
|
64
|
+
function buildExecutionEnvExport(env = {}) {
|
|
65
|
+
const entries = Object.entries(env);
|
|
66
|
+
if (!entries.length) {
|
|
67
|
+
return '';
|
|
68
|
+
}
|
|
69
|
+
return `export ${entries
|
|
70
|
+
.map(([key, value]) => `${key}=${shellQuote(value)}`)
|
|
71
|
+
.join(' ')}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Prefix a shell command with the export statement. Used by the backends
|
|
76
|
+
* whose command runs through a shell that does not inherit our environment:
|
|
77
|
+
* a running tmux server, ssh, and `sudo` (which resets the environment).
|
|
78
|
+
* @param {string} command - Shell command
|
|
79
|
+
* @param {object} env - Variables
|
|
80
|
+
* @returns {string} Prefixed command
|
|
81
|
+
*/
|
|
82
|
+
function prefixWithExecutionEnv(command, env = {}) {
|
|
83
|
+
const statement = buildExecutionEnvExport(env);
|
|
84
|
+
return statement ? `${statement}; ${command}` : command;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Child-process environment with the variables applied.
|
|
89
|
+
* @param {object} env - Variables
|
|
90
|
+
* @param {object} [base] - Base environment (defaults to process.env)
|
|
91
|
+
* @returns {object} Environment
|
|
92
|
+
*/
|
|
93
|
+
function withExecutionEnv(env = {}, base = process.env) {
|
|
94
|
+
return { ...base, ...env };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* True when this process was itself started by execution `executionId`, i.e.
|
|
99
|
+
* it is the next level of a stacked isolation that passes `--session-id`
|
|
100
|
+
* down (see command-builder.js).
|
|
101
|
+
* @param {string} executionId - Execution UUID
|
|
102
|
+
* @param {object} [env] - Environment to inspect
|
|
103
|
+
* @returns {boolean}
|
|
104
|
+
*/
|
|
105
|
+
function isNestedLaunchOf(executionId, env = process.env) {
|
|
106
|
+
return Boolean(executionId) && env[EXECUTION_ID_ENV] === executionId;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = {
|
|
110
|
+
EXECUTION_ID_ENV,
|
|
111
|
+
SESSION_NAME_ENV,
|
|
112
|
+
ATTEMPT_ENV,
|
|
113
|
+
buildExecutionEnv,
|
|
114
|
+
executionEnvFromOptions,
|
|
115
|
+
executionEnvAssignments,
|
|
116
|
+
buildExecutionEnvExport,
|
|
117
|
+
prefixWithExecutionEnv,
|
|
118
|
+
withExecutionEnv,
|
|
119
|
+
isNestedLaunchOf,
|
|
120
|
+
};
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Names an execution answers to (issue #231).
|
|
3
|
+
*
|
|
4
|
+
* An execution is found by its UUID, its backend session name, the session
|
|
5
|
+
* names it used before a resume (`options.sessionNameHistory`, #162), its
|
|
6
|
+
* display name (`options.name`, set by `--rename`) and its aliases
|
|
7
|
+
* (`options.aliases`, added by `--alias` and `--rename`). Aliases are kept
|
|
8
|
+
* apart from `sessionNameHistory` because that list names real backend
|
|
9
|
+
* sessions: `--cleanup` removes those containers and `--resume` derives the
|
|
10
|
+
* root session from it.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
function hasName(record, identifier) {
|
|
14
|
+
const options = record.options || {};
|
|
15
|
+
return (
|
|
16
|
+
options.name === identifier ||
|
|
17
|
+
(Array.isArray(options.aliases) && options.aliases.includes(identifier))
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Resolve an identifier among stored records: UUID, then current session
|
|
23
|
+
* name, then earlier session names, then display name and aliases.
|
|
24
|
+
* @param {object[]} records - Stored records
|
|
25
|
+
* @param {string} identifier - Any name of the execution
|
|
26
|
+
* @returns {object|null} The record, or null
|
|
27
|
+
*/
|
|
28
|
+
function findRecord(records, identifier) {
|
|
29
|
+
if (identifier === null || identifier === undefined || identifier === '') {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
return (
|
|
33
|
+
records.find((r) => r.uuid === identifier) ||
|
|
34
|
+
records.find((r) => r.options && r.options.sessionName === identifier) ||
|
|
35
|
+
records.find(
|
|
36
|
+
(r) =>
|
|
37
|
+
r.options &&
|
|
38
|
+
Array.isArray(r.options.sessionNameHistory) &&
|
|
39
|
+
r.options.sessionNameHistory.includes(identifier)
|
|
40
|
+
) ||
|
|
41
|
+
records.find((r) => hasName(r, identifier)) ||
|
|
42
|
+
null
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Every name the record answers to, UUID first, without duplicates.
|
|
48
|
+
* @param {object} record - Execution record
|
|
49
|
+
* @returns {string[]} Names
|
|
50
|
+
*/
|
|
51
|
+
function executionAliases(record) {
|
|
52
|
+
const options = record.options || {};
|
|
53
|
+
const names = [
|
|
54
|
+
record.uuid,
|
|
55
|
+
...(options.sessionNameHistory || []),
|
|
56
|
+
options.sessionName,
|
|
57
|
+
...(options.aliases || []),
|
|
58
|
+
options.name,
|
|
59
|
+
].filter((name) => typeof name === 'string' && name);
|
|
60
|
+
return [...new Set(names)];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** True once the record has a caller-given name (`--alias`/`--rename`). */
|
|
64
|
+
function hasUserNames(record) {
|
|
65
|
+
const options = record.options || {};
|
|
66
|
+
return Boolean(options.name) || (options.aliases || []).length > 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Run `mutate(records)` under the store's database lock and persist the
|
|
71
|
+
* records it returns as changed.
|
|
72
|
+
* @param {object} store - ExecutionStore
|
|
73
|
+
* @param {Function} mutate - (records) => changed record, or null for none
|
|
74
|
+
* @returns {*} What `mutate` returned
|
|
75
|
+
*/
|
|
76
|
+
function withStoreLock(store, mutate) {
|
|
77
|
+
const { LockManager } = require('./execution-store');
|
|
78
|
+
const lock = new LockManager(store.lockFilePath);
|
|
79
|
+
if (!lock.acquire()) {
|
|
80
|
+
throw new Error('Failed to acquire lock for database write');
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
const records = store.readLinoRecords();
|
|
84
|
+
const changed = mutate(records);
|
|
85
|
+
if (changed) {
|
|
86
|
+
store.writeLinoRecords(records);
|
|
87
|
+
if (store.useLinks) {
|
|
88
|
+
store.writeLinksRecord(changed);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return changed;
|
|
92
|
+
} finally {
|
|
93
|
+
lock.release();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function validateName(name) {
|
|
98
|
+
if (typeof name !== 'string' || !name.trim()) {
|
|
99
|
+
throw new Error('The new name must not be empty');
|
|
100
|
+
}
|
|
101
|
+
if (name.startsWith('-')) {
|
|
102
|
+
throw new Error(`Invalid name "${name}": names cannot start with "-"`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Add a name to an execution (`$ --alias <id> <name>`) or rename it
|
|
108
|
+
* (`$ --rename <id> <name>`, which keeps the previous display name as an
|
|
109
|
+
* alias). The backend session/container keeps its name; only lookup and the
|
|
110
|
+
* displayed name change.
|
|
111
|
+
* @param {object} store - ExecutionStore
|
|
112
|
+
* @param {string} identifier - Any current name of the execution
|
|
113
|
+
* @param {string} name - Name to add
|
|
114
|
+
* @param {object} [options] - {rename: boolean}
|
|
115
|
+
* @returns {{success: boolean, output?: string, error?: string}}
|
|
116
|
+
*/
|
|
117
|
+
function nameExecution(store, identifier, name, options = {}) {
|
|
118
|
+
if (!store) {
|
|
119
|
+
return { success: false, error: 'Execution tracking is disabled.' };
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
validateName(name);
|
|
123
|
+
let unchanged = false;
|
|
124
|
+
const record = withStoreLock(store, (records) => {
|
|
125
|
+
const target = findRecord(records, identifier);
|
|
126
|
+
if (!target) {
|
|
127
|
+
throw new Error(
|
|
128
|
+
`No execution found with UUID or session name: ${identifier}`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
const owner = findRecord(records, name);
|
|
132
|
+
if (owner && owner.uuid !== target.uuid) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
`Name "${name}" already refers to execution ${owner.uuid}`
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
const opts = (target.options = target.options || {});
|
|
138
|
+
const aliases = [...(opts.aliases || [])];
|
|
139
|
+
if (options.rename) {
|
|
140
|
+
const previous = opts.name || opts.sessionName || target.uuid;
|
|
141
|
+
if (previous === name) {
|
|
142
|
+
unchanged = true;
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
// A session name or UUID still resolves on its own; an earlier
|
|
146
|
+
// display name has to become an alias to keep resolving.
|
|
147
|
+
if (opts.name) {
|
|
148
|
+
aliases.push(previous);
|
|
149
|
+
}
|
|
150
|
+
opts.name = name;
|
|
151
|
+
} else if (owner) {
|
|
152
|
+
unchanged = true;
|
|
153
|
+
return null;
|
|
154
|
+
} else {
|
|
155
|
+
aliases.push(name);
|
|
156
|
+
}
|
|
157
|
+
opts.aliases = [...new Set(aliases)].filter((a) => a !== opts.name);
|
|
158
|
+
if (opts.aliases.length === 0) {
|
|
159
|
+
delete opts.aliases;
|
|
160
|
+
}
|
|
161
|
+
return target;
|
|
162
|
+
});
|
|
163
|
+
const current = record || store.get(identifier);
|
|
164
|
+
const action = options.rename ? 'Renamed' : 'Aliased';
|
|
165
|
+
return {
|
|
166
|
+
success: true,
|
|
167
|
+
output: `${unchanged ? 'Unchanged' : action} execution ${current.uuid}\naliases: [${executionAliases(current).join(', ')}]`,
|
|
168
|
+
};
|
|
169
|
+
} catch (error) {
|
|
170
|
+
return { success: false, error: error.message };
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
module.exports = {
|
|
175
|
+
executionAliases,
|
|
176
|
+
findRecord,
|
|
177
|
+
hasUserNames,
|
|
178
|
+
nameExecution,
|
|
179
|
+
withStoreLock,
|
|
180
|
+
};
|
|
@@ -336,6 +336,7 @@ function recoverKilledExecution(params = {}) {
|
|
|
336
336
|
record.options.exitEvidence = {
|
|
337
337
|
mainOom: terminalOom,
|
|
338
338
|
daemonRestart: logged.daemonRestart,
|
|
339
|
+
...evidence.sourceOf(logged),
|
|
339
340
|
};
|
|
340
341
|
let recoveryLimits = null;
|
|
341
342
|
if (opts.onKillResumeMemory && terminalOom) {
|
|
@@ -379,6 +380,23 @@ function recoverKilledExecution(params = {}) {
|
|
|
379
380
|
}
|
|
380
381
|
}
|
|
381
382
|
|
|
383
|
+
if (opts.commandHandoff && record.uuid) {
|
|
384
|
+
// `docker start` keeps the creation env; refresh START_ATTEMPT (#230).
|
|
385
|
+
try {
|
|
386
|
+
require('./docker-command-handoff').writeExecutionEnvHandoff(
|
|
387
|
+
containerName,
|
|
388
|
+
require('./execution-env').buildExecutionEnv({
|
|
389
|
+
executionId: record.uuid,
|
|
390
|
+
sessionName: containerName,
|
|
391
|
+
attempt: require('./execution-attempt').nextAttemptNumber(record),
|
|
392
|
+
}),
|
|
393
|
+
runner
|
|
394
|
+
);
|
|
395
|
+
} catch {
|
|
396
|
+
// Non-fatal: the command still runs, with the previous START_ATTEMPT.
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
382
400
|
const since = now().toISOString();
|
|
383
401
|
const {
|
|
384
402
|
appendLifecycle,
|
|
@@ -473,7 +491,9 @@ function recoverKilledExecution(params = {}) {
|
|
|
473
491
|
const updated = runner(getDockerCommand(), [
|
|
474
492
|
'update',
|
|
475
493
|
...resourceLimits.filter((s) =>
|
|
476
|
-
/^--(?:memory|memory-swap|cpus|cpu-quota|cpu-period)=/.test(
|
|
494
|
+
/^--(?:memory|memory-swap|memory-reservation|cpus|cpu-quota|cpu-period)=/.test(
|
|
495
|
+
s
|
|
496
|
+
)
|
|
477
497
|
),
|
|
478
498
|
containerName,
|
|
479
499
|
]);
|
|
@@ -34,6 +34,7 @@ const {
|
|
|
34
34
|
appendLifecycle,
|
|
35
35
|
archiveAttempt,
|
|
36
36
|
createAttempt,
|
|
37
|
+
nextAttemptNumber,
|
|
37
38
|
patchAttempt,
|
|
38
39
|
} = require('./execution-attempt');
|
|
39
40
|
const { runCommand } = require('./execution-control');
|
|
@@ -46,6 +47,7 @@ const { SessionState, probeSession } = require('./session-probe');
|
|
|
46
47
|
const { getDockerNetworks } = require('./docker-network-lifecycle');
|
|
47
48
|
const {
|
|
48
49
|
writeCommandHandoff,
|
|
50
|
+
writeExecutionEnvHandoff,
|
|
49
51
|
buildCommandHandoffArgs,
|
|
50
52
|
commandHandoffPath,
|
|
51
53
|
} = require('./docker-command-handoff');
|
|
@@ -110,6 +112,8 @@ function buildLaunchOptions(record) {
|
|
|
110
112
|
rootSession:
|
|
111
113
|
opts.rootSession || opts.sessionNameHistory?.[0] || opts.sessionName,
|
|
112
114
|
resumeCount: (Number(opts.resumeCount) || 0) + 1,
|
|
115
|
+
// START_ATTEMPT of the relaunched command (issue #230).
|
|
116
|
+
attemptNumber: nextAttemptNumber(record),
|
|
113
117
|
image: opts.image || null,
|
|
114
118
|
session: opts.sessionName,
|
|
115
119
|
detached: true,
|
|
@@ -123,6 +127,7 @@ function buildLaunchOptions(record) {
|
|
|
123
127
|
useCommandStream: opts.useCommandStream || false,
|
|
124
128
|
shell: opts.shell || 'auto',
|
|
125
129
|
privileged: opts.privileged || false,
|
|
130
|
+
init: opts.init || false,
|
|
126
131
|
env: opts.env || [],
|
|
127
132
|
volumes: opts.volumes || [],
|
|
128
133
|
mounts: opts.mounts || [],
|
|
@@ -289,6 +294,7 @@ function buildResumePlan(
|
|
|
289
294
|
session: newSessionName,
|
|
290
295
|
containerName: newSessionName,
|
|
291
296
|
sessionId: record.uuid,
|
|
297
|
+
attemptNumber: nextAttemptNumber(record),
|
|
292
298
|
rootSession:
|
|
293
299
|
opts.rootSession || opts.sessionNameHistory?.[0] || sessionName,
|
|
294
300
|
resumeCount: attempt,
|
|
@@ -530,7 +536,9 @@ async function resumeExecution(store, identifier, deps = {}) {
|
|
|
530
536
|
args: [
|
|
531
537
|
'update',
|
|
532
538
|
...resources.resourceLimits.filter((s) =>
|
|
533
|
-
/^--(?:memory|memory-swap|cpus|cpu-quota|cpu-period)=/.test(
|
|
539
|
+
/^--(?:memory|memory-swap|memory-reservation|cpus|cpu-quota|cpu-period)=/.test(
|
|
540
|
+
s
|
|
541
|
+
)
|
|
534
542
|
),
|
|
535
543
|
plan.sessionName,
|
|
536
544
|
],
|
|
@@ -657,6 +665,18 @@ async function resumeExecution(store, identifier, deps = {}) {
|
|
|
657
665
|
);
|
|
658
666
|
}
|
|
659
667
|
}
|
|
668
|
+
if (plan.mode === ResumeMode.DOCKER_START && opts.commandHandoff) {
|
|
669
|
+
// `docker start` keeps the creation env; refresh START_ATTEMPT (#230).
|
|
670
|
+
writeExecutionEnvHandoff(
|
|
671
|
+
plan.sessionName,
|
|
672
|
+
require('./execution-env').buildExecutionEnv({
|
|
673
|
+
executionId: record.uuid,
|
|
674
|
+
sessionName: plan.sessionName,
|
|
675
|
+
attempt: attempt.number,
|
|
676
|
+
}),
|
|
677
|
+
runner
|
|
678
|
+
);
|
|
679
|
+
}
|
|
660
680
|
for (const step of plan.steps) {
|
|
661
681
|
let result;
|
|
662
682
|
try {
|