start-command 0.35.3 → 0.36.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +10 -0
  3. package/package.json +1 -1
  4. package/src/bin/cli.js +43 -7
  5. package/src/lib/args-parser.js +12 -0
  6. package/src/lib/atomic-write.js +34 -0
  7. package/src/lib/attached-diagnostics.js +90 -0
  8. package/src/lib/cgroup-memory.js +43 -11
  9. package/src/lib/command-builder.js +40 -0
  10. package/src/lib/cpu-penalty-monitor.js +226 -0
  11. package/src/lib/cpu-penalty.js +175 -0
  12. package/src/lib/detached-finalize.js +47 -7
  13. package/src/lib/detached-output.js +61 -0
  14. package/src/lib/docker-cleanup.js +43 -4
  15. package/src/lib/docker-resource-limits.js +28 -0
  16. package/src/lib/docker-resource-options.js +180 -0
  17. package/src/lib/docker-runtime-args.js +15 -0
  18. package/src/lib/execution-attempt.js +153 -0
  19. package/src/lib/execution-recovery.js +191 -15
  20. package/src/lib/execution-resume-all.js +23 -1
  21. package/src/lib/execution-resume.js +203 -40
  22. package/src/lib/execution-store.js +56 -121
  23. package/src/lib/exit-evidence.js +123 -0
  24. package/src/lib/exit-reason.js +24 -6
  25. package/src/lib/isolation-log-utils.js +3 -3
  26. package/src/lib/isolation.js +36 -9
  27. package/src/lib/launch-owner.js +25 -0
  28. package/src/lib/launch-persistence.js +54 -0
  29. package/src/lib/query-commands.js +14 -3
  30. package/src/lib/resume-resources.js +88 -0
  31. package/src/lib/status-formatter.js +66 -6
  32. package/src/lib/store-lock.js +133 -0
  33. package/src/lib/usage.js +11 -0
  34. package/test/docker-runtime-options.js +6 -0
  35. package/test/execution-resume.js +10 -3
  36. package/test/exit-reason.js +6 -1
  37. package/test/isolation.js +5 -4
  38. package/test/regression-174.js +3 -1
  39. package/test/regression-176.js +5 -1
  40. package/test/regression-180.js +17 -10
  41. package/test/regression-182.js +7 -3
  42. package/test/regression-185.js +4 -9
  43. package/test/regression-187.js +547 -0
  44. package/test/regression-189.js +224 -0
  45. package/test/regression-190.js +111 -0
  46. package/test/regression-191.js +152 -0
  47. package/test/regression-192.js +66 -0
  48. package/test/regression-193.js +241 -0
  49. package/test/regression-194.js +182 -0
@@ -270,11 +270,14 @@ describe('resumeExecution', () => {
270
270
  watchers.push([name, policy, logPath]),
271
271
  });
272
272
  assert.strictEqual(result.success, true, result.error);
273
- assert.deepStrictEqual(calls, [['docker', 'start', 'box']]);
273
+ assert.deepStrictEqual(
274
+ calls.filter((call) => call[1] !== 'inspect'),
275
+ [['docker', 'start', 'box']]
276
+ );
274
277
  assert.strictEqual(watchers.length, 1);
275
278
  assert.strictEqual(watchers[0][0], 'box');
276
279
  assert.strictEqual(watchers[0][2], '/tmp/session.log');
277
- assert.strictEqual(store.saved.length, 1);
280
+ assert.strictEqual(store.saved.length, 3);
278
281
  assert.strictEqual(store.saved[0].uuid, record.uuid);
279
282
  assert.strictEqual(store.saved[0].status, 'executing');
280
283
  assert.strictEqual(store.saved[0].options.resumeCount, 1);
@@ -320,7 +323,10 @@ describe('resumeExecution', () => {
320
323
  });
321
324
  assert.strictEqual(result.success, false);
322
325
  assert.match(result.error, /no such container/);
323
- assert.strictEqual(store.saved.length, 0);
326
+ assert.strictEqual(store.saved.length, 2);
327
+ assert.strictEqual(store.saved.at(-1).status, 'executing');
328
+ assert.strictEqual(store.saved.at(-1).options.sessionName, 'box');
329
+ assert.strictEqual(store.saved.at(-1).options.launchPending, undefined);
324
330
  });
325
331
 
