nterminal 1.2.72 → 1.2.74
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/assets/brand/vscode.png +0 -0
- package/dist/client/assets/MarkdownPreview-CBeK-BXz.js +1 -0
- package/dist/client/assets/{TranscriptMarkdownBody-D39ylGl_.js → TranscriptMarkdownBody-DGwLuoPV.js} +1 -1
- package/dist/client/assets/{index-DO9sCZ_P.js → index-CQcvJBTH.js} +1 -1
- package/dist/client/assets/index-Cf49B48Z.js +57 -0
- package/dist/client/assets/index-DwmyYdjx.css +1 -0
- package/dist/client/brand/vscode.png +0 -0
- package/dist/client/index.html +2 -2
- package/dist/server/agent/agentProxy.d.ts +1 -0
- package/dist/server/agent/agentProxy.js +3 -2
- package/dist/server/agent/agentProxy.js.map +1 -1
- package/dist/server/code/codeServerManager.d.ts +81 -0
- package/dist/server/code/codeServerManager.js +764 -0
- package/dist/server/code/codeServerManager.js.map +1 -0
- package/dist/server/http.d.ts +2 -0
- package/dist/server/http.js +12 -0
- package/dist/server/http.js.map +1 -1
- package/dist/server/index.js +4 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/routes/agentManagementRoutes.js +397 -0
- package/dist/server/routes/agentManagementRoutes.js.map +1 -1
- package/dist/server/routes/codeServerRoutes.d.ts +13 -0
- package/dist/server/routes/codeServerRoutes.js +541 -0
- package/dist/server/routes/codeServerRoutes.js.map +1 -0
- package/dist/server/routes/webSocketRelay.d.ts +12 -0
- package/dist/server/routes/webSocketRelay.js +121 -0
- package/dist/server/routes/webSocketRelay.js.map +1 -0
- package/dist/server/terminal/TerminalManager.d.ts +1 -1
- package/dist/server/terminal/TerminalManager.js +30 -7
- package/dist/server/terminal/TerminalManager.js.map +1 -1
- package/dist/server/terminal/agentTranscriptStatus.d.ts +7 -0
- package/dist/server/terminal/agentTranscriptStatus.js +141 -44
- package/dist/server/terminal/agentTranscriptStatus.js.map +1 -1
- package/dist/shared/protocol.d.ts +49 -0
- package/dist/shared/protocol.js.map +1 -1
- package/dist/shared/types.d.ts +11 -0
- package/package.json +1 -1
- package/public/brand/vscode.png +0 -0
- package/dist/client/assets/MarkdownPreview-CLmBLNLL.js +0 -1
- package/dist/client/assets/index-C6Bpyyau.css +0 -1
- package/dist/client/assets/index-qz7KPVpf.js +0 -55
|
@@ -0,0 +1,764 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { createReadStream } from 'node:fs';
|
|
3
|
+
import { createServer } from 'node:net';
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import { constants as fsConstants } from 'node:fs';
|
|
6
|
+
import { access, lstat, mkdir, readFile, realpath, rename, rm, symlink, writeFile } from 'node:fs/promises';
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
const codeServerRepoLatestReleaseUrl = 'https://api.github.com/repos/coder/code-server/releases/latest';
|
|
10
|
+
const codeServerDownloadBaseUrl = 'https://github.com/coder/code-server/releases/download';
|
|
11
|
+
const codeServerIdleTimeoutSeconds = 60 * 60;
|
|
12
|
+
const installLogLimit = 400;
|
|
13
|
+
const startupLogLineLimit = 12;
|
|
14
|
+
const healthCheckTimeoutMs = 30_000;
|
|
15
|
+
const healthCheckIntervalMs = 500;
|
|
16
|
+
const idleSweepIntervalMs = 60_000;
|
|
17
|
+
const titleLockTtlMs = 24 * 60 * 60 * 1000;
|
|
18
|
+
export class CodeServerError extends Error {
|
|
19
|
+
statusCode;
|
|
20
|
+
code;
|
|
21
|
+
constructor(statusCode, code, message = code) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.statusCode = statusCode;
|
|
24
|
+
this.code = code;
|
|
25
|
+
this.name = 'CodeServerError';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export class CodeServerManager {
|
|
29
|
+
config;
|
|
30
|
+
terminalManager;
|
|
31
|
+
addonRoot;
|
|
32
|
+
versionsDir;
|
|
33
|
+
tmpDir;
|
|
34
|
+
currentPath;
|
|
35
|
+
extensionsDir;
|
|
36
|
+
userDataRoot;
|
|
37
|
+
instances = new Map();
|
|
38
|
+
titleLocks = new Map();
|
|
39
|
+
idleSweepTimer;
|
|
40
|
+
job = null;
|
|
41
|
+
constructor(config, terminalManager) {
|
|
42
|
+
this.config = config;
|
|
43
|
+
this.terminalManager = terminalManager;
|
|
44
|
+
const stateDir = path.dirname(config.statePath);
|
|
45
|
+
this.addonRoot = path.join(stateDir, 'addons', 'code-server');
|
|
46
|
+
this.versionsDir = path.join(this.addonRoot, 'versions');
|
|
47
|
+
this.tmpDir = path.join(this.addonRoot, 'tmp');
|
|
48
|
+
this.currentPath = path.join(this.addonRoot, 'current');
|
|
49
|
+
this.extensionsDir = path.join(this.addonRoot, 'extensions');
|
|
50
|
+
this.userDataRoot = path.join(this.addonRoot, 'user-data');
|
|
51
|
+
this.idleSweepTimer = setInterval(() => this.stopIdleInstances(), idleSweepIntervalMs);
|
|
52
|
+
this.idleSweepTimer.unref?.();
|
|
53
|
+
}
|
|
54
|
+
async status(options = {}) {
|
|
55
|
+
const support = standaloneReleaseSupport();
|
|
56
|
+
const version = await this.installedVersion();
|
|
57
|
+
const latestVersion = options.checkLatest && support.supported ? await this.latestVersion().catch(() => null) : undefined;
|
|
58
|
+
return {
|
|
59
|
+
supported: support.supported,
|
|
60
|
+
...(support.reason ? { unsupportedReason: support.reason } : {}),
|
|
61
|
+
installed: Boolean(version),
|
|
62
|
+
version,
|
|
63
|
+
installPath: this.currentPath,
|
|
64
|
+
...(latestVersion !== undefined ? { latestVersion, updateAvailable: version && latestVersion ? version !== latestVersion : null } : {}),
|
|
65
|
+
job: this.job,
|
|
66
|
+
instances: this.instanceSummaries()
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
async startInstall(options = { action: 'install' }) {
|
|
70
|
+
if (this.job?.status === 'running') {
|
|
71
|
+
return { job: this.job, status: await this.status() };
|
|
72
|
+
}
|
|
73
|
+
const support = standaloneReleaseSupport();
|
|
74
|
+
if (!support.supported) {
|
|
75
|
+
throw new CodeServerError(409, 'code_server_unsupported_platform', support.reason);
|
|
76
|
+
}
|
|
77
|
+
const job = this.createJob(options.action);
|
|
78
|
+
void this.runInstall(job, options).catch((error) => {
|
|
79
|
+
this.failJob(job, error instanceof Error ? error.message : String(error));
|
|
80
|
+
});
|
|
81
|
+
return { job, status: await this.status() };
|
|
82
|
+
}
|
|
83
|
+
async openForTerminal(terminalId, options = {}) {
|
|
84
|
+
const terminal = this.terminalManager.getTerminal(terminalId);
|
|
85
|
+
if (!terminal) {
|
|
86
|
+
throw new CodeServerError(404, 'terminal_not_found');
|
|
87
|
+
}
|
|
88
|
+
const executable = await this.executablePath();
|
|
89
|
+
if (!executable) {
|
|
90
|
+
throw new CodeServerError(409, 'code_server_not_installed', 'Install the code-server add-on first.');
|
|
91
|
+
}
|
|
92
|
+
const liveCwd = await this.terminalManager.resolveTerminalCwd(terminalId);
|
|
93
|
+
if (!liveCwd) {
|
|
94
|
+
throw new CodeServerError(409, 'terminal_cwd_unavailable');
|
|
95
|
+
}
|
|
96
|
+
const cwd = await safeRealPath(liveCwd);
|
|
97
|
+
const displayTitle = codeServerDisplayTitle({
|
|
98
|
+
...(options.serverName ? { serverName: options.serverName } : {}),
|
|
99
|
+
tabName: options.tabName ?? terminal.title,
|
|
100
|
+
cwd
|
|
101
|
+
});
|
|
102
|
+
const id = instanceIdForCwd(cwd);
|
|
103
|
+
const proxyBasePath = publicBasePathForInstance(id, options.publicBasePathTemplate);
|
|
104
|
+
const instance = await this.ensureInstance(id, cwd, executable, displayTitle, proxyBasePath);
|
|
105
|
+
const titleToken = this.createTitleLock(instance.id, displayTitle);
|
|
106
|
+
return {
|
|
107
|
+
url: instanceUrlForBasePath(instance.proxyBasePath, { titleToken }),
|
|
108
|
+
instance: this.instanceSummary(instance),
|
|
109
|
+
status: await this.status()
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
getInstance(id) {
|
|
113
|
+
const instance = this.instances.get(id);
|
|
114
|
+
if (!instance || !isChildRunning(instance.process)) {
|
|
115
|
+
if (instance) {
|
|
116
|
+
this.instances.delete(id);
|
|
117
|
+
this.deleteTitleLocksForInstance(id);
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
instance.lastAccessedAt = new Date().toISOString();
|
|
122
|
+
return this.instanceSummary(instance);
|
|
123
|
+
}
|
|
124
|
+
getTitleForInstance(instanceId, titleToken) {
|
|
125
|
+
this.pruneExpiredTitleLocks();
|
|
126
|
+
const instance = this.instances.get(instanceId);
|
|
127
|
+
if (!instance || !isChildRunning(instance.process)) {
|
|
128
|
+
if (instance) {
|
|
129
|
+
this.instances.delete(instanceId);
|
|
130
|
+
this.deleteTitleLocksForInstance(instanceId);
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
const token = titleToken?.trim();
|
|
135
|
+
if (token) {
|
|
136
|
+
const lock = this.titleLocks.get(token);
|
|
137
|
+
if (lock && lock.instanceId === instanceId && lock.expiresAt > Date.now()) {
|
|
138
|
+
return lock.title;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
instance.lastAccessedAt = new Date().toISOString();
|
|
142
|
+
return instance.displayTitle;
|
|
143
|
+
}
|
|
144
|
+
touchInstance(id) {
|
|
145
|
+
const instance = this.instances.get(id);
|
|
146
|
+
if (instance && isChildRunning(instance.process)) {
|
|
147
|
+
instance.lastAccessedAt = new Date().toISOString();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
async stop(instanceId) {
|
|
151
|
+
if (instanceId) {
|
|
152
|
+
const instance = this.instances.get(instanceId);
|
|
153
|
+
if (instance) {
|
|
154
|
+
stopProcess(instance.process);
|
|
155
|
+
this.instances.delete(instanceId);
|
|
156
|
+
this.deleteTitleLocksForInstance(instanceId);
|
|
157
|
+
}
|
|
158
|
+
return this.status();
|
|
159
|
+
}
|
|
160
|
+
for (const instance of this.instances.values()) {
|
|
161
|
+
stopProcess(instance.process);
|
|
162
|
+
}
|
|
163
|
+
this.instances.clear();
|
|
164
|
+
this.titleLocks.clear();
|
|
165
|
+
return this.status();
|
|
166
|
+
}
|
|
167
|
+
async startUninstall() {
|
|
168
|
+
if (this.job?.status === 'running') {
|
|
169
|
+
return { job: this.job, status: await this.status() };
|
|
170
|
+
}
|
|
171
|
+
const job = this.createJob('uninstall');
|
|
172
|
+
void this.runUninstall(job).catch((error) => {
|
|
173
|
+
this.failJob(job, error instanceof Error ? error.message : String(error));
|
|
174
|
+
});
|
|
175
|
+
return { job, status: await this.status() };
|
|
176
|
+
}
|
|
177
|
+
dispose() {
|
|
178
|
+
clearInterval(this.idleSweepTimer);
|
|
179
|
+
for (const instance of this.instances.values()) {
|
|
180
|
+
stopProcess(instance.process);
|
|
181
|
+
}
|
|
182
|
+
this.instances.clear();
|
|
183
|
+
this.titleLocks.clear();
|
|
184
|
+
}
|
|
185
|
+
async ensureInstance(id, cwd, executable, displayTitle, proxyBasePath) {
|
|
186
|
+
const current = this.instances.get(id);
|
|
187
|
+
if (current && isChildRunning(current.process)) {
|
|
188
|
+
if (current.proxyBasePath !== proxyBasePath) {
|
|
189
|
+
stopProcess(current.process);
|
|
190
|
+
this.instances.delete(id);
|
|
191
|
+
this.deleteTitleLocksForInstance(id);
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
current.displayTitle = displayTitle;
|
|
195
|
+
current.lastAccessedAt = new Date().toISOString();
|
|
196
|
+
return current;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (current) {
|
|
200
|
+
this.instances.delete(id);
|
|
201
|
+
}
|
|
202
|
+
await mkdir(this.extensionsDir, { recursive: true, mode: 0o700 });
|
|
203
|
+
const userDataDir = path.join(this.userDataRoot, id);
|
|
204
|
+
await mkdir(userDataDir, { recursive: true, mode: 0o700 });
|
|
205
|
+
const port = await allocateLocalPort();
|
|
206
|
+
const now = new Date().toISOString();
|
|
207
|
+
const child = spawn(executable, [
|
|
208
|
+
'--bind-addr',
|
|
209
|
+
`127.0.0.1:${port}`,
|
|
210
|
+
'--auth',
|
|
211
|
+
'none',
|
|
212
|
+
'--disable-telemetry',
|
|
213
|
+
'--disable-update-check',
|
|
214
|
+
'--abs-proxy-base-path',
|
|
215
|
+
proxyBasePath,
|
|
216
|
+
'--user-data-dir',
|
|
217
|
+
userDataDir,
|
|
218
|
+
'--extensions-dir',
|
|
219
|
+
this.extensionsDir,
|
|
220
|
+
cwd
|
|
221
|
+
], {
|
|
222
|
+
cwd,
|
|
223
|
+
env: buildCodeServerProcessEnv(),
|
|
224
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
225
|
+
});
|
|
226
|
+
const instance = { id, cwd, port, proxyBasePath, process: child, createdAt: now, lastAccessedAt: now, displayTitle };
|
|
227
|
+
this.instances.set(id, instance);
|
|
228
|
+
const startupErrors = [];
|
|
229
|
+
const startupOutput = [];
|
|
230
|
+
const onSpawnError = (error) => {
|
|
231
|
+
startupErrors.push(error);
|
|
232
|
+
};
|
|
233
|
+
child.once('error', onSpawnError);
|
|
234
|
+
child.on('exit', () => {
|
|
235
|
+
if (this.instances.get(id)?.process === child) {
|
|
236
|
+
this.instances.delete(id);
|
|
237
|
+
this.deleteTitleLocksForInstance(id);
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
child.stdout?.on('data', () => undefined);
|
|
241
|
+
child.stderr?.on('data', (chunk) => collectRecentLines(startupOutput, chunk.toString()));
|
|
242
|
+
try {
|
|
243
|
+
await waitForHealthy(port, child, {
|
|
244
|
+
getStartupError: () => startupErrors[0] ?? null,
|
|
245
|
+
getFailureDetail: () => startupOutput.join('\n')
|
|
246
|
+
});
|
|
247
|
+
return instance;
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
if (this.instances.get(id)?.process === child) {
|
|
251
|
+
this.instances.delete(id);
|
|
252
|
+
this.deleteTitleLocksForInstance(id);
|
|
253
|
+
}
|
|
254
|
+
stopProcess(child);
|
|
255
|
+
throw error;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
async runInstall(job, options) {
|
|
259
|
+
await mkdir(this.addonRoot, { recursive: true, mode: 0o700 });
|
|
260
|
+
await mkdir(this.versionsDir, { recursive: true, mode: 0o700 });
|
|
261
|
+
await mkdir(this.tmpDir, { recursive: true, mode: 0o700 });
|
|
262
|
+
const support = standaloneReleaseSupport();
|
|
263
|
+
if (!support.supported || !support.assetPlatform) {
|
|
264
|
+
throw new Error(support.reason ?? 'unsupported platform');
|
|
265
|
+
}
|
|
266
|
+
this.appendJobLog(job, 'Resolving latest code-server release...');
|
|
267
|
+
const release = await this.latestReleaseAsset(support.assetPlatform);
|
|
268
|
+
const version = release.version;
|
|
269
|
+
const finalDir = path.join(this.versionsDir, version);
|
|
270
|
+
if (!options.force && await pathExists(path.join(finalDir, 'bin', 'code-server'))) {
|
|
271
|
+
this.appendJobLog(job, `code-server ${version} is already installed.`);
|
|
272
|
+
await this.pointCurrentAt(finalDir, version);
|
|
273
|
+
this.completeJob(job);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
if (this.instances.size > 0) {
|
|
277
|
+
this.appendJobLog(job, 'Stopping running VS Code sessions before updating...');
|
|
278
|
+
await this.stop();
|
|
279
|
+
}
|
|
280
|
+
const workDir = path.join(this.tmpDir, `${Date.now()}-${randomUUID()}`);
|
|
281
|
+
await mkdir(workDir, { recursive: true, mode: 0o700 });
|
|
282
|
+
const archivePath = path.join(workDir, 'code-server.tar.gz');
|
|
283
|
+
this.appendJobLog(job, `Downloading ${release.archiveName}...`);
|
|
284
|
+
try {
|
|
285
|
+
await runCommand('curl', ['-fL', '--retry', '2', '--connect-timeout', '15', '-o', archivePath, release.downloadUrl], {
|
|
286
|
+
cwd: workDir,
|
|
287
|
+
onOutput: (line) => this.appendJobLog(job, line)
|
|
288
|
+
});
|
|
289
|
+
this.appendJobLog(job, 'Verifying SHA-256 digest...');
|
|
290
|
+
await verifyFileSha256(archivePath, release.sha256);
|
|
291
|
+
this.appendJobLog(job, 'Validating archive paths...');
|
|
292
|
+
await validateTarArchive(archivePath);
|
|
293
|
+
this.appendJobLog(job, 'Extracting archive...');
|
|
294
|
+
await runCommand('tar', ['-xzf', archivePath, '-C', workDir], {
|
|
295
|
+
cwd: workDir,
|
|
296
|
+
onOutput: (line) => this.appendJobLog(job, line)
|
|
297
|
+
});
|
|
298
|
+
const extractedDir = path.join(workDir, `code-server-${version}-${support.assetPlatform}`);
|
|
299
|
+
if (!await pathExists(path.join(extractedDir, 'bin', 'code-server'))) {
|
|
300
|
+
throw new Error('downloaded archive did not contain bin/code-server');
|
|
301
|
+
}
|
|
302
|
+
await rm(finalDir, { recursive: true, force: true });
|
|
303
|
+
await rename(extractedDir, finalDir);
|
|
304
|
+
await this.pointCurrentAt(finalDir, version);
|
|
305
|
+
this.appendJobLog(job, `Installed code-server ${version}.`);
|
|
306
|
+
this.completeJob(job);
|
|
307
|
+
}
|
|
308
|
+
finally {
|
|
309
|
+
await rm(workDir, { recursive: true, force: true });
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
async runUninstall(job) {
|
|
313
|
+
this.appendJobLog(job, 'Stopping code-server instances...');
|
|
314
|
+
await this.stop();
|
|
315
|
+
this.appendJobLog(job, `Removing ${this.addonRoot}...`);
|
|
316
|
+
await rm(this.addonRoot, { recursive: true, force: true });
|
|
317
|
+
this.appendJobLog(job, 'Removed code-server add-on.');
|
|
318
|
+
this.completeJob(job);
|
|
319
|
+
}
|
|
320
|
+
async latestVersion() {
|
|
321
|
+
return (await this.latestRelease()).version;
|
|
322
|
+
}
|
|
323
|
+
async latestReleaseAsset(assetPlatform) {
|
|
324
|
+
const release = await this.latestRelease();
|
|
325
|
+
const archiveName = `code-server-${release.version}-${assetPlatform}.tar.gz`;
|
|
326
|
+
const asset = release.assets.find((candidate) => candidate.name === archiveName);
|
|
327
|
+
if (!asset) {
|
|
328
|
+
throw new Error(`GitHub release did not include ${archiveName}`);
|
|
329
|
+
}
|
|
330
|
+
const downloadUrl = typeof asset.browser_download_url === 'string'
|
|
331
|
+
? asset.browser_download_url
|
|
332
|
+
: `${codeServerDownloadBaseUrl}/v${release.version}/${archiveName}`;
|
|
333
|
+
const sha256 = parseGitHubSha256Digest(asset.digest);
|
|
334
|
+
if (!sha256) {
|
|
335
|
+
throw new Error(`GitHub release asset ${archiveName} did not include a SHA-256 digest`);
|
|
336
|
+
}
|
|
337
|
+
return { version: release.version, archiveName, downloadUrl, sha256 };
|
|
338
|
+
}
|
|
339
|
+
async latestRelease() {
|
|
340
|
+
const response = await fetch(codeServerRepoLatestReleaseUrl, {
|
|
341
|
+
headers: {
|
|
342
|
+
accept: 'application/vnd.github+json',
|
|
343
|
+
'user-agent': 'nterminal-code-server-addon'
|
|
344
|
+
},
|
|
345
|
+
signal: AbortSignal.timeout(20_000)
|
|
346
|
+
});
|
|
347
|
+
if (!response.ok) {
|
|
348
|
+
throw new Error(`GitHub release lookup failed: HTTP ${response.status}`);
|
|
349
|
+
}
|
|
350
|
+
const json = await response.json();
|
|
351
|
+
const tag = typeof json.tag_name === 'string' ? json.tag_name : '';
|
|
352
|
+
if (!/^v\d+\.\d+\.\d+$/.test(tag)) {
|
|
353
|
+
throw new Error('GitHub release lookup returned an invalid tag');
|
|
354
|
+
}
|
|
355
|
+
const assets = Array.isArray(json.assets)
|
|
356
|
+
? json.assets.filter((asset) => asset !== null && typeof asset === 'object')
|
|
357
|
+
: [];
|
|
358
|
+
return { version: tag.slice(1), assets };
|
|
359
|
+
}
|
|
360
|
+
async installedVersion() {
|
|
361
|
+
try {
|
|
362
|
+
const version = (await readFile(path.join(this.currentPath, '.nterminal-code-server-version'), 'utf8')).trim();
|
|
363
|
+
if (/^\d+\.\d+\.\d+$/.test(version)) {
|
|
364
|
+
return version;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
// Fall back to probing the executable below.
|
|
369
|
+
}
|
|
370
|
+
const executable = await this.executablePath();
|
|
371
|
+
if (!executable) {
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
try {
|
|
375
|
+
const result = await runCommand(executable, ['--version'], { timeoutMs: 10_000 });
|
|
376
|
+
const firstLine = result.stdout.split(/\r?\n/).find(Boolean)?.trim() ?? '';
|
|
377
|
+
return /^\d+\.\d+\.\d+$/.test(firstLine) ? firstLine : null;
|
|
378
|
+
}
|
|
379
|
+
catch {
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
async executablePath() {
|
|
384
|
+
const executable = path.join(this.currentPath, 'bin', 'code-server');
|
|
385
|
+
try {
|
|
386
|
+
await access(executable, fsConstants.X_OK);
|
|
387
|
+
return executable;
|
|
388
|
+
}
|
|
389
|
+
catch {
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
async pointCurrentAt(finalDir, version) {
|
|
394
|
+
await writeFile(path.join(finalDir, '.nterminal-code-server-version'), `${version}\n`, { mode: 0o600 });
|
|
395
|
+
const nextLink = path.join(this.addonRoot, `current.${process.pid}.${Date.now()}.tmp`);
|
|
396
|
+
await rm(nextLink, { recursive: true, force: true });
|
|
397
|
+
await symlink(finalDir, nextLink);
|
|
398
|
+
await rm(this.currentPath, { recursive: true, force: true });
|
|
399
|
+
await rename(nextLink, this.currentPath);
|
|
400
|
+
}
|
|
401
|
+
createJob(action) {
|
|
402
|
+
const job = {
|
|
403
|
+
id: randomUUID(),
|
|
404
|
+
action,
|
|
405
|
+
status: 'running',
|
|
406
|
+
startedAt: new Date().toISOString(),
|
|
407
|
+
finishedAt: null,
|
|
408
|
+
logs: []
|
|
409
|
+
};
|
|
410
|
+
this.job = job;
|
|
411
|
+
return job;
|
|
412
|
+
}
|
|
413
|
+
appendJobLog(job, line) {
|
|
414
|
+
const text = line.trim();
|
|
415
|
+
if (!text) {
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
job.logs.push(text);
|
|
419
|
+
if (job.logs.length > installLogLimit) {
|
|
420
|
+
job.logs.splice(0, job.logs.length - installLogLimit);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
completeJob(job) {
|
|
424
|
+
job.status = 'succeeded';
|
|
425
|
+
job.finishedAt = new Date().toISOString();
|
|
426
|
+
}
|
|
427
|
+
failJob(job, message) {
|
|
428
|
+
job.status = 'failed';
|
|
429
|
+
job.finishedAt = new Date().toISOString();
|
|
430
|
+
job.error = message;
|
|
431
|
+
this.appendJobLog(job, message);
|
|
432
|
+
}
|
|
433
|
+
instanceSummaries() {
|
|
434
|
+
const summaries = [];
|
|
435
|
+
for (const [id, instance] of this.instances) {
|
|
436
|
+
if (!isChildRunning(instance.process)) {
|
|
437
|
+
this.instances.delete(id);
|
|
438
|
+
this.deleteTitleLocksForInstance(id);
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
summaries.push(this.instanceSummary(instance));
|
|
442
|
+
}
|
|
443
|
+
return summaries;
|
|
444
|
+
}
|
|
445
|
+
instanceSummary(instance) {
|
|
446
|
+
return {
|
|
447
|
+
id: instance.id,
|
|
448
|
+
cwd: instance.cwd,
|
|
449
|
+
url: instanceUrlForBasePath(instance.proxyBasePath),
|
|
450
|
+
port: instance.port,
|
|
451
|
+
createdAt: instance.createdAt,
|
|
452
|
+
lastAccessedAt: instance.lastAccessedAt,
|
|
453
|
+
displayTitle: instance.displayTitle
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
stopIdleInstances() {
|
|
457
|
+
const now = Date.now();
|
|
458
|
+
for (const [id, instance] of this.instances) {
|
|
459
|
+
if (!isChildRunning(instance.process)) {
|
|
460
|
+
this.instances.delete(id);
|
|
461
|
+
this.deleteTitleLocksForInstance(id);
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
const lastAccessedAt = Date.parse(instance.lastAccessedAt);
|
|
465
|
+
if (Number.isFinite(lastAccessedAt) && now - lastAccessedAt > codeServerIdleTimeoutSeconds * 1000) {
|
|
466
|
+
stopProcess(instance.process);
|
|
467
|
+
this.instances.delete(id);
|
|
468
|
+
this.deleteTitleLocksForInstance(id);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
createTitleLock(instanceId, title) {
|
|
473
|
+
this.pruneExpiredTitleLocks();
|
|
474
|
+
const token = randomUUID();
|
|
475
|
+
this.titleLocks.set(token, {
|
|
476
|
+
instanceId,
|
|
477
|
+
title,
|
|
478
|
+
expiresAt: Date.now() + titleLockTtlMs
|
|
479
|
+
});
|
|
480
|
+
return token;
|
|
481
|
+
}
|
|
482
|
+
pruneExpiredTitleLocks() {
|
|
483
|
+
const now = Date.now();
|
|
484
|
+
for (const [token, lock] of this.titleLocks) {
|
|
485
|
+
if (lock.expiresAt <= now || !this.instances.has(lock.instanceId)) {
|
|
486
|
+
this.titleLocks.delete(token);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
deleteTitleLocksForInstance(instanceId) {
|
|
491
|
+
for (const [token, lock] of this.titleLocks) {
|
|
492
|
+
if (lock.instanceId === instanceId) {
|
|
493
|
+
this.titleLocks.delete(token);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
export function instanceUrl(instanceId, options = {}) {
|
|
499
|
+
return instanceUrlForBasePath(instanceUrlPrefix(instanceId), options);
|
|
500
|
+
}
|
|
501
|
+
export function instanceUrlPrefix(instanceId) {
|
|
502
|
+
return `/code/instances/${encodeURIComponent(instanceId)}`;
|
|
503
|
+
}
|
|
504
|
+
export function instanceUrlForBasePath(basePath, options = {}) {
|
|
505
|
+
const normalized = basePath.endsWith('/') ? basePath : `${basePath}/`;
|
|
506
|
+
return options.titleToken ? `${normalized}?nterminalTitle=${encodeURIComponent(options.titleToken)}` : normalized;
|
|
507
|
+
}
|
|
508
|
+
function publicBasePathForInstance(instanceId, template) {
|
|
509
|
+
const raw = template?.trim();
|
|
510
|
+
if (!raw) {
|
|
511
|
+
return instanceUrlPrefix(instanceId);
|
|
512
|
+
}
|
|
513
|
+
if (!raw.includes('{instanceId}')) {
|
|
514
|
+
throw new CodeServerError(400, 'invalid_code_server_public_base_path');
|
|
515
|
+
}
|
|
516
|
+
const basePath = raw.split('{instanceId}').join(encodeURIComponent(instanceId));
|
|
517
|
+
if (basePath.length > 512 ||
|
|
518
|
+
!basePath.startsWith('/') ||
|
|
519
|
+
basePath.startsWith('//') ||
|
|
520
|
+
basePath.includes('?') ||
|
|
521
|
+
basePath.includes('#') ||
|
|
522
|
+
basePath.includes('\0')) {
|
|
523
|
+
throw new CodeServerError(400, 'invalid_code_server_public_base_path');
|
|
524
|
+
}
|
|
525
|
+
return basePath.endsWith('/') ? basePath.slice(0, -1) : basePath;
|
|
526
|
+
}
|
|
527
|
+
function standaloneReleaseSupport() {
|
|
528
|
+
if (process.platform !== 'linux') {
|
|
529
|
+
return { supported: false, reason: `Standalone install currently supports Linux only in NTerminal add-on mode (detected ${process.platform}).` };
|
|
530
|
+
}
|
|
531
|
+
if (process.arch === 'x64') {
|
|
532
|
+
return { supported: true, assetPlatform: 'linux-amd64' };
|
|
533
|
+
}
|
|
534
|
+
if (process.arch === 'arm64') {
|
|
535
|
+
return { supported: true, assetPlatform: 'linux-arm64' };
|
|
536
|
+
}
|
|
537
|
+
return { supported: false, reason: `code-server standalone releases are not available for ${process.platform}/${process.arch}.` };
|
|
538
|
+
}
|
|
539
|
+
export function buildCodeServerProcessEnv(baseEnv = process.env) {
|
|
540
|
+
const env = {};
|
|
541
|
+
const allowedKeys = [
|
|
542
|
+
'HOME',
|
|
543
|
+
'LANG',
|
|
544
|
+
'LC_ALL',
|
|
545
|
+
'LC_CTYPE',
|
|
546
|
+
'LOGNAME',
|
|
547
|
+
'PATH',
|
|
548
|
+
'SHELL',
|
|
549
|
+
'TMPDIR',
|
|
550
|
+
'USER',
|
|
551
|
+
'XDG_CACHE_HOME',
|
|
552
|
+
'XDG_CONFIG_HOME',
|
|
553
|
+
'XDG_DATA_HOME',
|
|
554
|
+
'XDG_RUNTIME_DIR'
|
|
555
|
+
];
|
|
556
|
+
for (const key of allowedKeys) {
|
|
557
|
+
const value = baseEnv[key];
|
|
558
|
+
if (value !== undefined) {
|
|
559
|
+
env[key] = value;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
env.TERM = 'xterm-256color';
|
|
563
|
+
env.COLORTERM = baseEnv.COLORTERM ?? 'truecolor';
|
|
564
|
+
env.CODE_SERVER_IDLE_TIMEOUT_SECONDS = String(codeServerIdleTimeoutSeconds);
|
|
565
|
+
env.VSCODE_PROXY_URI = './proxy/{{port}}';
|
|
566
|
+
ensureUtf8Locale(env);
|
|
567
|
+
return env;
|
|
568
|
+
}
|
|
569
|
+
function instanceIdForCwd(cwd) {
|
|
570
|
+
return createHash('sha256').update(cwd).digest('hex').slice(0, 24);
|
|
571
|
+
}
|
|
572
|
+
function codeServerDisplayTitle(options) {
|
|
573
|
+
const serverName = normalizeTitlePart(options.serverName) || 'Local';
|
|
574
|
+
const tabName = normalizeTitlePart(options.tabName) || path.basename(options.cwd) || 'Terminal';
|
|
575
|
+
return `[${serverName} - ${tabName}]`;
|
|
576
|
+
}
|
|
577
|
+
function normalizeTitlePart(value) {
|
|
578
|
+
return value?.replace(/\s+/g, ' ').trim().slice(0, 80) ?? '';
|
|
579
|
+
}
|
|
580
|
+
async function allocateLocalPort() {
|
|
581
|
+
return new Promise((resolve, reject) => {
|
|
582
|
+
const server = createServer();
|
|
583
|
+
server.once('error', reject);
|
|
584
|
+
server.listen(0, '127.0.0.1', () => {
|
|
585
|
+
const address = server.address();
|
|
586
|
+
server.close(() => {
|
|
587
|
+
if (address && typeof address === 'object') {
|
|
588
|
+
resolve(address.port);
|
|
589
|
+
}
|
|
590
|
+
else {
|
|
591
|
+
reject(new Error('failed to allocate local port'));
|
|
592
|
+
}
|
|
593
|
+
});
|
|
594
|
+
});
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
async function waitForHealthy(port, child, options = {}) {
|
|
598
|
+
const deadline = Date.now() + healthCheckTimeoutMs;
|
|
599
|
+
while (Date.now() < deadline) {
|
|
600
|
+
const startupError = options.getStartupError?.();
|
|
601
|
+
if (startupError) {
|
|
602
|
+
throw new Error(withFailureDetail(`code-server failed to start: ${startupError.message}`, options.getFailureDetail?.()));
|
|
603
|
+
}
|
|
604
|
+
if (!isChildRunning(child)) {
|
|
605
|
+
throw new Error(withFailureDetail(`code-server exited before becoming healthy (exit ${child.exitCode ?? child.signalCode ?? 'unknown'})`, options.getFailureDetail?.()));
|
|
606
|
+
}
|
|
607
|
+
try {
|
|
608
|
+
const response = await fetch(`http://127.0.0.1:${port}/healthz`, { signal: AbortSignal.timeout(1500) });
|
|
609
|
+
if (response.ok) {
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
catch {
|
|
614
|
+
// Keep polling until the process exits or the deadline passes.
|
|
615
|
+
}
|
|
616
|
+
await sleep(healthCheckIntervalMs);
|
|
617
|
+
}
|
|
618
|
+
stopProcess(child);
|
|
619
|
+
throw new Error(withFailureDetail('code-server did not become healthy in time', options.getFailureDetail?.()));
|
|
620
|
+
}
|
|
621
|
+
function isChildRunning(child) {
|
|
622
|
+
return child.exitCode === null && child.signalCode === null && !child.killed;
|
|
623
|
+
}
|
|
624
|
+
function stopProcess(child) {
|
|
625
|
+
if (!isChildRunning(child)) {
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
child.kill('SIGTERM');
|
|
629
|
+
setTimeout(() => {
|
|
630
|
+
if (isChildRunning(child)) {
|
|
631
|
+
child.kill('SIGKILL');
|
|
632
|
+
}
|
|
633
|
+
}, 5000).unref?.();
|
|
634
|
+
}
|
|
635
|
+
async function safeRealPath(value) {
|
|
636
|
+
try {
|
|
637
|
+
return await realpath(value);
|
|
638
|
+
}
|
|
639
|
+
catch {
|
|
640
|
+
return path.resolve(value);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
function collectRecentLines(target, text) {
|
|
644
|
+
for (const line of text.split(/\r?\n/)) {
|
|
645
|
+
const trimmed = line.trim();
|
|
646
|
+
if (trimmed) {
|
|
647
|
+
target.push(trimmed);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
if (target.length > startupLogLineLimit) {
|
|
651
|
+
target.splice(0, target.length - startupLogLineLimit);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
function withFailureDetail(message, detail) {
|
|
655
|
+
const trimmed = detail?.trim();
|
|
656
|
+
return trimmed ? `${message}\n${trimmed}` : message;
|
|
657
|
+
}
|
|
658
|
+
function parseGitHubSha256Digest(value) {
|
|
659
|
+
if (typeof value !== 'string') {
|
|
660
|
+
return null;
|
|
661
|
+
}
|
|
662
|
+
const match = /^sha256:([a-f0-9]{64})$/i.exec(value.trim());
|
|
663
|
+
return match?.[1]?.toLowerCase() ?? null;
|
|
664
|
+
}
|
|
665
|
+
async function verifyFileSha256(filePath, expectedSha256) {
|
|
666
|
+
const actual = await fileSha256(filePath);
|
|
667
|
+
if (actual !== expectedSha256.toLowerCase()) {
|
|
668
|
+
throw new Error(`code-server archive checksum mismatch: expected ${expectedSha256}, got ${actual}`);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
async function fileSha256(filePath) {
|
|
672
|
+
const hash = createHash('sha256');
|
|
673
|
+
for await (const chunk of createReadStream(filePath)) {
|
|
674
|
+
hash.update(chunk);
|
|
675
|
+
}
|
|
676
|
+
return hash.digest('hex');
|
|
677
|
+
}
|
|
678
|
+
function ensureUtf8Locale(env) {
|
|
679
|
+
const characterLocale = env.LC_ALL ?? env.LC_CTYPE ?? env.LANG;
|
|
680
|
+
if (isUtf8Locale(characterLocale)) {
|
|
681
|
+
if (!env.LANG) {
|
|
682
|
+
env.LANG = characterLocale;
|
|
683
|
+
}
|
|
684
|
+
if (!env.LC_ALL && !env.LC_CTYPE) {
|
|
685
|
+
env.LC_CTYPE = characterLocale;
|
|
686
|
+
}
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
env.LANG = 'en_US.UTF-8';
|
|
690
|
+
if (!env.LC_ALL) {
|
|
691
|
+
env.LC_CTYPE = 'en_US.UTF-8';
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
function isUtf8Locale(value) {
|
|
695
|
+
return Boolean(value && /\.?UTF-?8/i.test(value));
|
|
696
|
+
}
|
|
697
|
+
async function pathExists(value) {
|
|
698
|
+
try {
|
|
699
|
+
await lstat(value);
|
|
700
|
+
return true;
|
|
701
|
+
}
|
|
702
|
+
catch {
|
|
703
|
+
return false;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
async function validateTarArchive(archivePath) {
|
|
707
|
+
const result = await runCommand('tar', ['-tzf', archivePath], { timeoutMs: 30_000 });
|
|
708
|
+
const entries = result.stdout.split(/\r?\n/).filter(Boolean);
|
|
709
|
+
if (entries.length === 0) {
|
|
710
|
+
throw new Error('archive is empty');
|
|
711
|
+
}
|
|
712
|
+
for (const entry of entries) {
|
|
713
|
+
if (entry.startsWith('/') || entry.includes('\\') || entry.split('/').some((part) => part === '..')) {
|
|
714
|
+
throw new Error(`archive contains unsafe path: ${entry}`);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
function runCommand(command, args, options = {}) {
|
|
719
|
+
return new Promise((resolve, reject) => {
|
|
720
|
+
const child = spawn(command, args, {
|
|
721
|
+
cwd: options.cwd ?? os.homedir(),
|
|
722
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
723
|
+
});
|
|
724
|
+
const chunks = [];
|
|
725
|
+
const errChunks = [];
|
|
726
|
+
const timer = setTimeout(() => {
|
|
727
|
+
child.kill('SIGTERM');
|
|
728
|
+
reject(new Error(`${command} timed out`));
|
|
729
|
+
}, options.timeoutMs ?? 120_000);
|
|
730
|
+
timer.unref?.();
|
|
731
|
+
child.stdout.on('data', (chunk) => {
|
|
732
|
+
const text = chunk.toString();
|
|
733
|
+
chunks.push(text);
|
|
734
|
+
for (const line of text.split(/\r?\n/)) {
|
|
735
|
+
options.onOutput?.(line);
|
|
736
|
+
}
|
|
737
|
+
});
|
|
738
|
+
child.stderr.on('data', (chunk) => {
|
|
739
|
+
const text = chunk.toString();
|
|
740
|
+
errChunks.push(text);
|
|
741
|
+
for (const line of text.split(/\r?\n/)) {
|
|
742
|
+
options.onOutput?.(line);
|
|
743
|
+
}
|
|
744
|
+
});
|
|
745
|
+
child.on('error', (error) => {
|
|
746
|
+
clearTimeout(timer);
|
|
747
|
+
reject(error);
|
|
748
|
+
});
|
|
749
|
+
child.on('exit', (code) => {
|
|
750
|
+
clearTimeout(timer);
|
|
751
|
+
const stdout = chunks.join('');
|
|
752
|
+
const stderr = errChunks.join('');
|
|
753
|
+
if (code === 0) {
|
|
754
|
+
resolve({ stdout, stderr });
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
reject(new Error((stderr || stdout || `${command} exited ${code}`).trim()));
|
|
758
|
+
});
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
function sleep(ms) {
|
|
762
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
763
|
+
}
|
|
764
|
+
//# sourceMappingURL=codeServerManager.js.map
|