@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.
- package/CHANGELOG.md +15 -0
- package/README.md +232 -188
- package/dist/diagnostics.d.ts +6 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +150 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/errors.d.ts +5 -2
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +7 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +9 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -6
- package/dist/index.js.map +1 -1
- package/dist/package-resolution.d.ts +3 -0
- package/dist/package-resolution.d.ts.map +1 -0
- package/dist/package-resolution.js +44 -0
- package/dist/package-resolution.js.map +1 -0
- package/dist/plan.d.ts +3 -2
- package/dist/plan.d.ts.map +1 -1
- package/dist/plan.js +95 -86
- package/dist/plan.js.map +1 -1
- package/dist/preparation.d.ts +5 -0
- package/dist/preparation.d.ts.map +1 -0
- package/dist/preparation.js +175 -0
- package/dist/preparation.js.map +1 -0
- package/dist/profile.d.ts +5 -2
- package/dist/profile.d.ts.map +1 -1
- package/dist/profile.js +122 -1
- package/dist/profile.js.map +1 -1
- package/dist/proxy.d.ts +4 -2
- package/dist/proxy.d.ts.map +1 -1
- package/dist/proxy.js +62 -1
- package/dist/proxy.js.map +1 -1
- package/dist/resolve.d.ts.map +1 -1
- package/dist/resolve.js +44 -29
- package/dist/resolve.js.map +1 -1
- package/dist/session.d.ts +3 -3
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +427 -533
- package/dist/session.js.map +1 -1
- package/dist/spec.d.ts +3 -3
- package/dist/spec.d.ts.map +1 -1
- package/dist/spec.js +2 -33
- package/dist/spec.js.map +1 -1
- package/dist/systemd.d.ts +5 -2
- package/dist/systemd.d.ts.map +1 -1
- package/dist/systemd.js +57 -6
- package/dist/systemd.js.map +1 -1
- package/dist/types.d.ts +218 -68
- package/dist/types.d.ts.map +1 -1
- 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,
|
|
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 {
|
|
10
|
+
import { ensureCodeServerPrepared } from "./preparation.js";
|
|
11
|
+
import { persistCodeServerProfileIfChanged, readCodeServerProfileSnapshot, syncCodeServerProfile, } from "./profile.js";
|
|
10
12
|
import { waitForCodeServerReady } from "./readiness.js";
|
|
11
|
-
import {
|
|
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
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
118
|
-
const
|
|
119
|
-
|
|
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
|
|
122
|
-
sessionKey
|
|
123
|
-
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
|
|
129
|
-
if (existing.specHash ===
|
|
203
|
+
const status = await probeSessionRecord(existing, options.sanitizer);
|
|
204
|
+
if (existing.specHash === specHash && status.ready) {
|
|
130
205
|
const reused = {
|
|
131
|
-
...
|
|
206
|
+
...status,
|
|
132
207
|
state: "reusing_existing",
|
|
133
208
|
};
|
|
134
209
|
await writeSessionRecord({
|
|
135
210
|
...existing,
|
|
136
|
-
|
|
211
|
+
health: "ready",
|
|
212
|
+
preparation,
|
|
137
213
|
state: "reusing_existing",
|
|
138
214
|
updatedAt: nowIso(),
|
|
139
|
-
},
|
|
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(
|
|
148
|
-
launchPlan
|
|
149
|
-
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 (
|
|
155
|
-
await stopExistingRuntime(existing, options.profile,
|
|
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
|
-
|
|
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
|
-
|
|
243
|
+
updatedAt: nowIso(),
|
|
244
|
+
}, paths.recordPath);
|
|
177
245
|
try {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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:
|
|
183
|
-
host:
|
|
184
|
-
port:
|
|
185
|
-
process:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
310
|
+
systemdScope: options.systemd?.scope ?? null,
|
|
311
|
+
unitName: options.systemd?.unitName ?? null,
|
|
198
312
|
updatedAt: nowIso(),
|
|
199
313
|
};
|
|
200
|
-
await writeSessionRecord(record,
|
|
201
|
-
await writeDiagnosticsFile(
|
|
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:
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
|
339
|
+
code: normalized.code,
|
|
217
340
|
details: normalized.details,
|
|
218
|
-
message: normalized.
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
370
|
+
systemdScope: options.systemd?.scope ?? null,
|
|
371
|
+
unitName: options.systemd?.unitName ?? null,
|
|
252
372
|
updatedAt: nowIso(),
|
|
253
373
|
};
|
|
254
|
-
await writeSessionRecord(record,
|
|
255
|
-
await writeDiagnosticsFile(
|
|
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.
|
|
266
|
-
sessionKey
|
|
267
|
-
stateRoot
|
|
380
|
+
cause: normalized.summary,
|
|
381
|
+
sessionKey,
|
|
382
|
+
stateRoot,
|
|
268
383
|
});
|
|
269
384
|
}
|
|
270
385
|
}
|
|
271
|
-
async function stopCodeServerSessionInternal(options
|
|
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,
|
|
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
|
-
|
|
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(
|
|
293
|
-
const status = await recordToStatus(stoppedRecord, paths, handles);
|
|
406
|
+
await writeDiagnosticsFile(stoppedRecord, paths);
|
|
294
407
|
return {
|
|
295
|
-
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
429
|
+
stateRoot: path.dirname(path.dirname(path.dirname(record.userDataDir))),
|
|
441
430
|
});
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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:
|
|
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 ?
|
|
456
|
+
state: ready ? record.state : deriveDeadState(record),
|
|
484
457
|
stoppedAt: record.stoppedAt,
|
|
485
|
-
systemdScope:
|
|
486
|
-
unitName:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
559
|
+
bindAddr: options.launchPlan.bindAddr,
|
|
517
560
|
diagnostics: null,
|
|
518
|
-
extensionsDir:
|
|
519
|
-
|
|
561
|
+
extensionsDir: options.launchPlan.extensionsDir,
|
|
562
|
+
health: "starting",
|
|
563
|
+
lastStartSummary: options.lastStartSummary,
|
|
564
|
+
launchStrategy: options.launchStrategy,
|
|
520
565
|
pid: null,
|
|
521
|
-
port:
|
|
566
|
+
port: options.launchPlan.port,
|
|
567
|
+
preparation: options.preparation,
|
|
522
568
|
readyAt: null,
|
|
523
|
-
|
|
524
|
-
|
|
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:
|
|
529
|
-
trustedOrigins: [...
|
|
530
|
-
unitName:
|
|
575
|
+
systemdScope: null,
|
|
576
|
+
trustedOrigins: [...options.launchPlan.trustedOrigins],
|
|
577
|
+
unitName: null,
|
|
531
578
|
updatedAt: nowIso(),
|
|
532
|
-
userDataDir:
|
|
533
|
-
|
|
579
|
+
userDataDir: options.launchPlan.userDataDir,
|
|
580
|
+
watchdogMode: options.watchdogMode,
|
|
581
|
+
workspacePath: options.launchPlan.workspacePath,
|
|
534
582
|
};
|
|
535
583
|
}
|
|
536
|
-
|
|
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
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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
|
-
|
|
598
|
-
|
|
599
|
-
|
|
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
|
|
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
|
|
628
|
-
|
|
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
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
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
|
-
|
|
677
|
-
|
|
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(
|
|
670
|
+
.update(JSON.stringify(value))
|
|
683
671
|
.digest("hex");
|
|
684
672
|
}
|
|
685
|
-
function
|
|
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
|
|
741
|
-
return
|
|
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
|
|
683
|
+
async function safeSystemdSummary(scope, unitName) {
|
|
772
684
|
try {
|
|
773
|
-
return await
|
|
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
|
|
749
|
+
function createEmptyStopResult(sessionKey) {
|
|
865
750
|
return {
|
|
866
|
-
bindAddr: "",
|
|
867
751
|
diagnostics: null,
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
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, };
|