start-command 0.33.0 → 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/package.json +1 -1
- package/src/bin/cli.js +9 -1
- package/src/lib/detached-finalize.js +281 -0
- package/src/lib/docker-cleanup.js +160 -34
- package/src/lib/docker-post-mortem.js +289 -0
- package/src/lib/execution-resume-all.js +2 -1
- package/src/lib/execution-resume.js +2 -1
- package/src/lib/execution-store.js +46 -2
- package/src/lib/exit-reason.js +30 -0
- package/src/lib/isolation-log-utils.js +8 -1
- package/src/lib/isolation.js +17 -7
- package/src/lib/status-formatter.js +115 -24
- package/test/execution-store.js +5 -2
- package/test/regression-170.js +321 -0
- package/test/regression-171.js +562 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# start-command
|
|
2
2
|
|
|
3
|
+
## 0.34.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 22c1158: Persist the terminal state of a detached Docker execution and record the container post-mortem. The completion watcher now writes `status`, `exitCode`, `oomKilled` and a real `endTime` back into the store instead of leaving the record `executing` forever, so `--status` stops calling the clock at query time and reporting a different "finish" on every call. `endTime` carries its provenance in `endTimeSource` (`docker-finished-at`, `log-footer`, `observed-at`); a stale record keeps `endTime` empty and records `staleDetectedAt` instead of a fabricated finish time. The single `docker inspect` now also collects `StartedAt`, `FinishedAt` and `State.Error`, and both the detached and the attached path write a post-mortem block for a kept container and a one-line note for a removed one. Signal decoding (`128+n`) is shared by the log and `--status`, so `137` reads as `137 (SIGKILL - 128+9)` everywhere.
|
|
8
|
+
|
|
3
9
|
## 0.33.0
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
package/package.json
CHANGED
package/src/bin/cli.js
CHANGED
|
@@ -33,6 +33,7 @@ const {
|
|
|
33
33
|
} = require('../lib/user-manager');
|
|
34
34
|
const { handleFailure } = require('../lib/failure-handler');
|
|
35
35
|
const { ExecutionStore, ExecutionRecord } = require('../lib/execution-store');
|
|
36
|
+
const { reconcileFinalizedRecord } = require('../lib/detached-finalize');
|
|
36
37
|
const { dispatchQueryCommand, hasQueryMode } = require('../lib/query-commands');
|
|
37
38
|
const { printVersion } = require('../lib/version');
|
|
38
39
|
const { createStartBlock, createFinishBlock } = require('../lib/output-blocks');
|
|
@@ -469,6 +470,9 @@ async function runWithIsolation(
|
|
|
469
470
|
...buildDockerRuntimeMetadata(options),
|
|
470
471
|
shell: options.shell,
|
|
471
472
|
logPath: logFilePath,
|
|
473
|
+
// Lets the detached completion watcher write the terminal state back
|
|
474
|
+
// into this record once the container is gone (issue #170.1).
|
|
475
|
+
executionId: executionRecord ? executionRecord.uuid : null,
|
|
472
476
|
});
|
|
473
477
|
} else if (createdUser) {
|
|
474
478
|
// Run directly as the created user (no isolation environment)
|
|
@@ -504,7 +508,11 @@ async function runWithIsolation(
|
|
|
504
508
|
executionRecord.complete(exitCode);
|
|
505
509
|
}
|
|
506
510
|
try {
|
|
507
|
-
store.save(
|
|
511
|
+
store.save(
|
|
512
|
+
mode === 'detached'
|
|
513
|
+
? reconcileFinalizedRecord(store, executionRecord)
|
|
514
|
+
: executionRecord
|
|
515
|
+
);
|
|
508
516
|
} catch (err) {
|
|
509
517
|
if (config.verbose) {
|
|
510
518
|
console.error(
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detached completion finalizer (issue #170)
|
|
3
|
+
*
|
|
4
|
+
* A detached Docker session outlives the CLI process that started it: the
|
|
5
|
+
* foreground `start` invocation returns as soon as the container is up, so
|
|
6
|
+
* nobody is left to write the terminal state back into the execution store.
|
|
7
|
+
* The record therefore stayed `executing` forever, with `exitCode: null` and
|
|
8
|
+
* `endTime: null`, and every `--status` query had to re-derive the outcome from
|
|
9
|
+
* scratch — fabricating `endTime` with `new Date()` in the process.
|
|
10
|
+
*
|
|
11
|
+
* The detached completion watcher (see `docker-cleanup.js`) now re-invokes this
|
|
12
|
+
* module once the container is gone, handing over the facts it already read out
|
|
13
|
+
* of a single `docker inspect`. This module is deliberately tiny and
|
|
14
|
+
* dependency-light: it is executed as a short-lived child process on a host
|
|
15
|
+
* whose CLI invocation is long gone, so it must never throw and never block.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const path = require('path');
|
|
19
|
+
|
|
20
|
+
const {
|
|
21
|
+
SHELL_VARS,
|
|
22
|
+
describeExitCode,
|
|
23
|
+
normalizeDockerTimestamp,
|
|
24
|
+
} = require('./docker-post-mortem');
|
|
25
|
+
const { shellQuote } = require('./isolation-log-utils');
|
|
26
|
+
|
|
27
|
+
/** Provenance markers for `endTime` (issue #170.2). */
|
|
28
|
+
const END_TIME_SOURCE = {
|
|
29
|
+
/** `docker inspect .State.FinishedAt` — the container's own clock. */
|
|
30
|
+
DOCKER_FINISHED_AT: 'docker-finished-at',
|
|
31
|
+
/** The anchored `Finished:` line `start` itself wrote into the log. */
|
|
32
|
+
LOG_FOOTER: 'log-footer',
|
|
33
|
+
/** No real finish time exists: this is when the end was *observed*. */
|
|
34
|
+
OBSERVED_AT: 'observed-at',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Persist the terminal state of a detached execution.
|
|
39
|
+
*
|
|
40
|
+
* Purely additive with respect to the observation-vs-verdict discipline of
|
|
41
|
+
* issues #148/#151/#162: `oomKilled` and `exitReason` are recorded as facts
|
|
42
|
+
* next to the exit code, never as a substitute for it.
|
|
43
|
+
*
|
|
44
|
+
* @param {Object} options - Finalization input
|
|
45
|
+
* @param {Object} options.store - An `ExecutionStore`
|
|
46
|
+
* @param {string} options.executionId - UUID (or session name) of the record
|
|
47
|
+
* @param {number|string|null} [options.exitCode] - `.State.ExitCode`
|
|
48
|
+
* @param {boolean|string|null} [options.oomKilled] - `.State.OOMKilled`
|
|
49
|
+
* @param {string|null} [options.startedAt] - `.State.StartedAt`
|
|
50
|
+
* @param {string|null} [options.finishedAt] - `.State.FinishedAt`
|
|
51
|
+
* @param {string|null} [options.containerError] - `.State.Error`
|
|
52
|
+
* @returns {{updated: boolean, reason?: string, record?: Object}} Outcome
|
|
53
|
+
*/
|
|
54
|
+
function finalizeDetachedExecution(options = {}) {
|
|
55
|
+
const { store, executionId } = options;
|
|
56
|
+
if (!store || !executionId) {
|
|
57
|
+
return { updated: false, reason: 'missing-arguments' };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let record;
|
|
61
|
+
try {
|
|
62
|
+
record = store.get(executionId);
|
|
63
|
+
} catch {
|
|
64
|
+
return { updated: false, reason: 'store-unreadable' };
|
|
65
|
+
}
|
|
66
|
+
if (!record) {
|
|
67
|
+
return { updated: false, reason: 'record-not-found' };
|
|
68
|
+
}
|
|
69
|
+
if (record.status === 'executed' && record.endTime) {
|
|
70
|
+
// Already finalized (e.g. a `--status` query got there first, or the
|
|
71
|
+
// watcher ran twice after a resume). Nothing to correct.
|
|
72
|
+
return { updated: false, reason: 'already-final', record };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const described = describeExitCode(options.exitCode);
|
|
76
|
+
record.status = 'executed';
|
|
77
|
+
if (described.code !== null) {
|
|
78
|
+
record.exitCode = described.code;
|
|
79
|
+
} else if (record.exitCode === null || record.exitCode === undefined) {
|
|
80
|
+
record.exitCode = -1;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const finishedAt = normalizeDockerTimestamp(options.finishedAt);
|
|
84
|
+
if (finishedAt) {
|
|
85
|
+
record.endTime = new Date(finishedAt).toISOString();
|
|
86
|
+
record.endTimeSource = END_TIME_SOURCE.DOCKER_FINISHED_AT;
|
|
87
|
+
} else {
|
|
88
|
+
// Docker has no finish time for this container (it never started, or it
|
|
89
|
+
// was removed before we could look). Record *when we noticed*, and say so,
|
|
90
|
+
// rather than passing observation time off as a finish time (issue #170.2).
|
|
91
|
+
record.endTime = new Date().toISOString();
|
|
92
|
+
record.endTimeSource = END_TIME_SOURCE.OBSERVED_AT;
|
|
93
|
+
record.observedAt = record.endTime;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const startedAt = normalizeDockerTimestamp(options.startedAt);
|
|
97
|
+
if (startedAt) {
|
|
98
|
+
record.containerStartedAt = startedAt;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const oomKilled = normalizeBoolean(options.oomKilled);
|
|
102
|
+
if (oomKilled !== null) {
|
|
103
|
+
record.oomKilled = oomKilled;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const exitReason = resolveReason(record);
|
|
107
|
+
if (exitReason) {
|
|
108
|
+
record.exitReason = exitReason;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const containerError = normalizeContainerError(options.containerError);
|
|
112
|
+
if (containerError) {
|
|
113
|
+
record.options = { ...(record.options || {}), containerError };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
store.save(record);
|
|
118
|
+
} catch (err) {
|
|
119
|
+
return { updated: false, reason: `save-failed: ${err.message}`, record };
|
|
120
|
+
}
|
|
121
|
+
return { updated: true, record };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Derive the `exitReason` hint from the same evidence `--status` would use.
|
|
126
|
+
* Kept in a helper so a failure to read the log never aborts finalization.
|
|
127
|
+
* @param {Object} record - Execution record being finalized
|
|
128
|
+
* @returns {string|null} Reason hint, or null
|
|
129
|
+
*/
|
|
130
|
+
function resolveReason(record) {
|
|
131
|
+
try {
|
|
132
|
+
const { resolveExitReason } = require('./exit-reason');
|
|
133
|
+
const {
|
|
134
|
+
FATAL_MARKER_TAIL_BYTES,
|
|
135
|
+
readLogTail,
|
|
136
|
+
} = require('./isolation-log-utils');
|
|
137
|
+
const logTail = record.logPath
|
|
138
|
+
? readLogTail(record.logPath, FATAL_MARKER_TAIL_BYTES)
|
|
139
|
+
: null;
|
|
140
|
+
return resolveExitReason({
|
|
141
|
+
exitCode: record.exitCode,
|
|
142
|
+
logTail,
|
|
143
|
+
oomKilled: record.oomKilled,
|
|
144
|
+
});
|
|
145
|
+
} catch {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* @param {boolean|string|null|undefined} value - Raw shell-provided flag
|
|
152
|
+
* @returns {boolean|null} Parsed flag, or null when it carries no information
|
|
153
|
+
*/
|
|
154
|
+
function normalizeBoolean(value) {
|
|
155
|
+
if (typeof value === 'boolean') {
|
|
156
|
+
return value;
|
|
157
|
+
}
|
|
158
|
+
if (value === 'true') {
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
if (value === 'false') {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* @param {string|null|undefined} value - Raw `.State.Error`
|
|
169
|
+
* @returns {string|null} Error text, or null when docker reported none
|
|
170
|
+
*/
|
|
171
|
+
function normalizeContainerError(value) {
|
|
172
|
+
if (typeof value !== 'string') {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
const trimmed = value.trim();
|
|
176
|
+
if (trimmed === '' || trimmed === '<no value>' || trimmed === '(none)') {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
return trimmed;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Reconcile an in-memory record with one the detached watcher already
|
|
184
|
+
* finalized.
|
|
185
|
+
*
|
|
186
|
+
* A container that exits almost immediately can be finalized by the watcher
|
|
187
|
+
* before the foreground `start` invocation writes its own last update. Saving
|
|
188
|
+
* the stale in-memory copy would resurrect `status: executing` and throw the
|
|
189
|
+
* terminal facts away, so the stored record wins and only the fields the
|
|
190
|
+
* foreground process learned (its `options`, e.g. `containerId`) are merged in.
|
|
191
|
+
*
|
|
192
|
+
* @param {Object} store - An `ExecutionStore`
|
|
193
|
+
* @param {Object} record - The in-memory record about to be saved
|
|
194
|
+
* @returns {Object} The record to save
|
|
195
|
+
*/
|
|
196
|
+
function reconcileFinalizedRecord(store, record) {
|
|
197
|
+
try {
|
|
198
|
+
const stored = store.get(record.uuid);
|
|
199
|
+
if (!stored || stored.status !== 'executed' || !stored.endTime) {
|
|
200
|
+
return record;
|
|
201
|
+
}
|
|
202
|
+
stored.options = { ...(stored.options || {}), ...(record.options || {}) };
|
|
203
|
+
return stored;
|
|
204
|
+
} catch {
|
|
205
|
+
return record;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Shell fragment that hands the inspected docker facts to this module.
|
|
211
|
+
*
|
|
212
|
+
* Runs with the same runtime that started the session, inherits
|
|
213
|
+
* `START_APP_FOLDER`/`START_DISABLE_TRACKING` from the detached watcher, and is
|
|
214
|
+
* suffixed with `|| true` so a finalization failure can never abort the
|
|
215
|
+
* watcher's remaining cleanup work.
|
|
216
|
+
*
|
|
217
|
+
* @param {string} executionId - UUID of the record to finalize
|
|
218
|
+
* @returns {string} POSIX shell fragment
|
|
219
|
+
*/
|
|
220
|
+
function buildDetachedFinalizeSnippet(executionId) {
|
|
221
|
+
const v = SHELL_VARS;
|
|
222
|
+
return (
|
|
223
|
+
`${shellQuote(process.execPath)} ${shellQuote(__filename)} ` +
|
|
224
|
+
`${shellQuote(executionId)} "$${v.exit}" "$${v.oom}" ` +
|
|
225
|
+
`"$${v.started}" "$${v.finished}" "$${v.error}" ` +
|
|
226
|
+
`>/dev/null 2>&1 || true`
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Entry point used by the detached watcher:
|
|
232
|
+
* <runtime> detached-finalize.js <uuid> <exit> <oom> <started> <finished> <error>
|
|
233
|
+
* Always exits 0 — a bookkeeping failure must never turn into a visible error
|
|
234
|
+
* in a log the user is reading for the command's own output.
|
|
235
|
+
* @param {string[]} argv - Positional arguments
|
|
236
|
+
* @returns {void}
|
|
237
|
+
*/
|
|
238
|
+
function main(argv) {
|
|
239
|
+
const [
|
|
240
|
+
executionId,
|
|
241
|
+
exitCode,
|
|
242
|
+
oomKilled,
|
|
243
|
+
startedAt,
|
|
244
|
+
finishedAt,
|
|
245
|
+
containerError,
|
|
246
|
+
] = argv;
|
|
247
|
+
if (!executionId || process.env.START_DISABLE_TRACKING === 'true') {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
try {
|
|
251
|
+
const { ExecutionStore } = require('./execution-store');
|
|
252
|
+
const store = new ExecutionStore(
|
|
253
|
+
process.env.START_APP_FOLDER
|
|
254
|
+
? { appFolder: path.resolve(process.env.START_APP_FOLDER) }
|
|
255
|
+
: {}
|
|
256
|
+
);
|
|
257
|
+
finalizeDetachedExecution({
|
|
258
|
+
store,
|
|
259
|
+
executionId,
|
|
260
|
+
exitCode,
|
|
261
|
+
oomKilled,
|
|
262
|
+
startedAt,
|
|
263
|
+
finishedAt,
|
|
264
|
+
containerError,
|
|
265
|
+
});
|
|
266
|
+
} catch {
|
|
267
|
+
// Deliberately silent: see the exit-0 contract above.
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (require.main === module) {
|
|
272
|
+
main(process.argv.slice(2));
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
module.exports = {
|
|
276
|
+
END_TIME_SOURCE,
|
|
277
|
+
buildDetachedFinalizeSnippet,
|
|
278
|
+
finalizeDetachedExecution,
|
|
279
|
+
main,
|
|
280
|
+
reconcileFinalizedRecord,
|
|
281
|
+
};
|
|
@@ -7,6 +7,16 @@ const {
|
|
|
7
7
|
shellQuote,
|
|
8
8
|
} = require('./isolation-log-utils');
|
|
9
9
|
const { resolveMemoryExhaustion } = require('./exit-reason');
|
|
10
|
+
const {
|
|
11
|
+
buildDockerPostMortemSnippet,
|
|
12
|
+
buildDockerRemovalNoteSnippet,
|
|
13
|
+
buildDockerStateSnippet,
|
|
14
|
+
DOCKER_STATE_INSPECT_FORMAT,
|
|
15
|
+
formatContainerPostMortem,
|
|
16
|
+
formatContainerRemovalNote,
|
|
17
|
+
normalizeDockerTimestamp,
|
|
18
|
+
} = require('./docker-post-mortem');
|
|
19
|
+
const { buildDetachedFinalizeSnippet } = require('./detached-finalize');
|
|
10
20
|
|
|
11
21
|
const DOCKER_CONTAINER_CLEANUP_POLICY = {
|
|
12
22
|
DEFAULT: 'default',
|
|
@@ -91,27 +101,85 @@ function appendDockerContainerCleanupPolicyMessage(
|
|
|
91
101
|
return `${message}\nContainer will be removed after command completes.`;
|
|
92
102
|
}
|
|
93
103
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
104
|
+
/**
|
|
105
|
+
* Read a finished container's post-mortem facts in a single `docker inspect`.
|
|
106
|
+
*
|
|
107
|
+
* The attached path only ever inspected `State.OOMKilled`, which left its
|
|
108
|
+
* "Container kept for investigation" message unable to say *why* the container
|
|
109
|
+
* died — the same gap the detached watcher had (issue #171.1). Reading the
|
|
110
|
+
* facts once, before any `docker rm`, also makes them available to the removal
|
|
111
|
+
* path, where the container is about to stop existing (issue #171.3).
|
|
112
|
+
*
|
|
113
|
+
* @param {string} containerName - Container to inspect
|
|
114
|
+
* @returns {object|null} Facts accepted by the `docker-post-mortem` formatters,
|
|
115
|
+
* or null when the container could not be inspected at all
|
|
116
|
+
*/
|
|
117
|
+
function readDockerContainerState(containerName) {
|
|
118
|
+
const inspect = (format) => {
|
|
119
|
+
const result = spawnSync(
|
|
120
|
+
getDockerCommand(),
|
|
121
|
+
['inspect', '-f', format, containerName],
|
|
122
|
+
getDockerSpawnOptions({
|
|
123
|
+
encoding: 'utf8',
|
|
124
|
+
env: process.env,
|
|
125
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
126
|
+
})
|
|
127
|
+
);
|
|
128
|
+
if (result.error || result.status !== 0) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
return String(result.stdout || '').trim();
|
|
132
|
+
};
|
|
133
|
+
const state = inspect(DOCKER_STATE_INSPECT_FORMAT);
|
|
134
|
+
if (state === null) {
|
|
105
135
|
return null;
|
|
106
136
|
}
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
137
|
+
const [exitCode, oomKilled, startedAt, finishedAt] = state.split(/\s+/);
|
|
138
|
+
// `State.Error` is free-form text that may contain spaces, so it cannot ride
|
|
139
|
+
// along in the whitespace-separated template above.
|
|
140
|
+
const error = inspect('{{.State.Error}}');
|
|
141
|
+
const parsedExit = Number.parseInt(exitCode, 10);
|
|
142
|
+
return {
|
|
143
|
+
containerName,
|
|
144
|
+
exitCode: Number.isFinite(parsedExit) ? parsedExit : null,
|
|
145
|
+
oomKilled:
|
|
146
|
+
oomKilled === 'true' ? true : oomKilled === 'false' ? false : null,
|
|
147
|
+
startedAt: normalizeDockerTimestamp(startedAt),
|
|
148
|
+
finishedAt: normalizeDockerTimestamp(finishedAt),
|
|
149
|
+
error: error || null,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Write the post-mortem for an attached run into its log and return the same
|
|
155
|
+
* text for the console message.
|
|
156
|
+
*
|
|
157
|
+
* Kept and removed containers both get their facts recorded — a kept container
|
|
158
|
+
* gets the full block, a removed one the single line — so the log of an
|
|
159
|
+
* attached run carries exactly what the detached watcher writes (issues #171.2
|
|
160
|
+
* and #171.3).
|
|
161
|
+
*
|
|
162
|
+
* @param {object} params - Container name, inspected state, log path, and
|
|
163
|
+
* whether the container was removed
|
|
164
|
+
* @returns {string} Message lines to append, or '' when nothing is known
|
|
165
|
+
*/
|
|
166
|
+
function recordAttachedDockerPostMortem({
|
|
167
|
+
containerName,
|
|
168
|
+
state,
|
|
169
|
+
logPath = null,
|
|
170
|
+
removed = false,
|
|
171
|
+
}) {
|
|
172
|
+
if (!state) {
|
|
173
|
+
return '';
|
|
110
174
|
}
|
|
111
|
-
|
|
112
|
-
|
|
175
|
+
const facts = { ...state, containerName };
|
|
176
|
+
const text = removed
|
|
177
|
+
? formatContainerRemovalNote(facts)
|
|
178
|
+
: formatContainerPostMortem(facts);
|
|
179
|
+
if (logPath) {
|
|
180
|
+
appendLogFile(logPath, `\n${text}`);
|
|
113
181
|
}
|
|
114
|
-
return
|
|
182
|
+
return `\n${text.trimEnd()}`;
|
|
115
183
|
}
|
|
116
184
|
|
|
117
185
|
function readDockerContainerStatus(containerName) {
|
|
@@ -231,37 +299,68 @@ function buildSuccessfulNonOomCondition() {
|
|
|
231
299
|
);
|
|
232
300
|
}
|
|
233
301
|
|
|
234
|
-
|
|
302
|
+
/**
|
|
303
|
+
* Build the POSIX shell run by the detached completion watcher.
|
|
304
|
+
*
|
|
305
|
+
* The watcher is the only observer left once `start --detached` returns, so it
|
|
306
|
+
* carries three responsibilities that used to be missing or deferred:
|
|
307
|
+
* 1. it reads the container's post-mortem facts out of a single
|
|
308
|
+
* `docker inspect` (issue #171.1);
|
|
309
|
+
* 2. it writes those facts into the log on every path — kept *and* removed —
|
|
310
|
+
* so an operator reading the log never has to reconstruct them from a
|
|
311
|
+
* container that no longer exists (issues #171.2, #171.3);
|
|
312
|
+
* 3. it hands the same facts to the finalizer so the execution record stops
|
|
313
|
+
* being `executing` forever (issue #170.1).
|
|
314
|
+
*
|
|
315
|
+
* @param {string} containerName - Docker container name
|
|
316
|
+
* @param {string} policy - One of DOCKER_CONTAINER_CLEANUP_POLICY
|
|
317
|
+
* @param {string|null} logPath - Log file to append to, or null
|
|
318
|
+
* @param {string|null} [executionId] - Execution UUID to finalize, or null
|
|
319
|
+
* @returns {string} The shell script
|
|
320
|
+
*/
|
|
321
|
+
function buildDetachedDockerCompletionScript(
|
|
322
|
+
containerName,
|
|
323
|
+
policy,
|
|
324
|
+
logPath,
|
|
325
|
+
executionId = null
|
|
326
|
+
) {
|
|
235
327
|
const quotedName = shellQuote(containerName);
|
|
236
328
|
const parts = [];
|
|
237
329
|
|
|
238
330
|
if (logPath) {
|
|
239
331
|
const quotedLogPath = shellQuote(logPath);
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
332
|
+
const removalNote = buildDockerRemovalNoteSnippet(
|
|
333
|
+
containerName,
|
|
334
|
+
quotedLogPath
|
|
335
|
+
);
|
|
336
|
+
const postMortem = buildDockerPostMortemSnippet(
|
|
337
|
+
containerName,
|
|
338
|
+
quotedLogPath
|
|
243
339
|
);
|
|
244
|
-
|
|
245
|
-
|
|
340
|
+
const remove = `docker rm -f ${quotedName} >> ${quotedLogPath} 2>&1 || true; ${removalNote}`;
|
|
341
|
+
const keep = `${postMortem}; ${buildDockerKeptLogSnippet(containerName, quotedLogPath)}`;
|
|
342
|
+
|
|
343
|
+
parts.push(`docker logs -f ${quotedName} >> ${quotedLogPath} 2>&1`);
|
|
344
|
+
parts.push(buildDockerStateSnippet(containerName));
|
|
246
345
|
if (policy === DOCKER_CONTAINER_CLEANUP_POLICY.ALWAYS) {
|
|
247
|
-
parts.push(
|
|
346
|
+
parts.push(remove);
|
|
248
347
|
} else if (
|
|
249
348
|
policy === DOCKER_CONTAINER_CLEANUP_POLICY.DEFAULT ||
|
|
250
349
|
policy === DOCKER_CONTAINER_CLEANUP_POLICY.KEEP_ON_FAIL
|
|
251
350
|
) {
|
|
252
|
-
const successCondition = buildSuccessfulNonOomCondition();
|
|
253
351
|
parts.push(
|
|
254
|
-
`if ${
|
|
352
|
+
`if ${buildSuccessfulNonOomCondition()}; then ${remove}; else ${keep}; fi`
|
|
255
353
|
);
|
|
354
|
+
} else {
|
|
355
|
+
// KEEP: the container is never removed, so the log used to end with the
|
|
356
|
+
// raw command output and nothing else. The post-mortem is exactly the
|
|
357
|
+
// information an operator needs before deciding what to do with it.
|
|
358
|
+
parts.push(keep);
|
|
256
359
|
}
|
|
257
360
|
parts.push(`${createShellLogFooterSnippet()} >> ${quotedLogPath}`);
|
|
258
361
|
} else {
|
|
259
362
|
parts.push(`docker wait ${quotedName} >/dev/null 2>&1`);
|
|
260
|
-
parts.push(
|
|
261
|
-
`__start_command_state=$(docker inspect -f '{{.State.ExitCode}} {{.State.OOMKilled}}' ${quotedName} 2>/dev/null || printf '%s' '-1 false')`
|
|
262
|
-
);
|
|
263
|
-
parts.push('__start_command_exit=${__start_command_state%% *}');
|
|
264
|
-
parts.push('__start_command_oom=${__start_command_state##* }');
|
|
363
|
+
parts.push(buildDockerStateSnippet(containerName));
|
|
265
364
|
if (policy === DOCKER_CONTAINER_CLEANUP_POLICY.ALWAYS) {
|
|
266
365
|
parts.push(`docker rm -f ${quotedName} >/dev/null 2>&1 || true`);
|
|
267
366
|
} else if (
|
|
@@ -274,13 +373,39 @@ function buildDetachedDockerCompletionScript(containerName, policy, logPath) {
|
|
|
274
373
|
}
|
|
275
374
|
}
|
|
276
375
|
|
|
376
|
+
if (executionId) {
|
|
377
|
+
// Last, so the record is only marked terminal once the log is complete.
|
|
378
|
+
parts.push(buildDetachedFinalizeSnippet(executionId));
|
|
379
|
+
}
|
|
380
|
+
|
|
277
381
|
return parts.join('; ');
|
|
278
382
|
}
|
|
279
383
|
|
|
280
|
-
|
|
384
|
+
/**
|
|
385
|
+
* Start the detached completion watcher.
|
|
386
|
+
* @param {string} containerName - Docker container name
|
|
387
|
+
* @param {string} policy - One of DOCKER_CONTAINER_CLEANUP_POLICY
|
|
388
|
+
* @param {string|null} logPath - Log file to append to, or null
|
|
389
|
+
* @param {string|null} [executionId] - Execution UUID to finalize, or null
|
|
390
|
+
* @returns {void}
|
|
391
|
+
*/
|
|
392
|
+
function startDetachedDockerCompletionWatcher(
|
|
393
|
+
containerName,
|
|
394
|
+
policy,
|
|
395
|
+
logPath,
|
|
396
|
+
executionId = null
|
|
397
|
+
) {
|
|
281
398
|
const watcher = spawn(
|
|
282
399
|
'sh',
|
|
283
|
-
[
|
|
400
|
+
[
|
|
401
|
+
'-c',
|
|
402
|
+
buildDetachedDockerCompletionScript(
|
|
403
|
+
containerName,
|
|
404
|
+
policy,
|
|
405
|
+
logPath,
|
|
406
|
+
executionId
|
|
407
|
+
),
|
|
408
|
+
],
|
|
284
409
|
{
|
|
285
410
|
detached: true,
|
|
286
411
|
stdio: 'ignore',
|
|
@@ -327,8 +452,9 @@ module.exports = {
|
|
|
327
452
|
shouldCleanupDockerContainer,
|
|
328
453
|
getDockerContainerCleanupInstructions,
|
|
329
454
|
appendDockerContainerCleanupPolicyMessage,
|
|
330
|
-
|
|
455
|
+
readDockerContainerState,
|
|
331
456
|
readDockerContainerStatus,
|
|
457
|
+
recordAttachedDockerPostMortem,
|
|
332
458
|
removeDockerContainer,
|
|
333
459
|
buildDetachedDockerCompletionScript,
|
|
334
460
|
startDetachedDockerCompletionWatcher,
|