start-command 0.43.0 → 0.44.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/.changeset/README.md +1 -1
- package/CHANGELOG.md +13 -0
- package/README.md +2 -0
- package/package.json +5 -5
- package/src/lib/args-parser-queries.js +64 -3
- package/src/lib/docker-inspect-batch.js +3 -0
- package/src/lib/execution-attach.js +10 -1
- package/src/lib/execution-cleanup.js +8 -0
- package/src/lib/execution-control.js +14 -3
- package/src/lib/execution-pause.js +701 -0
- package/src/lib/execution-recovery.js +6 -0
- package/src/lib/execution-resume-all.js +14 -0
- package/src/lib/execution-resume.js +16 -1
- package/src/lib/execution-store.js +5 -1
- package/src/lib/log-sanitizer.js +4 -3
- package/src/lib/query-commands.js +33 -1
- package/src/lib/session-probe.js +27 -1
- package/src/lib/status-formatter.js +13 -3
- package/src/lib/status-probe.js +19 -5
- package/src/lib/usage.js +8 -1
- package/test/args-parser-attach-resume.js +1 -1
- package/test/args-parser-control.js +3 -3
- package/test/args-parser.js +1 -1
- package/test/fixtures/pause-golden.cjs +142 -0
- package/test/regression-243.js +4 -1
- package/test/regression-263.js +867 -0
- package/test/shared-golden.js +28 -3
|
@@ -34,6 +34,7 @@ const ResumeAllAction = {
|
|
|
34
34
|
RUNNING: 'running',
|
|
35
35
|
RECONCILED: 'reconciled',
|
|
36
36
|
UNKNOWN: 'unknown',
|
|
37
|
+
PAUSED: 'paused',
|
|
37
38
|
};
|
|
38
39
|
|
|
39
40
|
function formatResumeAllAsLinksNotation(summary) {
|
|
@@ -137,6 +138,19 @@ function resumeAllExecutions(store, deps = {}) {
|
|
|
137
138
|
continue;
|
|
138
139
|
}
|
|
139
140
|
|
|
141
|
+
if (probe.state === SessionState.PAUSED) {
|
|
142
|
+
// Neither restarted nor re-watched while frozen (issue #263).
|
|
143
|
+
executions.push(
|
|
144
|
+
describeRecord(
|
|
145
|
+
record,
|
|
146
|
+
probe,
|
|
147
|
+
ResumeAllAction.PAUSED,
|
|
148
|
+
`Skipped paused container: ${sessionName}. Use \`$ --unpause ${record.uuid}\` to continue it.`
|
|
149
|
+
)
|
|
150
|
+
);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
|
|
140
154
|
if (probe.alive && probe.backend === 'docker') {
|
|
141
155
|
startWatcher(
|
|
142
156
|
sessionName,
|
|
@@ -43,7 +43,11 @@ const {
|
|
|
43
43
|
rollbackLaunch,
|
|
44
44
|
failLaunchedPersistence,
|
|
45
45
|
} = require('./launch-persistence');
|
|
46
|
-
const {
|
|
46
|
+
const {
|
|
47
|
+
SessionState,
|
|
48
|
+
pausedSessionError,
|
|
49
|
+
probeSession,
|
|
50
|
+
} = require('./session-probe');
|
|
47
51
|
const { getDockerNetworks } = require('./docker-network-lifecycle');
|
|
48
52
|
const {
|
|
49
53
|
writeCommandHandoff,
|
|
@@ -217,6 +221,10 @@ function buildResumePlan(
|
|
|
217
221
|
return { error: 'Only detached isolated executions can be resumed.' };
|
|
218
222
|
}
|
|
219
223
|
|
|
224
|
+
if (probe.state === SessionState.PAUSED) {
|
|
225
|
+
return pausedSessionError('resume', record.uuid, sessionName);
|
|
226
|
+
}
|
|
227
|
+
|
|
220
228
|
if (probe.alive) {
|
|
221
229
|
return {
|
|
222
230
|
error:
|
|
@@ -481,6 +489,13 @@ async function resumeExecution(store, identifier, deps = {}) {
|
|
|
481
489
|
const runner = deps.runner || runCommand;
|
|
482
490
|
const probeFn = deps.probe || ((r) => probeSession(r, runner));
|
|
483
491
|
const probe = probeFn(record);
|
|
492
|
+
if (probe.state === SessionState.PAUSED) {
|
|
493
|
+
// Refused before any snapshot or resource change (issue #263).
|
|
494
|
+
return {
|
|
495
|
+
success: false,
|
|
496
|
+
...pausedSessionError('resume', record.uuid, probe.sessionName),
|
|
497
|
+
};
|
|
498
|
+
}
|
|
484
499
|
const opts = record.options || {};
|
|
485
500
|
let resources;
|
|
486
501
|
try {
|
|
@@ -663,7 +663,11 @@ class ExecutionStore {
|
|
|
663
663
|
const staleRecords = [];
|
|
664
664
|
|
|
665
665
|
for (const record of executingRecords) {
|
|
666
|
-
if (
|
|
666
|
+
if (
|
|
667
|
+
require('./launch-owner').hasActiveLaunch(record) ||
|
|
668
|
+
require('./execution-pause').isConfirmedPaused(record)
|
|
669
|
+
) {
|
|
670
|
+
// A paused container is alive, just frozen (issue #263).
|
|
667
671
|
continue;
|
|
668
672
|
}
|
|
669
673
|
let isStale = false;
|
package/src/lib/log-sanitizer.js
CHANGED
|
@@ -13,9 +13,10 @@
|
|
|
13
13
|
* keeps the rest of its line (issue #249, sensitive-data-sanitizer#45); the
|
|
14
14
|
* pre-pass that did this here is gone (issue #253).
|
|
15
15
|
*
|
|
16
|
-
* The package
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* The package stays on ^0.14.0: 0.20.0 and later drop the bytes after a
|
|
17
|
+
* credential that is followed by a NUL byte (sensitive-data-sanitizer#180,
|
|
18
|
+
* the blocker in .github/dependency-blockers.json). The Rust installer and its
|
|
19
|
+
* version check require the same release (rust/src/lib/log_sanitizer.rs).
|
|
19
20
|
*/
|
|
20
21
|
|
|
21
22
|
const fs = require('fs');
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
*
|
|
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
|
-
* --upload-log, --stop, --terminate, --
|
|
6
|
+
* --upload-log, --stop, --terminate, --pause, --unpause, --attach, --resume,
|
|
7
|
+
* --resume-all and
|
|
7
8
|
* --cleanup, plus --alias and --rename. `dispatchQueryCommand` is the single
|
|
8
9
|
* entry point the CLI calls before it starts interpreting the command line as
|
|
9
10
|
* a command to run.
|
|
@@ -54,6 +55,28 @@ function handleControlQuery(store, identifier, action) {
|
|
|
54
55
|
return reportResult(controlExecution(store, identifier, action));
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Handle --pause / --unpause (issue #263).
|
|
60
|
+
* @param {?object} store - Execution store
|
|
61
|
+
* @param {string} identifier - UUID or session name
|
|
62
|
+
* @param {string} action - pause or unpause
|
|
63
|
+
* @param {object} [options] - {outputFormat, pauseReason, pausedBy}
|
|
64
|
+
* @returns {number} Exit code
|
|
65
|
+
*/
|
|
66
|
+
function handlePauseQuery(store, identifier, action, options = {}) {
|
|
67
|
+
const pause = require('./execution-pause');
|
|
68
|
+
const deps = {
|
|
69
|
+
outputFormat: options.outputFormat,
|
|
70
|
+
reason: options.pauseReason || null,
|
|
71
|
+
by: options.pausedBy || null,
|
|
72
|
+
};
|
|
73
|
+
return reportResult(
|
|
74
|
+
action === pause.PauseAction.PAUSE
|
|
75
|
+
? pause.pauseExecution(store, identifier, deps)
|
|
76
|
+
: pause.unpauseExecution(store, identifier, deps)
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
57
80
|
function handleAttachQuery(store, identifier, readOnly) {
|
|
58
81
|
return reportResult(attachExecution(store, identifier, { readOnly }));
|
|
59
82
|
}
|
|
@@ -155,6 +178,8 @@ function hasQueryMode(options) {
|
|
|
155
178
|
hasValue(options.uploadLog) ||
|
|
156
179
|
hasValue(options.stop) ||
|
|
157
180
|
hasValue(options.terminate) ||
|
|
181
|
+
hasValue(options.pause) ||
|
|
182
|
+
hasValue(options.unpause) ||
|
|
158
183
|
hasValue(options.attach) ||
|
|
159
184
|
hasValue(options.resume) ||
|
|
160
185
|
options.resumeAll === true ||
|
|
@@ -193,6 +218,12 @@ async function dispatchQueryCommand(options, context = {}) {
|
|
|
193
218
|
ControlAction.TERMINATE
|
|
194
219
|
);
|
|
195
220
|
}
|
|
221
|
+
if (hasValue(options.pause)) {
|
|
222
|
+
return handlePauseQuery(store, options.pause, 'pause', options);
|
|
223
|
+
}
|
|
224
|
+
if (hasValue(options.unpause)) {
|
|
225
|
+
return handlePauseQuery(store, options.unpause, 'unpause', options);
|
|
226
|
+
}
|
|
196
227
|
if (hasValue(options.attach)) {
|
|
197
228
|
return handleAttachQuery(store, options.attach, options.readOnly === true);
|
|
198
229
|
}
|
|
@@ -234,6 +265,7 @@ module.exports = {
|
|
|
234
265
|
handleCleanup,
|
|
235
266
|
handleControlQuery,
|
|
236
267
|
handleListQuery,
|
|
268
|
+
handlePauseQuery,
|
|
237
269
|
handleResumeAllQuery,
|
|
238
270
|
handleResumeQuery,
|
|
239
271
|
handleStatusQuery,
|
package/src/lib/session-probe.js
CHANGED
|
@@ -13,12 +13,16 @@ const { getDockerCommand } = require('./docker-cleanup');
|
|
|
13
13
|
/**
|
|
14
14
|
* States a tracked isolation session can be observed in.
|
|
15
15
|
* - RUNNING: the session/container is alive and can be attached to
|
|
16
|
+
* - PAUSED: the container is frozen by `docker pause` (issue #263); it is
|
|
17
|
+
* alive (its process and memory are kept) but can be neither attached to nor
|
|
18
|
+
* resumed until `$ --unpause` thaws it
|
|
16
19
|
* - STOPPED: the container still exists but is not running (resumable)
|
|
17
20
|
* - MISSING: no trace of the session/container remains
|
|
18
21
|
* - UNKNOWN: the backend cannot be probed locally (e.g. ssh)
|
|
19
22
|
*/
|
|
20
23
|
const SessionState = {
|
|
21
24
|
RUNNING: 'running',
|
|
25
|
+
PAUSED: 'paused',
|
|
22
26
|
STOPPED: 'stopped',
|
|
23
27
|
MISSING: 'missing',
|
|
24
28
|
UNKNOWN: 'unknown',
|
|
@@ -39,6 +43,11 @@ function mapDockerStatusToState(status) {
|
|
|
39
43
|
if (normalized === 'running' || normalized === 'restarting') {
|
|
40
44
|
return SessionState.RUNNING;
|
|
41
45
|
}
|
|
46
|
+
if (normalized === 'paused') {
|
|
47
|
+
// Paused is alive, not stopped: reading it as stopped made `--resume`
|
|
48
|
+
// plan a `docker start`/snapshot of a frozen container (issue #263).
|
|
49
|
+
return SessionState.PAUSED;
|
|
50
|
+
}
|
|
42
51
|
return SessionState.STOPPED;
|
|
43
52
|
}
|
|
44
53
|
|
|
@@ -109,13 +118,30 @@ function probeSession(record, runner = runCommand) {
|
|
|
109
118
|
probe.state = result.success ? SessionState.RUNNING : SessionState.MISSING;
|
|
110
119
|
}
|
|
111
120
|
|
|
112
|
-
probe.alive =
|
|
121
|
+
probe.alive =
|
|
122
|
+
probe.state === SessionState.RUNNING || probe.state === SessionState.PAUSED;
|
|
113
123
|
return probe;
|
|
114
124
|
}
|
|
115
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Refusal for a control that cannot act on a paused container (issue #263).
|
|
128
|
+
* @param {string} action - What was refused, e.g. "attach to" or "resume"
|
|
129
|
+
* @param {string} uuid - Execution UUID, for the suggested command
|
|
130
|
+
* @param {string} sessionName - Container name
|
|
131
|
+
* @returns {{error: string}} Error result
|
|
132
|
+
*/
|
|
133
|
+
function pausedSessionError(action, uuid, sessionName) {
|
|
134
|
+
return {
|
|
135
|
+
error:
|
|
136
|
+
`Session "${sessionName}" is paused (frozen by docker pause), so start cannot ${action} it. ` +
|
|
137
|
+
`Use \`$ --unpause ${uuid}\` first to continue the same process, or \`$ --stop ${uuid}\` to end it.`,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
116
141
|
module.exports = {
|
|
117
142
|
SessionState,
|
|
118
143
|
mapDockerStatusToState,
|
|
119
144
|
parseScreenSessionState,
|
|
145
|
+
pausedSessionError,
|
|
120
146
|
probeSession,
|
|
121
147
|
};
|
|
@@ -33,6 +33,7 @@ const {
|
|
|
33
33
|
persistContainerRemoval,
|
|
34
34
|
} = require('./container-removal');
|
|
35
35
|
const { withDockerInspectBatch } = require('./docker-inspect-batch');
|
|
36
|
+
const pause = require('./execution-pause');
|
|
36
37
|
const {
|
|
37
38
|
backendExitCode,
|
|
38
39
|
dockerProbeSpawnOptions,
|
|
@@ -460,8 +461,11 @@ function enrichDetachedStatus(record) {
|
|
|
460
461
|
? readAttemptLogTail(record, FATAL_MARKER_TAIL_BYTES)
|
|
461
462
|
: null;
|
|
462
463
|
const probe = dockerProbe();
|
|
463
|
-
const resolved =
|
|
464
|
-
|
|
464
|
+
const resolved = pause.attachPauseState(
|
|
465
|
+
attachContainerRemoval(
|
|
466
|
+
resolveDetachedStatus(record, logTail, probe),
|
|
467
|
+
probe
|
|
468
|
+
),
|
|
465
469
|
probe
|
|
466
470
|
);
|
|
467
471
|
const enriched = attachMemoryExhaustion(
|
|
@@ -664,6 +668,7 @@ function formatRecordAsText(record) {
|
|
|
664
668
|
if (obj.staleDetectedAt) {
|
|
665
669
|
lines.push(`Stale Detected At: ${obj.staleDetectedAt}`);
|
|
666
670
|
}
|
|
671
|
+
lines.push(...pause.pauseTextLines(obj));
|
|
667
672
|
if (obj.containerState) {
|
|
668
673
|
// The container no longer exists; `containerId` is history (issue #225).
|
|
669
674
|
lines.push(`Container State: ${obj.containerState}`);
|
|
@@ -821,6 +826,7 @@ function queryStatus(store, identifier, outputFormat) {
|
|
|
821
826
|
() => {
|
|
822
827
|
const enrichedRecord = enrichDetachedStatus(record);
|
|
823
828
|
persistContainerRemoval(store, record, enrichedRecord);
|
|
829
|
+
pause.persistPauseReconciliation(store, enrichedRecord);
|
|
824
830
|
return attachProcessIds(enrichedRecord);
|
|
825
831
|
}
|
|
826
832
|
);
|
|
@@ -856,7 +862,11 @@ function listExecutions(store, outputFormat, options = {}) {
|
|
|
856
862
|
// One docker inspect for every listed container (issue #243).
|
|
857
863
|
const enriched = sortRecordsByStartTimeDesc(
|
|
858
864
|
withDockerInspectBatch(all, dockerProbe(), () =>
|
|
859
|
-
all.map(
|
|
865
|
+
all.map((record) => {
|
|
866
|
+
const prepared = prepareRecordForList(record);
|
|
867
|
+
pause.persistPauseReconciliation(store, prepared);
|
|
868
|
+
return prepared;
|
|
869
|
+
})
|
|
860
870
|
)
|
|
861
871
|
);
|
|
862
872
|
// --running reports the reconciled status, not the stored one: a record
|
package/src/lib/status-probe.js
CHANGED
|
@@ -29,7 +29,8 @@ const {
|
|
|
29
29
|
*
|
|
30
30
|
* @param {string} sessionName - Container name
|
|
31
31
|
* @param {Object|null} [probe] - Receives `sessionNotFound` (issue #225)
|
|
32
|
-
* @returns {{running: boolean, exitCode: number|null, oomKilled: boolean|null
|
|
32
|
+
* @returns {{running: boolean, exitCode: number|null, oomKilled: boolean|null,
|
|
33
|
+
* status: ?string, paused: boolean}|null} State,
|
|
33
34
|
* or null when the container cannot be inspected (not found yet, removed, or docker error)
|
|
34
35
|
*/
|
|
35
36
|
function inspectDockerState(sessionName, probe = null) {
|
|
@@ -37,7 +38,8 @@ function inspectDockerState(sessionName, probe = null) {
|
|
|
37
38
|
getDockerCommand(),
|
|
38
39
|
dockerProbeSpawnOptions(),
|
|
39
40
|
'{{.State.Running}} {{.State.ExitCode}} {{.State.OOMKilled}} ' +
|
|
40
|
-
'{{.State.StartedAt}} {{.State.FinishedAt}}'
|
|
41
|
+
'{{.State.StartedAt}} {{.State.FinishedAt}} ' +
|
|
42
|
+
'{{.State.Status}} {{.State.Paused}}',
|
|
41
43
|
sessionName
|
|
42
44
|
);
|
|
43
45
|
if (probe) {
|
|
@@ -50,9 +52,15 @@ function inspectDockerState(sessionName, probe = null) {
|
|
|
50
52
|
}
|
|
51
53
|
// Parsed positionally and defensively: an older docker (or a stubbed one)
|
|
52
54
|
// may answer with fewer fields than the template asks for.
|
|
53
|
-
const [
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
const [
|
|
56
|
+
runningRaw,
|
|
57
|
+
exitRaw,
|
|
58
|
+
oomKilledRaw,
|
|
59
|
+
startedRaw,
|
|
60
|
+
finishedRaw,
|
|
61
|
+
statusRaw,
|
|
62
|
+
pausedRaw,
|
|
63
|
+
] = stdout.trim().split(/\s+/);
|
|
56
64
|
const exitCode = Number.parseInt(exitRaw, 10);
|
|
57
65
|
return {
|
|
58
66
|
running: runningRaw === 'true',
|
|
@@ -61,6 +69,9 @@ function inspectDockerState(sessionName, probe = null) {
|
|
|
61
69
|
oomKilledRaw === 'true' ? true : oomKilledRaw === 'false' ? false : null,
|
|
62
70
|
startedAt: normalizeDockerTimestamp(startedRaw),
|
|
63
71
|
finishedAt: normalizeDockerTimestamp(finishedRaw),
|
|
72
|
+
// A paused container is still `.State.Running` (issue #263).
|
|
73
|
+
status: statusRaw || null,
|
|
74
|
+
paused: pausedRaw === 'true' || statusRaw === 'paused',
|
|
64
75
|
};
|
|
65
76
|
}
|
|
66
77
|
|
|
@@ -99,6 +110,9 @@ function readDockerState(record, probe = null) {
|
|
|
99
110
|
) {
|
|
100
111
|
return null;
|
|
101
112
|
}
|
|
113
|
+
if (probe) {
|
|
114
|
+
probe.dockerState = state;
|
|
115
|
+
}
|
|
102
116
|
return state;
|
|
103
117
|
}
|
|
104
118
|
|
package/src/lib/usage.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Print usage information */
|
|
2
2
|
function printUsage() {
|
|
3
|
-
console.log(`Usage: $ [options] [--] <command> | $ --status <uuid> [--output-format <fmt>] | $ --list [--running] [--output-format <fmt>] | $ --upload-log <id> | $ --stop <id> | $ --terminate <id> | $ --attach <id> [--read-only] | $ --resume <id> [-- <command>] | $ --resume-all | $ --alias <id> <name> | $ --rename <id> <name>
|
|
3
|
+
console.log(`Usage: $ [options] [--] <command> | $ --status <uuid> [--output-format <fmt>] | $ --list [--running] [--output-format <fmt>] | $ --upload-log <id> | $ --stop <id> | $ --terminate <id> | $ --pause <id> | $ --unpause <id> | $ --attach <id> [--read-only] | $ --resume <id> [-- <command>] | $ --resume-all | $ --alias <id> <name> | $ --rename <id> <name>
|
|
4
4
|
|
|
5
5
|
Options:
|
|
6
6
|
--help, -h Show this usage and exit successfully
|
|
@@ -54,6 +54,11 @@ Options:
|
|
|
54
54
|
--no-sanitize With --upload-log, explicitly upload without secret redaction
|
|
55
55
|
--stop <id> Ask a detached isolated execution to stop gracefully
|
|
56
56
|
--terminate <id> Terminate a detached isolated execution immediately
|
|
57
|
+
--pause <id> Freeze a detached Docker execution (docker pause): CPU is
|
|
58
|
+
released, memory stays allocated (--stop frees it)
|
|
59
|
+
--pause-reason <text> With --pause, record why the execution was frozen
|
|
60
|
+
--paused-by <name> With --pause, record who froze the execution
|
|
61
|
+
--unpause <id> Thaw a paused Docker execution; the same process continues
|
|
57
62
|
--attach <id> Attach to a running detached isolated execution
|
|
58
63
|
--read-only With --attach, follow output without sending input
|
|
59
64
|
--resume <id> Restart a stopped detached execution in the same environment
|
|
@@ -93,6 +98,8 @@ Examples:
|
|
|
93
98
|
$ --upload-log my-screen-session # Upload stored execution log
|
|
94
99
|
$ --stop my-screen-session # Ask detached execution to stop gracefully
|
|
95
100
|
$ --terminate my-screen-session # Terminate detached execution immediately
|
|
101
|
+
$ --pause my-docker-session --pause-reason 'rate limit' # Freeze, keep RAM
|
|
102
|
+
$ --unpause my-docker-session # Continue the same frozen process
|
|
96
103
|
$ --list --running # Only executions that are still running
|
|
97
104
|
$ --attach my-docker-session # Re-enter a running detached session
|
|
98
105
|
$ --attach my-docker-session --read-only # Follow its output without stdin
|
|
@@ -142,7 +142,7 @@ describe('query mode exclusivity', () => {
|
|
|
142
142
|
test(`rejects ${args.join(' ')}`, () => {
|
|
143
143
|
assert.throws(() => {
|
|
144
144
|
parse(args);
|
|
145
|
-
}, /Cannot combine --status, --list, --upload-log, --stop, --terminate, --attach, --resume, --resume-all, --cleanup, --alias, or --rename/);
|
|
145
|
+
}, /Cannot combine --status, --list, --upload-log, --stop, --terminate, --pause, --unpause, --attach, --resume, --resume-all, --cleanup, --alias, or --rename/);
|
|
146
146
|
});
|
|
147
147
|
}
|
|
148
148
|
});
|
|
@@ -54,19 +54,19 @@ describe('control options', () => {
|
|
|
54
54
|
it('should reject combining query and control modes', () => {
|
|
55
55
|
assert.throws(() => {
|
|
56
56
|
parseArgs(['--status', 'uuid-here', '--stop', 'my-session']);
|
|
57
|
-
}, /Cannot combine --status, --list, --upload-log, --stop, --terminate, --attach, --resume, --resume-all, --cleanup, --alias, or --rename/);
|
|
57
|
+
}, /Cannot combine --status, --list, --upload-log, --stop, --terminate, --pause, --unpause, --attach, --resume, --resume-all, --cleanup, --alias, or --rename/);
|
|
58
58
|
});
|
|
59
59
|
|
|
60
60
|
it('should reject combining upload-log with control modes', () => {
|
|
61
61
|
assert.throws(() => {
|
|
62
62
|
parseArgs(['--upload-log', 'uuid-here', '--terminate', 'my-session']);
|
|
63
|
-
}, /Cannot combine --status, --list, --upload-log, --stop, --terminate, --attach, --resume, --resume-all, --cleanup, --alias, or --rename/);
|
|
63
|
+
}, /Cannot combine --status, --list, --upload-log, --stop, --terminate, --pause, --unpause, --attach, --resume, --resume-all, --cleanup, --alias, or --rename/);
|
|
64
64
|
});
|
|
65
65
|
|
|
66
66
|
it('should reject output-format with control modes', () => {
|
|
67
67
|
assert.throws(() => {
|
|
68
68
|
parseArgs(['--stop', 'my-session', '--output-format', 'json']);
|
|
69
|
-
}, /--output-format option is only valid with --status, --list, --resume,
|
|
69
|
+
}, /--output-format option is only valid with --status, --list, --resume, --resume-all, --pause, or --unpause/);
|
|
70
70
|
});
|
|
71
71
|
|
|
72
72
|
it('should default control options to null', () => {
|
package/test/args-parser.js
CHANGED
|
@@ -859,7 +859,7 @@ describe('output-format option', () => {
|
|
|
859
859
|
it('should throw error for output-format without status', () => {
|
|
860
860
|
assert.throws(() => {
|
|
861
861
|
parseArgs(['--output-format', 'json', '--', 'npm', 'test']);
|
|
862
|
-
}, /--output-format option is only valid with --status, --list, --resume,
|
|
862
|
+
}, /--output-format option is only valid with --status, --list, --resume, --resume-all, --pause, or --unpause/);
|
|
863
863
|
});
|
|
864
864
|
|
|
865
865
|
it('should accept all valid output formats', () => {
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Issue #263: runs one `pause-control` shared golden case
|
|
3
|
+
* (parity/fixtures/contracts.json) through the JavaScript `--pause` /
|
|
4
|
+
* `--unpause` control path. rust/tests/shared_golden.rs runs the same cases
|
|
5
|
+
* through the Rust path, so both runtimes are held to identical reports,
|
|
6
|
+
* errors, docker argv and persisted records.
|
|
7
|
+
*
|
|
8
|
+
* The in-memory docker follows the rules spelled out in
|
|
9
|
+
* `pause-control.docker` of the fixture file; keep it in step with
|
|
10
|
+
* `GoldenDocker` in rust/tests/shared_golden.rs.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const TIMESTAMP =
|
|
14
|
+
/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})/g;
|
|
15
|
+
|
|
16
|
+
/** Replace every ISO-8601 timestamp, which differs per run, with a marker. */
|
|
17
|
+
function normalize(value) {
|
|
18
|
+
if (typeof value === 'string') {
|
|
19
|
+
return value.replace(TIMESTAMP, '<timestamp>');
|
|
20
|
+
}
|
|
21
|
+
if (Array.isArray(value)) {
|
|
22
|
+
return value.map(normalize);
|
|
23
|
+
}
|
|
24
|
+
if (value && typeof value === 'object') {
|
|
25
|
+
return Object.fromEntries(
|
|
26
|
+
Object.entries(value).map(([key, entry]) => [key, normalize(entry)])
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function goldenDocker(initial, name) {
|
|
33
|
+
const docker = { state: initial, calls: [] };
|
|
34
|
+
const ok = (stdout = '') => ({
|
|
35
|
+
success: true,
|
|
36
|
+
stdout,
|
|
37
|
+
stderr: '',
|
|
38
|
+
status: 0,
|
|
39
|
+
error: null,
|
|
40
|
+
});
|
|
41
|
+
const fail = (stderr) => ({
|
|
42
|
+
success: false,
|
|
43
|
+
stdout: '',
|
|
44
|
+
stderr,
|
|
45
|
+
status: 1,
|
|
46
|
+
error: null,
|
|
47
|
+
});
|
|
48
|
+
docker.runner = (command, args, extra = {}) => {
|
|
49
|
+
docker.calls.push({ args: [...args], bounded: Boolean(extra.timeout) });
|
|
50
|
+
const verb = args[0];
|
|
51
|
+
if (docker.state === 'removed' && verb !== 'rm') {
|
|
52
|
+
return fail(`Error response from daemon: No such container: ${name}`);
|
|
53
|
+
}
|
|
54
|
+
const alive = docker.state === 'running' || docker.state === 'paused';
|
|
55
|
+
switch (verb) {
|
|
56
|
+
case 'inspect': {
|
|
57
|
+
const format = args[args.indexOf('-f') + 1];
|
|
58
|
+
if (format === '{{.State.Status}} {{.State.Paused}}') {
|
|
59
|
+
return ok(`${docker.state} ${docker.state === 'paused'}\n`);
|
|
60
|
+
}
|
|
61
|
+
if (format === '{{.State.Status}}') {
|
|
62
|
+
return ok(`${docker.state}\n`);
|
|
63
|
+
}
|
|
64
|
+
if (format === '{{.Id}} {{.State.Pid}}') {
|
|
65
|
+
return ok(`abc263def ${alive ? 4242 : 0}\n`);
|
|
66
|
+
}
|
|
67
|
+
return ok('');
|
|
68
|
+
}
|
|
69
|
+
case 'pause':
|
|
70
|
+
if (docker.state !== 'running') {
|
|
71
|
+
return fail(`container ${name} is not running`);
|
|
72
|
+
}
|
|
73
|
+
docker.state = 'paused';
|
|
74
|
+
return ok(`${name}\n`);
|
|
75
|
+
case 'unpause':
|
|
76
|
+
if (docker.state !== 'paused') {
|
|
77
|
+
return fail(`container ${name} is not paused`);
|
|
78
|
+
}
|
|
79
|
+
docker.state = 'running';
|
|
80
|
+
return ok(`${name}\n`);
|
|
81
|
+
default:
|
|
82
|
+
return ok('');
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
return docker;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Run one case and return its normalized observable result.
|
|
90
|
+
* @param {{ExecutionRecord: Function, pause: object}} modules - Code under test
|
|
91
|
+
* @param {object} store - An empty ExecutionStore
|
|
92
|
+
* @param {object} section - `pause-control` from the contracts file
|
|
93
|
+
* @param {object} fixture - One of `section.cases`
|
|
94
|
+
* @returns {object} The value `fixture.expected` must equal
|
|
95
|
+
*/
|
|
96
|
+
function runPauseCase({ ExecutionRecord, pause }, store, section, fixture) {
|
|
97
|
+
const base = section.record;
|
|
98
|
+
store.save(
|
|
99
|
+
ExecutionRecord.fromObject({
|
|
100
|
+
...base,
|
|
101
|
+
status: fixture.status || base.status,
|
|
102
|
+
exitCode:
|
|
103
|
+
fixture.exitCode === undefined ? base.exitCode : fixture.exitCode,
|
|
104
|
+
options: { ...base.options, ...(fixture.options || {}) },
|
|
105
|
+
})
|
|
106
|
+
);
|
|
107
|
+
const docker = goldenDocker(fixture.docker, base.options.sessionName);
|
|
108
|
+
const deps = {
|
|
109
|
+
runner: docker.runner,
|
|
110
|
+
outputFormat: fixture.outputFormat,
|
|
111
|
+
reason: fixture.reason,
|
|
112
|
+
by: fixture.by,
|
|
113
|
+
};
|
|
114
|
+
const result =
|
|
115
|
+
fixture.action === 'pause'
|
|
116
|
+
? pause.pauseExecution(store, fixture.identifier, deps)
|
|
117
|
+
: pause.unpauseExecution(store, fixture.identifier, deps);
|
|
118
|
+
const saved = store.get(base.uuid).toObject();
|
|
119
|
+
let output = result.output === undefined ? null : result.output;
|
|
120
|
+
if (output !== null && fixture.outputFormat === 'json') {
|
|
121
|
+
output = JSON.parse(output);
|
|
122
|
+
}
|
|
123
|
+
return normalize({
|
|
124
|
+
success: result.success,
|
|
125
|
+
output,
|
|
126
|
+
error: result.error === undefined ? null : result.error,
|
|
127
|
+
dockerCalls: docker.calls.map((call) => call.args),
|
|
128
|
+
unboundedDockerCalls: docker.calls
|
|
129
|
+
.filter((call) => !call.bounded)
|
|
130
|
+
.map((call) => call.args),
|
|
131
|
+
dockerState: docker.state,
|
|
132
|
+
record: {
|
|
133
|
+
status: saved.status,
|
|
134
|
+
exitCode: saved.exitCode === undefined ? null : saved.exitCode,
|
|
135
|
+
containerState:
|
|
136
|
+
saved.containerState === undefined ? null : saved.containerState,
|
|
137
|
+
options: saved.options,
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
module.exports = { normalize, goldenDocker, runPauseCase };
|
package/test/regression-243.js
CHANGED
|
@@ -82,6 +82,9 @@ const CONTAINERS = [
|
|
|
82
82
|
'.State.OOMKilled': String(oom),
|
|
83
83
|
'.State.StartedAt': startedAt || '0001-01-01T00:00:00Z',
|
|
84
84
|
'.State.FinishedAt': finishedAt || '0001-01-01T00:00:00Z',
|
|
85
|
+
// Paused state for --status/--list (issue #263).
|
|
86
|
+
'.State.Status': running ? 'running' : 'exited',
|
|
87
|
+
'.State.Paused': 'false',
|
|
85
88
|
}));
|
|
86
89
|
|
|
87
90
|
/**
|
|
@@ -290,7 +293,7 @@ posix('issue #243: one docker inspect per query, same output', () => {
|
|
|
290
293
|
});
|
|
291
294
|
|
|
292
295
|
describe('issue #243: batched inspect answers', () => {
|
|
293
|
-
const row = (name, id) => `/${name} ${id} 7 true 0 false s f`;
|
|
296
|
+
const row = (name, id) => `/${name} ${id} 7 true 0 false s f running false`;
|
|
294
297
|
|
|
295
298
|
it('matches lines by name or ID prefix, not by order', () => {
|
|
296
299
|
const answers = parseBatchInspect(
|