@trebired/code-server-kit 0.3.0 → 1.0.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 (48) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +175 -208
  3. package/dist/diagnostics.d.ts +6 -0
  4. package/dist/diagnostics.d.ts.map +1 -0
  5. package/dist/diagnostics.js +150 -0
  6. package/dist/diagnostics.js.map +1 -0
  7. package/dist/errors.d.ts +5 -2
  8. package/dist/errors.d.ts.map +1 -1
  9. package/dist/errors.js +7 -1
  10. package/dist/errors.js.map +1 -1
  11. package/dist/index.d.ts +9 -7
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +8 -6
  14. package/dist/index.js.map +1 -1
  15. package/dist/plan.d.ts +3 -2
  16. package/dist/plan.d.ts.map +1 -1
  17. package/dist/plan.js +68 -68
  18. package/dist/plan.js.map +1 -1
  19. package/dist/preparation.d.ts +5 -0
  20. package/dist/preparation.d.ts.map +1 -0
  21. package/dist/preparation.js +194 -0
  22. package/dist/preparation.js.map +1 -0
  23. package/dist/profile.d.ts +5 -2
  24. package/dist/profile.d.ts.map +1 -1
  25. package/dist/profile.js +122 -1
  26. package/dist/profile.js.map +1 -1
  27. package/dist/proxy.d.ts +4 -2
  28. package/dist/proxy.d.ts.map +1 -1
  29. package/dist/proxy.js +62 -1
  30. package/dist/proxy.js.map +1 -1
  31. package/dist/resolve.d.ts.map +1 -1
  32. package/dist/resolve.js +43 -1
  33. package/dist/resolve.js.map +1 -1
  34. package/dist/session.d.ts +2 -2
  35. package/dist/session.d.ts.map +1 -1
  36. package/dist/session.js +416 -534
  37. package/dist/session.js.map +1 -1
  38. package/dist/spec.d.ts +3 -3
  39. package/dist/spec.d.ts.map +1 -1
  40. package/dist/spec.js +2 -33
  41. package/dist/spec.js.map +1 -1
  42. package/dist/systemd.d.ts +5 -2
  43. package/dist/systemd.d.ts.map +1 -1
  44. package/dist/systemd.js +57 -6
  45. package/dist/systemd.js.map +1 -1
  46. package/dist/types.d.ts +215 -67
  47. package/dist/types.d.ts.map +1 -1
  48. package/package.json +2 -2
package/dist/session.js CHANGED
@@ -2,21 +2,21 @@ import fs from "node:fs";
2
2
  import { createHash } from "node:crypto";
3
3
  import net from "node:net";
4
4
  import path from "node:path";
5
- import { CodeServerInvalidConfigurationError, CodeServerSessionLifecycleError, CodeServerSystemdCollisionError, isCodeServerKitError, } from "./errors.js";
5
+ import { CodeServerInvalidConfigurationError, CodeServerSessionLifecycleError, CodeServerSessionReuseConflictError, isCodeServerKitError, } from "./errors.js";
6
+ import { collectCodeServerStartupDiagnostics, normalizeCodeServerStartupFailure, sanitizeCodeServerDiagnostics, } from "./diagnostics.js";
6
7
  import { launchCodeServerProcess } from "./launch.js";
7
8
  import { logPackageInitialized, resolveLogger } from "./logging.js";
8
9
  import { createCodeServerLaunchPlan } from "./plan.js";
9
- import { syncCodeServerProfile } from "./profile.js";
10
+ import { ensureCodeServerPrepared } from "./preparation.js";
11
+ import { persistCodeServerProfileIfChanged, readCodeServerProfileSnapshot, syncCodeServerProfile, } from "./profile.js";
10
12
  import { waitForCodeServerReady } from "./readiness.js";
11
- import { normalizeCodeServerStartupFailure } from "./spec.js";
12
- import { buildDefaultCodeServerUnitName, launchCodeServerWithSystemd, readCodeServerSystemdJournal, readCodeServerSystemdStatus, stopCodeServerSystemdUnit, } from "./systemd.js";
13
+ import { launchCodeServerWithSystemd, readCodeServerSystemdStatus, restartCodeServerSystemdUnit, summarizeCodeServerSystemdJournal, } from "./systemd.js";
13
14
  const DEFAULT_LAUNCH_STRATEGY = "direct";
14
15
  const DEFAULT_READY_RETRY_INTERVAL_MS = 100;
15
16
  const DEFAULT_READY_TIMEOUT_MS = 30000;
16
- const DEFAULT_TAIL_LENGTH = 8192;
17
+ const handles = new Map();
18
+ const inflightStarts = new Map();
17
19
  function createCodeServerSessionManager(options = {}) {
18
- const handles = new Map();
19
- const log = resolveLogger(options.logger, options.loggerAdapter);
20
20
  logPackageInitialized({
21
21
  adapter: options.loggerAdapter,
22
22
  logger: options.logger,
@@ -24,13 +24,13 @@ function createCodeServerSessionManager(options = {}) {
24
24
  });
25
25
  return {
26
26
  async getStatus(input) {
27
- const request = {
27
+ return await getCodeServerSessionStatusInternal({
28
28
  logger: input.logger ?? options.logger,
29
29
  loggerAdapter: input.loggerAdapter ?? options.loggerAdapter,
30
+ sanitizer: input.sanitizer,
30
31
  sessionKey: input.sessionKey,
31
32
  stateRoot: input.stateRoot,
32
- };
33
- return await getCodeServerSessionStatusInternal(request, handles);
33
+ });
34
34
  },
35
35
  async readDiagnostics(input) {
36
36
  return await readCodeServerSessionDiagnostics({
@@ -43,14 +43,11 @@ function createCodeServerSessionManager(options = {}) {
43
43
  logger: input.logger,
44
44
  loggerAdapter: input.loggerAdapter,
45
45
  profile: input.profile,
46
+ sanitizer: input.sanitizer,
46
47
  sessionKey: input.sessionKey,
47
48
  signal: "SIGTERM",
48
49
  stateRoot: input.stateRoot,
49
- }) ?? {
50
- diagnostics: null,
51
- status: createStoppedPlaceholderStatus(input),
52
- stopped: false,
53
- };
50
+ }) ?? createEmptyStopResult(input.sessionKey);
54
51
  const start = await this.start(input);
55
52
  return {
56
53
  start,
@@ -64,14 +61,14 @@ function createCodeServerSessionManager(options = {}) {
64
61
  logger: input.logger ?? options.logger,
65
62
  loggerAdapter: input.loggerAdapter ?? options.loggerAdapter,
66
63
  resolveFrom: input.resolveFrom ?? options.resolveFrom,
67
- }, handles);
64
+ });
68
65
  },
69
66
  async stop(input) {
70
67
  return await stopCodeServerSessionInternal({
71
68
  ...input,
72
69
  logger: input.logger ?? options.logger,
73
70
  loggerAdapter: input.loggerAdapter ?? options.loggerAdapter,
74
- }, handles);
71
+ });
75
72
  },
