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
|
@@ -25,6 +25,7 @@ const os = require('os');
|
|
|
25
25
|
const { execFileSync, spawnSync } = require('child_process');
|
|
26
26
|
const crypto = require('crypto');
|
|
27
27
|
const { atomicWrite } = require('./atomic-write');
|
|
28
|
+
const { decodeStoreContent } = require('./store-decode-cache');
|
|
28
29
|
|
|
29
30
|
// Synchronous wrapper using Bun's native ESM support
|
|
30
31
|
// This works because Bun handles ESM/CJS interop
|
|
@@ -38,6 +39,12 @@ function encodeSync(data) {
|
|
|
38
39
|
}
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
const DECODE_LOG = {
|
|
43
|
+
memory: () => 'Reused the decoded store: the file is unchanged',
|
|
44
|
+
copy: () => 'Read the decoded copy of the store: the file is unchanged',
|
|
45
|
+
decode: (content) => `Decoded the store (${content.length} characters)`,
|
|
46
|
+
};
|
|
47
|
+
|
|
41
48
|
function decodeSync(notation) {
|
|
42
49
|
try {
|
|
43
50
|
const codec = require('lino-objects-codec');
|
|
@@ -52,6 +59,8 @@ const DEFAULT_APP_FOLDER = path.join(os.homedir(), '.start-command');
|
|
|
52
59
|
const LINO_DB_FILE = 'executions.lino';
|
|
53
60
|
const LINKS_DB_FILE = 'executions.links';
|
|
54
61
|
const LOCK_FILE = 'executions.lock';
|
|
62
|
+
// Decoded records as JSON, for the next process (issue #243)
|
|
63
|
+
const DECODED_COPY_FILE = 'executions.decoded.js.json';
|
|
55
64
|
const { LockManager } = require('./store-lock');
|
|
56
65
|
|
|
57
66
|
/**
|
|
@@ -77,6 +86,8 @@ class ExecutionRecord {
|
|
|
77
86
|
this.endTime = options.endTime || null;
|
|
78
87
|
this.attempt = options.attempt;
|
|
79
88
|
this.attemptHistory = options.attemptHistory;
|
|
89
|
+
// UUID of the execution this one continues (`--continues`, issue #231).
|
|
90
|
+
this.continues = options.continues || undefined;
|
|
80
91
|
// Provenance of `endTime` (issue #170.2). `--status` used to fabricate a
|
|
81
92
|
// finish time with `new Date()` whenever the record had none, which made an
|
|
82
93
|
// observation indistinguishable from a real finish time. One of
|
|
@@ -172,6 +183,9 @@ class ExecutionRecord {
|
|
|
172
183
|
if (this.attemptHistory) {
|
|
173
184
|
obj.attemptHistory = this.attemptHistory;
|
|
174
185
|
}
|
|
186
|
+
if (this.continues) {
|
|
187
|
+
obj.continues = this.continues;
|
|
188
|
+
}
|
|
175
189
|
if (this.endTimeSource !== undefined && this.endTimeSource !== null) {
|
|
176
190
|
obj.endTimeSource = this.endTimeSource;
|
|
177
191
|
}
|
|
@@ -257,6 +271,7 @@ class ExecutionStore {
|
|
|
257
271
|
this.linoDbPath = path.join(this.appFolder, LINO_DB_FILE);
|
|
258
272
|
this.linksDbPath = path.join(this.appFolder, LINKS_DB_FILE);
|
|
259
273
|
this.lockFilePath = path.join(this.appFolder, LOCK_FILE);
|
|
274
|
+
this.decodedCopyPath = path.join(this.appFolder, DECODED_COPY_FILE);
|
|
260
275
|
this.useLinks = options.useLinks !== false && isClinkInstalled();
|
|
261
276
|
this.verbose = options.verbose || false;
|
|
262
277
|
|
|
@@ -297,7 +312,14 @@ class ExecutionStore {
|
|
|
297
312
|
return [];
|
|
298
313
|
}
|
|
299
314
|
|
|
300
|
-
|
|
315
|
+
// Decoded once per content, not once per call (issue #243).
|
|
316
|
+
const { data, source } = decodeStoreContent(
|
|
317
|
+
this.linoDbPath,
|
|
318
|
+
content,
|
|
319
|
+
decodeSync,
|
|
320
|
+
{ copyPath: this.decodedCopyPath }
|
|
321
|
+
);
|
|
322
|
+
this.log(DECODE_LOG[source](content));
|
|
301
323
|
if (!Array.isArray(data)) {
|
|
302
324
|
return [];
|
|
303
325
|
}
|
|
@@ -544,33 +566,17 @@ class ExecutionStore {
|
|
|
544
566
|
/**
|
|
545
567
|
* Get an execution record by UUID or session name
|
|
546
568
|
* First tries exact UUID match, then the current session name, then any
|
|
547
|
-
* session name the execution used before it was resumed
|
|
569
|
+
* session name the execution used before it was resumed, then the display
|
|
570
|
+
* name and aliases
|
|
548
571
|
* @param {string} identifier - UUID or session name
|
|
549
572
|
* @returns {ExecutionRecord|null}
|
|
550
573
|
*/
|
|
551
574
|
get(identifier) {
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
return byUuid;
|
|
557
|
-
}
|
|
558
|
-
// Fall back to session name lookup (stored in options.sessionName)
|
|
559
|
-
const bySessionName = records.find(
|
|
560
|
-
(r) => r.options && r.options.sessionName === identifier
|
|
561
|
-
);
|
|
562
|
-
if (bySessionName) {
|
|
563
|
-
return bySessionName;
|
|
564
|
-
}
|
|
565
|
-
// Finally, honor session names this record used before it was resumed, so
|
|
566
|
-
// one logical session stays addressable across restarts (issue #162).
|
|
567
|
-
const byPreviousSessionName = records.find(
|
|
568
|
-
(r) =>
|
|
569
|
-
r.options &&
|
|
570
|
-
Array.isArray(r.options.sessionNameHistory) &&
|
|
571
|
-
r.options.sessionNameHistory.includes(identifier)
|
|
575
|
+
// Names, display names and aliases resolve too (issues #162, #231).
|
|
576
|
+
return require('./execution-identity').findRecord(
|
|
577
|
+
this.readLinoRecords(),
|
|
578
|
+
identifier
|
|
572
579
|
);
|
|
573
|
-
return byPreviousSessionName || null;
|
|
574
580
|
}
|
|
575
581
|
|
|
576
582
|
/**
|
|
@@ -926,4 +932,5 @@ module.exports = {
|
|
|
926
932
|
LINO_DB_FILE,
|
|
927
933
|
LINKS_DB_FILE,
|
|
928
934
|
LOCK_FILE,
|
|
935
|
+
DECODED_COPY_FILE,
|
|
929
936
|
};
|
package/src/lib/exit-evidence.js
CHANGED
|
@@ -5,6 +5,16 @@ const MAIN_OOM = 'Exit evidence: main-oom (recent cgroup oom_kill delta)';
|
|
|
5
5
|
const DOCKER_OOM = 'Exit evidence: main-oom (recent Docker oom event)';
|
|
6
6
|
const DAEMON_RESTART =
|
|
7
7
|
'Exit evidence: docker-daemon-restart (Docker service journal)';
|
|
8
|
+
const DAEMON_RESTART_API =
|
|
9
|
+
'Exit evidence: docker-daemon-restart (Docker API: containers finished together)';
|
|
10
|
+
const DAEMON_RESTART_PREFIX = 'Exit evidence: docker-daemon-restart';
|
|
11
|
+
// dockerd gives every container its stop timeout and kills the rest within its
|
|
12
|
+
// 15 s shutdown timeout, so the containers of one restart finish this close.
|
|
13
|
+
const DAEMON_RESTART_WINDOW_MS = 15000;
|
|
14
|
+
const CONTAINER_STATE_FORMAT =
|
|
15
|
+
'{{.Id}} {{.State.FinishedAt}} {{.State.StartedAt}} {{.State.ExitCode}} {{.State.OOMKilled}}';
|
|
16
|
+
// Bounds one `docker inspect` call on hosts with many stopped containers.
|
|
17
|
+
const MAX_PEER_CONTAINERS = 256;
|
|
8
18
|
|
|
9
19
|
function recentOomDelta(sample, finishedAt) {
|
|
10
20
|
const fields = String(sample || '')
|
|
@@ -39,6 +49,42 @@ function daemonRestartEvidence(journal, containerId) {
|
|
|
39
49
|
return Boolean(exited && forced);
|
|
40
50
|
}
|
|
41
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Socket-only daemon-restart evidence (issue #241): another container that
|
|
54
|
+
* finished within the same shutdown window, was not OOM-killed, and was either
|
|
55
|
+
* stopped by a signal (137/143) or started again afterwards (restart policy;
|
|
56
|
+
* moby resets its ExitCode to 0 but keeps FinishedAt). `docker events` cannot
|
|
57
|
+
* be used: its buffer does not survive the restart, while State does.
|
|
58
|
+
* @param {string} states - `CONTAINER_STATE_FORMAT` lines, one per container
|
|
59
|
+
*/
|
|
60
|
+
function daemonRestartApiEvidence(states, containerId, finishedAt) {
|
|
61
|
+
const finished = Date.parse(finishedAt);
|
|
62
|
+
return (
|
|
63
|
+
Number.isFinite(finished) &&
|
|
64
|
+
String(states || '')
|
|
65
|
+
.split('\n')
|
|
66
|
+
.some((line) => {
|
|
67
|
+
const [id, peerFinished, peerStarted, exit, oom] = line
|
|
68
|
+
.trim()
|
|
69
|
+
.split(/\s+/);
|
|
70
|
+
const end = Date.parse(peerFinished);
|
|
71
|
+
if (
|
|
72
|
+
!/^[a-f0-9]{64}$/.test(id || '') ||
|
|
73
|
+
id === containerId ||
|
|
74
|
+
oom !== 'false' ||
|
|
75
|
+
!Number.isFinite(end) ||
|
|
76
|
+
end <= 0 ||
|
|
77
|
+
Math.abs(end - finished) > DAEMON_RESTART_WINDOW_MS
|
|
78
|
+
) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
return (
|
|
82
|
+
exit === '137' || exit === '143' || Date.parse(peerStarted) > end
|
|
83
|
+
);
|
|
84
|
+
})
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
42
88
|
function recentDockerOom(events, finishedAt) {
|
|
43
89
|
const finished = Date.parse(finishedAt) / 1000;
|
|
44
90
|
return (
|
|
@@ -68,6 +114,10 @@ function collectExitEvidence(
|
|
|
68
114
|
// A local journal cannot diagnose a remote daemon. Require a local socket.
|
|
69
115
|
const local =
|
|
70
116
|
!process.env.DOCKER_HOST || process.env.DOCKER_HOST.startsWith('unix://');
|
|
117
|
+
const sampledOom =
|
|
118
|
+
(oomKilled === true || oomKilled === 'true') &&
|
|
119
|
+
(Number(exitCode) === 137 || Number(exitCode) < 0) &&
|
|
120
|
+
recentOomDelta(sample, finishedAt);
|
|
71
121
|
if (
|
|
72
122
|
Number(exitCode) === 137 &&
|
|
73
123
|
containerName &&
|
|
@@ -114,18 +164,35 @@ function collectExitEvidence(
|
|
|
114
164
|
if (events.success && recentDockerOom(events.stdout, finishedAt)) {
|
|
115
165
|
return DOCKER_OOM;
|
|
116
166
|
}
|
|
167
|
+
// Without a host journal (a mounted docker.sock, a remote daemon) the
|
|
168
|
+
// only record of the restart is the state of the other containers.
|
|
169
|
+
const peers = sampledOom
|
|
170
|
+
? { success: false, stdout: '' }
|
|
171
|
+
: run(docker, ['ps', '-a', '-q', '--no-trunc']);
|
|
172
|
+
const ids = peers.stdout
|
|
173
|
+
.split(/\s+/)
|
|
174
|
+
.filter(
|
|
175
|
+
(peer) => /^[a-f0-9]{64}$/.test(peer) && peer !== id.stdout.trim()
|
|
176
|
+
)
|
|
177
|
+
.slice(0, MAX_PEER_CONTAINERS);
|
|
178
|
+
// A container removed after `docker ps` fails inspect with status 1 but
|
|
179
|
+
// the others are still printed, so the output is used either way.
|
|
180
|
+
const states = ids.length
|
|
181
|
+
? run(docker, ['inspect', '-f', CONTAINER_STATE_FORMAT, ...ids])
|
|
182
|
+
: { success: false, stdout: '' };
|
|
183
|
+
if (
|
|
184
|
+
daemonRestartApiEvidence(states.stdout, id.stdout.trim(), finishedAt)
|
|
185
|
+
) {
|
|
186
|
+
return DAEMON_RESTART_API;
|
|
187
|
+
}
|
|
117
188
|
if (process.env.START_DEBUG === '1') {
|
|
118
189
|
console.error(
|
|
119
|
-
`[DEBUG] Exit evidence: container=${id.stdout.trim()} finishedAt=${finishedAt} eventsAvailable=${events.success} oomEvents=${JSON.stringify(events.stdout)} sample=${JSON.stringify(sample || '')}`
|
|
190
|
+
`[DEBUG] Exit evidence: container=${id.stdout.trim()} finishedAt=${finishedAt} eventsAvailable=${events.success} oomEvents=${JSON.stringify(events.stdout)} peerContainers=${ids.length} peerStates=${JSON.stringify(states.stdout)} sample=${JSON.stringify(sample || '')}`
|
|
120
191
|
);
|
|
121
192
|
}
|
|
122
193
|
}
|
|
123
194
|
}
|
|
124
|
-
if (
|
|
125
|
-
(oomKilled === true || oomKilled === 'true') &&
|
|
126
|
-
(Number(exitCode) === 137 || Number(exitCode) < 0) &&
|
|
127
|
-
recentOomDelta(sample, finishedAt)
|
|
128
|
-
) {
|
|
195
|
+
if (sampledOom) {
|
|
129
196
|
return MAIN_OOM;
|
|
130
197
|
}
|
|
131
198
|
return 'Exit evidence: unavailable (no attributed exit-time OOM or daemon restart evidence)';
|
|
@@ -134,12 +201,72 @@ function collectExitEvidence(
|
|
|
134
201
|
function fromLog(log) {
|
|
135
202
|
const lines = String(log || '').split('\n');
|
|
136
203
|
const last = lines.filter((line) => line.startsWith('Exit evidence:')).at(-1);
|
|
204
|
+
const daemonRestart = Boolean(last?.startsWith(DAEMON_RESTART_PREFIX));
|
|
137
205
|
return {
|
|
138
206
|
mainOom: last === MAIN_OOM || last === DOCKER_OOM,
|
|
139
|
-
daemonRestart
|
|
207
|
+
daemonRestart,
|
|
208
|
+
// Which source attributed the restart: the host journal or the API.
|
|
209
|
+
...(daemonRestart
|
|
210
|
+
? { daemonRestartSource: last === DAEMON_RESTART_API ? 'api' : 'journal' }
|
|
211
|
+
: {}),
|
|
140
212
|
};
|
|
141
213
|
}
|
|
142
214
|
|
|
215
|
+
/** The stored `daemonRestartSource`, kept only when a restart was attributed. */
|
|
216
|
+
function sourceOf(logged) {
|
|
217
|
+
return logged?.daemonRestart && logged.daemonRestartSource
|
|
218
|
+
? { daemonRestartSource: logged.daemonRestartSource }
|
|
219
|
+
: {};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// A watcher that shares the daemon with its task (a mounted docker.sock) dies
|
|
223
|
+
// in the same restart and never logs evidence, so `--status` collects it from
|
|
224
|
+
// the container state that survived, once per process (issue #241).
|
|
225
|
+
const lostWatcherEvidence = new Map();
|
|
226
|
+
function collectLostWatcherEvidence(record, state, runner) {
|
|
227
|
+
const name = record?.options?.sessionName;
|
|
228
|
+
if (
|
|
229
|
+
!name ||
|
|
230
|
+
record.options.exitEvidence ||
|
|
231
|
+
state?.running !== false ||
|
|
232
|
+
state.exitCode !== 137 ||
|
|
233
|
+
!state.finishedAt
|
|
234
|
+
) {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
const key = `${name}\0${state.finishedAt}`;
|
|
238
|
+
if (!lostWatcherEvidence.has(key)) {
|
|
239
|
+
const logged = fromLog(
|
|
240
|
+
collectExitEvidence(
|
|
241
|
+
{
|
|
242
|
+
containerName: name,
|
|
243
|
+
finishedAt: state.finishedAt,
|
|
244
|
+
exitCode: state.exitCode,
|
|
245
|
+
oomKilled: state.oomKilled,
|
|
246
|
+
},
|
|
247
|
+
runner
|
|
248
|
+
)
|
|
249
|
+
);
|
|
250
|
+
lostWatcherEvidence.set(key, {
|
|
251
|
+
mainOom: logged.mainOom,
|
|
252
|
+
daemonRestart: logged.daemonRestart,
|
|
253
|
+
...sourceOf(logged),
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
return { ...lostWatcherEvidence.get(key) };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Stores the lost watcher's evidence when the log tail carries none. */
|
|
260
|
+
function addLostWatcherEvidence(enriched, record, state, logTail) {
|
|
261
|
+
const lost =
|
|
262
|
+
state && !/^Exit evidence:/m.test(logTail || '')
|
|
263
|
+
? collectLostWatcherEvidence(record, state)
|
|
264
|
+
: null;
|
|
265
|
+
if (lost) {
|
|
266
|
+
enriched.options = { ...enriched.options, exitEvidence: lost };
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
143
270
|
function buildExitEvidenceSnippet(containerName, quotedLogPath) {
|
|
144
271
|
const stderr = process.env.START_DEBUG === '1' ? '2>&1' : '2>/dev/null';
|
|
145
272
|
return `${shellQuote(process.execPath)} ${shellQuote(__filename)} ${shellQuote(containerName)} "$__start_command_cgroup" "$__start_command_finished" "$__start_command_exit" "$__start_command_oom" >> ${quotedLogPath} ${stderr}`;
|
|
@@ -149,11 +276,16 @@ module.exports = {
|
|
|
149
276
|
MAIN_OOM,
|
|
150
277
|
DOCKER_OOM,
|
|
151
278
|
DAEMON_RESTART,
|
|
279
|
+
DAEMON_RESTART_API,
|
|
152
280
|
recentOomDelta,
|
|
281
|
+
daemonRestartApiEvidence,
|
|
153
282
|
recentDockerOom,
|
|
154
283
|
daemonRestartEvidence,
|
|
155
284
|
collectExitEvidence,
|
|
156
285
|
fromLog,
|
|
286
|
+
sourceOf,
|
|
287
|
+
collectLostWatcherEvidence,
|
|
288
|
+
addLostWatcherEvidence,
|
|
157
289
|
buildExitEvidenceSnippet,
|
|
158
290
|
};
|
|
159
291
|
if (require.main === module) {
|
package/src/lib/isolation.js
CHANGED
|
@@ -201,7 +201,8 @@ function runScreenWithLogCapture(
|
|
|
201
201
|
sessionName,
|
|
202
202
|
shellInfo,
|
|
203
203
|
user = null,
|
|
204
|
-
logPath = null
|
|
204
|
+
logPath = null,
|
|
205
|
+
env = null
|
|
205
206
|
) {
|
|
206
207
|
return _runScreenWithLogCapture(
|
|
207
208
|
command,
|
|
@@ -210,7 +211,8 @@ function runScreenWithLogCapture(
|
|
|
210
211
|
user,
|
|
211
212
|
wrapCommandWithUser,
|
|
212
213
|
isInteractiveShellCommand,
|
|
213
|
-
logPath
|
|
214
|
+
logPath,
|
|
215
|
+
env
|
|
214
216
|
);
|
|
215
217
|
}
|
|
216
218
|
|
|
@@ -241,6 +243,7 @@ function runInScreen(command, options = {}) {
|
|
|
241
243
|
keepAlive: options.keepAlive,
|
|
242
244
|
logPath: options.logPath,
|
|
243
245
|
wrapCommandWithUser,
|
|
246
|
+
env: options.executionEnv,
|
|
244
247
|
})
|
|
245
248
|
);
|
|
246
249
|
} else {
|
|
@@ -249,7 +252,8 @@ function runInScreen(command, options = {}) {
|
|
|
249
252
|
sessionName,
|
|
250
253
|
shellInfo,
|
|
251
254
|
options.user,
|
|
252
|
-
options.logPath
|
|
255
|
+
options.logPath,
|
|
256
|
+
options.executionEnv
|
|
253
257
|
);
|
|
254
258
|
}
|
|
255
259
|
} catch (err) {
|
|
@@ -909,6 +913,20 @@ function runIsolated(backend, command, options = {}) {
|
|
|
909
913
|
session: options.sessionStack ? options.sessionStack[0] : options.session,
|
|
910
914
|
logPath: options.logPath,
|
|
911
915
|
};
|
|
916
|
+
// START_* identity (issue #230): docker passes it with `-e`; a bare screen
|
|
917
|
+
// shell inherits the spawn env; everything else gets an `export` prefix.
|
|
918
|
+
const identity = require('./execution-env');
|
|
919
|
+
currentOptions.executionEnv =
|
|
920
|
+
identity.executionEnvFromOptions(currentOptions);
|
|
921
|
+
if (
|
|
922
|
+
backend !== 'docker' &&
|
|
923
|
+
!(backend === 'screen' && isInteractiveShellCommand(effectiveCommand))
|
|
924
|
+
) {
|
|
925
|
+
effectiveCommand = identity.prefixWithExecutionEnv(
|
|
926
|
+
effectiveCommand,
|
|
927
|
+
currentOptions.executionEnv
|
|
928
|
+
);
|
|
929
|
+
}
|
|
912
930
|
|
|
913
931
|
switch (backend) {
|
|
914
932
|
case 'screen':
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Launch options about the identity of a new execution:
|
|
3
|
+
* --append-attempt run as the next attempt of the tracked execution named
|
|
4
|
+
* by --session-id instead of being refused (issue #229)
|
|
5
|
+
* --continues <id> record that this execution continues an earlier one,
|
|
6
|
+
* which `--status` and `--upload-log` follow (issue #231)
|
|
7
|
+
*
|
|
8
|
+
* Both only apply when a command is launched, not to the query modes.
|
|
9
|
+
*/
|
|
10
|
+
const { activeQueryModes } = require('./args-parser-queries');
|
|
11
|
+
|
|
12
|
+
function createLaunchIdentityOptionDefaults() {
|
|
13
|
+
return {
|
|
14
|
+
appendAttempt: false,
|
|
15
|
+
continues: null,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Parse one launch identity option.
|
|
21
|
+
* @param {string[]} args - Arguments array
|
|
22
|
+
* @param {number} index - Current index
|
|
23
|
+
* @param {object} options - Options object to populate
|
|
24
|
+
* @returns {number} Number of arguments consumed (0 if not recognized)
|
|
25
|
+
*/
|
|
26
|
+
function parse(args, index, options) {
|
|
27
|
+
const arg = args[index];
|
|
28
|
+
if (arg === '--append-attempt') {
|
|
29
|
+
options.appendAttempt = true;
|
|
30
|
+
return 1;
|
|
31
|
+
}
|
|
32
|
+
if (arg === '--continues') {
|
|
33
|
+
if (index + 1 < args.length && !args[index + 1].startsWith('-')) {
|
|
34
|
+
options.continues = args[index + 1];
|
|
35
|
+
return 2;
|
|
36
|
+
}
|
|
37
|
+
throw new Error(
|
|
38
|
+
'Option --continues requires a UUID or session name argument'
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
if (arg.startsWith('--continues=')) {
|
|
42
|
+
const value = arg.slice('--continues='.length);
|
|
43
|
+
if (!value) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
'Option --continues requires a UUID or session name argument'
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
options.continues = value;
|
|
49
|
+
return 1;
|
|
50
|
+
}
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Validate the launch identity options.
|
|
56
|
+
* @param {object} options - Parsed wrapper options
|
|
57
|
+
* @throws {Error} When the combination is not supported
|
|
58
|
+
*/
|
|
59
|
+
function validate(options) {
|
|
60
|
+
const used = [
|
|
61
|
+
...(options.appendAttempt ? ['--append-attempt'] : []),
|
|
62
|
+
...(options.continues !== null && options.continues !== undefined
|
|
63
|
+
? ['--continues']
|
|
64
|
+
: []),
|
|
65
|
+
];
|
|
66
|
+
if (used.length === 0) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (activeQueryModes(options).length > 0) {
|
|
70
|
+
throw new Error(`${used[0]} option is only valid when launching a command`);
|
|
71
|
+
}
|
|
72
|
+
if (options.appendAttempt && !options.sessionId) {
|
|
73
|
+
throw new Error('--append-attempt option requires --session-id <uuid>');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = {
|
|
78
|
+
createLaunchIdentityOptionDefaults,
|
|
79
|
+
parse,
|
|
80
|
+
validate,
|
|
81
|
+
};
|