start-command 0.25.1 → 0.25.3
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 +14 -0
- package/package.json +1 -1
- package/src/bin/cli.js +29 -34
- package/src/lib/args-parser.js +2 -2
- package/src/lib/execution-store.js +14 -5
- package/src/lib/isolation-log-utils.js +80 -3
- package/src/lib/isolation.js +80 -50
- package/src/lib/screen-isolation.js +121 -7
- package/src/lib/spawn-helpers.js +32 -9
- package/src/lib/status-formatter.js +125 -4
- package/test/isolation-log-utils.test.js +17 -3
- package/test/screen-integration.test.js +98 -1
- package/test/session-name-status.test.js +311 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# start-command
|
|
2
2
|
|
|
3
|
+
## 0.25.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 873781e: Record detached isolation output in the tracked log path in real time.
|
|
8
|
+
|
|
9
|
+
## 0.25.2
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 8f58d2b: fix: support --session name lookups in --status and track detached session lifecycle
|
|
14
|
+
|
|
15
|
+
`--status` now accepts session names in addition to UUIDs. Detached mode no longer incorrectly reports immediate completion — status is determined at query time by checking if the actual session is still running.
|
|
16
|
+
|
|
3
17
|
## 0.25.1
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
package/package.json
CHANGED
package/src/bin/cli.js
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
const process = require('process');
|
|
4
4
|
const os = require('os');
|
|
5
|
-
const fs = require('fs');
|
|
6
5
|
const path = require('path');
|
|
7
6
|
|
|
8
7
|
// Import modules
|
|
@@ -20,6 +19,7 @@ const {
|
|
|
20
19
|
createLogHeader,
|
|
21
20
|
createLogFooter,
|
|
22
21
|
writeLogFile,
|
|
22
|
+
appendLogFile,
|
|
23
23
|
createLogPath,
|
|
24
24
|
getDefaultDockerImage,
|
|
25
25
|
} = require('../lib/isolation');
|
|
@@ -333,7 +333,7 @@ Options:
|
|
|
333
333
|
--auto-remove-docker-container Auto-remove docker container after exit
|
|
334
334
|
--shell <shell> Shell to use in isolation environments: auto, bash, zsh, sh (default: auto)
|
|
335
335
|
--use-command-stream Use command-stream library for execution (experimental)
|
|
336
|
-
--status <
|
|
336
|
+
--status <id> Show status of execution by UUID or session name (--output-format: links-notation|json|text)
|
|
337
337
|
--cleanup Clean up stale "executing" records (crashed/killed processes)
|
|
338
338
|
--cleanup-dry-run Show stale records that would be cleaned up (without cleaning)
|
|
339
339
|
--version, -v Show version information
|
|
@@ -405,7 +405,7 @@ async function runWithIsolation(
|
|
|
405
405
|
}
|
|
406
406
|
|
|
407
407
|
// Create log file path
|
|
408
|
-
const logFilePath = createLogPath(environment || 'direct');
|
|
408
|
+
const logFilePath = createLogPath(environment || 'direct', sessionId);
|
|
409
409
|
|
|
410
410
|
// Get session name (will be generated by runIsolated if not provided)
|
|
411
411
|
const sessionName =
|
|
@@ -468,12 +468,9 @@ async function runWithIsolation(
|
|
|
468
468
|
}
|
|
469
469
|
}
|
|
470
470
|
|
|
471
|
-
// Add isolation info to extra lines
|
|
471
|
+
// Add isolation info to extra lines (session name for reconnecting, see issue #67)
|
|
472
472
|
if (environment) {
|
|
473
473
|
extraLines.push(`[Isolation] Environment: ${environment}, Mode: ${mode}`);
|
|
474
|
-
// Always add the session name so users can reconnect to detached sessions
|
|
475
|
-
// This is important for screen, tmux, docker where the session/container name
|
|
476
|
-
// is different from the session UUID used for tracking (see issue #67)
|
|
477
474
|
extraLines.push(`[Isolation] Session: ${sessionName}`);
|
|
478
475
|
}
|
|
479
476
|
if (effectiveImage) {
|
|
@@ -562,6 +559,8 @@ async function runWithIsolation(
|
|
|
562
559
|
);
|
|
563
560
|
}
|
|
564
561
|
|
|
562
|
+
writeLogFile(logFilePath, logContent);
|
|
563
|
+
|
|
565
564
|
let result;
|
|
566
565
|
|
|
567
566
|
if (environment) {
|
|
@@ -569,7 +568,7 @@ async function runWithIsolation(
|
|
|
569
568
|
// Note: Isolation environments currently use native spawn/execSync
|
|
570
569
|
// Future: Add command-stream support with raw() function for multiplexers
|
|
571
570
|
result = await runIsolated(environment, cmd, {
|
|
572
|
-
session:
|
|
571
|
+
session: sessionName,
|
|
573
572
|
image: effectiveImage,
|
|
574
573
|
endpoint: options.endpoint,
|
|
575
574
|
detached: mode === 'detached',
|
|
@@ -577,6 +576,7 @@ async function runWithIsolation(
|
|
|
577
576
|
keepAlive: options.keepAlive,
|
|
578
577
|
autoRemoveDockerContainer: options.autoRemoveDockerContainer,
|
|
579
578
|
shell: options.shell,
|
|
579
|
+
logPath: logFilePath,
|
|
580
580
|
});
|
|
581
581
|
} else if (createdUser) {
|
|
582
582
|
// Run directly as the created user (no isolation environment)
|
|
@@ -595,16 +595,19 @@ async function runWithIsolation(
|
|
|
595
595
|
if (!result.success && result.message) {
|
|
596
596
|
console.error(`Error: ${result.message}`);
|
|
597
597
|
}
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
598
|
+
if (mode === 'detached' && result.success) {
|
|
599
|
+
appendLogFile(logFilePath, `${result.message}\n`);
|
|
600
|
+
} else {
|
|
601
|
+
// Add result to log content
|
|
602
|
+
appendLogFile(logFilePath, `${result.message}\n`);
|
|
603
|
+
appendLogFile(logFilePath, createLogFooter(endTime, exitCode));
|
|
604
|
+
}
|
|
604
605
|
|
|
605
|
-
// Update execution record
|
|
606
|
+
// Update execution record: detached keeps "executing" (resolved at query time)
|
|
606
607
|
if (executionRecord && store) {
|
|
607
|
-
|
|
608
|
+
if (mode !== 'detached') {
|
|
609
|
+
executionRecord.complete(exitCode);
|
|
610
|
+
}
|
|
608
611
|
try {
|
|
609
612
|
store.save(executionRecord);
|
|
610
613
|
} catch (err) {
|
|
@@ -614,7 +617,6 @@ async function runWithIsolation(
|
|
|
614
617
|
);
|
|
615
618
|
}
|
|
616
619
|
}
|
|
617
|
-
// Clear global reference since we've completed normally
|
|
618
620
|
currentExecutionRecord = null;
|
|
619
621
|
}
|
|
620
622
|
|
|
@@ -676,9 +678,7 @@ async function runDirect(cmd, sessionId) {
|
|
|
676
678
|
const shellArgs = isWindows ? ['-Command', cmd] : ['-c', cmd];
|
|
677
679
|
|
|
678
680
|
// Setup logging
|
|
679
|
-
const
|
|
680
|
-
const logFilename = generateLogFilename();
|
|
681
|
-
const logFilePath = path.join(logDir, logFilename);
|
|
681
|
+
const logFilePath = createLogPath('direct', sessionId);
|
|
682
682
|
|
|
683
683
|
let logContent = '';
|
|
684
684
|
const startTime = getTimestamp();
|
|
@@ -827,9 +827,7 @@ async function runDirectWithCommandStream(
|
|
|
827
827
|
const shell = isWindows ? 'powershell.exe' : process.env.SHELL || '/bin/sh';
|
|
828
828
|
|
|
829
829
|
// Setup logging
|
|
830
|
-
const
|
|
831
|
-
const logFilename = generateLogFilename();
|
|
832
|
-
const logFilePath = path.join(logDir, logFilename);
|
|
830
|
+
const logFilePath = createLogPath('direct', sessionId);
|
|
833
831
|
|
|
834
832
|
let logContent = '';
|
|
835
833
|
const startTime = getTimestamp();
|
|
@@ -913,6 +911,7 @@ async function runDirectWithCommandStream(
|
|
|
913
911
|
|
|
914
912
|
let exitCode = 0;
|
|
915
913
|
try {
|
|
914
|
+
writeLogFile(logFilePath, logContent);
|
|
916
915
|
// Use raw() to pass the command without auto-escaping
|
|
917
916
|
// This is important for complex commands with pipes, redirects, etc.
|
|
918
917
|
const result = await $cmd`${raw(cmd)}`;
|
|
@@ -926,14 +925,17 @@ async function runDirectWithCommandStream(
|
|
|
926
925
|
// Collect output for log
|
|
927
926
|
if (result.stdout) {
|
|
928
927
|
logContent += result.stdout;
|
|
928
|
+
appendLogFile(logFilePath, result.stdout);
|
|
929
929
|
}
|
|
930
930
|
if (result.stderr) {
|
|
931
931
|
logContent += result.stderr;
|
|
932
|
+
appendLogFile(logFilePath, result.stderr);
|
|
932
933
|
}
|
|
933
934
|
} catch (err) {
|
|
934
935
|
exitCode = err.code || 1;
|
|
935
936
|
const errorMessage = `Error executing command: ${err.message}`;
|
|
936
937
|
logContent += `\n${errorMessage}\n`;
|
|
938
|
+
appendLogFile(logFilePath, `\n${errorMessage}\n`);
|
|
937
939
|
console.error(`\n${errorMessage}`);
|
|
938
940
|
}
|
|
939
941
|
|
|
@@ -946,7 +948,10 @@ async function runDirectWithCommandStream(
|
|
|
946
948
|
|
|
947
949
|
// Write log file
|
|
948
950
|
try {
|
|
949
|
-
|
|
951
|
+
appendLogFile(
|
|
952
|
+
logFilePath,
|
|
953
|
+
`\n${'='.repeat(50)}\nFinished: ${endTime}\nExit Code: ${exitCode}\n`
|
|
954
|
+
);
|
|
950
955
|
} catch (err) {
|
|
951
956
|
console.error(`\nWarning: Could not save log file: ${err.message}`);
|
|
952
957
|
}
|
|
@@ -987,13 +992,3 @@ async function runDirectWithCommandStream(
|
|
|
987
992
|
|
|
988
993
|
process.exit(exitCode);
|
|
989
994
|
}
|
|
990
|
-
|
|
991
|
-
/**
|
|
992
|
-
* Generate unique log filename for direct execution
|
|
993
|
-
* @returns {string} Log filename
|
|
994
|
-
*/
|
|
995
|
-
function generateLogFilename() {
|
|
996
|
-
const timestamp = Date.now();
|
|
997
|
-
const random = Math.random().toString(36).substring(2, 8);
|
|
998
|
-
return `start-command-${timestamp}-${random}.log`;
|
|
999
|
-
}
|
package/src/lib/args-parser.js
CHANGED
|
@@ -418,13 +418,13 @@ function parseOption(args, index, options) {
|
|
|
418
418
|
return 1;
|
|
419
419
|
}
|
|
420
420
|
|
|
421
|
-
// --status <uuid>
|
|
421
|
+
// --status <uuid-or-session-name>
|
|
422
422
|
if (arg === '--status') {
|
|
423
423
|
if (index + 1 < args.length && !args[index + 1].startsWith('-')) {
|
|
424
424
|
options.status = args[index + 1];
|
|
425
425
|
return 2;
|
|
426
426
|
} else {
|
|
427
|
-
throw new Error(`Option ${arg} requires a UUID argument`);
|
|
427
|
+
throw new Error(`Option ${arg} requires a UUID or session name argument`);
|
|
428
428
|
}
|
|
429
429
|
}
|
|
430
430
|
|
|
@@ -470,14 +470,23 @@ class ExecutionStore {
|
|
|
470
470
|
}
|
|
471
471
|
|
|
472
472
|
/**
|
|
473
|
-
* Get an execution record by UUID
|
|
474
|
-
*
|
|
473
|
+
* Get an execution record by UUID or session name
|
|
474
|
+
* First tries exact UUID match, then falls back to session name lookup
|
|
475
|
+
* @param {string} identifier - UUID or session name
|
|
475
476
|
* @returns {ExecutionRecord|null}
|
|
476
477
|
*/
|
|
477
|
-
get(
|
|
478
|
+
get(identifier) {
|
|
478
479
|
const records = this.readLinoRecords();
|
|
479
|
-
|
|
480
|
-
|
|
480
|
+
// First try exact UUID match
|
|
481
|
+
const byUuid = records.find((r) => r.uuid === identifier);
|
|
482
|
+
if (byUuid) {
|
|
483
|
+
return byUuid;
|
|
484
|
+
}
|
|
485
|
+
// Fall back to session name lookup (stored in options.sessionName)
|
|
486
|
+
const bySessionName = records.find(
|
|
487
|
+
(r) => r.options && r.options.sessionName === identifier
|
|
488
|
+
);
|
|
489
|
+
return bySessionName || null;
|
|
481
490
|
}
|
|
482
491
|
|
|
483
492
|
/**
|
|
@@ -5,6 +5,18 @@ const fs = require('fs');
|
|
|
5
5
|
const os = require('os');
|
|
6
6
|
const path = require('path');
|
|
7
7
|
|
|
8
|
+
function getTempRoot() {
|
|
9
|
+
return process.env.START_TEMP_ROOT || path.join(os.tmpdir(), 'start-command');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function ensureDirectory(dirPath) {
|
|
13
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function ensureParentDirectory(filePath) {
|
|
17
|
+
ensureDirectory(path.dirname(filePath));
|
|
18
|
+
}
|
|
19
|
+
|
|
8
20
|
/**
|
|
9
21
|
* Generate timestamp for logging
|
|
10
22
|
* @returns {string} ISO timestamp without 'T' and 'Z'
|
|
@@ -77,6 +89,7 @@ function createLogFooter(endTime, exitCode) {
|
|
|
77
89
|
*/
|
|
78
90
|
function writeLogFile(logPath, content) {
|
|
79
91
|
try {
|
|
92
|
+
ensureParentDirectory(logPath);
|
|
80
93
|
fs.writeFileSync(logPath, content, 'utf8');
|
|
81
94
|
return true;
|
|
82
95
|
} catch (err) {
|
|
@@ -85,23 +98,79 @@ function writeLogFile(logPath, content) {
|
|
|
85
98
|
}
|
|
86
99
|
}
|
|
87
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Append to a log file, creating its parent directory when needed.
|
|
103
|
+
* @param {string} logPath - Path to log file
|
|
104
|
+
* @param {string} content - Log content to append
|
|
105
|
+
* @returns {boolean} Success status
|
|
106
|
+
*/
|
|
107
|
+
function appendLogFile(logPath, content) {
|
|
108
|
+
try {
|
|
109
|
+
ensureParentDirectory(logPath);
|
|
110
|
+
fs.appendFileSync(logPath, content, 'utf8');
|
|
111
|
+
return true;
|
|
112
|
+
} catch (err) {
|
|
113
|
+
console.error(`\nWarning: Could not append log file: ${err.message}`);
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
88
118
|
/**
|
|
89
119
|
* Get log directory from environment or use system temp
|
|
90
120
|
* @returns {string} Log directory path
|
|
91
121
|
*/
|
|
92
122
|
function getLogDir() {
|
|
93
|
-
return process.env.START_LOG_DIR ||
|
|
123
|
+
return process.env.START_LOG_DIR || path.join(getTempRoot(), 'logs');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Get a start-command temporary directory for sidecar files.
|
|
128
|
+
* @param {...string} segments - Optional path segments below the temp directory
|
|
129
|
+
* @returns {string} Directory path
|
|
130
|
+
*/
|
|
131
|
+
function getTempDir(...segments) {
|
|
132
|
+
const dir = path.join(getTempRoot(), 'tmp', ...segments);
|
|
133
|
+
ensureDirectory(dir);
|
|
134
|
+
return dir;
|
|
94
135
|
}
|
|
95
136
|
|
|
96
137
|
/**
|
|
97
138
|
* Create log file path
|
|
98
139
|
* @param {string} environment - The isolation environment
|
|
140
|
+
* @param {string|null} executionId - Optional execution UUID/session ID
|
|
99
141
|
* @returns {string} Full path to log file
|
|
100
142
|
*/
|
|
101
|
-
function createLogPath(environment) {
|
|
143
|
+
function createLogPath(environment, executionId = null) {
|
|
102
144
|
const logDir = getLogDir();
|
|
145
|
+
if (executionId) {
|
|
146
|
+
return environment === 'direct'
|
|
147
|
+
? path.join(logDir, 'direct', `${executionId}.log`)
|
|
148
|
+
: path.join(logDir, 'isolation', environment, `${executionId}.log`);
|
|
149
|
+
}
|
|
103
150
|
const logFilename = generateLogFilename(environment);
|
|
104
|
-
return
|
|
151
|
+
return environment === 'direct'
|
|
152
|
+
? path.join(logDir, 'direct', logFilename)
|
|
153
|
+
: path.join(logDir, 'isolation', environment, logFilename);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function shellQuote(value) {
|
|
157
|
+
return `'${String(value).replace(/'/g, "'\\''")}'`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function createShellLogFooterSnippet() {
|
|
161
|
+
const dateCommand =
|
|
162
|
+
"date '+%Y-%m-%d %H:%M:%S.%3N' 2>/dev/null || date '+%Y-%m-%d %H:%M:%S'";
|
|
163
|
+
return `printf '\\n==================================================\\nFinished: %s\\nExit Code: %s\\n' "$(${dateCommand})" "$__start_command_exit"`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function wrapCommandWithLogFooter(command, options = {}) {
|
|
167
|
+
const shell = options.shell || 'sh';
|
|
168
|
+
const keepAlive = Boolean(options.keepAlive);
|
|
169
|
+
const footer = createShellLogFooterSnippet();
|
|
170
|
+
const afterFooter = keepAlive
|
|
171
|
+
? `exec ${shellQuote(shell)}`
|
|
172
|
+
: 'exit "$__start_command_exit"';
|
|
173
|
+
return `(${command}); __start_command_exit=$?; ${footer}; ${afterFooter}`;
|
|
105
174
|
}
|
|
106
175
|
|
|
107
176
|
/**
|
|
@@ -136,12 +205,20 @@ function runAsIsolatedUser(cmd, username) {
|
|
|
136
205
|
}
|
|
137
206
|
|
|
138
207
|
module.exports = {
|
|
208
|
+
getTempRoot,
|
|
209
|
+
ensureDirectory,
|
|
210
|
+
ensureParentDirectory,
|
|
139
211
|
getTimestamp,
|
|
140
212
|
generateLogFilename,
|
|
141
213
|
createLogHeader,
|
|
142
214
|
createLogFooter,
|
|
143
215
|
writeLogFile,
|
|
216
|
+
appendLogFile,
|
|
144
217
|
getLogDir,
|
|
218
|
+
getTempDir,
|
|
145
219
|
createLogPath,
|
|
220
|
+
shellQuote,
|
|
221
|
+
createShellLogFooterSnippet,
|
|
222
|
+
wrapCommandWithLogFooter,
|
|
146
223
|
runAsIsolatedUser,
|
|
147
224
|
};
|
package/src/lib/isolation.js
CHANGED
|
@@ -13,8 +13,15 @@ const {
|
|
|
13
13
|
getScreenVersion,
|
|
14
14
|
supportsLogfileOption,
|
|
15
15
|
runScreenWithLogCapture: _runScreenWithLogCapture,
|
|
16
|
+
startDetachedScreenWithLogCapture,
|
|
16
17
|
resetScreenVersionCache,
|
|
17
18
|
} = require('./screen-isolation');
|
|
19
|
+
const {
|
|
20
|
+
appendLogFile,
|
|
21
|
+
createShellLogFooterSnippet,
|
|
22
|
+
shellQuote,
|
|
23
|
+
wrapCommandWithLogFooter,
|
|
24
|
+
} = require('./isolation-log-utils');
|
|
18
25
|
|
|
19
26
|
/**
|
|
20
27
|
* Check if a command is available on the system
|
|
@@ -170,14 +177,21 @@ function wrapCommandWithUser(command, user) {
|
|
|
170
177
|
* @param {string|null} user - Username to run command as (optional)
|
|
171
178
|
* @returns {Promise<{success: boolean, sessionName: string, message: string, output: string}>}
|
|
172
179
|
*/
|
|
173
|
-
function runScreenWithLogCapture(
|
|
180
|
+
function runScreenWithLogCapture(
|
|
181
|
+
command,
|
|
182
|
+
sessionName,
|
|
183
|
+
shellInfo,
|
|
184
|
+
user = null,
|
|
185
|
+
logPath = null
|
|
186
|
+
) {
|
|
174
187
|
return _runScreenWithLogCapture(
|
|
175
188
|
command,
|
|
176
189
|
sessionName,
|
|
177
190
|
shellInfo,
|
|
178
191
|
user,
|
|
179
192
|
wrapCommandWithUser,
|
|
180
|
-
isInteractiveShellCommand
|
|
193
|
+
isInteractiveShellCommand,
|
|
194
|
+
logPath
|
|
181
195
|
);
|
|
182
196
|
}
|
|
183
197
|
|
|
@@ -199,54 +213,24 @@ function runInScreen(command, options = {}) {
|
|
|
199
213
|
|
|
200
214
|
const sessionName = options.session || generateSessionName('screen');
|
|
201
215
|
const shellInfo = getShell();
|
|
202
|
-
const { shell, shellArg } = shellInfo;
|
|
203
216
|
|
|
204
217
|
try {
|
|
205
|
-
// Wrap command with user switch if specified
|
|
206
|
-
let effectiveCommand = wrapCommandWithUser(command, options.user);
|
|
207
|
-
|
|
208
218
|
if (options.detached) {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
if (DEBUG) {
|
|
218
|
-
console.log(`[DEBUG] Running: screen ${screenArgs.join(' ')}`);
|
|
219
|
-
console.log(`[DEBUG] keepAlive: ${options.keepAlive || false}`);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// Use spawnSync with array args (not execSync string) to avoid quoting issues (issue #25)
|
|
223
|
-
const result = spawnSync('screen', screenArgs, {
|
|
224
|
-
stdio: 'inherit',
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
if (result.error) {
|
|
228
|
-
throw result.error;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
let message = `Command started in detached screen session: ${sessionName}`;
|
|
232
|
-
if (options.keepAlive) {
|
|
233
|
-
message += `\nSession will stay alive after command completes.`;
|
|
234
|
-
} else {
|
|
235
|
-
message += `\nSession will exit automatically after command completes.`;
|
|
236
|
-
}
|
|
237
|
-
message += `\nReattach with: screen -r ${sessionName}`;
|
|
238
|
-
|
|
239
|
-
return Promise.resolve({
|
|
240
|
-
success: true,
|
|
241
|
-
sessionName,
|
|
242
|
-
message,
|
|
243
|
-
});
|
|
219
|
+
return Promise.resolve(
|
|
220
|
+
startDetachedScreenWithLogCapture(command, sessionName, shellInfo, {
|
|
221
|
+
user: options.user,
|
|
222
|
+
keepAlive: options.keepAlive,
|
|
223
|
+
logPath: options.logPath,
|
|
224
|
+
wrapCommandWithUser,
|
|
225
|
+
})
|
|
226
|
+
);
|
|
244
227
|
} else {
|
|
245
228
|
return runScreenWithLogCapture(
|
|
246
229
|
command,
|
|
247
230
|
sessionName,
|
|
248
231
|
shellInfo,
|
|
249
|
-
options.user
|
|
232
|
+
options.user,
|
|
233
|
+
options.logPath
|
|
250
234
|
);
|
|
251
235
|
}
|
|
252
236
|
} catch (err) {
|
|
@@ -294,12 +278,40 @@ function runInTmux(command, options = {}) {
|
|
|
294
278
|
console.log(`[DEBUG] keepAlive: ${options.keepAlive || false}`);
|
|
295
279
|
}
|
|
296
280
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
281
|
+
if (options.logPath) {
|
|
282
|
+
const loggedCommand = wrapCommandWithLogFooter(effectiveCommand, {
|
|
283
|
+
shell,
|
|
284
|
+
keepAlive: options.keepAlive,
|
|
285
|
+
});
|
|
286
|
+
spawnSync('tmux', ['new-session', '-d', '-s', sessionName, shell], {
|
|
300
287
|
stdio: 'inherit',
|
|
301
|
-
}
|
|
302
|
-
|
|
288
|
+
});
|
|
289
|
+
spawnSync(
|
|
290
|
+
'tmux',
|
|
291
|
+
[
|
|
292
|
+
'pipe-pane',
|
|
293
|
+
'-t',
|
|
294
|
+
sessionName,
|
|
295
|
+
'-o',
|
|
296
|
+
`cat >> ${shellQuote(options.logPath)}`,
|
|
297
|
+
],
|
|
298
|
+
{ stdio: 'inherit' }
|
|
299
|
+
);
|
|
300
|
+
spawnSync(
|
|
301
|
+
'tmux',
|
|
302
|
+
['send-keys', '-t', sessionName, loggedCommand, 'C-m'],
|
|
303
|
+
{
|
|
304
|
+
stdio: 'inherit',
|
|
305
|
+
}
|
|
306
|
+
);
|
|
307
|
+
} else {
|
|
308
|
+
execSync(
|
|
309
|
+
`tmux new-session -d -s "${sessionName}" "${effectiveCommand}"`,
|
|
310
|
+
{
|
|
311
|
+
stdio: 'inherit',
|
|
312
|
+
}
|
|
313
|
+
);
|
|
314
|
+
}
|
|
303
315
|
|
|
304
316
|
let message = `Command started in detached tmux session: ${sessionName}`;
|
|
305
317
|
if (options.keepAlive) {
|
|
@@ -401,8 +413,8 @@ function runInSsh(command, options = {}) {
|
|
|
401
413
|
? `${remoteShell} ${shellInteractiveFlag}`
|
|
402
414
|
: remoteShell;
|
|
403
415
|
const remoteCommand = isInteractiveShellCommand(command)
|
|
404
|
-
? `nohup ${command} > /tmp/${sessionName}.log 2>&1 &`
|
|
405
|
-
: `nohup ${shellInvocation} -c ${JSON.stringify(command)} > /tmp/${sessionName}.log 2>&1 &`;
|
|
416
|
+
? `mkdir -p /tmp/start-command/logs/isolation/ssh && nohup ${command} > /tmp/start-command/logs/isolation/ssh/${sessionName}.log 2>&1 &`
|
|
417
|
+
: `mkdir -p /tmp/start-command/logs/isolation/ssh && nohup ${shellInvocation} -c ${JSON.stringify(command)} > /tmp/start-command/logs/isolation/ssh/${sessionName}.log 2>&1 &`;
|
|
406
418
|
const sshArgs = [sshTarget, remoteCommand];
|
|
407
419
|
|
|
408
420
|
if (DEBUG) {
|
|
@@ -419,7 +431,7 @@ function runInSsh(command, options = {}) {
|
|
|
419
431
|
return Promise.resolve({
|
|
420
432
|
success: true,
|
|
421
433
|
sessionName,
|
|
422
|
-
message: `Command started in detached SSH session on ${sshTarget}\nSession: ${sessionName}\nView logs: ssh ${sshTarget} "tail -f /tmp/${sessionName}.log"`,
|
|
434
|
+
message: `Command started in detached SSH session on ${sshTarget}\nSession: ${sessionName}\nView logs: ssh ${sshTarget} "tail -f /tmp/start-command/logs/isolation/ssh/${sessionName}.log"`,
|
|
423
435
|
});
|
|
424
436
|
} else {
|
|
425
437
|
const extraFlags = shellInteractiveFlag ? [shellInteractiveFlag] : [];
|
|
@@ -568,6 +580,19 @@ function runInDocker(command, options = {}) {
|
|
|
568
580
|
encoding: 'utf8',
|
|
569
581
|
}).trim();
|
|
570
582
|
|
|
583
|
+
if (options.logPath) {
|
|
584
|
+
const loggerScript = [
|
|
585
|
+
`docker logs -f ${shellQuote(containerName)} >> ${shellQuote(options.logPath)} 2>&1`,
|
|
586
|
+
`__start_command_exit=$(docker inspect -f '{{.State.ExitCode}}' ${shellQuote(containerName)} 2>/dev/null || printf '%s' '-1')`,
|
|
587
|
+
`${createShellLogFooterSnippet()} >> ${shellQuote(options.logPath)}`,
|
|
588
|
+
].join('; ');
|
|
589
|
+
const logger = spawn('sh', ['-c', loggerScript], {
|
|
590
|
+
detached: true,
|
|
591
|
+
stdio: 'ignore',
|
|
592
|
+
});
|
|
593
|
+
logger.unref();
|
|
594
|
+
}
|
|
595
|
+
|
|
571
596
|
let message = `Command started in detached docker container: ${containerName}`;
|
|
572
597
|
message += `\nContainer ID: ${containerId.substring(0, 12)}`;
|
|
573
598
|
if (options.keepAlive) {
|
|
@@ -582,6 +607,9 @@ function runInDocker(command, options = {}) {
|
|
|
582
607
|
}
|
|
583
608
|
message += `\nAttach with: docker attach ${containerName}`;
|
|
584
609
|
message += `\nView logs: docker logs ${containerName}`;
|
|
610
|
+
if (options.logPath) {
|
|
611
|
+
message += `\nLive log: ${options.logPath}`;
|
|
612
|
+
}
|
|
585
613
|
|
|
586
614
|
return Promise.resolve({
|
|
587
615
|
success: true,
|
|
@@ -696,6 +724,7 @@ function runIsolated(backend, command, options = {}) {
|
|
|
696
724
|
? options.endpointStack[0]
|
|
697
725
|
: options.endpoint,
|
|
698
726
|
session: options.sessionStack ? options.sessionStack[0] : options.session,
|
|
727
|
+
logPath: options.logPath,
|
|
699
728
|
};
|
|
700
729
|
|
|
701
730
|
switch (backend) {
|
|
@@ -746,6 +775,7 @@ module.exports = {
|
|
|
746
775
|
createLogHeader,
|
|
747
776
|
createLogFooter,
|
|
748
777
|
writeLogFile,
|
|
778
|
+
appendLogFile,
|
|
749
779
|
getLogDir,
|
|
750
780
|
createLogPath,
|
|
751
781
|
getScreenVersion,
|