76
73
  };
77
74
  }
@@ -85,190 +82,299 @@ async function startCodeServerSession(options) {
85
82
  return await manager.start(options);
86
83
  }
87
84
  async function stopCodeServerSession(options) {
88
- const manager = createCodeServerSessionManager({
85
+ return await createCodeServerSessionManager({
89
86
  logger: options.logger,
90
87
  loggerAdapter: options.loggerAdapter,
91
- });
92
- return await manager.stop(options);
88
+ }).stop(options);
93
89
  }
94
90
  async function restartCodeServerSession(options) {
95
- const manager = createCodeServerSessionManager({
91
+ return await createCodeServerSessionManager({
96
92
  installation: options.installation,
97
93
  logger: options.logger,
98
94
  loggerAdapter: options.loggerAdapter,
99
95
  resolveFrom: options.resolveFrom,
100
- });
101
- return await manager.restart(options);
96
+ }).restart(options);
102
97
  }
103
98
  async function getCodeServerSessionStatus(options) {
104
- const manager = createCodeServerSessionManager({
99
+ return await createCodeServerSessionManager({
105
100
  logger: options.logger,
106
101
  loggerAdapter: options.loggerAdapter,
107
- });
108
- return await manager.getStatus(options);
102
+ }).getStatus(options);
109
103
  }
110
104
  async function readCodeServerSessionDiagnostics(options) {
111
105
  const paths = getSessionPaths(options.stateRoot, options.sessionKey);
112
- const diagnostics = await readJsonFile(paths.diagnosticsPath);
113
- return diagnostics ?? null;
106
+ return await readJsonFile(paths.diagnosticsPath);
114
107
  }
