start-command 0.39.0 → 0.39.2

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.
@@ -100,6 +100,28 @@ function supportsLogfileOption() {
100
100
  return version.patch >= 1;
101
101
  }
102
102
 
103
+ /**
104
+ * Polls without the session in `screen -ls` and without an exit code before
105
+ * the session counts as gone. `screen -dmS` can return before the session is
106
+ * listed, so a missing session alone is no proof that the command finished;
107
+ * its exit code is written just before the session ends.
108
+ */
109
+ const SCREEN_UNLISTED_GRACE_POLLS = 20;
110
+
111
+ /**
112
+ * Whether an attached screen run is over.
113
+ * @param {boolean} listed - The session is in `screen -ls`
114
+ * @param {boolean} exitCodeReady - The exit-code sidecar has content (or no
115
+ * sidecar is expected, as for a bare shell)
116
+ * @param {number} unlistedPolls - Consecutive polls without the session
117
+ * @returns {boolean}
118
+ */
119
+ function isScreenRunFinished(listed, exitCodeReady, unlistedPolls) {
120
+ return (
121
+ !listed && (exitCodeReady || unlistedPolls >= SCREEN_UNLISTED_GRACE_POLLS)
122
+ );
123
+ }
124
+
103
125
  /**
104
126
  * Run command in GNU Screen using detached mode with log capture.
105
127
  *
@@ -346,58 +368,64 @@ function runScreenWithLogCapture(
346
368
  const checkInterval = 100; // ms
347
369
  const maxWait = 300000; // 5 minutes max
348
370
  let waited = 0;
371
+ let unlistedPolls = 0;
372
+
373
+ const exitCodeReady = () => {
374
+ if (isBareShell) {
375
+ return true;
376
+ }
377
+ try {
378
+ return fs.readFileSync(exitCodeFile, 'utf8').trim() !== '';
379
+ } catch {
380
+ return false;
381
+ }
382
+ };
383
+
384
+ const finish = () => {
385
+ readAndDisplayOutput().then((output) => {
386
+ const exitCode = readExitCode();
387
+ cleanupTempFiles();
388
+ resolve({
389
+ success: exitCode === 0,
390
+ sessionName,
391
+ message: `Screen session "${sessionName}" exited with code ${exitCode}`,
392
+ exitCode,
393
+ output,
394
+ });
395
+ });
396
+ };
349
397
 
350
398
  const checkCompletion = () => {
399
+ let listed;
351
400
  try {
352
401
  // Check if session still exists
353
- const sessions = execSync('screen -ls', {
402
+ listed = execSync('screen -ls', {
354
403
  encoding: 'utf8',
355
404
  stdio: ['pipe', 'pipe', 'pipe'],
356
- });
357
-
358
- if (!sessions.includes(sessionName)) {
359
- // Session ended, read output and exit code
360
- readAndDisplayOutput().then((output) => {
361
- const exitCode = readExitCode();
362
- cleanupTempFiles();
363
- resolve({
364
- success: exitCode === 0,
365
- sessionName,
366
- message: `Screen session "${sessionName}" exited with code ${exitCode}`,
367
- exitCode,
368
- output,
369
- });
370
- });
371
- return;
372
- }
373
-
374
- waited += checkInterval;
375
- if (waited >= maxWait) {
376
- cleanupTempFiles();
377
- resolve({
378
- success: false,
379
- sessionName,
380
- message: `Screen session "${sessionName}" timed out after ${maxWait / 1000} seconds`,
381
- exitCode: 1,
382
- });
383
- return;
384
- }
385
-
386
- setTimeout(checkCompletion, checkInterval);
405
+ }).includes(sessionName);
387
406
  } catch {
388
407
  // screen -ls failed, session probably ended
389
- readAndDisplayOutput().then((output) => {
390
- const exitCode = readExitCode();
391
- cleanupTempFiles();
392
- resolve({
393
- success: exitCode === 0,
394
- sessionName,
395
- message: `Screen session "${sessionName}" exited with code ${exitCode}`,
396
- exitCode,
397
- output,
398
- });
408
+ listed = false;
409
+ }
410
+ unlistedPolls = listed ? 0 : unlistedPolls + 1;
411
+ if (isScreenRunFinished(listed, exitCodeReady(), unlistedPolls)) {
412
+ finish();
413
+ return;
414
+ }
415
+
416
+ waited += checkInterval;
417
+ if (waited >= maxWait) {
418
+ cleanupTempFiles();
419
+ resolve({
420
+ success: false,
421
+ sessionName,
422
+ message: `Screen session "${sessionName}" timed out after ${maxWait / 1000} seconds`,
423
+ exitCode: 1,
399
424
  });
425
+ return;
400
426
  }
427
+
428
+ setTimeout(checkCompletion, checkInterval);
401
429
  };
402
430
 
403
431
  // Start checking after a brief delay
@@ -522,7 +550,9 @@ function resetScreenVersionCache() {
522
550
  }
523
551
 
524
552
  module.exports = {
553
+ SCREEN_UNLISTED_GRACE_POLLS,
525
554
  getScreenVersion,
555
+ isScreenRunFinished,
526
556
  supportsLogfileOption,
527
557
  runScreenWithLogCapture,
528
558
  startDetachedScreenWithLogCapture,
package/test/isolation.js CHANGED
@@ -812,3 +812,35 @@ describe('isInteractiveShellCommand (issue #84)', () => {
812
812
  assert.strictEqual(isInteractiveShellCommand(' '), false);
813
813
  });
814
814
  });
815
+
816
+ describe('isScreenRunFinished', () => {
817
+ // `screen -dmS` can return before `screen -ls` lists the session; a failed
818
+ // command then read as exit 0 (Rust CI run 39873000384).
819
+ const {
820
+ SCREEN_UNLISTED_GRACE_POLLS,
821
+ isScreenRunFinished,
822
+ } = require('../src/lib/screen-isolation');
823
+
824
+ it('waits while the session is listed', () => {
825
+ assert.strictEqual(isScreenRunFinished(true, true, 0), false);
826
+ });
827
+
828
+ it('finishes once the session is gone and its exit code is written', () => {
829
+ assert.strictEqual(isScreenRunFinished(false, true, 1), true);
830
+ });
831
+
832
+ it('keeps polling an unlisted session without an exit code', () => {
833
+ assert.strictEqual(isScreenRunFinished(false, false, 1), false);
834
+ assert.strictEqual(
835
+ isScreenRunFinished(false, false, SCREEN_UNLISTED_GRACE_POLLS - 1),
836
+ false
837
+ );
838
+ });
839
+
840
+ it('gives up after the grace polls', () => {
841
+ assert.strictEqual(
842
+ isScreenRunFinished(false, false, SCREEN_UNLISTED_GRACE_POLLS),
843
+ true
844
+ );
845
+ });
846
+ });