326
332
  test('relaunches through the isolation backend when the container is gone', async () => {
@@ -329,6 +335,7 @@ describe('resumeExecution', () => {
329
335
  const launches = [];
330
336
  const result = await resumeExecution(store, 'box', {
331
337
  probe: () => probeWith(SessionState.MISSING),
338
+ startWatcher: () => {},
332
339
  runIsolated: (backend, command, options) => {
333
340
  launches.push({ backend, command, options });
334
341
  return Promise.resolve({ success: true, containerId: 'newid' });
@@ -111,7 +111,11 @@ describe('resolveExitReason', () => {
111
111
  });
112
112
 
113
113
  it('should report the cgroup OOM observation when there is no marker', () => {
114
- const reason = resolveExitReason({ exitCode: 137, oomKilled: true });
114
+ const reason = resolveExitReason({
115
+ exitCode: 137,
116
+ oomKilled: true,
117
+ exitEvidence: { mainOom: true },
118
+ });
115
119
  assert.strictEqual(reason, 'memory-exhaustion (cgroup-oom-killer)');
116
120
  });
117
121
 
@@ -173,6 +177,7 @@ describe('resolveMemoryExhaustion', () => {
173
177
  resolveMemoryExhaustion({
174
178
  exitCode: 137,
175
179
  logTail: 'killed\n',
180
+ exitEvidence: { mainOom: true },
176
181
  oomKilled: true,
177
182
  }),
178
183
  {
package/test/isolation.js CHANGED
@@ -16,6 +16,7 @@ const {
16
16
  supportsLogfileOption,
17
17
  resetScreenVersionCache,
18
18
  getDefaultDockerImage,
19
+ canRunLinuxDockerImages,
19
20
  } = require('../src/lib/isolation');
20
21
 
21
22
  describe('Isolation Module', () => {
@@ -665,8 +666,8 @@ describe('detectShellInEnvironment', () => {
665
666
  });
666
667
 
667
668
  it('should auto-detect shell in docker if docker is available', () => {
668
- if (!isCommandAvailable('docker')) {
669
- console.log(' Skipping: docker not installed');
669
+ if (!canRunLinuxDockerImages()) {
670
+ console.log(' Skipping: Docker cannot run Linux container images');
670
671
  return;
671
672
  }
672
673
  // Use alpine:latest which is commonly available and has sh
@@ -703,8 +704,8 @@ describe('Shell option forwarding to isolation runners', () => {
703
704
 
704
705
  describe('runInDocker shell option', () => {
705
706
  it('should accept shell option in options object', async () => {
706
- if (!isCommandAvailable('docker')) {
707
- console.log(' Skipping: docker not installed');
707
+ if (!canRunLinuxDockerImages()) {
708
+ console.log(' Skipping: Docker cannot run Linux container images');
708
709
  return;
709
710
  }
710
711
 
@@ -100,9 +100,11 @@ function runWatcher({ policy, running, state, withLog = true }) {
100
100
  [
101
101
  '#!/bin/sh',
102
102
  `dir='${dir}'`,
103
+ '[ "$START_COMMAND_CGROUP_SAMPLING" = 1 ] && exit 1',
103
104
  'case "$1" in',
104
105
  ' inspect)',
105
106
  ' case "$3" in',
107
+ ' *HostConfig*) exit 1 ;;',
106
108
  ' *State.Running*)',
107
109
  ' answer=$(head -n 1 "$dir/running")',
108
110
  ' if [ "$(wc -l < "$dir/running")" -gt 1 ]; then',
@@ -173,7 +175,7 @@ describe('issue #174: docker logs -f returning is not the container exiting', ()
173
175
  });
174
176
 
175
177
  expect(run.status).toBe(0);
176
- expect(run.calls).toContain('wait');
178
+ expect(run.calls).toContain('inspect-running false');
177
179
  expect(run.calls.indexOf('wait')).toBeGreaterThan(
178
180
  run.calls.indexOf('logs')
179
181
  );
@@ -313,7 +313,11 @@ describe('issue #176: snapshot resume keeps the resource limits', () => {
313
313
  expect(saved[0].options.sessionName).toBe('box-resume-1');
314
314
  expect(result.output).toContain('--pids-limit=64');
315
315
  // A snapshot container has no recovery selector.
316
- expect(watchers[0][4]).toEqual({ recoverOnKill: false });
316
+ expect(watchers[0][4]).toEqual({
317
+ recoverOnKill: false,
318
+ since: saved[0].attempt.startedAt,
319
+ attemptNumber: saved[0].attempt.number,
320
+ });
317
321
  });
318
322
 
319
323
  it('shows the limits in [Isolation] lines and stores them as metadata', () => {
@@ -70,20 +70,20 @@ describe('issue #180: the sticky OOMKilled flag is not an exit reason', () => {
70
70
  {
71
71
  exitCode: 137,
72
72
  oomKilled: true,
73
- reason: CGROUP_OOM_EXIT_REASON,
74
- memory: true,
73
+ reason: 'signal (SIGKILL; cause unknown)',
74
+ memory: false,
75
75
  },
76
76
  {
77
77
  exitCode: 137,
78
78
  oomKilled: false,
79
- reason: 'signal (SIGKILL)',
79
+ reason: 'signal (SIGKILL; cause unknown)',
80
80
  memory: false,
81
81
  },
82
82
  {
83
83
  exitCode: -1,
84
84
  oomKilled: true,
85
- reason: CGROUP_OOM_EXIT_REASON,
86
- memory: true,
85
+ reason: null,
86
+ memory: false,
87
87
  },
88
88
  {
89
89
  exitCode: 139,
@@ -104,10 +104,17 @@ describe('issue #180: the sticky OOMKilled flag is not an exit reason', () => {
104
104
  });
105
105
  }
106
106
 
107
- it('blames the flag on the command only for SIGKILL or an unknown exit', () => {
108
- expect(isOomKillOfCommand({ exitCode: 137, oomKilled: true })).toBe(true);
109
- expect(isOomKillOfCommand({ exitCode: null, oomKilled: true })).toBe(true);
110
- expect(isOomKillOfCommand({ exitCode: -1, oomKilled: true })).toBe(true);
107
+ it('requires fresh attribution in addition to SIGKILL or an unknown exit', () => {
108
+ expect(isOomKillOfCommand({ exitCode: 137, oomKilled: true })).toBe(false);
109
+ expect(
110
+ isOomKillOfCommand({
111
+ exitCode: 137,
112
+ oomKilled: true,
113
+ exitEvidence: { mainOom: true },
114
+ })
115
+ ).toBe(true);
116
+ expect(isOomKillOfCommand({ exitCode: null, oomKilled: true })).toBe(false);
117
+ expect(isOomKillOfCommand({ exitCode: -1, oomKilled: true })).toBe(false);
111
118
  expect(isOomKillOfCommand({ exitCode: 0, oomKilled: true })).toBe(false);
112
119
  expect(isOomKillOfCommand({ exitCode: 1, oomKilled: true })).toBe(false);
113
120
  expect(isOomKillOfCommand({ exitCode: 137, oomKilled: false })).toBe(false);
@@ -191,6 +198,6 @@ describe('issue #180: the sticky OOMKilled flag is not an exit reason', () => {
191
198
  oomKilled: true,
192
199
  logPath: null,
193
200
  });
194
- expect(main).toContain('Docker reports it was OOM-killed.');
201
+ expect(main).toContain('Docker reports a process in it was OOM-killed.');
195
202
  });
196
203
  });
@@ -285,9 +285,13 @@ describe('issue #182: the watcher samples the cgroup while the container runs',
285
285
  timeout: 20000,
286
286
  });
287
287
  expect(result.status).toBe(0);
288
- expect(fs.readFileSync(path.join(host.dir, 'sample'), 'utf8')).toBe(
289
- OOM_SAMPLE
290
- );
288
+ expect(
289
+ fs
290
+ .readFileSync(path.join(host.dir, 'sample'), 'utf8')
291
+ .split(' ')
292
+ .slice(0, 4)
293
+ .join(' ')
294
+ ).toBe(OOM_SAMPLE);
291
295
  expect(fs.readFileSync(logPath, 'utf8')).toBe(
292
296
  `${formatCgroupMemoryLogLine(OOM_SAMPLE)}\n`
293
297
  );
@@ -97,15 +97,10 @@ describe('issue #185: counter-only OOM scope is unknown', () => {
97
97
  }
98
98
  for (const exitCode of [137, null, -1]) {
99
99
  const input = { exitCode, oomKilled: false, cgroupMemory };
100
- expect(resolveExitReason(input)).toBe(CGROUP_OOM_EXIT_REASON);
101
- expect(resolveMemoryExhaustion(input)).toEqual(
102
- exitCode === null
103
- ? null
104
- : {
105
- memoryExhausted: true,
106
- memoryExhaustedReason: 'cgroup memory.events reported oom_kill=3',
107
- }
108
- );
100
+ expect(resolveExitReason(input)).not.toBe(CGROUP_OOM_EXIT_REASON);
101
+ expect(resolveMemoryExhaustion(input)).toBeNull();
102
+ const attributed = { ...input, exitEvidence: { mainOom: true } };
103
+ expect(resolveExitReason(attributed)).toBe(CGROUP_OOM_EXIT_REASON);
109
104
  }
110
105
  });
111
106
  });