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