start-command 0.37.9 → 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 +6 -0
- package/README.md +3 -0
- package/package.json +1 -1
- package/src/bin/cli.js +62 -26
- package/src/lib/args-parser-queries.js +45 -1
- package/src/lib/args-parser.js +10 -0
- package/src/lib/docker-command-handoff.js +34 -6
- package/src/lib/docker-runtime-args.js +7 -0
- package/src/lib/execution-attempt.js +19 -9
- package/src/lib/execution-chain.js +182 -0
- package/src/lib/execution-env.js +120 -0
- package/src/lib/execution-identity.js +180 -0
- package/src/lib/execution-recovery.js +17 -0
- package/src/lib/execution-resume.js +17 -0
- package/src/lib/execution-store.js +11 -22
- 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/query-commands.js +18 -3
- package/src/lib/screen-isolation.js +11 -2
- package/src/lib/spawn-helpers.js +33 -16
- package/src/lib/status-formatter.js +5 -1
- package/src/lib/usage.js +8 -1
- package/test/args-parser-attach-resume.js +1 -1
- package/test/args-parser-control.js +2 -2
- 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 +15 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* START_* variables that tell a launched command which execution it belongs
|
|
3
|
+
* to (issue #230). Every backend exports the same three variables, next to
|
|
4
|
+
* the kill-recovery `START_COMMAND_RECOVERY_ATTEMPT`:
|
|
5
|
+
*
|
|
6
|
+
* - START_EXECUTION_ID: the execution UUID (`--session-id`)
|
|
7
|
+
* - START_SESSION_NAME: the isolation session/container name (`--session`);
|
|
8
|
+
* empty for direct runs, which have no session
|
|
9
|
+
* - START_ATTEMPT: 1 for the first run, n for the n-th attempt (`--resume`,
|
|
10
|
+
* kill recovery or `--append-attempt`), the same number `--status` shows
|
|
11
|
+
*/
|
|
12
|
+
const { shellQuote } = require('./isolation-log-utils');
|
|
13
|
+
|
|
14
|
+
const EXECUTION_ID_ENV = 'START_EXECUTION_ID';
|
|
15
|
+
const SESSION_NAME_ENV = 'START_SESSION_NAME';
|
|
16
|
+
const ATTEMPT_ENV = 'START_ATTEMPT';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Build the START_* variables for one attempt.
|
|
20
|
+
* @param {object} params - {executionId, sessionName, attempt}
|
|
21
|
+
* @returns {object} Variables in a stable order, or {} without an execution id
|
|
22
|
+
*/
|
|
23
|
+
function buildExecutionEnv({ executionId, sessionName, attempt } = {}) {
|
|
24
|
+
if (!executionId) {
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
const number = Number(attempt);
|
|
28
|
+
return {
|
|
29
|
+
[EXECUTION_ID_ENV]: String(executionId),
|
|
30
|
+
[SESSION_NAME_ENV]: sessionName ? String(sessionName) : '',
|
|
31
|
+
[ATTEMPT_ENV]: String(
|
|
32
|
+
Number.isSafeInteger(number) && number > 0 ? number : 1
|
|
33
|
+
),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Build the variables from isolation options (`runIsolated`, `docker run`).
|
|
39
|
+
* @param {object} options - {sessionId|executionId, containerName|session, attemptNumber}
|
|
40
|
+
* @returns {object} Variables, or {}
|
|
41
|
+
*/
|
|
42
|
+
function executionEnvFromOptions(options = {}) {
|
|
43
|
+
return buildExecutionEnv({
|
|
44
|
+
executionId: options.sessionId || options.executionId,
|
|
45
|
+
sessionName: options.containerName || options.session,
|
|
46
|
+
attempt: options.attemptNumber,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* `KEY=value` assignments, e.g. for `docker run -e`.
|
|
52
|
+
* @param {object} env - Variables
|
|
53
|
+
* @returns {string[]} Assignments
|
|
54
|
+
*/
|
|
55
|
+
function executionEnvAssignments(env = {}) {
|
|
56
|
+
return Object.entries(env).map(([key, value]) => `${key}=${value}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* POSIX `export` statement for the variables, without a trailing separator.
|
|
61
|
+
* @param {object} env - Variables
|
|
62
|
+
* @returns {string} Statement, or '' when there is nothing to export
|
|
63
|
+
*/
|
|
64
|
+
function buildExecutionEnvExport(env = {}) {
|
|
65
|
+
const entries = Object.entries(env);
|
|
66
|
+
if (!entries.length) {
|
|
67
|
+
return '';
|
|
68
|
+
}
|
|
69
|
+
return `export ${entries
|
|
70
|
+
.map(([key, value]) => `${key}=${shellQuote(value)}`)
|
|
71
|
+
.join(' ')}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Prefix a shell command with the export statement. Used by the backends
|
|
76
|
+
* whose command runs through a shell that does not inherit our environment:
|
|
77
|
+
* a running tmux server, ssh, and `sudo` (which resets the environment).
|
|
78
|
+
* @param {string} command - Shell command
|
|
79
|
+
* @param {object} env - Variables
|
|
80
|
+
* @returns {string} Prefixed command
|
|
81
|
+
*/
|
|
82
|
+
function prefixWithExecutionEnv(command, env = {}) {
|
|
83
|
+
const statement = buildExecutionEnvExport(env);
|
|
84
|
+
return statement ? `${statement}; ${command}` : command;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Child-process environment with the variables applied.
|
|
89
|
+
* @param {object} env - Variables
|
|
90
|
+
* @param {object} [base] - Base environment (defaults to process.env)
|
|
91
|
+
* @returns {object} Environment
|
|
92
|
+
*/
|
|
93
|
+
function withExecutionEnv(env = {}, base = process.env) {
|
|
94
|
+
return { ...base, ...env };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* True when this process was itself started by execution `executionId`, i.e.
|
|
99
|
+
* it is the next level of a stacked isolation that passes `--session-id`
|
|
100
|
+
* down (see command-builder.js).
|
|
101
|
+
* @param {string} executionId - Execution UUID
|
|
102
|
+
* @param {object} [env] - Environment to inspect
|
|
103
|
+
* @returns {boolean}
|
|
104
|
+
*/
|
|
105
|
+
function isNestedLaunchOf(executionId, env = process.env) {
|
|
106
|
+
return Boolean(executionId) && env[EXECUTION_ID_ENV] === executionId;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = {
|
|
110
|
+
EXECUTION_ID_ENV,
|
|
111
|
+
SESSION_NAME_ENV,
|
|
112
|
+
ATTEMPT_ENV,
|
|
113
|
+
buildExecutionEnv,
|
|
114
|
+
executionEnvFromOptions,
|
|
115
|
+
executionEnvAssignments,
|
|
116
|
+
buildExecutionEnvExport,
|
|
117
|
+
prefixWithExecutionEnv,
|
|
118
|
+
withExecutionEnv,
|
|
119
|
+
isNestedLaunchOf,
|
|
120
|
+
};
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Names an execution answers to (issue #231).
|
|
3
|
+
*
|
|
4
|
+
* An execution is found by its UUID, its backend session name, the session
|
|
5
|
+
* names it used before a resume (`options.sessionNameHistory`, #162), its
|
|
6
|
+
* display name (`options.name`, set by `--rename`) and its aliases
|
|
7
|
+
* (`options.aliases`, added by `--alias` and `--rename`). Aliases are kept
|
|
8
|
+
* apart from `sessionNameHistory` because that list names real backend
|
|
9
|
+
* sessions: `--cleanup` removes those containers and `--resume` derives the
|
|
10
|
+
* root session from it.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
function hasName(record, identifier) {
|
|
14
|
+
const options = record.options || {};
|
|
15
|
+
return (
|
|
16
|
+
options.name === identifier ||
|
|
17
|
+
(Array.isArray(options.aliases) && options.aliases.includes(identifier))
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Resolve an identifier among stored records: UUID, then current session
|
|
23
|
+
* name, then earlier session names, then display name and aliases.
|
|
24
|
+
* @param {object[]} records - Stored records
|
|
25
|
+
* @param {string} identifier - Any name of the execution
|
|
26
|
+
* @returns {object|null} The record, or null
|
|
27
|
+
*/
|
|
28
|
+
function findRecord(records, identifier) {
|
|
29
|
+
if (identifier === null || identifier === undefined || identifier === '') {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
return (
|
|
33
|
+
records.find((r) => r.uuid === identifier) ||
|
|
34
|
+
records.find((r) => r.options && r.options.sessionName === identifier) ||
|
|
35
|
+
records.find(
|
|
36
|
+
(r) =>
|
|
37
|
+
r.options &&
|
|
38
|
+
Array.isArray(r.options.sessionNameHistory) &&
|
|
39
|
+
r.options.sessionNameHistory.includes(identifier)
|
|
40
|
+
) ||
|
|
41
|
+
records.find((r) => hasName(r, identifier)) ||
|
|
42
|
+
null
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Every name the record answers to, UUID first, without duplicates.
|
|
48
|
+
* @param {object} record - Execution record
|
|
49
|
+
* @returns {string[]} Names
|
|
50
|
+
*/
|
|
51
|
+
function executionAliases(record) {
|
|
52
|
+
const options = record.options || {};
|
|
53
|
+
const names = [
|
|
54
|
+
record.uuid,
|
|
55
|
+
...(options.sessionNameHistory || []),
|
|
56
|
+
options.sessionName,
|
|
57
|
+
...(options.aliases || []),
|
|
58
|
+
options.name,
|
|
59
|
+
].filter((name) => typeof name === 'string' && name);
|
|
60
|
+
return [...new Set(names)];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** True once the record has a caller-given name (`--alias`/`--rename`). */
|
|
64
|
+
function hasUserNames(record) {
|
|
65
|
+
const options = record.options || {};
|
|
66
|
+
return Boolean(options.name) || (options.aliases || []).length > 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Run `mutate(records)` under the store's database lock and persist the
|
|
71
|
+
* records it returns as changed.
|
|
72
|
+
* @param {object} store - ExecutionStore
|
|
73
|
+
* @param {Function} mutate - (records) => changed record, or null for none
|
|
74
|
+
* @returns {*} What `mutate` returned
|
|
75
|
+
*/
|
|
76
|
+
function withStoreLock(store, mutate) {
|
|
77
|
+
const { LockManager } = require('./execution-store');
|
|
78
|
+
const lock = new LockManager(store.lockFilePath);
|
|
79
|
+
if (!lock.acquire()) {
|
|
80
|
+
throw new Error('Failed to acquire lock for database write');
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
const records = store.readLinoRecords();
|
|
84
|
+
const changed = mutate(records);
|
|
85
|
+
if (changed) {
|
|
86
|
+
store.writeLinoRecords(records);
|
|
87
|
+
if (store.useLinks) {
|
|
88
|
+
store.writeLinksRecord(changed);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return changed;
|
|
92
|
+
} finally {
|
|
93
|
+
lock.release();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function validateName(name) {
|
|
98
|
+
if (typeof name !== 'string' || !name.trim()) {
|
|
99
|
+
throw new Error('The new name must not be empty');
|
|
100
|
+
}
|
|
101
|
+
if (name.startsWith('-')) {
|
|
102
|
+
throw new Error(`Invalid name "${name}": names cannot start with "-"`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Add a name to an execution (`$ --alias <id> <name>`) or rename it
|
|
108
|
+
* (`$ --rename <id> <name>`, which keeps the previous display name as an
|
|
109
|
+
* alias). The backend session/container keeps its name; only lookup and the
|
|
110
|
+
* displayed name change.
|
|
111
|
+
* @param {object} store - ExecutionStore
|
|
112
|
+
* @param {string} identifier - Any current name of the execution
|
|
113
|
+
* @param {string} name - Name to add
|
|
114
|
+
* @param {object} [options] - {rename: boolean}
|
|
115
|
+
* @returns {{success: boolean, output?: string, error?: string}}
|
|
116
|
+
*/
|
|
117
|
+
function nameExecution(store, identifier, name, options = {}) {
|
|
118
|
+
if (!store) {
|
|
119
|
+
return { success: false, error: 'Execution tracking is disabled.' };
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
validateName(name);
|
|
123
|
+
let unchanged = false;
|
|
124
|
+
const record = withStoreLock(store, (records) => {
|
|
125
|
+
const target = findRecord(records, identifier);
|
|
126
|
+
if (!target) {
|
|
127
|
+
throw new Error(
|
|
128
|
+
`No execution found with UUID or session name: ${identifier}`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
const owner = findRecord(records, name);
|
|
132
|
+
if (owner && owner.uuid !== target.uuid) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
`Name "${name}" already refers to execution ${owner.uuid}`
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
const opts = (target.options = target.options || {});
|
|
138
|
+
const aliases = [...(opts.aliases || [])];
|
|
139
|
+
if (options.rename) {
|
|
140
|
+
const previous = opts.name || opts.sessionName || target.uuid;
|
|
141
|
+
if (previous === name) {
|
|
142
|
+
unchanged = true;
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
// A session name or UUID still resolves on its own; an earlier
|
|
146
|
+
// display name has to become an alias to keep resolving.
|
|
147
|
+
if (opts.name) {
|
|
148
|
+
aliases.push(previous);
|
|
149
|
+
}
|
|
150
|
+
opts.name = name;
|
|
151
|
+
} else if (owner) {
|
|
152
|
+
unchanged = true;
|
|
153
|
+
return null;
|
|
154
|
+
} else {
|
|
155
|
+
aliases.push(name);
|
|
156
|
+
}
|
|
157
|
+
opts.aliases = [...new Set(aliases)].filter((a) => a !== opts.name);
|
|
158
|
+
if (opts.aliases.length === 0) {
|
|
159
|
+
delete opts.aliases;
|
|
160
|
+
}
|
|
161
|
+
return target;
|
|
162
|
+
});
|
|
163
|
+
const current = record || store.get(identifier);
|
|
164
|
+
const action = options.rename ? 'Renamed' : 'Aliased';
|
|
165
|
+
return {
|
|
166
|
+
success: true,
|
|
167
|
+
output: `${unchanged ? 'Unchanged' : action} execution ${current.uuid}\naliases: [${executionAliases(current).join(', ')}]`,
|
|
168
|
+
};
|
|
169
|
+
} catch (error) {
|
|
170
|
+
return { success: false, error: error.message };
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
module.exports = {
|
|
175
|
+
executionAliases,
|
|
176
|
+
findRecord,
|
|
177
|
+
hasUserNames,
|
|
178
|
+
nameExecution,
|
|
179
|
+
withStoreLock,
|
|
180
|
+
};
|
|
@@ -379,6 +379,23 @@ function recoverKilledExecution(params = {}) {
|
|
|
379
379
|
}
|
|
380
380
|
}
|
|
381
381
|
|
|
382
|
+
if (opts.commandHandoff && record.uuid) {
|
|
383
|
+
// `docker start` keeps the creation env; refresh START_ATTEMPT (#230).
|
|
384
|
+
try {
|
|
385
|
+
require('./docker-command-handoff').writeExecutionEnvHandoff(
|
|
386
|
+
containerName,
|
|
387
|
+
require('./execution-env').buildExecutionEnv({
|
|
388
|
+
executionId: record.uuid,
|
|
389
|
+
sessionName: containerName,
|
|
390
|
+
attempt: require('./execution-attempt').nextAttemptNumber(record),
|
|
391
|
+
}),
|
|
392
|
+
runner
|
|
393
|
+
);
|
|
394
|
+
} catch {
|
|
395
|
+
// Non-fatal: the command still runs, with the previous START_ATTEMPT.
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
382
399
|
const since = now().toISOString();
|
|
383
400
|
const {
|
|
384
401
|
appendLifecycle,
|
|
@@ -34,6 +34,7 @@ const {
|
|
|
34
34
|
appendLifecycle,
|
|
35
35
|
archiveAttempt,
|
|
36
36
|
createAttempt,
|
|
37
|
+
nextAttemptNumber,
|
|
37
38
|
patchAttempt,
|
|
38
39
|
} = require('./execution-attempt');
|
|
39
40
|
const { runCommand } = require('./execution-control');
|
|
@@ -46,6 +47,7 @@ const { SessionState, probeSession } = require('./session-probe');
|
|
|
46
47
|
const { getDockerNetworks } = require('./docker-network-lifecycle');
|
|
47
48
|
const {
|
|
48
49
|
writeCommandHandoff,
|
|
50
|
+
writeExecutionEnvHandoff,
|
|
49
51
|
buildCommandHandoffArgs,
|
|
50
52
|
commandHandoffPath,
|
|
51
53
|
} = require('./docker-command-handoff');
|
|
@@ -110,6 +112,8 @@ function buildLaunchOptions(record) {
|
|
|
110
112
|
rootSession:
|
|
111
113
|
opts.rootSession || opts.sessionNameHistory?.[0] || opts.sessionName,
|
|
112
114
|
resumeCount: (Number(opts.resumeCount) || 0) + 1,
|
|
115
|
+
// START_ATTEMPT of the relaunched command (issue #230).
|
|
116
|
+
attemptNumber: nextAttemptNumber(record),
|
|
113
117
|
image: opts.image || null,
|
|
114
118
|
session: opts.sessionName,
|
|
115
119
|
detached: true,
|
|
@@ -289,6 +293,7 @@ function buildResumePlan(
|
|
|
289
293
|
session: newSessionName,
|
|
290
294
|
containerName: newSessionName,
|
|
291
295
|
sessionId: record.uuid,
|
|
296
|
+
attemptNumber: nextAttemptNumber(record),
|
|
292
297
|
rootSession:
|
|
293
298
|
opts.rootSession || opts.sessionNameHistory?.[0] || sessionName,
|
|
294
299
|
resumeCount: attempt,
|
|
@@ -657,6 +662,18 @@ async function resumeExecution(store, identifier, deps = {}) {
|
|
|
657
662
|
);
|
|
658
663
|
}
|
|
659
664
|
}
|
|
665
|
+
if (plan.mode === ResumeMode.DOCKER_START && opts.commandHandoff) {
|
|
666
|
+
// `docker start` keeps the creation env; refresh START_ATTEMPT (#230).
|
|
667
|
+
writeExecutionEnvHandoff(
|
|
668
|
+
plan.sessionName,
|
|
669
|
+
require('./execution-env').buildExecutionEnv({
|
|
670
|
+
executionId: record.uuid,
|
|
671
|
+
sessionName: plan.sessionName,
|
|
672
|
+
attempt: attempt.number,
|
|
673
|
+
}),
|
|
674
|
+
runner
|
|
675
|
+
);
|
|
676
|
+
}
|
|
660
677
|
for (const step of plan.steps) {
|
|
661
678
|
let result;
|
|
662
679
|
try {
|
|
@@ -77,6 +77,8 @@ class ExecutionRecord {
|
|
|
77
77
|
this.endTime = options.endTime || null;
|
|
78
78
|
this.attempt = options.attempt;
|
|
79
79
|
this.attemptHistory = options.attemptHistory;
|
|
80
|
+
// UUID of the execution this one continues (`--continues`, issue #231).
|
|
81
|
+
this.continues = options.continues || undefined;
|
|
80
82
|
// Provenance of `endTime` (issue #170.2). `--status` used to fabricate a
|
|
81
83
|
// finish time with `new Date()` whenever the record had none, which made an
|
|
82
84
|
// observation indistinguishable from a real finish time. One of
|
|
@@ -172,6 +174,9 @@ class ExecutionRecord {
|
|
|
172
174
|
if (this.attemptHistory) {
|
|
173
175
|
obj.attemptHistory = this.attemptHistory;
|
|
174
176
|
}
|
|
177
|
+
if (this.continues) {
|
|
178
|
+
obj.continues = this.continues;
|
|
179
|
+
}
|
|
175
180
|
if (this.endTimeSource !== undefined && this.endTimeSource !== null) {
|
|
176
181
|
obj.endTimeSource = this.endTimeSource;
|
|
177
182
|
}
|
|
@@ -544,33 +549,17 @@ class ExecutionStore {
|
|
|
544
549
|
/**
|
|
545
550
|
* Get an execution record by UUID or session name
|
|
546
551
|
* First tries exact UUID match, then the current session name, then any
|
|
547
|
-
* session name the execution used before it was resumed
|
|
552
|
+
* session name the execution used before it was resumed, then the display
|
|
553
|
+
* name and aliases
|
|
548
554
|
* @param {string} identifier - UUID or session name
|
|
549
555
|
* @returns {ExecutionRecord|null}
|
|
550
556
|
*/
|
|
551
557
|
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)
|
|
558
|
+
// Names, display names and aliases resolve too (issues #162, #231).
|
|
559
|
+
return require('./execution-identity').findRecord(
|
|
560
|
+
this.readLinoRecords(),
|
|
561
|
+
identifier
|
|
572
562
|
);
|
|
573
|
-
return byPreviousSessionName || null;
|
|
574
563
|
}
|
|
575
564
|
|
|
576
565
|
/**
|
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
|
+
};
|