115
- async function startCodeServerSessionInternal(options, handles) {
108
+ async function startCodeServerSessionInternal(options) {
109
+ const sessionKey = normalizeSessionKey(options.sessionKey);
110
+ const stateRoot = path.resolve(options.stateRoot);
111
+ const requestedSpecHash = hashNormalizedSpec({
112
+ launchStrategy: options.launchStrategy ?? DEFAULT_LAUNCH_STRATEGY,
113
+ env: options.env ?? {},
114
+ host: options.host ?? null,
115
+ port: options.port ?? null,
116
+ trustedOrigins: options.trustedOrigins ?? [],
117
+ workspacePath: options.workspacePath ?? null,
118
+ profile: normalizeProfileConfig(options.profile),
119
+ systemd: options.systemd ?? null,
120
+ });
121
+ const inflightKey = `${stateRoot}:${sessionKey}`;
122
+ const running = inflightStarts.get(inflightKey);
123
+ if (running) {
124
+ if (running.specHash === requestedSpecHash) {
125
+ return await running.promise;
126
+ }
127
+ throw new CodeServerSessionReuseConflictError("A code-server session start is already in flight for this session key with a different effective spec.", {
128
+ sessionKey,
129
+ stateRoot,
130
+ });
131
+ }
132
+ const promise = (async () => {
133
+ const paths = getSessionPaths(stateRoot, sessionKey);
134
+ const existing = await readJsonFile(paths.recordPath);
135
+ const existingHost = existing ? extractHost(existing.bindAddr) : undefined;
136
+ const launchPlan = await createCodeServerLaunchPlan({
137
+ ...options,
138
+ host: options.bindAddr ? undefined : (options.host ?? existingHost),
139
+ port: options.bindAddr ? undefined : (options.port ?? existing?.port),
140
+ dataRoot: options.dataRoot ?? path.join(paths.sessionDir, "runtime"),
141
+ });
142
+ const specHash = hashNormalizedSpec({
143
+ launchStrategy: options.launchStrategy ?? DEFAULT_LAUNCH_STRATEGY,
144
+ plan: {
145
+ args: launchPlan.args,
146
+ bindAddr: launchPlan.bindAddr,
147
+ command: launchPlan.command,
148
+ trustedOrigins: launchPlan.trustedOrigins,
149
+ workspacePath: launchPlan.workspacePath,
150
+ },
151
+ profile: normalizeProfileConfig(options.profile),
152
+ systemd: options.systemd ?? null,
153
+ });
154
+ return await startCodeServerSessionInner({
155
+ existing,
156
+ launchPlan,
157
+ options,
158
+ paths,
159
+ sessionKey,
160
+ specHash,
161
+ stateRoot,
162
+ });
163
+ })();
164
+ inflightStarts.set(inflightKey, {
165
+ promise,
166
+ specHash: requestedSpecHash,
167
+ });
168
+ try {
169
+ return await promise;
170
+ }
171
+ finally {
172
+ inflightStarts.delete(inflightKey);
173
+ }
174
+ }
175
+ async function startCodeServerSessionInner(context) {
176
+ const { existing, launchPlan, options, paths, sessionKey, specHash, stateRoot } = context;
116
177
  const log = resolveLogger(options.logger, options.loggerAdapter);
117
- const basePaths = getSessionPaths(options.stateRoot, options.sessionKey);
118
- const existing = await readJsonFile(basePaths.recordPath);
119
- const context = await prepareSessionContext(options, existing);
178
+ const launchStrategy = options.launchStrategy ?? DEFAULT_LAUNCH_STRATEGY;
179
+ const preparation = options.preparation?.mode === "skip"
180
+ ? launchPlan.preparationStatus
181
+ : (await ensureCodeServerPrepared({
182
+ logger: options.logger,
183
+ loggerAdapter: options.loggerAdapter,
184
+ resolveFrom: options.resolveFrom,
185
+ strictWatchdog: options.preparation?.strictWatchdog,
186
+ })).status;
187
+ const watchdogMode = preparation.watchdogMode;
188
+ await mkdirp(paths.sessionDir);
120
189
  log.info("session", "starting code-server session", {
121
- launchStrategy: context.launchStrategy,
122
- sessionKey: options.sessionKey,
123
- stateRoot: context.stateRoot,
190
+ launchStrategy,
191
+ sessionKey,
192
+ stateRoot,
124
193
  });
125
- await mkdirp(context.paths.sessionDir);
126
- await maybeRestoreProfile(options.profile, context.plan.userDataDir);
127
194
  if (existing) {
128
- const liveStatus = await probeSessionRecord(existing, handles, context.paths);
129
- if (existing.specHash === context.specHash && liveStatus.ready) {
195
+ const status = await probeSessionRecord(existing, options.sanitizer);
196
+ if (existing.specHash === specHash && status.ready) {
130
197
  const reused = {
131
- ...liveStatus,
198
+ ...status,
132
199
  state: "reusing_existing",
133
200
  };
134
201
  await writeSessionRecord({
135
202
  ...existing,
136
- diagnostics: existing.diagnostics,
203
+ health: "ready",
204
+ preparation,
137
205
  state: "reusing_existing",
138
206
  updatedAt: nowIso(),
139
- }, context.paths.recordPath);
140
- log.info("session", "reusing existing code-server session", {
141
- port: reused.port,
142
- sessionKey: reused.sessionKey,
143
- });
207
+ }, paths.recordPath);
144
208
  return {
145
209
  created: false,
146
210
  diagnostics: reused.diagnostics,
147
- handle: handles.get(options.sessionKey) ?? null,
148
- launchPlan: context.plan,
149
- launchStrategy: context.launchStrategy,
211
+ handle: handles.get(sessionKey) ?? null,
212
+ launchPlan,
213
+ launchStrategy,
150
214
  reused: true,
151
215
  status: reused,
152
216
  };
153
217
  }
154
- if (existing.specHash !== context.specHash && isLiveState(liveStatus.state)) {
155
- await stopExistingRuntime(existing, options.profile, handles, options.logger, options.loggerAdapter);
156
- await writeSessionRecord({
157
- ...existing,
158
- state: "stale",
159
- stoppedAt: nowIso(),
160
- updatedAt: nowIso(),
161
- }, context.paths.recordPath);
162
- }
163
- else if (!liveStatus.ready && isLiveState(existing.state)) {
164
- await writeSessionRecord({
165
- ...existing,
166
- state: "stale",
167
- stoppedAt: nowIso(),
168
- updatedAt: nowIso(),
169
- }, context.paths.recordPath);
218
+ if (isLiveOrStartingState(status.state)) {
219
+ await stopExistingRuntime(existing, options.profile, undefined, options.logger, options.loggerAdapter);
170
220
  }
171
221
  }
172
- const baseRecord = createBaseRecord(context);
222
+ await maybeRestoreProfile(options.profile, launchPlan.userDataDir);
223
+ const baseRecord = createBaseRecord({
224
+ lastStartSummary: null,
225
+ launchPlan,
226
+ launchStrategy,
227
+ preparation,
228
+ sessionKey,
229
+ specHash,
230
+ watchdogMode,
231
+ });
173
232
  await writeSessionRecord({
174
233
  ...baseRecord,
175
234
  state: "launching",
176
- }, context.paths.recordPath);
235
+ updatedAt: nowIso(),
236
+ }, paths.recordPath);
177
237
  try {
178
- const launched = context.launchStrategy === "direct"
179
- ? await startDirectSession(context, handles)
180
- : await startSystemdSession(context, existing);
238
+ let handle = null;
239
+ let journalTail = "";
240
+ if (launchStrategy === "direct") {
241
+ handle = await launchCodeServerProcess({
242
+ plan: launchPlan,
243
+ });
244
+ handles.set(sessionKey, handle);
245
+ }
246
+ else {
247
+ if (!options.systemd?.scope) {
248
+ throw new CodeServerInvalidConfigurationError("systemd session launches require an explicit scope of 'user' or 'system'.", {
249
+ sessionKey,
250
+ });
251
+ }
252
+ await launchCodeServerWithSystemd({
253
+ extraProperties: options.systemd.extraProperties,
254
+ logger: options.logger,
255
+ loggerAdapter: options.loggerAdapter,
256
+ plan: launchPlan,
257
+ scope: options.systemd.scope,
258
+ sessionKey,
259
+ unitName: options.systemd.unitName,
260
+ });
261
+ }
181
262
  const ready = await waitForCodeServerReady({
182
- failureProbe: launched.failureProbe,
183
- host: context.plan.host,
184
- port: context.plan.port,
185
- process: launched.handle ?? undefined,
263
+ failureProbe: options.failureProbe,
264
+ host: launchPlan.host,
265
+ port: launchPlan.port,
266
+ process: handle ?? undefined,
186
267
  retryIntervalMs: options.readinessRetryIntervalMs ?? DEFAULT_READY_RETRY_INTERVAL_MS,
187
268
  timeoutMs: options.readinessTimeoutMs ?? DEFAULT_READY_TIMEOUT_MS,
188
269
  });
189
- const diagnostics = await buildDiagnosticsSnapshot(context, launched.handle, ready.elapsedMs, launched.systemdStatus);
270
+ if (launchStrategy === "systemd" && options.systemd?.scope) {
271
+ journalTail = await summarizeCodeServerSystemdJournal({
272
+ lines: 50,
273
+ scope: options.systemd.scope,
274
+ unitName: options.systemd.unitName ?? `trebired-code-server-kit-${sessionKey}.service`,
275
+ });
276
+ }
277
+ const normalizedFailure = collectCodeServerStartupDiagnostics({
278
+ journal: journalTail,
279
+ launchStrategy,
280
+ preparationStatus: preparation,
281
+ process: handle,
282
+ sanitizer: options.sanitizer,
283
+ watchdogMode,
284
+ });
285
+ const diagnostics = createDiagnosticsSnapshot({
286
+ handle,
287
+ journalTail,
288
+ normalizedFailure,
289
+ readyElapsedMs: ready.elapsedMs,
290
+ });
190
291
  const record = {
191
292
  ...baseRecord,
192
293
  diagnostics,
193
- pid: launched.handle?.pid ?? launched.systemdStatus?.execMainPid ?? null,
294
+ health: "ready",
295
+ lastStartSummary: normalizedFailure.summary,
296
+ pid: handle?.pid ?? null,
297
+ preparation,
194
298
  readyAt: nowIso(),
299
+ sanitizedDiagnostics: normalizedFailure.sanitized ?? null,
195
300
  startedAt: nowIso(),
196
301
  state: "ready",
197
- unitName: launched.unitName,
302
+ systemdScope: options.systemd?.scope ?? null,
303
+ unitName: options.systemd?.unitName ?? null,
198
304
  updatedAt: nowIso(),
199
305
  };
200
- await writeSessionRecord(record, context.paths.recordPath);
201
- await writeDiagnosticsFile(diagnostics, context.paths);
202
- const status = await recordToStatus(record, context.paths, handles);
306
+ await writeSessionRecord(record, paths.recordPath);
307
+ await writeDiagnosticsFile(record, paths);
203
308
  return {
204
309
  created: true,
205
- diagnostics: status.diagnostics,
206
- handle: launched.handle,
207
- launchPlan: context.plan,
208
- launchStrategy: context.launchStrategy,
310
+ diagnostics: await readCodeServerSessionDiagnostics({
311
+ sessionKey,
312
+ stateRoot,
313
+ }),
314
+ handle,
315
+ launchPlan,
316
+ launchStrategy,
209
317
  reused: false,
210
- status,
318
+ status: await probeSessionRecord(record, options.sanitizer),
211
319
  };
212
320
  }
