start-command 0.37.8 → 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 +16 -0
- package/README.md +8 -0
- package/package.json +1 -1
- package/src/bin/cli.js +66 -26
- package/src/lib/args-parser-queries.js +90 -9
- package/src/lib/args-parser.js +20 -1
- package/src/lib/container-removal.js +163 -0
- package/src/lib/detached-finalize.js +29 -2
- package/src/lib/docker-cleanup.js +7 -4
- package/src/lib/docker-command-handoff.js +34 -6
- package/src/lib/docker-post-mortem.js +2 -0
- package/src/lib/docker-runtime-args.js +7 -0
- package/src/lib/docker-snapshot-resume.js +222 -0
- package/src/lib/docker-snapshot-safety.js +124 -12
- package/src/lib/execution-attempt.js +21 -9
- package/src/lib/execution-chain.js +182 -0
- package/src/lib/execution-cleanup.js +193 -0
- package/src/lib/execution-control.js +2 -1
- package/src/lib/execution-env.js +120 -0
- package/src/lib/execution-identity.js +180 -0
- package/src/lib/execution-recovery.js +19 -0
- package/src/lib/execution-resume.js +77 -6
- package/src/lib/execution-store.js +63 -22
- package/src/lib/isolation.js +25 -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 +34 -6
- package/src/lib/screen-isolation.js +11 -2
- package/src/lib/snapshot-resume-options.js +174 -0
- package/src/lib/spawn-helpers.js +33 -16
- package/src/lib/status-formatter.js +64 -35
- package/src/lib/status-links-notation.js +97 -0
- package/src/lib/store-lock.js +3 -0
- package/src/lib/usage.js +15 -1
- package/test/args-parser-attach-resume.js +1 -1
- package/test/args-parser-control.js +2 -2
- package/test/regression-224.js +807 -0
- package/test/regression-225.js +415 -0
- package/test/regression-226.js +81 -0
- 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 +35 -0
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a new launch may do with its execution id (issue #229).
|
|
3
|
+
*
|
|
4
|
+
* A launch with an explicit `--session-id` that is already tracked used to
|
|
5
|
+
* replace the record and truncate the log. It is now refused unless
|
|
6
|
+
* `--append-attempt` is given, which keeps the record, archives the previous
|
|
7
|
+
* attempt into `attemptHistory`, and appends the new attempt to the same log
|
|
8
|
+
* after a lifecycle header (the layout `--resume` already writes).
|
|
9
|
+
*
|
|
10
|
+
* Two cases keep the earlier behaviour:
|
|
11
|
+
* - the next level of a stacked isolation, which receives the same
|
|
12
|
+
* `--session-id` from its parent and runs with START_EXECUTION_ID already
|
|
13
|
+
* set to it (`isNestedLaunchOf`);
|
|
14
|
+
* - a generated UUID, which cannot collide.
|
|
15
|
+
*/
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const { isNestedLaunchOf, ATTEMPT_ENV } = require('./execution-env');
|
|
18
|
+
const { findRecord, withStoreLock } = require('./execution-identity');
|
|
19
|
+
|
|
20
|
+
const LOG_HEADER = '=== Start Command Log ===';
|
|
21
|
+
|
|
22
|
+
class LaunchRefusedError extends Error {}
|
|
23
|
+
|
|
24
|
+
function alreadyExistsMessage(uuid) {
|
|
25
|
+
return `execution ${uuid} already exists; use $ --resume ${uuid} -- <command> to continue it, or pass --append-attempt`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isPidAlive(pid) {
|
|
29
|
+
if (!pid) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
process.kill(pid, 0);
|
|
34
|
+
return true;
|
|
35
|
+
} catch (error) {
|
|
36
|
+
return error.code === 'EPERM';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** True while the stored execution may still be running. */
|
|
41
|
+
function isExecutionLive(record) {
|
|
42
|
+
if (require('./launch-owner').hasActiveLaunch(record)) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
if (record.status !== 'executing') {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
if (record.options && record.options.isolated) {
|
|
49
|
+
try {
|
|
50
|
+
return require('./session-probe').probeSession(record).alive === true;
|
|
51
|
+
} catch {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return isPidAlive(record.pid);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Attempts already in a log written without tracking, from its headers. */
|
|
59
|
+
function countLoggedAttempts(logPath) {
|
|
60
|
+
try {
|
|
61
|
+
return fs.readFileSync(logPath, 'utf8').split(LOG_HEADER).length - 1;
|
|
62
|
+
} catch {
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function logSize(logPath) {
|
|
68
|
+
try {
|
|
69
|
+
return fs.statSync(logPath).size;
|
|
70
|
+
} catch {
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Decide how a launch uses its execution id, before anything is printed or
|
|
77
|
+
* created.
|
|
78
|
+
* @param {object} params - {store, sessionId, explicitSessionId,
|
|
79
|
+
* appendAttempt, logPath, env}
|
|
80
|
+
* @returns {{append: boolean, previous: object|null, attemptNumber: number,
|
|
81
|
+
* logPath: string, nested: boolean, checked: boolean}}
|
|
82
|
+
* @throws {LaunchRefusedError} When the launch must not run
|
|
83
|
+
*/
|
|
84
|
+
function preflightLaunch(params) {
|
|
85
|
+
const { store, sessionId, logPath } = params;
|
|
86
|
+
const env = params.env || process.env;
|
|
87
|
+
const fresh = {
|
|
88
|
+
append: false,
|
|
89
|
+
previous: null,
|
|
90
|
+
attemptNumber: 1,
|
|
91
|
+
logPath,
|
|
92
|
+
nested: false,
|
|
93
|
+
checked: false,
|
|
94
|
+
};
|
|
95
|
+
if (!params.explicitSessionId) {
|
|
96
|
+
return fresh;
|
|
97
|
+
}
|
|
98
|
+
if (isNestedLaunchOf(sessionId, env)) {
|
|
99
|
+
const inherited = Number(env[ATTEMPT_ENV]);
|
|
100
|
+
return {
|
|
101
|
+
...fresh,
|
|
102
|
+
nested: true,
|
|
103
|
+
attemptNumber:
|
|
104
|
+
Number.isSafeInteger(inherited) && inherited > 0 ? inherited : 1,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const existing = store ? store.get(sessionId) : null;
|
|
108
|
+
if (existing && existing.uuid !== sessionId) {
|
|
109
|
+
throw new LaunchRefusedError(
|
|
110
|
+
`execution id ${sessionId} is already used as a name of execution ${existing.uuid}; pick another --session-id`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
if (existing) {
|
|
114
|
+
if (!params.appendAttempt) {
|
|
115
|
+
throw new LaunchRefusedError(alreadyExistsMessage(sessionId));
|
|
116
|
+
}
|
|
117
|
+
if (isExecutionLive(existing)) {
|
|
118
|
+
throw new LaunchRefusedError(
|
|
119
|
+
`execution ${sessionId} is still running; wait for it to finish or use $ --stop ${sessionId} before --append-attempt`
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
const { nextAttemptNumber } = require('./execution-attempt');
|
|
123
|
+
return {
|
|
124
|
+
...fresh,
|
|
125
|
+
append: true,
|
|
126
|
+
previous: existing,
|
|
127
|
+
attemptNumber: nextAttemptNumber(existing),
|
|
128
|
+
logPath: existing.logPath || logPath,
|
|
129
|
+
checked: true,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
// Untracked (or deleted) executions are recognised by their log file.
|
|
133
|
+
if (logPath && fs.existsSync(logPath)) {
|
|
134
|
+
if (!params.appendAttempt) {
|
|
135
|
+
throw new LaunchRefusedError(alreadyExistsMessage(sessionId));
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
...fresh,
|
|
139
|
+
append: true,
|
|
140
|
+
attemptNumber: countLoggedAttempts(logPath) + 1,
|
|
141
|
+
checked: true,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
return { ...fresh, checked: true };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Turn the record a launch built into the next attempt of `previous`,
|
|
149
|
+
* keeping the identity, history and log of the execution.
|
|
150
|
+
* @param {object} record - Freshly built ExecutionRecord (mutated)
|
|
151
|
+
* @param {object} previous - Stored record of the same UUID
|
|
152
|
+
* @returns {object} The record
|
|
153
|
+
*/
|
|
154
|
+
function mergeAppendedAttempt(record, previous) {
|
|
155
|
+
const {
|
|
156
|
+
archiveAttempt,
|
|
157
|
+
createAttempt,
|
|
158
|
+
currentAttemptNumber,
|
|
159
|
+
} = require('./execution-attempt');
|
|
160
|
+
const prior = require('./execution-store').ExecutionRecord.fromObject(
|
|
161
|
+
JSON.parse(JSON.stringify(previous.toObject()))
|
|
162
|
+
);
|
|
163
|
+
if (!prior.attempt && currentAttemptNumber(prior) === 1) {
|
|
164
|
+
// A launch writes a fresh log, so its first attempt starts at offset 0.
|
|
165
|
+
prior.attempt = { number: 1, startedAt: prior.startTime, logOffset: 0 };
|
|
166
|
+
}
|
|
167
|
+
const attempt = createAttempt(prior, {
|
|
168
|
+
mode: 'append-attempt',
|
|
169
|
+
sessionName: record.options.sessionName || null,
|
|
170
|
+
});
|
|
171
|
+
archiveAttempt(prior);
|
|
172
|
+
const keep = prior.options || {};
|
|
173
|
+
const history = [...(keep.sessionNameHistory || [])];
|
|
174
|
+
if (keep.sessionName && keep.sessionName !== record.options.sessionName) {
|
|
175
|
+
history.push(keep.sessionName);
|
|
176
|
+
}
|
|
177
|
+
record.attempt = attempt;
|
|
178
|
+
record.attemptHistory = prior.attemptHistory;
|
|
179
|
+
record.startTime = prior.startTime;
|
|
180
|
+
record.logPath = prior.logPath || record.logPath;
|
|
181
|
+
record.continues = record.continues || prior.continues;
|
|
182
|
+
record.options = {
|
|
183
|
+
...record.options,
|
|
184
|
+
...(history.length ? { sessionNameHistory: [...new Set(history)] } : {}),
|
|
185
|
+
...(keep.aliases ? { aliases: keep.aliases } : {}),
|
|
186
|
+
...(keep.name ? { name: keep.name } : {}),
|
|
187
|
+
...(keep.resumeCount ? { resumeCount: keep.resumeCount } : {}),
|
|
188
|
+
};
|
|
189
|
+
return record;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Persist the launch's record, refusing if another launch claimed the id in
|
|
194
|
+
* the meantime: a new id must still be free, an appended attempt must still
|
|
195
|
+
* follow the attempt the preflight saw.
|
|
196
|
+
* @param {object} store - ExecutionStore
|
|
197
|
+
* @param {object} record - Record to save
|
|
198
|
+
* @param {object} launch - preflightLaunch() result
|
|
199
|
+
*/
|
|
200
|
+
function claimLaunch(store, record, launch) {
|
|
201
|
+
if (!launch.checked) {
|
|
202
|
+
store.save(record);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
withStoreLock(store, (records) => {
|
|
206
|
+
const index = records.findIndex((r) => r.uuid === record.uuid);
|
|
207
|
+
if (launch.previous) {
|
|
208
|
+
const current = records[index];
|
|
209
|
+
if (
|
|
210
|
+
!current ||
|
|
211
|
+
require('./launch-owner').hasActiveLaunch(current) ||
|
|
212
|
+
(current.attempt?.number || 0) !==
|
|
213
|
+
(launch.previous.attempt?.number || 0)
|
|
214
|
+
) {
|
|
215
|
+
throw new LaunchRefusedError(
|
|
216
|
+
`execution ${record.uuid} was claimed by another launch`
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
records[index] = record;
|
|
220
|
+
} else {
|
|
221
|
+
if (findRecord(records, record.uuid)) {
|
|
222
|
+
throw new LaunchRefusedError(alreadyExistsMessage(record.uuid));
|
|
223
|
+
}
|
|
224
|
+
records.push(record);
|
|
225
|
+
}
|
|
226
|
+
return record;
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Header a launch writes into its log: unchanged for a fresh log; for an
|
|
232
|
+
* appended attempt, preceded by a lifecycle line in the format `--resume`
|
|
233
|
+
* writes, so the attempts stay apart.
|
|
234
|
+
* @param {string} content - Header content
|
|
235
|
+
* @param {object} launch - preflightLaunch() result
|
|
236
|
+
* @param {object|null} record - Record of the launch, when tracked
|
|
237
|
+
* @returns {string} Content to write
|
|
238
|
+
*/
|
|
239
|
+
function launchLogContent(content, launch, record = null) {
|
|
240
|
+
if (!launch.append) {
|
|
241
|
+
return content;
|
|
242
|
+
}
|
|
243
|
+
const lifecycle = {
|
|
244
|
+
event: 'attempt-appended',
|
|
245
|
+
at: new Date().toISOString(),
|
|
246
|
+
uuid: record ? record.uuid : null,
|
|
247
|
+
attemptNumber: launch.attemptNumber,
|
|
248
|
+
...(record && record.attempt ? record.attempt : {}),
|
|
249
|
+
};
|
|
250
|
+
return `\n[Start Command Lifecycle] ${JSON.stringify(lifecycle)}\n${content}`;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Save the initial record of a direct run, before it starts. A refused
|
|
255
|
+
* claim ends the launch with exit code 1; other failures only lose tracking.
|
|
256
|
+
* @param {object|null} store - ExecutionStore
|
|
257
|
+
* @param {object|null} record - Execution record
|
|
258
|
+
* @param {object} launch - prepareLaunch() result
|
|
259
|
+
* @param {object} [config] - CLI configuration ({verbose})
|
|
260
|
+
*/
|
|
261
|
+
function claimDirectLaunch(store, record, launch, config = {}) {
|
|
262
|
+
if (!record || !store) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
try {
|
|
266
|
+
claimLaunch(store, record, launch);
|
|
267
|
+
} catch (err) {
|
|
268
|
+
if (err instanceof LaunchRefusedError) {
|
|
269
|
+
console.error(`Error: ${err.message}`);
|
|
270
|
+
process.exit(1);
|
|
271
|
+
}
|
|
272
|
+
if (config.verbose) {
|
|
273
|
+
console.error(
|
|
274
|
+
`[Tracking] Warning: Could not save execution record: ${err.message}`
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Write the launch header: a fresh log, or, for an appended attempt, a
|
|
282
|
+
* lifecycle line and the header after the earlier attempts.
|
|
283
|
+
* @param {string} logPath - Log path
|
|
284
|
+
* @param {string} content - Header content
|
|
285
|
+
* @param {object} launch - preflightLaunch() result
|
|
286
|
+
* @param {object|null} record - Record of the launch, when tracked
|
|
287
|
+
*/
|
|
288
|
+
function writeLaunchLog(logPath, content, launch, record = null) {
|
|
289
|
+
const { appendLogFile, writeLogFile } = require('./isolation-log-utils');
|
|
290
|
+
const text = launchLogContent(content, launch, record);
|
|
291
|
+
if (launch.append) {
|
|
292
|
+
appendLogFile(logPath, text);
|
|
293
|
+
} else {
|
|
294
|
+
writeLogFile(logPath, text);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Preflight of the CLI: how this launch uses its id (#229) and which
|
|
300
|
+
* execution it continues (#231).
|
|
301
|
+
* @param {object} store - ExecutionStore, or null when tracking is off
|
|
302
|
+
* @param {object} options - Parsed wrapper options
|
|
303
|
+
* @param {string} sessionId - Execution UUID of the launch
|
|
304
|
+
* @param {string} logPath - Default log path of the launch
|
|
305
|
+
* @returns {object} preflightLaunch() result plus `continues`
|
|
306
|
+
* @throws {Error} When the launch must not run
|
|
307
|
+
*/
|
|
308
|
+
function prepareLaunch(store, options, sessionId, logPath) {
|
|
309
|
+
const launch = preflightLaunch({
|
|
310
|
+
store,
|
|
311
|
+
sessionId,
|
|
312
|
+
explicitSessionId: Boolean(options.sessionId),
|
|
313
|
+
appendAttempt: Boolean(options.appendAttempt),
|
|
314
|
+
logPath,
|
|
315
|
+
});
|
|
316
|
+
launch.continues = options.continues
|
|
317
|
+
? require('./execution-chain').resolveContinues(
|
|
318
|
+
store,
|
|
319
|
+
options.continues,
|
|
320
|
+
sessionId
|
|
321
|
+
)
|
|
322
|
+
: null;
|
|
323
|
+
return launch;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Apply the preflight to the record a launch built: the continued UUID and,
|
|
328
|
+
* for `--append-attempt`, the identity and history of the stored execution.
|
|
329
|
+
* @param {object|null} record - Freshly built ExecutionRecord (mutated)
|
|
330
|
+
* @param {object} launch - prepareLaunch() result
|
|
331
|
+
* @returns {object|null} The record
|
|
332
|
+
*/
|
|
333
|
+
function applyLaunch(record, launch) {
|
|
334
|
+
if (!record) {
|
|
335
|
+
return record;
|
|
336
|
+
}
|
|
337
|
+
if (launch.continues) {
|
|
338
|
+
record.continues = launch.continues;
|
|
339
|
+
}
|
|
340
|
+
if (launch.previous) {
|
|
341
|
+
mergeAppendedAttempt(record, launch.previous);
|
|
342
|
+
} else if (launch.attemptNumber > 1) {
|
|
343
|
+
// A nested level or an untracked log: number the attempt all the same.
|
|
344
|
+
record.attempt = {
|
|
345
|
+
number: launch.attemptNumber,
|
|
346
|
+
startedAt: new Date().toISOString(),
|
|
347
|
+
logOffset: launch.append ? logSize(launch.logPath) : 0,
|
|
348
|
+
mode: launch.nested ? 'nested' : 'append-attempt',
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
return record;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* START_* variables of a run without a backend session (direct, or
|
|
356
|
+
* `--isolated-user` alone): the session name is empty.
|
|
357
|
+
* @param {string} sessionId - Execution UUID
|
|
358
|
+
* @param {object} launch - prepareLaunch() result
|
|
359
|
+
* @returns {object} Variables
|
|
360
|
+
*/
|
|
361
|
+
function directExecutionEnv(sessionId, launch) {
|
|
362
|
+
return require('./execution-env').buildExecutionEnv({
|
|
363
|
+
executionId: sessionId,
|
|
364
|
+
sessionName: '',
|
|
365
|
+
attempt: launch.attemptNumber,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Child environment of a direct run.
|
|
371
|
+
* @param {string} sessionId - Execution UUID
|
|
372
|
+
* @param {object} launch - prepareLaunch() result
|
|
373
|
+
* @returns {object} Child environment
|
|
374
|
+
*/
|
|
375
|
+
function directLaunchEnv(sessionId, launch) {
|
|
376
|
+
return require('./execution-env').withExecutionEnv(
|
|
377
|
+
directExecutionEnv(sessionId, launch)
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Command of an `--isolated-user` run without a backend: `sudo` resets the
|
|
383
|
+
* environment, so the variables are exported by the command itself.
|
|
384
|
+
* @param {string} cmd - Shell command
|
|
385
|
+
* @param {string} sessionId - Execution UUID
|
|
386
|
+
* @param {object} launch - prepareLaunch() result
|
|
387
|
+
* @returns {string} Prefixed command
|
|
388
|
+
*/
|
|
389
|
+
function isolatedUserCommand(cmd, sessionId, launch) {
|
|
390
|
+
return require('./execution-env').prefixWithExecutionEnv(
|
|
391
|
+
cmd,
|
|
392
|
+
directExecutionEnv(sessionId, launch)
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
module.exports = {
|
|
397
|
+
LaunchRefusedError,
|
|
398
|
+
alreadyExistsMessage,
|
|
399
|
+
applyLaunch,
|
|
400
|
+
claimDirectLaunch,
|
|
401
|
+
claimLaunch,
|
|
402
|
+
directExecutionEnv,
|
|
403
|
+
directLaunchEnv,
|
|
404
|
+
isolatedUserCommand,
|
|
405
|
+
isExecutionLive,
|
|
406
|
+
launchLogContent,
|
|
407
|
+
mergeAppendedAttempt,
|
|
408
|
+
prepareLaunch,
|
|
409
|
+
preflightLaunch,
|
|
410
|
+
writeLaunchLog,
|
|
411
|
+
};
|
package/src/lib/log-uploader.js
CHANGED
|
@@ -200,7 +200,21 @@ function uploadExecutionLog(store, identifier, options = {}) {
|
|
|
200
200
|
};
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
-
|
|
203
|
+
// A continuation chain uploads every execution, oldest first (#231).
|
|
204
|
+
let prepared;
|
|
205
|
+
try {
|
|
206
|
+
prepared = require('./execution-chain').prepareUploadLog(store, record);
|
|
207
|
+
} catch (error) {
|
|
208
|
+
return {
|
|
209
|
+
success: false,
|
|
210
|
+
error: `Could not join chain logs: ${error.message}`,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
return uploadLogPath(prepared.path, options);
|
|
215
|
+
} finally {
|
|
216
|
+
prepared.cleanup();
|
|
217
|
+
}
|
|
204
218
|
}
|
|
205
219
|
|
|
206
220
|
module.exports = {
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* Every option in this module addresses an execution that already exists in
|
|
5
5
|
* the tracking store instead of launching a new one: --status, --list,
|
|
6
6
|
* --upload-log, --stop, --terminate, --attach, --resume, --resume-all and
|
|
7
|
-
* --cleanup. `dispatchQueryCommand` is the single
|
|
8
|
-
* before it starts interpreting the command line as
|
|
7
|
+
* --cleanup, plus --alias and --rename. `dispatchQueryCommand` is the single
|
|
8
|
+
* entry point the CLI calls before it starts interpreting the command line as
|
|
9
|
+
* a command to run.
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
const { queryStatus, listExecutions } = require('./status-formatter');
|
|
@@ -13,6 +14,8 @@ const { ControlAction, controlExecution } = require('./execution-control');
|
|
|
13
14
|
const { attachExecution } = require('./execution-attach');
|
|
14
15
|
const { resumeAllExecutions, resumeExecution } = require('./execution-resume');
|
|
15
16
|
const { uploadExecutionLog } = require('./log-uploader');
|
|
17
|
+
const { snapshotResumeSettings } = require('./snapshot-resume-options');
|
|
18
|
+
const { cleanupExecution } = require('./execution-cleanup');
|
|
16
19
|
|
|
17
20
|
/**
|
|
18
21
|
* Print a handler result, returning the process exit code.
|
|
@@ -68,6 +71,8 @@ async function handleResumeQuery(
|
|
|
68
71
|
outputFormat,
|
|
69
72
|
resourceOptions,
|
|
70
73
|
removeOriginal: resourceOptions.removeOriginal === true,
|
|
74
|
+
removeOriginalAfter: resourceOptions.removeOriginalAfter ?? 0,
|
|
75
|
+
snapshotOptions: snapshotResumeSettings(resourceOptions),
|
|
71
76
|
})
|
|
72
77
|
);
|
|
73
78
|
}
|
|
@@ -78,12 +83,21 @@ function handleResumeAllQuery(store, outputFormat) {
|
|
|
78
83
|
|
|
79
84
|
/**
|
|
80
85
|
* Handle --cleanup / --cleanup-dry-run.
|
|
81
|
-
* Cleans up stale "executing" records (processes that crashed or were killed)
|
|
86
|
+
* Cleans up stale "executing" records (processes that crashed or were killed),
|
|
87
|
+
* or with a target the Docker containers and snapshot images of one execution.
|
|
82
88
|
* @param {?object} store - Execution store
|
|
83
89
|
* @param {boolean} dryRun - If true, just report what would be cleaned
|
|
90
|
+
* @param {?string} [target] - UUID or session name of one execution
|
|
84
91
|
* @returns {number} Exit code
|
|
85
92
|
*/
|
|
86
|
-
function handleCleanup(store, dryRun) {
|
|
93
|
+
function handleCleanup(store, dryRun, target = null) {
|
|
94
|
+
if (hasValue(target)) {
|
|
95
|
+
const result = cleanupExecution(store, target, { dryRun });
|
|
96
|
+
if (!result.success && result.output) {
|
|
97
|
+
console.log(result.output);
|
|
98
|
+
}
|
|
99
|
+
return reportResult(result);
|
|
100
|
+
}
|
|
87
101
|
if (!store) {
|
|
88
102
|
console.error('Error: Execution tracking is disabled.');
|
|
89
103
|
return 1;
|
|
@@ -144,7 +158,9 @@ function hasQueryMode(options) {
|
|
|
144
158
|
hasValue(options.attach) ||
|
|
145
159
|
hasValue(options.resume) ||
|
|
146
160
|
options.resumeAll === true ||
|
|
147
|
-
options.cleanup === true
|
|
161
|
+
options.cleanup === true ||
|
|
162
|
+
hasValue(options.alias) ||
|
|
163
|
+
hasValue(options.rename)
|
|
148
164
|
);
|
|
149
165
|
}
|
|
150
166
|
|
|
@@ -193,7 +209,19 @@ async function dispatchQueryCommand(options, context = {}) {
|
|
|
193
209
|
return handleResumeAllQuery(store, outputFormat);
|
|
194
210
|
}
|
|
195
211
|
if (options.cleanup) {
|
|
196
|
-
return handleCleanup(store, options.cleanupDryRun);
|
|
212
|
+
return handleCleanup(store, options.cleanupDryRun, options.cleanupTarget);
|
|
213
|
+
}
|
|
214
|
+
if (hasValue(options.alias) || hasValue(options.rename)) {
|
|
215
|
+
const { nameExecution } = require('./execution-identity');
|
|
216
|
+
const rename = hasValue(options.rename);
|
|
217
|
+
return reportResult(
|
|
218
|
+
nameExecution(
|
|
219
|
+
store,
|
|
220
|
+
rename ? options.rename : options.alias,
|
|
221
|
+
options.newName,
|
|
222
|
+
{ rename }
|
|
223
|
+
)
|
|
224
|
+
);
|
|
197
225
|
}
|
|
198
226
|
|
|
199
227
|
return null;
|
|
@@ -128,6 +128,8 @@ function supportsLogfileOption() {
|
|
|
128
128
|
* @param {string|null} user - Username to run command as (optional)
|
|
129
129
|
* @param {Function} wrapCommandWithUser - Function to wrap command with user
|
|
130
130
|
* @param {Function} isInteractiveShellCommand - Function to check if command is interactive shell
|
|
131
|
+
* @param {string|null} logPath - Log file to append to (optional)
|
|
132
|
+
* @param {object|null} env - Extra environment, e.g. START_* (optional)
|
|
131
133
|
* @returns {Promise<{success: boolean, sessionName: string, message: string, output: string, exitCode: number}>}
|
|
132
134
|
*/
|
|
133
135
|
function runScreenWithLogCapture(
|
|
@@ -137,7 +139,8 @@ function runScreenWithLogCapture(
|
|
|
137
139
|
user = null,
|
|
138
140
|
wrapCommandWithUser,
|
|
139
141
|
isInteractiveShellCommand,
|
|
140
|
-
logPath = null
|
|
142
|
+
logPath = null,
|
|
143
|
+
env = null
|
|
141
144
|
) {
|
|
142
145
|
const { shell, shellArg } = shellInfo;
|
|
143
146
|
const screenTempDir = getTempDir('isolation', 'screen');
|
|
@@ -237,8 +240,10 @@ function runScreenWithLogCapture(
|
|
|
237
240
|
}
|
|
238
241
|
|
|
239
242
|
// Use spawnSync with array args (not execSync string) to avoid quoting issues (issue #25)
|
|
243
|
+
// Each `screen -dmS` server inherits this env (START_*, issue #230).
|
|
240
244
|
const result = spawnSync('screen', screenArgs, {
|
|
241
245
|
stdio: 'inherit',
|
|
246
|
+
env: { ...process.env, ...env },
|
|
242
247
|
});
|
|
243
248
|
|
|
244
249
|
if (result.error) {
|
|
@@ -424,6 +429,7 @@ function startDetachedScreenWithLogCapture(
|
|
|
424
429
|
keepAlive = false,
|
|
425
430
|
logPath = null,
|
|
426
431
|
wrapCommandWithUser,
|
|
432
|
+
env = null,
|
|
427
433
|
} = options;
|
|
428
434
|
|
|
429
435
|
const screenTempDir = getTempDir('isolation', 'screen');
|
|
@@ -471,7 +477,10 @@ function startDetachedScreenWithLogCapture(
|
|
|
471
477
|
}
|
|
472
478
|
|
|
473
479
|
try {
|
|
474
|
-
const result = spawnSync('screen', screenArgs, {
|
|
480
|
+
const result = spawnSync('screen', screenArgs, {
|
|
481
|
+
stdio: 'inherit',
|
|
482
|
+
env: { ...process.env, ...env },
|
|
483
|
+
});
|
|
475
484
|
if (result.error) {
|
|
476
485
|
throw result.error;
|
|
477
486
|
}
|