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