213
321
  catch (error) {
214
- const normalized = normalizeCodeServerStartupFailure(error);
322
+ const normalized = normalizeCodeServerStartupFailure(error, {
323
+ launchStrategy,
324
+ preparationStatus: preparation,
325
+ sanitizer: options.sanitizer,
326
+ watchdogMode,
327
+ });
328
+ const handle = handles.get(sessionKey) ?? null;
215
329
  const failure = {
216
- code: normalized.code ?? "session_start_failed",
330
+ code: normalized.code,
217
331
  details: normalized.details,
218
- message: normalized.message,
332
+ message: normalized.summary,
219
333
  name: normalized.name,
220
334
  };
221
- if (context.launchStrategy === "systemd" && context.systemdScope && context.unitName) {
222
- try {
223
- await stopCodeServerSystemdUnit({
224
- logger: options.logger,
225
- loggerAdapter: options.loggerAdapter,
226
- resetFailed: true,
227
- scope: context.systemdScope,
228
- unitName: context.unitName,
229
- });
230
- }
231
- catch {
232
- }
233
- }
234
- const handle = handles.get(options.sessionKey);
235
335
  if (handle) {
236
336
  try {
237
337
  handle.kill("SIGTERM");
238
338
  }
239
339
  catch {
240
340
  }
241
- handles.delete(options.sessionKey);
341
+ handles.delete(sessionKey);
242
342
  }
243
- const diagnostics = await buildFailureDiagnostics(context, handles.get(options.sessionKey) ?? null);
244
343
  const record = {
245
344
  ...baseRecord,
246
- diagnostics,
345
+ diagnostics: createDiagnosticsSnapshot({
346
+ handle,
347
+ journalTail: launchStrategy === "systemd" && options.systemd?.scope && options.systemd.unitName
348
+ ? await safeSystemdSummary(options.systemd.scope, options.systemd.unitName)
349
+ : "",
350
+ normalizedFailure: normalized,
351
+ readyElapsedMs: null,
352
+ }),
247
353
  failure,
248
- pid: diagnostics.pid ?? null,
354
+ health: "failed",
355
+ lastStartSummary: normalized.summary,
356
+ pid: handle?.pid ?? null,
357
+ preparation,
358
+ sanitizedDiagnostics: normalized.sanitized ?? null,
249
359
  startedAt: nowIso(),
250
360
  state: "failed",
251
- unitName: context.unitName,
361
+ systemdScope: options.systemd?.scope ?? null,
362
+ unitName: options.systemd?.unitName ?? null,
252
363
  updatedAt: nowIso(),
253
364
  };
254
- await writeSessionRecord(record, context.paths.recordPath);
255
- await writeDiagnosticsFile(diagnostics, context.paths);
256
- log.fail("session", "code-server session failed to start", {
257
- code: failure.code,
258
- message: failure.message,
259
- sessionKey: options.sessionKey,
260
- });
365
+ await writeSessionRecord(record, paths.recordPath);
366
+ await writeDiagnosticsFile(record, paths);
261
367
  if (isCodeServerKitError(error)) {
262
368
  throw error;
263
369
  }
264
370
  throw new CodeServerSessionLifecycleError("Could not start the code-server session.", {
265
- cause: normalized.message,
266
- sessionKey: options.sessionKey,
267
- stateRoot: options.stateRoot,
371
+ cause: normalized.summary,
372
+ sessionKey,
373
+ stateRoot,
268
374
  });
269
375
  }
270
376
  }
271
- async function stopCodeServerSessionInternal(options, handles) {
377
+ async function stopCodeServerSessionInternal(options) {
272
378
  const log = resolveLogger(options.logger, options.loggerAdapter);
273
379
  const paths = getSessionPaths(options.stateRoot, options.sessionKey);
274
380
  const record = await readJsonFile(paths.recordPath);
@@ -278,369 +384,243 @@ async function stopCodeServerSessionInternal(options, handles) {
278
384
  launchStrategy: record.launchStrategy,
279
385
  sessionKey: options.sessionKey,
280
386
  });
281
- await stopExistingRuntime(record, options.profile, handles, options.logger, options.loggerAdapter, options.signal);
282
- const diagnostics = await buildStopDiagnostics(record, handles, paths);
387
+ await stopExistingRuntime(record, options.profile, options.signal, options.logger, options.loggerAdapter);
283
388
  const stoppedRecord = {
284
389
  ...record,
285
- diagnostics,
390
+ health: "stopped",
286
391
  pid: null,
287
392
  state: "stopped",
288
393
  stoppedAt: nowIso(),
289
394
  updatedAt: nowIso(),
290
395
  };
291
396
  await writeSessionRecord(stoppedRecord, paths.recordPath);
292
- await writeDiagnosticsFile(diagnostics, paths);
293
- const status = await recordToStatus(stoppedRecord, paths, handles);
397
+ await writeDiagnosticsFile(stoppedRecord, paths);
294
398
  return {
295
- diagnostics: status.diagnostics,
399
+ diagnostics: await readCodeServerSessionDiagnostics({
400
+ sessionKey: options.sessionKey,
401
+ stateRoot: options.stateRoot,
402
+ }),
296
403
  signal: options.signal,
297
- status,
404
+ status: await probeSessionRecord(stoppedRecord, options.sanitizer),
298
405
  stopped: true,
299
406
  };
300
407
  }
301
- async function getCodeServerSessionStatusInternal(options, handles) {
302
- const log = resolveLogger(options.logger, options.loggerAdapter);
408
+ async function getCodeServerSessionStatusInternal(options) {
303
409
  const paths = getSessionPaths(options.stateRoot, options.sessionKey);
304
410
  const record = await readJsonFile(paths.recordPath);
305
411
  if (!record)
306
412
  return null;
307
- const status = await probeSessionRecord(record, handles, paths);
308
- log.info("session", "read code-server session status", {
309
- ready: status.ready,
310
- sessionKey: options.sessionKey,
311
- state: status.state,
312
- });
313
- return status;
314
- }
315
- async function prepareSessionContext(options, existing) {
316
- const sessionKey = normalizeSessionKey(options.sessionKey);
317
- const stateRoot = path.resolve(options.stateRoot);
318
- const paths = getSessionPaths(stateRoot, sessionKey);
319
- const launchStrategy = options.launchStrategy ?? DEFAULT_LAUNCH_STRATEGY;
320
- const dataRoot = options.dataRoot
321
- ? path.resolve(options.dataRoot)
322
- : path.join(paths.sessionDir, "runtime");
323
- const existingHost = existing?.bindAddr ? extractHost(existing.bindAddr) : undefined;
324
- const plan = await createCodeServerLaunchPlan({
325
- ...options,
326
- dataRoot,
327
- host: options.bindAddr ? undefined : (options.host ?? existingHost),
328
- port: options.bindAddr ? undefined : (options.port ?? existing?.port),
329
- });
330
- const systemdScope = launchStrategy === "systemd"
331
- ? options.systemd?.scope ?? null
332
- : null;
333
- if (launchStrategy === "systemd" && !systemdScope) {
334
- throw new CodeServerInvalidConfigurationError("systemd session launches require an explicit scope of 'user' or 'system'.", {
335
- launchStrategy,
336
- sessionKey,
337
- });
338
- }
339
- const unitName = launchStrategy === "systemd"
340
- ? options.systemd?.unitName ?? buildDefaultCodeServerUnitName(sessionKey)
341
- : null;
342
- const specHash = hashNormalizedSpec({
343
- env: sortEnv({
344
- ...plan.env,
345
- }),
346
- launchStrategy,
347
- plan: {
348
- args: plan.args,
349
- bindAddr: plan.bindAddr,
350
- command: plan.command,
351
- cwd: plan.cwd,
352
- extensionsDir: plan.extensionsDir,
353
- trustedOrigins: plan.trustedOrigins,
354
- userDataDir: plan.userDataDir,
355
- workspacePath: plan.workspacePath,
356
- },
357
- profile: normalizeProfileConfig(options.profile),
358
- systemd: launchStrategy === "systemd"
359
- ? {
360
- extraProperties: options.systemd?.extraProperties ?? [],
361
- scope: systemdScope,
362
- unitName,
363
- }
364
- : null,
365
- });
366
- return {
367
- launchStrategy,
368
- paths,
369
- plan,
370
- sessionKey,
371
- specHash,
372
- stateRoot,
373
- systemdScope,
374
- unitName,
375
- };
376
- }
377
- async function startDirectSession(context, handles) {
378
- const stdoutTail = createTailBuffer();
379
- const stderrTail = createTailBuffer();
380
- const handle = await launchCodeServerProcess({
381
- plan: context.plan,
382
- stderr(text) {
383
- stderrTail.push(text);
384
- },
385
- stdout(text) {
386
- stdoutTail.push(text);
387
- },
388
- });
389
- handles.set(context.sessionKey, handle);
390
- return {
391
- failureProbe: null,
392
- handle: decorateHandleWithTails(handle, stdoutTail, stderrTail),
393
- systemdStatus: null,
394
- unitName: null,
395
- };
413
+ return await probeSessionRecord(record, options.sanitizer);
396
414
  }
397
- async function startSystemdSession(context, existing) {
398
- const scope = context.systemdScope;
399
- const unitName = context.unitName;
400
- const statusBefore = await safeReadSystemdStatus(scope, unitName);
401
- if (statusBefore && !statusBefore.notFound) {
402
- if (existing?.specHash === context.specHash && statusBefore.reusable) {
403
- return {
404
- failureProbe: createSystemdFailureProbe(scope, unitName),
405
- handle: null,
406
- systemdStatus: statusBefore,
407
- unitName,
408
- };
409
- }
410
- await stopCodeServerSystemdUnit({
411
- resetFailed: true,
412
- scope,
413
- unitName,
414
- });
415
- }
416
- await launchCodeServerWithSystemd({
417
- extraProperties: context.plan.workspacePath ? [] : [],
418
- plan: context.plan,
419
- scope,
420
- sessionKey: context.sessionKey,
421
- unitName,
422
- });
423
- const statusAfter = await safeReadSystemdStatus(scope, unitName);
424
- if (!statusAfter || statusAfter.notFound) {
425
- throw new CodeServerSystemdCollisionError("systemd reported that the launched code-server unit does not exist.", {
426
- scope,
427
- unitName,
428
- });
429
- }
430
- return {
431
- failureProbe: createSystemdFailureProbe(scope, unitName),
432
- handle: null,
433
- systemdStatus: statusAfter,
434
- unitName,
435
- };
436
- }
437
- async function probeSessionRecord(record, handles, paths) {
415
+ async function probeSessionRecord(record, sanitizer) {
438
416
  const diagnostics = await readCodeServerSessionDiagnostics({
439
417
  sessionKey: record.sessionKey,
440
- stateRoot: path.resolve(paths.stateRoot),
418
+ stateRoot: path.dirname(path.dirname(path.dirname(record.userDataDir))),
441
419
  });
442
- if (record.launchStrategy === "systemd" && record.systemdScope && record.unitName) {
443
- const status = await safeReadSystemdStatus(record.systemdScope, record.unitName);
444
- const ready = !!status?.reusable && await canConnect(record.bindAddr, record.port);
445
- return {
446
- bindAddr: record.bindAddr,
447
- diagnostics,
448
- extensionsDir: record.extensionsDir,
449
- failure: record.failure ?? null,
450
- launchStrategy: record.launchStrategy,
451
- pid: status?.execMainPid ?? null,
452
- port: record.port,
453
- ready,
454
- readyAt: ready ? record.readyAt : null,
455
- sessionKey: record.sessionKey,
456
- specHash: record.specHash,
457
- startedAt: record.startedAt,
458
- state: ready ? (record.state === "reusing_existing" ? "reusing_existing" : "ready") : deriveDeadState(record, status),
459
- stoppedAt: record.stoppedAt,
460
- systemdScope: record.systemdScope,
461
- unitName: record.unitName,
462
- updatedAt: record.updatedAt,
463
- userDataDir: record.userDataDir,
464
- workspacePath: record.workspacePath,
465
- };
466
- }
467
- const live = record.pid ? isPidAlive(record.pid) : false;
468
- const ready = live && await canConnect(record.bindAddr, record.port);
469
- const handle = handles.get(record.sessionKey);
420
+ const ready = record.launchStrategy === "systemd"
421
+ ? await probeSystemdReady(record)
422
+ : await probeDirectReady(record);
423
+ const sanitizedDiagnostics = sanitizer && diagnostics?.normalizedFailure
424
+ ? sanitizeCodeServerDiagnostics(diagnostics.normalizedFailure, sanitizer)
425
+ : record.sanitizedDiagnostics ?? null;
470
426
  return {
471
427
  bindAddr: record.bindAddr,
472
428
  diagnostics,
473
429
  extensionsDir: record.extensionsDir,
474
430
  failure: record.failure ?? null,
431
+ health: ready ? "ready" : record.health,
432
+ lastStartSummary: record.lastStartSummary ?? null,
475
433
  launchStrategy: record.launchStrategy,
476
- pid: handle?.pid ?? record.pid,
434
+ pid: record.launchStrategy === "direct"
435
+ ? handles.get(record.sessionKey)?.pid ?? record.pid
436
+ : record.pid,
477
437
  port: record.port,
438
+ preparation: record.preparation ?? null,
478
439
  ready,
479
440
  readyAt: ready ? record.readyAt : null,
441
+ sanitizedDiagnostics,
480
442
  sessionKey: record.sessionKey,
481
443
  specHash: record.specHash,
482
444
  startedAt: record.startedAt,
483
- state: ready ? (record.state === "reusing_existing" ? "reusing_existing" : "ready") : deriveDirectDeadState(record, live),
445
+ state: ready ? record.state : deriveDeadState(record),
484
446
  stoppedAt: record.stoppedAt,
485
- systemdScope: null,
486
- unitName: null,
447
+ systemdScope: record.systemdScope,
448
+ unitName: record.unitName,
487
449
  updatedAt: record.updatedAt,
488
450
  userDataDir: record.userDataDir,
451
+ watchdogMode: record.watchdogMode,
489
452
  workspacePath: record.workspacePath,
490
453
  };
491
454
  }
492
- async function stopExistingRuntime(record, profile, handles, logger, loggerAdapter, signal) {
455
+ async function probeSystemdReady(record) {
456
+ if (!record.systemdScope || !record.unitName)
457
+ return false;
458
+ try {
459
+ const status = await readCodeServerSystemdStatus({
460
+ scope: record.systemdScope,
461
+ unitName: record.unitName,
462
+ });
463
+ return status.reusable && await canConnect(record.bindAddr, record.port);
464
+ }
465
+ catch {
466
+ return false;
467
+ }
468
+ }
469
+ async function probeDirectReady(record) {
470
+ const pid = handles.get(record.sessionKey)?.pid ?? record.pid;
471
+ if (!pid || !isPidAlive(pid))
472
+ return false;
473
+ return await canConnect(record.bindAddr, record.port);
474
+ }
475
+ async function stopExistingRuntime(record, profile, signal, logger, loggerAdapter) {
493
476
  if (record.launchStrategy === "systemd" && record.systemdScope && record.unitName) {
494
- await stopCodeServerSystemdUnit({
477
+ await restartCodeServerSystemdUnit({
495
478
  logger,
496
479
  loggerAdapter,
497
- resetFailed: true,
498
480
  scope: record.systemdScope,
499
481
  unitName: record.unitName,
500
482
  });
501
483
  }
502
- else if (record.pid) {
484
+ else {
503
485
  const handle = handles.get(record.sessionKey);
504
486
  if (handle) {
505
487
  handle.kill(signal ?? "SIGTERM");
506
488
  handles.delete(record.sessionKey);
507
489
  }
508
- else if (isPidAlive(record.pid)) {
490
+ else if (record.pid && isPidAlive(record.pid)) {
509
491
  process.kill(record.pid, signal ?? "SIGTERM");
510
492
  }
511
493
  }
512
494
  await maybePersistProfile(profile, record.userDataDir);
513
495
  }
514
- function createBaseRecord(context) {
496
+ async function maybeRestoreProfile(profile, userDataDir) {
497
+ if (!profile?.restoreFrom)
498
+ return;
499
+ const restorePolicy = profile.restorePolicy ?? "if-missing-or-empty";
500
+ if (restorePolicy === "if-missing-or-empty") {
501
+ const snapshot = await readCodeServerProfileSnapshot({
502
+ items: profile.items,
503
+ pathMap: profile.pathMap,
504
+ rootDir: userDataDir,
505
+ snapshotExtensions: profile.snapshotExtensions,
506
+ });
507
+ if (snapshot.entries.some((entry) => entry.present)) {
508
+ return;
509
+ }
510
+ }
511
+ await syncCodeServerProfile({
512
+ items: profile.items,
513
+ pathMap: profile.pathMap,
514
+ skipMissing: profile.skipMissing,
515
+ skipUnreadable: profile.skipUnreadable,
516
+ sourceDir: profile.restoreFrom,
517
+ targetDir: userDataDir,
518
+ });
519
+ }
520
+ async function maybePersistProfile(profile, userDataDir) {
521
+ if (!profile?.persistTo)
522
+ return;
523
+ const persistPolicy = profile.persistPolicy ?? "if-changed";
524
+ if (persistPolicy === "always") {
525
+ await syncCodeServerProfile({
526
+ items: profile.items,
527
+ pathMap: profile.pathMap,
528
+ skipMissing: profile.skipMissing,
529
+ skipUnreadable: profile.skipUnreadable,
530
+ sourceDir: userDataDir,
531
+ targetDir: profile.persistTo,
532
+ });
533
+ return;
534
+ }
535
+ await persistCodeServerProfileIfChanged({
536
+ items: profile.items,
537
+ pathMap: profile.pathMap,
538
+ signatureMode: profile.signatureMode,
539
+ skipMissing: profile.skipMissing,
540
+ skipUnreadable: profile.skipUnreadable,
541
+ snapshotExtensions: profile.snapshotExtensions,
542
+ sourceDir: userDataDir,
543
+ targetDir: profile.persistTo,
544
+ });
545
+ }
546
+ function createBaseRecord(options) {
515
547
  return {
516
- bindAddr: context.plan.bindAddr,
548
+ bindAddr: options.launchPlan.bindAddr,
517
549
  diagnostics: null,
518
- extensionsDir: context.plan.extensionsDir,
519
- launchStrategy: context.launchStrategy,
550
+ extensionsDir: options.launchPlan.extensionsDir,
551
+ health: "starting",
552
+ lastStartSummary: options.lastStartSummary,
553
+ launchStrategy: options.launchStrategy,
520
554
  pid: null,
521
- port: context.plan.port,
555
+ port: options.launchPlan.port,
556
+ preparation: options.preparation,
522
557
  readyAt: null,
523
- sessionKey: context.sessionKey,
524
- specHash: context.specHash,
558
+ sanitizedDiagnostics: null,
559
+ sessionKey: options.sessionKey,
560
+ specHash: options.specHash,
525
561
  startedAt: null,
526
562
  state: "planned",
527
563
  stoppedAt: null,
528
- systemdScope: context.systemdScope,
529
- trustedOrigins: [...context.plan.trustedOrigins],
530
- unitName: context.unitName,
564
+ systemdScope: null,
565
+ trustedOrigins: [...options.launchPlan.trustedOrigins],
566
+ unitName: null,
531
567
  updatedAt: nowIso(),
532
- userDataDir: context.plan.userDataDir,
533
- workspacePath: context.plan.workspacePath,
568
+ userDataDir: options.launchPlan.userDataDir,
569
+ watchdogMode: options.watchdogMode,
570
+ workspacePath: options.launchPlan.workspacePath,
534
571
  };
535
572
  }
536
- async function buildDiagnosticsSnapshot(context, handle, readyElapsedMs, systemdStatus) {
537
- const summary = {
538
- bindAddr: context.plan.bindAddr,
539
- launchStrategy: context.launchStrategy,
540
- port: context.plan.port,
541
- };
542
- if (handle) {
543
- summary.pid = handle.pid ?? null;
544
- return {
545
- pid: handle.pid ?? null,
546
- readyElapsedMs,
547
- stderrTail: trimTail(handle.getStderr()),
548
- stdoutTail: trimTail(handle.getStdout()),
549
- summary,
550
- updatedAt: nowIso(),
551
- };
552
- }
553
- const journalTail = context.systemdScope && context.unitName
554
- ? await safeReadSystemdJournal(context.systemdScope, context.unitName)
555
- : "";
556
- if (systemdStatus) {
557
- summary.activeState = systemdStatus.activeState;
558
- summary.subState = systemdStatus.subState;
559
- }
573
+ function createDiagnosticsSnapshot(options) {
560
574
  return {
561
- activeState: systemdStatus?.activeState ?? null,
562
- journalTail,
563
- pid: systemdStatus?.execMainPid ?? null,
564
- readyElapsedMs,
565
- subState: systemdStatus?.subState ?? null,
566
- summary,
567
- unitName: context.unitName,
568
- updatedAt: nowIso(),
569
- };
570
- }
571
- async function buildFailureDiagnostics(context, handle) {
572
- if (handle) {
573
- return {
574
- pid: handle.pid ?? null,
575
- stderrTail: trimTail(handle.getStderr()),
576
- stdoutTail: trimTail(handle.getStdout()),
577
- summary: {
578
- bindAddr: context.plan.bindAddr,
579
- launchStrategy: context.launchStrategy,
580
- port: context.plan.port,
581
- },
582
- updatedAt: nowIso(),
583
- };
584
- }
585
- const journalTail = context.systemdScope && context.unitName
586
- ? await safeReadSystemdJournal(context.systemdScope, context.unitName)
587
- : "";
588
- const status = context.systemdScope && context.unitName
589
- ? await safeReadSystemdStatus(context.systemdScope, context.unitName)
590
- : null;
591
- return {
592
- activeState: status?.activeState ?? null,
593
- journalTail,
594
- pid: status?.execMainPid ?? null,
595
- subState: status?.subState ?? null,
575
+ journalTail: options.journalTail || undefined,
576
+ pid: options.handle?.pid ?? null,
577
+ readyElapsedMs: options.readyElapsedMs,
578
+ stderrTail: options.handle?.getStderr(),
579
+ stdoutTail: options.handle?.getStdout(),
596
580
  summary: {
597
- bindAddr: context.plan.bindAddr,
598
- launchStrategy: context.launchStrategy,
599
- port: context.plan.port,
581
+ category: options.normalizedFailure.category,
582
+ details: options.normalizedFailure.details,
583
+ summary: options.normalizedFailure.summary,
584
+ watchdogMode: options.normalizedFailure.watchdogMode,
600
585
  },
601
- unitName: context.unitName,
602
586
  updatedAt: nowIso(),
603
587
  };
604
588
  }
605
- async function buildStopDiagnostics(record, handles, paths) {
606
- const existing = await readCodeServerSessionDiagnostics({
607
- sessionKey: record.sessionKey,
608
- stateRoot: paths.stateRoot,
609
- });
610
- const handle = handles.get(record.sessionKey);
611
- return {
612
- journalTail: existing?.journalTail,
613
- pid: handle?.pid ?? record.pid ?? null,
614
- readyElapsedMs: existing?.readyElapsedMs ?? null,
615
- stderrTail: trimTail(handle?.getStderr() ?? existing?.stderrTail ?? ""),
616
- stdoutTail: trimTail(handle?.getStdout() ?? existing?.stdoutTail ?? ""),
617
- summary: existing?.summary ?? {},
618
- updatedAt: nowIso(),
619
- };
620
- }
621
- async function writeDiagnosticsFile(snapshot, paths) {
622
- if (!snapshot)
623
- return;
589
+ async function writeDiagnosticsFile(record, paths) {
624
590
  await mkdirp(path.dirname(paths.diagnosticsPath));
591
+ const snapshot = record.diagnostics;
625
592
  const diagnostics = {
626
593
  diagnosticsPath: paths.diagnosticsPath,
627
- journalTail: snapshot.journalTail,
628
- readyElapsedMs: snapshot.readyElapsedMs ?? null,
594
+ journalTail: snapshot?.journalTail,
595
+ normalizedFailure: snapshot?.summary
596
+ ? {
597
+ category: String(snapshot.summary.category ?? "unknown"),
598
+ }
599
+ : null,
600
+ readyElapsedMs: snapshot?.readyElapsedMs ?? null,
629
601
  recordPath: paths.recordPath,
630
- stderrTail: snapshot.stderrTail,
631
- stdoutTail: snapshot.stdoutTail,
632
- summary: snapshot.summary ?? {},
633
- updatedAt: snapshot.updatedAt,
602
+ stderrTail: snapshot?.stderrTail,
603
+ stdoutTail: snapshot?.stdoutTail,
604
+ summary: snapshot?.summary ?? {},
605
+ updatedAt: snapshot?.updatedAt ?? nowIso(),
634
606
  };
607
+ if (snapshot?.summary) {
608
+ const summary = snapshot.summary;
609
+ diagnostics.normalizedFailure = {
610
+ category: String(summary.category ?? "unknown"),
611
+ code: String(summary.category ?? "unknown"),
612
+ details: summary.details ?? {},
613
+ launchStrategy: record.launchStrategy,
614
+ summary: String(summary.summary ?? ""),
615
+ watchdogMode: record.watchdogMode,
616
+ };
617
+ }
635
618
  await fs.promises.writeFile(paths.diagnosticsPath, `${JSON.stringify(diagnostics, null, 2)}\n`, "utf8");
636
619
  }
637
620
  async function writeSessionRecord(record, recordPath) {
638
621
  await mkdirp(path.dirname(recordPath));
639
622
  await fs.promises.writeFile(recordPath, `${JSON.stringify(record, null, 2)}\n`, "utf8");
640
623
  }
641
- async function recordToStatus(record, paths, handles) {
642
- return await probeSessionRecord(record, handles, paths);
643
- }
644
624
  function getSessionPaths(stateRoot, sessionKey) {
645
625
  const normalizedStateRoot = path.resolve(stateRoot);
646
626
  const safeKey = normalizeSessionKey(sessionKey);
@@ -659,118 +639,38 @@ function normalizeSessionKey(value) {
659
639
  }
660
640
  return normalized.replace(/[^A-Za-z0-9._-]+/g, "-");
661
641
  }
662
- function sortEnv(value) {
663
- const entries = Object.entries(value)
664
- .filter(([, current]) => current !== undefined)
665
- .sort(([left], [right]) => left.localeCompare(right));
666
- return Object.fromEntries(entries);
667
- }
668
642
  function normalizeProfileConfig(profile) {
669
643
  if (!profile)
670
644
  return null;
671
645
  return {
672
646
  items: [...(profile.items ?? [])].sort(),
673
647
  pathMap: profile.pathMap ?? {},
648
+ persistPolicy: profile.persistPolicy ?? "if-changed",
674
649
  persistTo: profile.persistTo ? path.resolve(profile.persistTo) : null,
675
650
  restoreFrom: profile.restoreFrom ? path.resolve(profile.restoreFrom) : null,
676
- skipMissing: profile.skipMissing ?? true,
677
- skipUnreadable: profile.skipUnreadable ?? true,
651
+ restorePolicy: profile.restorePolicy ?? "if-missing-or-empty",
652
+ signatureMode: profile.signatureMode ?? "content-hash",
653
+ snapshotExtensions: profile.snapshotExtensions ?? false,
678
654
  };
679
655
  }
680
656
  function hashNormalizedSpec(value) {
681
657
  return createHash("sha256")
682
- .update(stableStringify(value))
658
+ .update(JSON.stringify(value))
683
659
  .digest("hex");
684
660
  }
685
- function stableStringify(value) {
686
- if (Array.isArray(value)) {
687
- return `[${value.map((item) => stableStringify(item)).join(",")}]`;
688
- }
689
- if (value && typeof value === "object") {
690
- const entries = Object.entries(value)
691
- .sort(([left], [right]) => left.localeCompare(right))
692
- .map(([key, current]) => `${JSON.stringify(key)}:${stableStringify(current)}`);
693
- return `{${entries.join(",")}}`;
694
- }
695
- return JSON.stringify(value);
696
- }
697
- async function maybeRestoreProfile(profile, userDataDir) {
698
- if (!profile?.restoreFrom)
699
- return;
700
- await syncCodeServerProfile({
701
- items: profile.items,
702
- pathMap: profile.pathMap,
703
- skipMissing: profile.skipMissing,
704
- skipUnreadable: profile.skipUnreadable,
705
- sourceDir: profile.restoreFrom,
706
- targetDir: userDataDir,
707
- });
708
- }
709
- async function maybePersistProfile(profile, userDataDir) {
710
- if (!profile?.persistTo)
711
- return;
712
- await syncCodeServerProfile({
713
- items: profile.items,
714
- pathMap: profile.pathMap,
715
- skipMissing: profile.skipMissing,
716
- skipUnreadable: profile.skipUnreadable,
717
- sourceDir: userDataDir,
718
- targetDir: profile.persistTo,
719
- });
720
- }
721
- function isLiveState(state) {
722
- return state === "planned" || state === "launching" || state === "ready" || state === "reusing_existing";
723
- }
724
- function deriveDeadState(record, status) {
725
- if (status?.failed)
726
- return "failed";
727
- if (record.state === "stopped")
728
- return "stopped";
729
- return "stale";
730
- }
731
- function deriveDirectDeadState(record, live) {
732
- if (live)
733
- return record.state;
661
+ function deriveDeadState(record) {
734
662
  if (record.state === "failed")
735
663
  return "failed";
736
664
  if (record.state === "stopped")
737
665
  return "stopped";
738
666
  return "stale";
739
667
  }
740
- function createSystemdFailureProbe(scope, unitName) {
741
- return async () => {
742
- const status = await safeReadSystemdStatus(scope, unitName);
743
- if (!status)
744
- return null;
745
- if (status.failed) {
746
- return {
747
- code: "systemd_unit_failed",
748
- details: {
749
- activeState: status.activeState,
750
- result: status.result,
751
- subState: status.subState,
752
- unitName,
753
- },
754
- message: "systemd reported that the code-server unit failed during startup.",
755
- };
756
- }
757
- return null;
758
- };
759
- }
760
- async function safeReadSystemdStatus(scope, unitName) {
761
- try {
762
- return await readCodeServerSystemdStatus({
763
- scope,
764
- unitName,
765
- });
766
- }
767
- catch {
768
- return null;
769
- }
668
+ function isLiveOrStartingState(state) {
669
+ return state === "launching" || state === "planned" || state === "ready" || state === "reusing_existing";
770
670
  }
771
- async function safeReadSystemdJournal(scope, unitName) {
671
+ async function safeSystemdSummary(scope, unitName) {
772
672
  try {
773
- return await readCodeServerSystemdJournal({
673
+ return await summarizeCodeServerSystemdJournal({
774
674
  scope,
775
675
  unitName,
776
676
  });
@@ -816,33 +716,6 @@ function extractHost(bindAddr) {
816
716
  }
817
717
  return bindAddr.slice(0, bindAddr.lastIndexOf(":"));
818
718
  }
819
- function createTailBuffer(limit = DEFAULT_TAIL_LENGTH) {
820
- let text = "";
821
- return {
822
- push(next) {
823
- text = trimTail(`${text}${next}`, limit);
824
- },
825
- value() {
826
- return text;
827
- },
828
- };
829
- }
830
- function decorateHandleWithTails(handle, stdoutTail, stderrTail) {
831
- return {
832
- ...handle,
833
- getStderr() {
834
- return stderrTail.value() || handle.getStderr();
835
- },
836
- getStdout() {
837
- return stdoutTail.value() || handle.getStdout();
838
- },
839
- };
840
- }
841
- function trimTail(value, limit = DEFAULT_TAIL_LENGTH) {
842
- return value.length > limit
843
- ? value.slice(value.length - limit)
844
- : value;
845
- }
846
719
  async function readJsonFile(filePath) {
847
720
  try {
848
721
  const contents = await fs.promises.readFile(filePath, "utf8");
@@ -861,27 +734,36 @@ async function mkdirp(dirPath) {
861
734
  function nowIso() {
862
735
  return new Date().toISOString();
863
736
  }
864
- function createStoppedPlaceholderStatus(options) {
737
+ function createEmptyStopResult(sessionKey) {
865
738
  return {
866
- bindAddr: "",
867
739
  diagnostics: null,
868
- extensionsDir: "",
869
- failure: null,
870
- launchStrategy: "direct",
871
- pid: null,
872
- port: 0,
873
- ready: false,
874
- readyAt: null,
875
- sessionKey: options.sessionKey,
876
- specHash: "",
877
- startedAt: null,
878
- state: "stopped",
879
- stoppedAt: null,
880
- systemdScope: null,
881
- unitName: null,
882
- updatedAt: nowIso(),
883
- userDataDir: "",
884
- workspacePath: null,
740
+ status: {
741
+ bindAddr: "",
742
+ diagnostics: null,
743
+ extensionsDir: "",
744
+ failure: null,
745
+ health: "stopped",
746
+ lastStartSummary: null,
747
+ launchStrategy: "direct",
748
+ pid: null,
749
+ port: 0,
750
+ preparation: null,
751
+ ready: false,
752
+ readyAt: null,
753
+ sanitizedDiagnostics: null,
754
+ sessionKey,
755
+ specHash: "",
756
+ startedAt: null,
757
+ state: "stopped",
758
+ stoppedAt: nowIso(),
759
+ systemdScope: null,
760
+ unitName: null,
761
+ updatedAt: nowIso(),
762
+ userDataDir: "",
763
+ watchdogMode: "disabled_fallback",
764
+ workspacePath: null,
765
+ },
766
+ stopped: false,
885
767
  };
886
768
  }
887
769
  export { createCodeServerSessionManager, getCodeServerSessionStatus, readCodeServerSessionDiagnostics, restartCodeServerSession, startCodeServerSession, stopCodeServerSession, };