@velarscript/desktop 0.11.1 → 0.12.1
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/README.md +56 -214
- package/dist/build.d.ts +4 -12
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +11 -59
- package/dist/build.js.map +1 -1
- package/dist/compiler.d.ts.map +1 -1
- package/dist/compiler.js +4 -702
- package/dist/compiler.js.map +1 -1
- package/dist/config.d.ts +0 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +5 -7
- package/dist/config.js.map +1 -1
- package/dist/host.js +1 -1
- package/dist/host.js.map +1 -1
- package/dist/package-host.d.ts.map +1 -1
- package/dist/package-host.js +2 -2
- package/dist/package-host.js.map +1 -1
- package/dist/test-runtime.d.ts.map +1 -1
- package/dist/test-runtime.js +0 -130
- package/dist/test-runtime.js.map +1 -1
- package/dist/worker.js +3 -545
- package/native/macos/VelarDesktopHost.swift +0 -16
- package/native/node/worker.js +19 -929
- package/package.json +4 -4
- package/native/macos/VelarTerminalHost.swift +0 -203
- package/native/node/project-transactions.js +0 -157
package/native/node/worker.js
CHANGED
|
@@ -4,11 +4,6 @@ import { createInterface } from "node:readline";
|
|
|
4
4
|
import { basename, dirname, isAbsolute, relative, resolve } from "node:path";
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
6
|
import { StringDecoder } from "node:string_decoder";
|
|
7
|
-
import {
|
|
8
|
-
createDesktopProjectTransactionOwner,
|
|
9
|
-
desktopProjectChangePage,
|
|
10
|
-
desktopProjectChangeView,
|
|
11
|
-
} from "./project-transactions.js";
|
|
12
7
|
|
|
13
8
|
const MAX_MESSAGE_BYTES = 128 * 1024 * 1024;
|
|
14
9
|
const MAX_FILE_BYTES = 16 * 1024 * 1024;
|
|
@@ -20,23 +15,9 @@ const MAX_PATH_UNITS = 4096;
|
|
|
20
15
|
const MAX_FILE_WATCHERS = 128;
|
|
21
16
|
const MAX_WATCH_PATHS = 4096;
|
|
22
17
|
const MAX_WATCH_TEXT_UNITS = 2 * 1024 * 1024;
|
|
23
|
-
const MAX_LANGUAGE_SERVER_MESSAGE_BYTES = 16 * 1024 * 1024;
|
|
24
|
-
const MAX_LANGUAGE_SERVER_QUEUED_BYTES = 64 * 1024 * 1024;
|
|
25
|
-
const MAX_LANGUAGE_SERVERS = 4;
|
|
26
|
-
const MAX_PROJECT_TASKS = 4;
|
|
27
|
-
const MAX_PROJECT_CHANGE_HANDLES = 16;
|
|
28
|
-
const MAX_PROJECT_CHANGE_QUEUE_ITEMS = 100;
|
|
29
|
-
const MAX_PROJECT_CHANGE_QUEUE_BYTES = 16 * 1024 * 1024;
|
|
30
|
-
const MAX_TERMINALS = 4;
|
|
31
|
-
const MAX_TERMINAL_CHUNK_BYTES = 1024 * 1024;
|
|
32
|
-
const MAX_TERMINAL_QUEUED_BYTES = 4 * 1024 * 1024;
|
|
33
|
-
const TERMINAL_PAUSE_BYTES = 2 * 1024 * 1024;
|
|
34
|
-
const TERMINAL_RESUME_BYTES = 1024 * 1024;
|
|
35
18
|
const WATCH_DEBOUNCE_MS = 20;
|
|
36
19
|
const PROCESS_STOP_CONFIRMATION_TIMEOUT_MS = 5000;
|
|
37
20
|
const PROCESS_EXIT_PIPE_CONFIRMATION_TIMEOUT_MS = 5000;
|
|
38
|
-
const HOST_HANDSHAKE_TIMEOUT_MS = 5000;
|
|
39
|
-
const HOST_PIPE_WRITE_TIMEOUT_MS = 5000;
|
|
40
21
|
const FATAL_DRAIN_TIMEOUT_MS = 8000;
|
|
41
22
|
const processTerminationMarker = Object.freeze({});
|
|
42
23
|
const processRootExitMarker = Object.freeze({});
|
|
@@ -58,12 +39,6 @@ const activeRequests = new Map();
|
|
|
58
39
|
const fileMutationTails = new Map();
|
|
59
40
|
const fileWatchers = new Map();
|
|
60
41
|
let nextFileWatcherHandle = 1;
|
|
61
|
-
const languageServers = new Map();
|
|
62
|
-
let nextLanguageServerHandle = 1_000_000_000;
|
|
63
|
-
const projectChangeHandles = new Map();
|
|
64
|
-
let nextProjectChangeHandle = 3_000_000_000;
|
|
65
|
-
const terminals = new Map();
|
|
66
|
-
let nextTerminalHandle = 2_000_000_000;
|
|
67
42
|
let nextTextReplacementIdentity = 1;
|
|
68
43
|
let fatalDrainStarted = false;
|
|
69
44
|
let activeOwner = null;
|
|
@@ -74,14 +49,6 @@ let appDataLexicalRoot = null;
|
|
|
74
49
|
let projectRoot = null;
|
|
75
50
|
let projectLexicalRoot = null;
|
|
76
51
|
let projectRootUpdate = Promise.resolve();
|
|
77
|
-
let projectTransactionOwner = null;
|
|
78
|
-
let projectTransactionOwnerPending = null;
|
|
79
|
-
let projectTransactionGeneration = 0;
|
|
80
|
-
let languageServerPath = null;
|
|
81
|
-
let projectTaskPath = null;
|
|
82
|
-
let buildEnginePath = null;
|
|
83
|
-
let terminalHostPath = null;
|
|
84
|
-
const terminalGranted = config.permissions.terminal === true;
|
|
85
52
|
|
|
86
53
|
class HttpTransportFailure extends Error {
|
|
87
54
|
constructor(phase) {
|
|
@@ -90,59 +57,7 @@ class HttpTransportFailure extends Error {
|
|
|
90
57
|
this.phase = phase;
|
|
91
58
|
}
|
|
92
59
|
}
|
|
93
|
-
// A bundled child that stops answering must fail the one request that waits on
|
|
94
|
-
// it, never leave the host waiting without a bound.
|
|
95
|
-
class HostDeadlineFailure extends Error {
|
|
96
|
-
constructor(message) {
|
|
97
|
-
super(message);
|
|
98
|
-
this.name = "HostDeadlineError";
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
function boundedHostWait(value, message, timeout) {
|
|
102
|
-
let timer = null;
|
|
103
|
-
const guarded = value.then(
|
|
104
|
-
(result) => { if (timer !== null) clearTimeout(timer); return result; },
|
|
105
|
-
(error) => { if (timer !== null) clearTimeout(timer); throw error; },
|
|
106
|
-
);
|
|
107
|
-
return Promise.race([guarded, new Promise((_, rejectDeadline) => {
|
|
108
|
-
timer = setTimeout(() => rejectDeadline(new HostDeadlineFailure(message)), timeout);
|
|
109
|
-
})]);
|
|
110
|
-
}
|
|
111
60
|
const launchDirectory = await realpath(launchRoot);
|
|
112
|
-
if (config.languageServer !== undefined) {
|
|
113
|
-
if (!config.languageServer || typeof config.languageServer !== "object" || Array.isArray(config.languageServer)
|
|
114
|
-
|| Object.keys(config.languageServer).some(key => key !== "path")
|
|
115
|
-
|| config.languageServer.path !== "host/language-server.js") throw new Error("Invalid bundled language-server configuration");
|
|
116
|
-
const candidate = resolve(dirname(configPath), config.languageServer.path);
|
|
117
|
-
languageServerPath = await realpath(candidate);
|
|
118
|
-
if (!(await stat(languageServerPath)).isFile()) throw new Error("Bundled language server must be an ordinary file");
|
|
119
|
-
}
|
|
120
|
-
if (config.projectTask !== undefined) {
|
|
121
|
-
if (!config.projectTask || typeof config.projectTask !== "object" || Array.isArray(config.projectTask)
|
|
122
|
-
|| Object.keys(config.projectTask).some(key => !["path", "buildEnginePath"].includes(key))
|
|
123
|
-
|| config.projectTask.path !== "host/project-task.js" || config.projectTask.buildEnginePath !== "host/build-engine") {
|
|
124
|
-
throw new Error("Invalid bundled project-task configuration");
|
|
125
|
-
}
|
|
126
|
-
const resourcesRoot = await realpath(dirname(configPath));
|
|
127
|
-
projectTaskPath = await realpath(resolve(dirname(configPath), config.projectTask.path));
|
|
128
|
-
buildEnginePath = await realpath(resolve(dirname(configPath), config.projectTask.buildEnginePath));
|
|
129
|
-
if (!contained(resourcesRoot, projectTaskPath) || !contained(resourcesRoot, buildEnginePath)
|
|
130
|
-
|| !(await stat(projectTaskPath)).isFile() || !(await stat(buildEnginePath)).isFile()) {
|
|
131
|
-
throw new Error("Bundled project task tools must be ordinary files inside Desktop resources");
|
|
132
|
-
}
|
|
133
|
-
await access(buildEnginePath, fsConstants.X_OK);
|
|
134
|
-
}
|
|
135
|
-
if (config.terminalHost !== undefined) {
|
|
136
|
-
if (!config.terminalHost || typeof config.terminalHost !== "object" || Array.isArray(config.terminalHost)
|
|
137
|
-
|| Object.keys(config.terminalHost).some(key => key !== "path")
|
|
138
|
-
|| config.terminalHost.path !== "host/terminal-host") throw new Error("Invalid bundled terminal-host configuration");
|
|
139
|
-
const resourcesRoot = await realpath(dirname(configPath));
|
|
140
|
-
terminalHostPath = await realpath(resolve(dirname(configPath), config.terminalHost.path));
|
|
141
|
-
if (!contained(resourcesRoot, terminalHostPath) || !(await stat(terminalHostPath)).isFile()) {
|
|
142
|
-
throw new Error("Bundled terminal host must be an ordinary file inside Desktop resources");
|
|
143
|
-
}
|
|
144
|
-
await access(terminalHostPath, fsConstants.X_OK);
|
|
145
|
-
}
|
|
146
61
|
if (fileScopes.has("app-data")) {
|
|
147
62
|
const dataRoot = resolve(appDataRoot, "data");
|
|
148
63
|
await mkdir(dataRoot, { recursive: true });
|
|
@@ -158,10 +73,7 @@ rebuildFileRoots();
|
|
|
158
73
|
const reader = createInterface({ input: process.stdin, crlfDelay: Infinity, terminal: false });
|
|
159
74
|
reader.once("close", () => {
|
|
160
75
|
if (activeOwner !== null) retireOwner(activeOwner);
|
|
161
|
-
retireProjectTransactionOwner(new Error("Desktop capability host closed"));
|
|
162
76
|
for (const task of fileWatchers.values()) releaseFileWatcher(task, new Error("Desktop capability host closed"));
|
|
163
|
-
for (const task of languageServers.values()) releaseLanguageServer(task, new Error("Desktop capability host closed"));
|
|
164
|
-
for (const task of terminals.values()) releaseTerminal(task, new Error("Desktop capability host closed"));
|
|
165
77
|
for (const activity of activeRequests.values()) cancelActivity(activity);
|
|
166
78
|
});
|
|
167
79
|
reader.on("line", async (line) => {
|
|
@@ -261,10 +173,6 @@ async function dispatch(capability, operation, args, owner, activity) {
|
|
|
261
173
|
if (operation === "wait") return processWait(args, owner);
|
|
262
174
|
if (operation === "stop") return processStop(args, owner);
|
|
263
175
|
}
|
|
264
|
-
if (capability === "language-server") return languageServerOperation(operation, args, owner, activity);
|
|
265
|
-
if (capability === "project-task") return projectTaskOperation(operation, args, owner, activity);
|
|
266
|
-
if (capability === "project-changes") return projectChangeOperation(operation, args, owner, activity);
|
|
267
|
-
if (capability === "terminal") return terminalOperation(operation, args, owner, activity);
|
|
268
176
|
if (capability === "http") {
|
|
269
177
|
if (operation === "request") return httpRequest(args, owner, activity);
|
|
270
178
|
if (operation === "read") return httpRead(args, owner);
|
|
@@ -291,9 +199,6 @@ async function replaceProjectRoot(path) {
|
|
|
291
199
|
const metadata = await stat(canonical);
|
|
292
200
|
if (!metadata.isDirectory()) throw new TypeError("Desktop project grant must identify a directory");
|
|
293
201
|
for (const task of fileWatchers.values()) releaseFileWatcher(task, new Error("Desktop project grant changed"));
|
|
294
|
-
for (const task of languageServers.values()) releaseLanguageServer(task, new Error("Desktop project grant changed"));
|
|
295
|
-
for (const task of terminals.values()) releaseTerminal(task, new Error("Desktop project grant changed"));
|
|
296
|
-
retireProjectTransactionOwner(new Error("Desktop project grant changed"));
|
|
297
202
|
for (const activity of activeRequests.values()) cancelActivity(activity);
|
|
298
203
|
for (const [handle, task] of processHandles) retainRetiredProcess(handle, task);
|
|
299
204
|
for (const [handle, request] of httpHandles) {
|
|
@@ -306,214 +211,6 @@ async function replaceProjectRoot(path) {
|
|
|
306
211
|
rebuildFileRoots();
|
|
307
212
|
}
|
|
308
213
|
|
|
309
|
-
function closeRetiredProjectTransactionOwner(owner) {
|
|
310
|
-
if (!owner.retired || owner.activeOperations > 0) return;
|
|
311
|
-
owner.close();
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
function retireProjectTransactionOwner(error) {
|
|
315
|
-
projectTransactionGeneration += 1;
|
|
316
|
-
const current = projectTransactionOwner;
|
|
317
|
-
projectTransactionOwner = null;
|
|
318
|
-
if (current !== null) {
|
|
319
|
-
current.retired = true;
|
|
320
|
-
closeRetiredProjectTransactionOwner(current);
|
|
321
|
-
}
|
|
322
|
-
for (const task of projectChangeHandles.values()) releaseProjectChangeHandle(task, error);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
async function currentProjectTransactionOwner() {
|
|
326
|
-
if (projectRoot === null || !fileScopes.has("project")) {
|
|
327
|
-
throw new Error("Desktop project changes require the project file grant");
|
|
328
|
-
}
|
|
329
|
-
if (projectTransactionOwner?.root === projectRoot && !projectTransactionOwner.retired) return projectTransactionOwner;
|
|
330
|
-
const generation = projectTransactionGeneration;
|
|
331
|
-
if (projectTransactionOwnerPending?.root !== projectRoot || projectTransactionOwnerPending.generation !== generation) {
|
|
332
|
-
const root = projectRoot;
|
|
333
|
-
const pending = createDesktopProjectTransactionOwner(root, appDataRoot).then((created) => ({
|
|
334
|
-
...created,
|
|
335
|
-
activeOperations: 0,
|
|
336
|
-
retired: false,
|
|
337
|
-
}));
|
|
338
|
-
projectTransactionOwnerPending = { root, generation, pending };
|
|
339
|
-
}
|
|
340
|
-
const pendingOwner = projectTransactionOwnerPending;
|
|
341
|
-
try {
|
|
342
|
-
const created = await pendingOwner.pending;
|
|
343
|
-
if (generation !== projectTransactionGeneration || projectRoot !== pendingOwner.root) {
|
|
344
|
-
created.retired = true;
|
|
345
|
-
closeRetiredProjectTransactionOwner(created);
|
|
346
|
-
throw new Error("Desktop project grant changed while project transactions were opening");
|
|
347
|
-
}
|
|
348
|
-
projectTransactionOwner = created;
|
|
349
|
-
return created;
|
|
350
|
-
} finally {
|
|
351
|
-
if (projectTransactionOwnerPending === pendingOwner) projectTransactionOwnerPending = null;
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
async function withProjectTransactionOperation(owner, action) {
|
|
356
|
-
owner.activeOperations += 1;
|
|
357
|
-
try {
|
|
358
|
-
return await action(owner.controller);
|
|
359
|
-
} finally {
|
|
360
|
-
owner.activeOperations -= 1;
|
|
361
|
-
closeRetiredProjectTransactionOwner(owner);
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
function allocateProjectChangeHandle() {
|
|
366
|
-
let candidate = nextProjectChangeHandle;
|
|
367
|
-
for (let attempts = 0; attempts <= MAX_PROJECT_CHANGE_HANDLES; attempts += 1) {
|
|
368
|
-
if (!projectChangeHandles.has(candidate)) {
|
|
369
|
-
nextProjectChangeHandle = candidate >= 3_000_000_015 ? 3_000_000_000 : candidate + 1;
|
|
370
|
-
return candidate;
|
|
371
|
-
}
|
|
372
|
-
candidate = candidate >= 3_000_000_015 ? 3_000_000_000 : candidate + 1;
|
|
373
|
-
}
|
|
374
|
-
throw new RangeError("Desktop project change handle space is unavailable");
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
function projectChangeUpdate(task) {
|
|
378
|
-
const changes = [...task.queue.values()];
|
|
379
|
-
const update = { changes, rescan: task.rescan };
|
|
380
|
-
task.queue.clear();
|
|
381
|
-
task.queuedBytes = 0;
|
|
382
|
-
task.rescan = false;
|
|
383
|
-
return update;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
function settleProjectChangeSubscription(task) {
|
|
387
|
-
if (task.pending === null || !task.rescan && task.queue.size === 0) return;
|
|
388
|
-
const pending = task.pending;
|
|
389
|
-
task.pending = null;
|
|
390
|
-
pending.activity.cancel = null;
|
|
391
|
-
pending.resolve(projectChangeUpdate(task));
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
function enqueueProjectChange(task, value) {
|
|
395
|
-
if (task.closed || task.rescan) return;
|
|
396
|
-
try {
|
|
397
|
-
const change = desktopProjectChangeView(value);
|
|
398
|
-
const bytes = Buffer.byteLength(JSON.stringify(change), "utf8");
|
|
399
|
-
const previous = task.queue.get(change.transactionId);
|
|
400
|
-
if (previous !== undefined) task.queuedBytes -= Buffer.byteLength(JSON.stringify(previous), "utf8");
|
|
401
|
-
if (!task.queue.has(change.transactionId) && task.queue.size >= MAX_PROJECT_CHANGE_QUEUE_ITEMS
|
|
402
|
-
|| task.queuedBytes + bytes > MAX_PROJECT_CHANGE_QUEUE_BYTES) {
|
|
403
|
-
task.queue.clear();
|
|
404
|
-
task.queuedBytes = 0;
|
|
405
|
-
task.rescan = true;
|
|
406
|
-
} else {
|
|
407
|
-
task.queue.set(change.transactionId, change);
|
|
408
|
-
task.queuedBytes += bytes;
|
|
409
|
-
}
|
|
410
|
-
} catch {
|
|
411
|
-
task.queue.clear();
|
|
412
|
-
task.queuedBytes = 0;
|
|
413
|
-
task.rescan = true;
|
|
414
|
-
}
|
|
415
|
-
settleProjectChangeSubscription(task);
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
function releaseProjectChangeHandle(task, error = null) {
|
|
419
|
-
if (task.closed) return false;
|
|
420
|
-
task.closed = true;
|
|
421
|
-
projectChangeHandles.delete(task.handle);
|
|
422
|
-
task.unsubscribe();
|
|
423
|
-
if (task.pending !== null) {
|
|
424
|
-
const pending = task.pending;
|
|
425
|
-
task.pending = null;
|
|
426
|
-
pending.activity.cancel = null;
|
|
427
|
-
if (error === null) pending.resolve(null);
|
|
428
|
-
else pending.reject(error);
|
|
429
|
-
}
|
|
430
|
-
return true;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
function ownedProjectChangeHandle(value, owner) {
|
|
434
|
-
const handle = integer(value, 3_000_000_000, 3_000_000_015, "ProjectChanges handle");
|
|
435
|
-
const task = projectChangeHandles.get(handle);
|
|
436
|
-
if (!task || task.closed) throw new Error("Desktop ProjectChanges handle is unknown or already released");
|
|
437
|
-
if (task.owner !== owner) throw new Error("Desktop ProjectChanges handle belongs to another document generation");
|
|
438
|
-
return task;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
async function startProjectChanges(args, owner) {
|
|
442
|
-
if (args.length !== 0) throw new TypeError("projectChanges start arguments are invalid");
|
|
443
|
-
if (projectChangeHandles.size >= MAX_PROJECT_CHANGE_HANDLES) throw new RangeError("Desktop cannot own more than 16 project change handles");
|
|
444
|
-
const transactionOwner = await currentProjectTransactionOwner();
|
|
445
|
-
const handle = allocateProjectChangeHandle();
|
|
446
|
-
const task = {
|
|
447
|
-
handle,
|
|
448
|
-
owner,
|
|
449
|
-
transactionOwner,
|
|
450
|
-
queue: new Map(),
|
|
451
|
-
queuedBytes: 0,
|
|
452
|
-
rescan: false,
|
|
453
|
-
pending: null,
|
|
454
|
-
closed: false,
|
|
455
|
-
unsubscribe: null,
|
|
456
|
-
};
|
|
457
|
-
task.unsubscribe = transactionOwner.controller.subscribe((change) => enqueueProjectChange(task, change));
|
|
458
|
-
projectChangeHandles.set(handle, task);
|
|
459
|
-
return handle;
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
function listProjectChanges(args, owner) {
|
|
463
|
-
if (args.length !== 2) throw new TypeError("ProjectChanges.list arguments are invalid");
|
|
464
|
-
const task = ownedProjectChangeHandle(args[0], owner);
|
|
465
|
-
const limit = integer(args[1], 1, 100, "ProjectChanges.list limit");
|
|
466
|
-
return desktopProjectChangePage(task.transactionOwner.controller.list({ limit: limit + 1 }), limit);
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
function getProjectChange(args, owner) {
|
|
470
|
-
if (args.length !== 2) throw new TypeError("ProjectChanges.get arguments are invalid");
|
|
471
|
-
const task = ownedProjectChangeHandle(args[0], owner);
|
|
472
|
-
const change = task.transactionOwner.controller.get(args[1]);
|
|
473
|
-
return change === undefined ? null : desktopProjectChangeView(change);
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
function subscribeProjectChanges(args, owner, activity) {
|
|
477
|
-
if (args.length !== 1) throw new TypeError("ProjectChanges.subscribe arguments are invalid");
|
|
478
|
-
const task = ownedProjectChangeHandle(args[0], owner);
|
|
479
|
-
if (task.pending !== null) throw new Error("ProjectChanges.subscribe already has an active pull");
|
|
480
|
-
if (task.rescan || task.queue.size > 0) return projectChangeUpdate(task);
|
|
481
|
-
return new Promise((resolveNext, rejectNext) => {
|
|
482
|
-
const pending = { resolve: resolveNext, reject: rejectNext, activity };
|
|
483
|
-
task.pending = pending;
|
|
484
|
-
setActivityCancellation(activity, () => {
|
|
485
|
-
if (task.pending !== pending) return;
|
|
486
|
-
task.pending = null;
|
|
487
|
-
pending.reject(new Error("Desktop project change subscription was cancelled"));
|
|
488
|
-
});
|
|
489
|
-
});
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
async function mutateProjectChange(args, owner, operation) {
|
|
493
|
-
if (args.length !== 2) throw new TypeError(`ProjectChanges.${operation} arguments are invalid`);
|
|
494
|
-
const task = ownedProjectChangeHandle(args[0], owner);
|
|
495
|
-
const input = { transactionId: args[1] };
|
|
496
|
-
await withProjectTransactionOperation(task.transactionOwner, (controller) => controller[operation](input));
|
|
497
|
-
const change = task.transactionOwner.controller.get(args[1]);
|
|
498
|
-
if (change === undefined) throw new Error(`Project transaction disappeared after ${operation}`);
|
|
499
|
-
return desktopProjectChangeView(change);
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
function projectChangeOperation(operation, args, owner, activity) {
|
|
503
|
-
if (operation === "start") return startProjectChanges(args, owner);
|
|
504
|
-
if (operation === "list") return listProjectChanges(args, owner);
|
|
505
|
-
if (operation === "get") return getProjectChange(args, owner);
|
|
506
|
-
if (operation === "subscribe") return subscribeProjectChanges(args, owner, activity);
|
|
507
|
-
if (operation === "apply") return mutateProjectChange(args, owner, "apply");
|
|
508
|
-
if (operation === "rollback") return mutateProjectChange(args, owner, "rollback");
|
|
509
|
-
if (operation === "close") {
|
|
510
|
-
if (args.length !== 1) throw new TypeError("ProjectChanges.close arguments are invalid");
|
|
511
|
-
releaseProjectChangeHandle(ownedProjectChangeHandle(args[0], owner));
|
|
512
|
-
return null;
|
|
513
|
-
}
|
|
514
|
-
throw new Error(`Unknown project-changes operation '${operation}'`);
|
|
515
|
-
}
|
|
516
|
-
|
|
517
214
|
function allocateFileWatcherHandle() {
|
|
518
215
|
let candidate = nextFileWatcherHandle;
|
|
519
216
|
for (let attempts = 0; attempts <= MAX_FILE_WATCHERS; attempts += 1) {
|
|
@@ -660,245 +357,6 @@ function closeFileWatchHandle(args, owner) {
|
|
|
660
357
|
return releaseFileWatcher(task);
|
|
661
358
|
}
|
|
662
359
|
|
|
663
|
-
function allocateLanguageServerHandle() {
|
|
664
|
-
let candidate = nextLanguageServerHandle;
|
|
665
|
-
for (let attempts = 0; attempts <= MAX_LANGUAGE_SERVERS; attempts += 1) {
|
|
666
|
-
if (!languageServers.has(candidate)) {
|
|
667
|
-
nextLanguageServerHandle = candidate >= 1_000_000_003 ? 1_000_000_000 : candidate + 1;
|
|
668
|
-
return candidate;
|
|
669
|
-
}
|
|
670
|
-
candidate = candidate >= 1_000_000_003 ? 1_000_000_000 : candidate + 1;
|
|
671
|
-
}
|
|
672
|
-
throw new RangeError("Desktop language-server handle space is unavailable");
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
function ownedLanguageServer(value, owner) {
|
|
676
|
-
const handle = integer(value, 1, Number.MAX_SAFE_INTEGER, "Desktop language-server handle");
|
|
677
|
-
const task = languageServers.get(handle);
|
|
678
|
-
if (!task) throw new Error("Desktop language-server handle is unknown or already released");
|
|
679
|
-
if (task.owner !== owner) throw new Error("Desktop language server belongs to another document generation");
|
|
680
|
-
return task;
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
function settleLanguageServerPull(task) {
|
|
684
|
-
if (task.waiter === null) return;
|
|
685
|
-
if (task.queue.length > 0) {
|
|
686
|
-
const waiter = task.waiter;
|
|
687
|
-
task.waiter = null;
|
|
688
|
-
waiter.activity.cancel = null;
|
|
689
|
-
const value = task.queue.shift();
|
|
690
|
-
task.queuedBytes -= Buffer.byteLength(value, "utf8");
|
|
691
|
-
waiter.resolve(value);
|
|
692
|
-
} else if (task.failure || task.settled) {
|
|
693
|
-
const waiter = task.waiter;
|
|
694
|
-
task.waiter = null;
|
|
695
|
-
waiter.activity.cancel = null;
|
|
696
|
-
if (task.failure) waiter.reject(task.failure);
|
|
697
|
-
else waiter.resolve(null);
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
function failLanguageServer(task, error) {
|
|
702
|
-
if (!task.failure) task.failure = error instanceof Error ? error : new Error("Bundled language server failed");
|
|
703
|
-
settleLanguageServerPull(task);
|
|
704
|
-
signalTree(task.child, "SIGKILL");
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
function enqueueLanguageServerMessage(task, body) {
|
|
708
|
-
const bytes = Buffer.byteLength(body, "utf8");
|
|
709
|
-
if (bytes < 1 || bytes > MAX_LANGUAGE_SERVER_MESSAGE_BYTES) {
|
|
710
|
-
failLanguageServer(task, new RangeError("Bundled language-server message exceeds 16 MiB"));
|
|
711
|
-
return;
|
|
712
|
-
}
|
|
713
|
-
if (task.waiter !== null) {
|
|
714
|
-
const waiter = task.waiter;
|
|
715
|
-
task.waiter = null;
|
|
716
|
-
waiter.activity.cancel = null;
|
|
717
|
-
waiter.resolve(body);
|
|
718
|
-
return;
|
|
719
|
-
}
|
|
720
|
-
if (task.queuedBytes + bytes > MAX_LANGUAGE_SERVER_QUEUED_BYTES) {
|
|
721
|
-
failLanguageServer(task, new RangeError("Bundled language-server queue exceeds 64 MiB"));
|
|
722
|
-
return;
|
|
723
|
-
}
|
|
724
|
-
task.queue.push(body);
|
|
725
|
-
task.queuedBytes += bytes;
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
function parseLanguageServerOutput(task, chunk) {
|
|
729
|
-
task.buffer = Buffer.concat([task.buffer, chunk]);
|
|
730
|
-
while (!task.failure) {
|
|
731
|
-
const boundary = task.buffer.indexOf("\r\n\r\n");
|
|
732
|
-
if (boundary === -1) {
|
|
733
|
-
if (task.buffer.byteLength > 64 * 1024) failLanguageServer(task, new RangeError("Bundled language-server header exceeds 64 KiB"));
|
|
734
|
-
return;
|
|
735
|
-
}
|
|
736
|
-
if (boundary > 64 * 1024) { failLanguageServer(task, new RangeError("Bundled language-server header exceeds 64 KiB")); return; }
|
|
737
|
-
const header = task.buffer.subarray(0, boundary).toString("ascii");
|
|
738
|
-
const match = /(?:^|\r\n)Content-Length:\s*(\d+)/iu.exec(header);
|
|
739
|
-
if (!match) { failLanguageServer(task, new Error("Bundled language server emitted an invalid frame")); return; }
|
|
740
|
-
const size = Number(match[1]);
|
|
741
|
-
if (!Number.isSafeInteger(size) || size < 1 || size > MAX_LANGUAGE_SERVER_MESSAGE_BYTES) {
|
|
742
|
-
failLanguageServer(task, new RangeError("Bundled language-server message exceeds 16 MiB"));
|
|
743
|
-
return;
|
|
744
|
-
}
|
|
745
|
-
const end = boundary + 4 + size;
|
|
746
|
-
if (task.buffer.byteLength < end) return;
|
|
747
|
-
const body = task.buffer.subarray(boundary + 4, end).toString("utf8");
|
|
748
|
-
task.buffer = task.buffer.subarray(end);
|
|
749
|
-
try { JSON.parse(body); }
|
|
750
|
-
catch { failLanguageServer(task, new Error("Bundled language server emitted invalid JSON")); return; }
|
|
751
|
-
enqueueLanguageServerMessage(task, body);
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
async function startLanguageServer(args, owner, activity) {
|
|
756
|
-
if (args.length !== 0) throw new TypeError("languageServer start arguments are invalid");
|
|
757
|
-
if (languageServerPath === null) throw new Error("This Desktop package does not contain an official language server");
|
|
758
|
-
if (projectRoot === null || !fileScopes.has("project")) throw new Error("Desktop language services require the project file grant");
|
|
759
|
-
if (languageServers.size >= MAX_LANGUAGE_SERVERS) throw new RangeError("Desktop cannot own more than 4 language servers");
|
|
760
|
-
const handle = allocateLanguageServerHandle();
|
|
761
|
-
const environment = Object.create(null);
|
|
762
|
-
for (const name of ["HOME", "LANG", "LC_ALL", "PATH", "TMPDIR"]) {
|
|
763
|
-
if (typeof process.env[name] === "string") environment[name] = process.env[name];
|
|
764
|
-
}
|
|
765
|
-
environment.VELAR_LANGUAGE_SERVER_WORKSPACE_ROOT = projectLexicalRoot ?? projectRoot;
|
|
766
|
-
environment.VELAR_LANGUAGE_SERVER_CANONICAL_ROOT = projectRoot;
|
|
767
|
-
const child = spawn(process.execPath, [languageServerPath], {
|
|
768
|
-
cwd: projectRoot,
|
|
769
|
-
env: environment,
|
|
770
|
-
shell: false,
|
|
771
|
-
windowsHide: true,
|
|
772
|
-
detached: process.platform !== "win32",
|
|
773
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
774
|
-
});
|
|
775
|
-
let resolveClosed;
|
|
776
|
-
const closed = new Promise(resolve => { resolveClosed = resolve; });
|
|
777
|
-
const task = { handle, owner, child, buffer: Buffer.alloc(0), queue: [], queuedBytes: 0, waiter: null, stderr: "", settled: false, failure: null, ownershipPublished: false, closed, resolveClosed };
|
|
778
|
-
languageServers.set(handle, task);
|
|
779
|
-
setActivityCancellation(activity, () => releaseLanguageServer(task, new Error("Desktop language-server start was cancelled")));
|
|
780
|
-
child.stdout.on("data", chunk => parseLanguageServerOutput(task, chunk));
|
|
781
|
-
child.stderr.on("data", chunk => {
|
|
782
|
-
if (task.stderr.length < 64 * 1024) task.stderr += chunk.toString("utf8").slice(0, 64 * 1024 - task.stderr.length);
|
|
783
|
-
});
|
|
784
|
-
child.once("error", error => failLanguageServer(task, error));
|
|
785
|
-
child.once("close", (code, signal) => {
|
|
786
|
-
task.settled = true;
|
|
787
|
-
task.resolveClosed();
|
|
788
|
-
if (!task.failure && code !== 0) task.failure = new Error(`Bundled language server exited with ${code === null ? signal ?? "an unknown signal" : `code ${code}`}${task.stderr ? `: ${task.stderr}` : ""}`);
|
|
789
|
-
settleLanguageServerPull(task);
|
|
790
|
-
if (task.ownershipPublished) respond({ protocolVersion: 1, hostEvent: "language-server-settled", owner, handle });
|
|
791
|
-
});
|
|
792
|
-
try {
|
|
793
|
-
await new Promise((resolveStart, rejectStart) => {
|
|
794
|
-
child.once("spawn", resolveStart);
|
|
795
|
-
child.once("error", rejectStart);
|
|
796
|
-
});
|
|
797
|
-
} catch (error) {
|
|
798
|
-
releaseLanguageServer(task, error instanceof Error ? error : new Error("Bundled language server failed to start"));
|
|
799
|
-
throw error;
|
|
800
|
-
}
|
|
801
|
-
activity.cancel = null;
|
|
802
|
-
task.ownershipPublished = true;
|
|
803
|
-
respond({ protocolVersion: 1, hostEvent: "language-server-owned", owner, handle, pid: child.pid });
|
|
804
|
-
if (activity.cancelled || owner !== activeOwner) {
|
|
805
|
-
releaseLanguageServer(task, new Error("Desktop document generation is no longer active"));
|
|
806
|
-
throw new Error("Desktop document generation is no longer active");
|
|
807
|
-
}
|
|
808
|
-
return handle;
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
async function sendLanguageServer(args, owner) {
|
|
812
|
-
if (args.length !== 2 || typeof args[1] !== "string") throw new TypeError("languageServer send arguments are invalid");
|
|
813
|
-
const task = ownedLanguageServer(args[0], owner);
|
|
814
|
-
if (task.failure) throw task.failure;
|
|
815
|
-
if (task.settled || !task.child.stdin.writable) throw new Error("Bundled language server is closed");
|
|
816
|
-
const bytes = Buffer.byteLength(args[1], "utf8");
|
|
817
|
-
if (bytes < 1 || bytes > MAX_LANGUAGE_SERVER_MESSAGE_BYTES) throw new RangeError("Language-server request exceeds 16 MiB");
|
|
818
|
-
try { JSON.parse(args[1]); }
|
|
819
|
-
catch { throw new TypeError("Language-server request must contain valid JSON"); }
|
|
820
|
-
const frame = `Content-Length: ${bytes}\r\n\r\n${args[1]}`;
|
|
821
|
-
try {
|
|
822
|
-
await boundedHostWait(
|
|
823
|
-
new Promise((resolveWrite, rejectWrite) => task.child.stdin.write(frame, error => error ? rejectWrite(error) : resolveWrite())),
|
|
824
|
-
`Bundled language server did not accept a request within ${HOST_PIPE_WRITE_TIMEOUT_MS} milliseconds`,
|
|
825
|
-
HOST_PIPE_WRITE_TIMEOUT_MS,
|
|
826
|
-
);
|
|
827
|
-
} catch (error) {
|
|
828
|
-
// A half-written frame leaves the protocol stream unusable, so a write that
|
|
829
|
-
// never drains retires the server instead of corrupting later requests.
|
|
830
|
-
if (error instanceof HostDeadlineFailure) failLanguageServer(task, error);
|
|
831
|
-
throw error;
|
|
832
|
-
}
|
|
833
|
-
return null;
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
function nextLanguageServer(args, owner, activity) {
|
|
837
|
-
if (args.length !== 1) throw new TypeError("languageServer next arguments are invalid");
|
|
838
|
-
const task = ownedLanguageServer(args[0], owner);
|
|
839
|
-
if (task.waiter !== null) throw new Error("LanguageServer.next already has an active pull");
|
|
840
|
-
if (task.queue.length > 0) {
|
|
841
|
-
const value = task.queue.shift();
|
|
842
|
-
task.queuedBytes -= Buffer.byteLength(value, "utf8");
|
|
843
|
-
return value;
|
|
844
|
-
}
|
|
845
|
-
if (task.failure) throw task.failure;
|
|
846
|
-
if (task.settled) return null;
|
|
847
|
-
return new Promise((resolveNext, rejectNext) => {
|
|
848
|
-
const waiter = { resolve: resolveNext, reject: rejectNext, activity };
|
|
849
|
-
task.waiter = waiter;
|
|
850
|
-
setActivityCancellation(activity, () => {
|
|
851
|
-
if (task.waiter !== waiter) return;
|
|
852
|
-
task.waiter = null;
|
|
853
|
-
waiter.reject(new Error("Desktop language-server pull was cancelled"));
|
|
854
|
-
});
|
|
855
|
-
});
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
function releaseLanguageServer(task, error = null) {
|
|
859
|
-
if (languageServers.get(task.handle) !== task) return false;
|
|
860
|
-
languageServers.delete(task.handle);
|
|
861
|
-
if (task.waiter !== null) {
|
|
862
|
-
const waiter = task.waiter;
|
|
863
|
-
task.waiter = null;
|
|
864
|
-
waiter.activity.cancel = null;
|
|
865
|
-
if (error) waiter.reject(error);
|
|
866
|
-
else waiter.resolve(null);
|
|
867
|
-
}
|
|
868
|
-
try { task.child.stdin.end(); } catch {}
|
|
869
|
-
if (error) signalTree(task.child, "SIGTERM");
|
|
870
|
-
else setTimeout(() => { if (!task.settled) signalTree(task.child, "SIGTERM"); }, 500).unref();
|
|
871
|
-
setTimeout(() => { if (!task.settled) signalTree(task.child, "SIGKILL"); }, 2500).unref();
|
|
872
|
-
return true;
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
async function closeLanguageServer(args, owner) {
|
|
876
|
-
if (args.length !== 1) throw new TypeError("languageServer close arguments are invalid");
|
|
877
|
-
const task = ownedLanguageServer(args[0], owner);
|
|
878
|
-
releaseLanguageServer(task);
|
|
879
|
-
let timer = null;
|
|
880
|
-
try {
|
|
881
|
-
const closed = await Promise.race([
|
|
882
|
-
task.closed.then(() => true),
|
|
883
|
-
new Promise(resolve => { timer = setTimeout(() => resolve(false), 5000); }),
|
|
884
|
-
]);
|
|
885
|
-
if (!closed) {
|
|
886
|
-
signalTree(task.child, "SIGKILL");
|
|
887
|
-
throw new Error("Bundled language server did not close within 5000 milliseconds");
|
|
888
|
-
}
|
|
889
|
-
} finally {
|
|
890
|
-
if (timer !== null) clearTimeout(timer);
|
|
891
|
-
}
|
|
892
|
-
return null;
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
function languageServerOperation(operation, args, owner, activity) {
|
|
896
|
-
if (operation === "start") return startLanguageServer(args, owner, activity);
|
|
897
|
-
if (operation === "send") return sendLanguageServer(args, owner);
|
|
898
|
-
if (operation === "next") return nextLanguageServer(args, owner, activity);
|
|
899
|
-
if (operation === "close") return closeLanguageServer(args, owner);
|
|
900
|
-
throw new Error(`Unknown language-server operation '${operation}'`);
|
|
901
|
-
}
|
|
902
360
|
|
|
903
361
|
async function fsOperation(operation, args, owner, activity) {
|
|
904
362
|
if (operation === "watchStart") return startFileWatch(args, owner);
|
|
@@ -1151,360 +609,6 @@ async function commitTextReplacement(path, data, mode) {
|
|
|
1151
609
|
finally { await rm(temporary, {force: true, recursive: false}); }
|
|
1152
610
|
}
|
|
1153
611
|
|
|
1154
|
-
function allocateTerminalHandle() {
|
|
1155
|
-
let candidate = nextTerminalHandle;
|
|
1156
|
-
for (let attempts = 0; attempts <= MAX_TERMINALS; attempts += 1) {
|
|
1157
|
-
if (!terminals.has(candidate)) {
|
|
1158
|
-
nextTerminalHandle = candidate >= 2_000_000_003 ? 2_000_000_000 : candidate + 1;
|
|
1159
|
-
return candidate;
|
|
1160
|
-
}
|
|
1161
|
-
candidate = candidate >= 2_000_000_003 ? 2_000_000_000 : candidate + 1;
|
|
1162
|
-
}
|
|
1163
|
-
throw new RangeError("Desktop terminal handle space is unavailable");
|
|
1164
|
-
}
|
|
1165
|
-
|
|
1166
|
-
function ownedTerminal(value, owner) {
|
|
1167
|
-
const handle = integer(value, 1, Number.MAX_SAFE_INTEGER, "Desktop terminal handle");
|
|
1168
|
-
const task = terminals.get(handle);
|
|
1169
|
-
if (!task) throw new Error("Desktop terminal handle is unknown or already released");
|
|
1170
|
-
if (task.owner !== owner) throw new Error("Desktop terminal belongs to another document generation");
|
|
1171
|
-
return task;
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
function terminalFrame(kind, payload = Buffer.alloc(0)) {
|
|
1175
|
-
if (!(payload instanceof Buffer) || payload.byteLength > MAX_TERMINAL_CHUNK_BYTES) throw new RangeError("Terminal frame exceeds 1 MiB");
|
|
1176
|
-
const frame = Buffer.allocUnsafe(5 + payload.byteLength);
|
|
1177
|
-
frame[0] = kind;
|
|
1178
|
-
frame.writeUInt32BE(payload.byteLength, 1);
|
|
1179
|
-
payload.copy(frame, 5);
|
|
1180
|
-
return frame;
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
async function writeTerminalFrame(task, frame) {
|
|
1184
|
-
if (task.failure) throw task.failure;
|
|
1185
|
-
if (task.settled || task.closing || !task.child.stdin.writable) throw new Error("Desktop terminal is closed");
|
|
1186
|
-
try {
|
|
1187
|
-
await boundedHostWait(
|
|
1188
|
-
new Promise((resolveWrite, rejectWrite) => {
|
|
1189
|
-
task.child.stdin.write(frame, error => error ? rejectWrite(error) : resolveWrite(null));
|
|
1190
|
-
}),
|
|
1191
|
-
`Bundled terminal host did not accept a frame within ${HOST_PIPE_WRITE_TIMEOUT_MS} milliseconds`,
|
|
1192
|
-
HOST_PIPE_WRITE_TIMEOUT_MS,
|
|
1193
|
-
);
|
|
1194
|
-
} catch (error) {
|
|
1195
|
-
// A half-written frame desynchronizes the terminal transport, so a write
|
|
1196
|
-
// that never drains retires the session instead of corrupting it.
|
|
1197
|
-
if (error instanceof HostDeadlineFailure) failTerminal(task, error);
|
|
1198
|
-
throw error;
|
|
1199
|
-
}
|
|
1200
|
-
return null;
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
function settleTerminalPull(task) {
|
|
1204
|
-
if (task.waiter === null) return;
|
|
1205
|
-
if (task.queue.length > 0) {
|
|
1206
|
-
const waiter = task.waiter;
|
|
1207
|
-
task.waiter = null;
|
|
1208
|
-
waiter.activity.cancel = null;
|
|
1209
|
-
const value = task.queue.shift();
|
|
1210
|
-
task.queuedBytes -= Buffer.byteLength(value, "utf8");
|
|
1211
|
-
if (task.paused && task.queuedBytes <= TERMINAL_RESUME_BYTES) {
|
|
1212
|
-
task.paused = false;
|
|
1213
|
-
task.child.stdout.resume();
|
|
1214
|
-
}
|
|
1215
|
-
waiter.resolve(value);
|
|
1216
|
-
} else if (task.failure || task.settled) {
|
|
1217
|
-
const waiter = task.waiter;
|
|
1218
|
-
task.waiter = null;
|
|
1219
|
-
waiter.activity.cancel = null;
|
|
1220
|
-
if (task.failure) waiter.reject(task.failure);
|
|
1221
|
-
else { task.outputEnded = true; waiter.resolve(null); }
|
|
1222
|
-
}
|
|
1223
|
-
}
|
|
1224
|
-
|
|
1225
|
-
function enqueueTerminalText(task, value) {
|
|
1226
|
-
if (value.length === 0) return;
|
|
1227
|
-
const bytes = Buffer.byteLength(value, "utf8");
|
|
1228
|
-
if (bytes > MAX_TERMINAL_CHUNK_BYTES || task.queuedBytes + bytes > MAX_TERMINAL_QUEUED_BYTES) {
|
|
1229
|
-
failTerminal(task, new RangeError("Desktop terminal output queue exceeded 4 MiB"));
|
|
1230
|
-
return;
|
|
1231
|
-
}
|
|
1232
|
-
if (task.waiter !== null) {
|
|
1233
|
-
const waiter = task.waiter;
|
|
1234
|
-
task.waiter = null;
|
|
1235
|
-
waiter.activity.cancel = null;
|
|
1236
|
-
waiter.resolve(value);
|
|
1237
|
-
return;
|
|
1238
|
-
}
|
|
1239
|
-
task.queue.push(value);
|
|
1240
|
-
task.queuedBytes += bytes;
|
|
1241
|
-
if (!task.paused && task.queuedBytes >= TERMINAL_PAUSE_BYTES) {
|
|
1242
|
-
task.paused = true;
|
|
1243
|
-
task.child.stdout.pause();
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
1246
|
-
|
|
1247
|
-
function signalTerminalTree(task, signal) {
|
|
1248
|
-
if (task.shellPid !== null) {
|
|
1249
|
-
try { process.kill(-task.shellPid, signal); }
|
|
1250
|
-
catch { try { process.kill(task.shellPid, signal); } catch {} }
|
|
1251
|
-
}
|
|
1252
|
-
signalTree(task.child, signal);
|
|
1253
|
-
}
|
|
1254
|
-
|
|
1255
|
-
function failTerminal(task, error) {
|
|
1256
|
-
if (!task.failure) task.failure = error instanceof Error ? error : new Error("Desktop terminal failed");
|
|
1257
|
-
settleTerminalPull(task);
|
|
1258
|
-
signalTerminalTree(task, "SIGKILL");
|
|
1259
|
-
}
|
|
1260
|
-
|
|
1261
|
-
async function terminalMetadata(task) {
|
|
1262
|
-
const stream = task.child.stdio[3];
|
|
1263
|
-
if (!stream) throw new Error("Bundled terminal host ownership channel is unavailable");
|
|
1264
|
-
let text = "";
|
|
1265
|
-
stream.setEncoding("utf8");
|
|
1266
|
-
// The ownership handshake is the one reply the host cannot proceed without,
|
|
1267
|
-
// so it carries its own bound: a terminal host that neither publishes its
|
|
1268
|
-
// shell nor exits fails this open instead of blocking it forever.
|
|
1269
|
-
const metadata = await boundedHostWait(new Promise((resolveMetadata, rejectMetadata) => {
|
|
1270
|
-
stream.on("data", chunk => {
|
|
1271
|
-
text += chunk;
|
|
1272
|
-
if (Buffer.byteLength(text, "utf8") > 256) rejectMetadata(new RangeError("Bundled terminal host metadata exceeds 256 bytes"));
|
|
1273
|
-
});
|
|
1274
|
-
stream.once("error", rejectMetadata);
|
|
1275
|
-
stream.once("end", () => resolveMetadata(text));
|
|
1276
|
-
task.child.once("error", rejectMetadata);
|
|
1277
|
-
task.child.once("close", () => rejectMetadata(new Error("Bundled terminal host closed before publishing shell ownership")));
|
|
1278
|
-
}), `Bundled terminal host did not publish shell ownership within ${HOST_HANDSHAKE_TIMEOUT_MS} milliseconds`, HOST_HANDSHAKE_TIMEOUT_MS);
|
|
1279
|
-
let value;
|
|
1280
|
-
try { value = JSON.parse(metadata); }
|
|
1281
|
-
catch { throw new Error("Bundled terminal host returned invalid ownership metadata"); }
|
|
1282
|
-
if (!value || typeof value !== "object" || Array.isArray(value)
|
|
1283
|
-
|| Object.keys(value).some(key => !["protocolVersion", "pid"].includes(key))
|
|
1284
|
-
|| value.protocolVersion !== 1 || !Number.isSafeInteger(value.pid) || value.pid < 1 || value.pid === task.child.pid) {
|
|
1285
|
-
throw new Error("Bundled terminal host returned invalid ownership metadata");
|
|
1286
|
-
}
|
|
1287
|
-
return value.pid;
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
|
-
async function terminalOpen(args, owner, activity) {
|
|
1291
|
-
if (!terminalGranted) throw new Error("Desktop terminal access requires desktop.permissions.terminal");
|
|
1292
|
-
if (terminalHostPath === null) throw new Error("This Desktop package does not contain the official terminal host");
|
|
1293
|
-
if (projectRoot === null || !fileScopes.has("project")) throw new Error("Desktop terminals require the project file grant");
|
|
1294
|
-
if (args.length !== 1 || !args[0] || typeof args[0] !== "object" || Array.isArray(args[0])
|
|
1295
|
-
|| Object.keys(args[0]).some(key => !["columns", "rows"].includes(key))) throw new TypeError("Terminal options are invalid");
|
|
1296
|
-
const columns = integer(args[0].columns ?? 80, 20, 1000, "Terminal columns");
|
|
1297
|
-
const rows = integer(args[0].rows ?? 24, 5, 1000, "Terminal rows");
|
|
1298
|
-
if (terminals.size >= MAX_TERMINALS) throw new RangeError("Desktop cannot own more than 4 terminals");
|
|
1299
|
-
if (activity.cancelled) throw new Error("Desktop host request was cancelled");
|
|
1300
|
-
const handle = allocateTerminalHandle();
|
|
1301
|
-
const environment = Object.create(null);
|
|
1302
|
-
for (const name of ["HOME", "LANG", "LC_ALL", "LC_CTYPE", "TMPDIR"]) {
|
|
1303
|
-
if (typeof process.env[name] === "string") environment[name] = process.env[name];
|
|
1304
|
-
}
|
|
1305
|
-
environment.TERM = "xterm-256color";
|
|
1306
|
-
environment.COLORTERM = "truecolor";
|
|
1307
|
-
const child = spawn(terminalHostPath, [projectLexicalRoot ?? projectRoot, String(columns), String(rows)], {
|
|
1308
|
-
cwd: projectRoot,
|
|
1309
|
-
env: environment,
|
|
1310
|
-
shell: false,
|
|
1311
|
-
windowsHide: true,
|
|
1312
|
-
detached: true,
|
|
1313
|
-
stdio: ["pipe", "pipe", "pipe", "pipe"],
|
|
1314
|
-
});
|
|
1315
|
-
let resolveResult;
|
|
1316
|
-
let rejectResult;
|
|
1317
|
-
const result = new Promise((resolve, reject) => { resolveResult = resolve; rejectResult = reject; });
|
|
1318
|
-
result.catch(() => {});
|
|
1319
|
-
const task = {
|
|
1320
|
-
handle, owner, child, shellPid: null, queue: [], queuedBytes: 0, waiter: null, paused: false,
|
|
1321
|
-
decoder: new StringDecoder("utf8"), failure: null, settled: false, closing: false, ownershipPublished: false, outputEnded: false,
|
|
1322
|
-
result, resolveResult, rejectResult, stderr: "",
|
|
1323
|
-
};
|
|
1324
|
-
terminals.set(handle, task);
|
|
1325
|
-
setActivityCancellation(activity, () => releaseTerminal(task, new Error("Desktop terminal start was cancelled")));
|
|
1326
|
-
child.stdout.on("data", chunk => enqueueTerminalText(task, task.decoder.write(chunk)));
|
|
1327
|
-
child.stderr.on("data", chunk => {
|
|
1328
|
-
if (task.stderr.length < 64 * 1024) task.stderr += chunk.toString("utf8").slice(0, 64 * 1024 - task.stderr.length);
|
|
1329
|
-
});
|
|
1330
|
-
child.once("error", error => failTerminal(task, error));
|
|
1331
|
-
child.once("close", (code, signal) => {
|
|
1332
|
-
enqueueTerminalText(task, task.decoder.end());
|
|
1333
|
-
task.settled = true;
|
|
1334
|
-
if (task.failure) task.rejectResult(task.failure);
|
|
1335
|
-
else if (code === null) task.rejectResult(new Error(`Bundled terminal host exited with ${signal ?? "an unknown signal"}${task.stderr ? `: ${task.stderr}` : ""}`));
|
|
1336
|
-
else task.resolveResult({code});
|
|
1337
|
-
settleTerminalPull(task);
|
|
1338
|
-
if (task.ownershipPublished) respond({ protocolVersion: 1, hostEvent: "terminal-settled", owner, handle });
|
|
1339
|
-
});
|
|
1340
|
-
try {
|
|
1341
|
-
const ownership = terminalMetadata(task);
|
|
1342
|
-
// A failed spawn rejects both waits; keep the handshake observed so the
|
|
1343
|
-
// losing rejection cannot escalate into a fatal drain of the whole host.
|
|
1344
|
-
void ownership.catch(() => {});
|
|
1345
|
-
await new Promise((resolveStart, rejectStart) => {
|
|
1346
|
-
child.once("spawn", resolveStart);
|
|
1347
|
-
child.once("error", rejectStart);
|
|
1348
|
-
});
|
|
1349
|
-
task.shellPid = await ownership;
|
|
1350
|
-
} catch (error) {
|
|
1351
|
-
releaseTerminal(task, error instanceof Error ? error : new Error("Bundled terminal host failed to start"));
|
|
1352
|
-
throw error;
|
|
1353
|
-
}
|
|
1354
|
-
activity.cancel = null;
|
|
1355
|
-
task.ownershipPublished = true;
|
|
1356
|
-
respond({ protocolVersion: 1, hostEvent: "terminal-owned", owner, handle, pids: [child.pid, task.shellPid] });
|
|
1357
|
-
if (activity.cancelled || owner !== activeOwner) {
|
|
1358
|
-
releaseTerminal(task, new Error("Desktop document generation is no longer active"));
|
|
1359
|
-
throw new Error("Desktop document generation is no longer active");
|
|
1360
|
-
}
|
|
1361
|
-
return {handle, pid: task.shellPid};
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1364
|
-
function terminalWrite(args, owner) {
|
|
1365
|
-
if (args.length !== 2 || typeof args[1] !== "string" || Buffer.byteLength(args[1], "utf8") < 1
|
|
1366
|
-
|| Buffer.byteLength(args[1], "utf8") > MAX_TERMINAL_CHUNK_BYTES) throw new RangeError("Terminal write requires 1 byte through 1 MiB of text");
|
|
1367
|
-
return writeTerminalFrame(ownedTerminal(args[0], owner), terminalFrame(1, Buffer.from(args[1], "utf8")));
|
|
1368
|
-
}
|
|
1369
|
-
|
|
1370
|
-
function terminalResize(args, owner) {
|
|
1371
|
-
if (args.length !== 3) throw new TypeError("Terminal resize arguments are invalid");
|
|
1372
|
-
const columns = integer(args[1], 20, 1000, "Terminal columns");
|
|
1373
|
-
const rows = integer(args[2], 5, 1000, "Terminal rows");
|
|
1374
|
-
const payload = Buffer.allocUnsafe(8);
|
|
1375
|
-
payload.writeUInt32BE(columns, 0);
|
|
1376
|
-
payload.writeUInt32BE(rows, 4);
|
|
1377
|
-
return writeTerminalFrame(ownedTerminal(args[0], owner), terminalFrame(2, payload));
|
|
1378
|
-
}
|
|
1379
|
-
|
|
1380
|
-
function terminalNext(args, owner, activity) {
|
|
1381
|
-
if (args.length !== 1) throw new TypeError("Terminal next arguments are invalid");
|
|
1382
|
-
const task = ownedTerminal(args[0], owner);
|
|
1383
|
-
if (task.waiter !== null) throw new Error("TerminalSession.next already has an active pull");
|
|
1384
|
-
if (task.queue.length > 0) {
|
|
1385
|
-
const value = task.queue.shift();
|
|
1386
|
-
task.queuedBytes -= Buffer.byteLength(value, "utf8");
|
|
1387
|
-
if (task.paused && task.queuedBytes <= TERMINAL_RESUME_BYTES) { task.paused = false; task.child.stdout.resume(); }
|
|
1388
|
-
return value;
|
|
1389
|
-
}
|
|
1390
|
-
if (task.failure) throw task.failure;
|
|
1391
|
-
if (task.settled) { task.outputEnded = true; return null; }
|
|
1392
|
-
return new Promise((resolveNext, rejectNext) => {
|
|
1393
|
-
const waiter = {resolve: resolveNext, reject: rejectNext, activity};
|
|
1394
|
-
task.waiter = waiter;
|
|
1395
|
-
setActivityCancellation(activity, () => {
|
|
1396
|
-
if (task.waiter !== waiter) return;
|
|
1397
|
-
task.waiter = null;
|
|
1398
|
-
waiter.reject(new Error("Desktop terminal pull was cancelled"));
|
|
1399
|
-
});
|
|
1400
|
-
});
|
|
1401
|
-
}
|
|
1402
|
-
|
|
1403
|
-
async function terminalWait(args, owner) {
|
|
1404
|
-
if (args.length !== 1) throw new TypeError("Terminal wait arguments are invalid");
|
|
1405
|
-
const task = ownedTerminal(args[0], owner);
|
|
1406
|
-
if (!task.outputEnded) throw new Error("Terminal output must be consumed before wait()");
|
|
1407
|
-
const result = await task.result;
|
|
1408
|
-
terminals.delete(task.handle);
|
|
1409
|
-
return result;
|
|
1410
|
-
}
|
|
1411
|
-
|
|
1412
|
-
function releaseTerminal(task, error = null) {
|
|
1413
|
-
if (terminals.get(task.handle) !== task || task.closing) return false;
|
|
1414
|
-
task.closing = true;
|
|
1415
|
-
if (error && !task.failure) task.failure = error;
|
|
1416
|
-
if (task.waiter !== null) {
|
|
1417
|
-
const waiter = task.waiter;
|
|
1418
|
-
task.waiter = null;
|
|
1419
|
-
waiter.activity.cancel = null;
|
|
1420
|
-
if (error) waiter.reject(error);
|
|
1421
|
-
else if (task.queue.length === 0) waiter.resolve(null);
|
|
1422
|
-
}
|
|
1423
|
-
try { task.child.stdin.end(terminalFrame(3)); } catch {}
|
|
1424
|
-
setTimeout(() => { if (!task.settled) signalTerminalTree(task, "SIGTERM"); }, 500).unref();
|
|
1425
|
-
setTimeout(() => { if (!task.settled) signalTerminalTree(task, "SIGKILL"); }, 2500).unref();
|
|
1426
|
-
if (error) void task.result.finally(() => { if (terminals.get(task.handle) === task) terminals.delete(task.handle); }).catch(() => {});
|
|
1427
|
-
return true;
|
|
1428
|
-
}
|
|
1429
|
-
|
|
1430
|
-
async function terminalClose(args, owner) {
|
|
1431
|
-
if (args.length !== 1) throw new TypeError("Terminal close arguments are invalid");
|
|
1432
|
-
const task = ownedTerminal(args[0], owner);
|
|
1433
|
-
releaseTerminal(task);
|
|
1434
|
-
const result = await task.result;
|
|
1435
|
-
terminals.delete(task.handle);
|
|
1436
|
-
return result;
|
|
1437
|
-
}
|
|
1438
|
-
|
|
1439
|
-
function terminalOperation(operation, args, owner, activity) {
|
|
1440
|
-
if (operation === "open") return terminalOpen(args, owner, activity);
|
|
1441
|
-
if (operation === "write") return terminalWrite(args, owner);
|
|
1442
|
-
if (operation === "resize") return terminalResize(args, owner);
|
|
1443
|
-
if (operation === "next") return terminalNext(args, owner, activity);
|
|
1444
|
-
if (operation === "wait") return terminalWait(args, owner);
|
|
1445
|
-
if (operation === "close") return terminalClose(args, owner);
|
|
1446
|
-
throw new Error(`Unknown terminal operation '${operation}'`);
|
|
1447
|
-
}
|
|
1448
|
-
|
|
1449
|
-
async function projectTaskStart(args, owner, activity) {
|
|
1450
|
-
if (projectTaskPath === null || buildEnginePath === null) throw new Error("This Desktop package does not contain official project tasks");
|
|
1451
|
-
if (projectRoot === null || !fileScopes.has("project")) throw new Error("Desktop project tasks require the project file grant");
|
|
1452
|
-
if (args.length !== 3) throw new TypeError("Project task start arguments are invalid");
|
|
1453
|
-
const [command, commandArgs, options] = args;
|
|
1454
|
-
if (!["check", "test", "browserTest", "build", "fix", "package", "run"].includes(command)) throw new TypeError("Project task command is invalid");
|
|
1455
|
-
if (!Array.isArray(commandArgs) || commandArgs.length > 1000 || command !== "run" && commandArgs.length > 0) {
|
|
1456
|
-
throw new TypeError("Only a run project task accepts a bounded List<string> of program arguments");
|
|
1457
|
-
}
|
|
1458
|
-
let argumentUnits = 0;
|
|
1459
|
-
for (const item of commandArgs) {
|
|
1460
|
-
if (typeof item !== "string" || item.length > 1024 * 1024 || item.includes("\0")) throw new TypeError("Project task arguments must contain bounded strings");
|
|
1461
|
-
argumentUnits += item.length;
|
|
1462
|
-
if (argumentUnits > 1024 * 1024) throw new RangeError("Project task arguments cannot exceed 1 MiB");
|
|
1463
|
-
}
|
|
1464
|
-
if (!options || typeof options !== "object" || Array.isArray(options)
|
|
1465
|
-
|| Object.keys(options).some(key => !["timeout", "maxOutputBytes"].includes(key))) throw new TypeError("Project task options are invalid");
|
|
1466
|
-
const timeout = integer(options.timeout ?? 120000, 0, 600000, "Project task timeout");
|
|
1467
|
-
const maxOutputBytes = integer(options.maxOutputBytes ?? 4 * 1024 * 1024, 1, 16 * 1024 * 1024, "Project task maxOutputBytes");
|
|
1468
|
-
let activeProjectTasks = 0;
|
|
1469
|
-
for (const task of processHandles.values()) if (task.kind === "project-task") activeProjectTasks += 1;
|
|
1470
|
-
if (activeProjectTasks >= MAX_PROJECT_TASKS) throw new RangeError("Desktop cannot own more than 4 project tasks");
|
|
1471
|
-
if (processHandles.size >= 128) throw new RangeError("Desktop process handle limit reached");
|
|
1472
|
-
if (activity.cancelled) throw new Error("Desktop host request was cancelled");
|
|
1473
|
-
const handle = nextProcessHandle++;
|
|
1474
|
-
const environment = Object.create(null);
|
|
1475
|
-
for (const name of ["HOME", "LANG", "LC_ALL", "TMPDIR"]) if (typeof process.env[name] === "string") environment[name] = process.env[name];
|
|
1476
|
-
environment.ESBUILD_BINARY_PATH = buildEnginePath;
|
|
1477
|
-
if (command === "package") environment.VELAR_DESKTOP_PACKAGE_TEMPLATE_ROOT = await realpath(dirname(configPath));
|
|
1478
|
-
const toolArguments = [projectTaskPath, command, projectLexicalRoot ?? projectRoot];
|
|
1479
|
-
if (command === "run" && commandArgs.length > 0) toolArguments.push("--", ...commandArgs);
|
|
1480
|
-
let task = null;
|
|
1481
|
-
setActivityCancellation(activity, () => { if (task !== null) retainRetiredProcess(handle, task); });
|
|
1482
|
-
task = await launchChild(process.execPath, toolArguments, {
|
|
1483
|
-
cwd: projectRoot,
|
|
1484
|
-
environment,
|
|
1485
|
-
stdin: "",
|
|
1486
|
-
timeout,
|
|
1487
|
-
maxOutputBytes,
|
|
1488
|
-
}, () => respond({ protocolVersion: 1, hostEvent: "process-settled", owner, handle }));
|
|
1489
|
-
task.owner = owner;
|
|
1490
|
-
task.kind = "project-task";
|
|
1491
|
-
processHandles.set(handle, task);
|
|
1492
|
-
respond({ protocolVersion: 1, hostEvent: "process-owned", owner, handle, pid: task.pid });
|
|
1493
|
-
task.result.catch(() => {});
|
|
1494
|
-
if (activity.cancelled || owner !== activeOwner) {
|
|
1495
|
-
retainRetiredProcess(handle, task);
|
|
1496
|
-
throw new Error("Desktop document generation is no longer active");
|
|
1497
|
-
}
|
|
1498
|
-
return {handle, pid: task.pid};
|
|
1499
|
-
}
|
|
1500
|
-
|
|
1501
|
-
function projectTaskOperation(operation, args, owner, activity) {
|
|
1502
|
-
if (operation === "start") return projectTaskStart(args, owner, activity);
|
|
1503
|
-
if (operation === "read") return processRead(args, owner, "project-task");
|
|
1504
|
-
if (operation === "wait") return processWait(args, owner, "project-task");
|
|
1505
|
-
if (operation === "stop") return processStop(args, owner, "project-task");
|
|
1506
|
-
throw new Error(`Unknown project-task operation '${operation}'`);
|
|
1507
|
-
}
|
|
1508
612
|
|
|
1509
613
|
async function processRun(args, owner, activity) {
|
|
1510
614
|
const started = await processStart(args, owner, activity);
|
|
@@ -1547,8 +651,8 @@ async function processStart(args, owner, activity) {
|
|
|
1547
651
|
return { handle, pid: task.pid };
|
|
1548
652
|
}
|
|
1549
653
|
|
|
1550
|
-
async function processWait(args, owner
|
|
1551
|
-
const [handle, task] = ownedProcess(args[0], owner
|
|
654
|
+
async function processWait(args, owner) {
|
|
655
|
+
const [handle, task] = ownedProcess(args[0], owner);
|
|
1552
656
|
if (task.reading) throw new Error("Process wait() cannot run while next() is pending");
|
|
1553
657
|
task.waitStarted = true;
|
|
1554
658
|
if (task.waitRetained && !task.settled) signalTree(task.child, "SIGKILL");
|
|
@@ -1558,16 +662,16 @@ async function processWait(args, owner, kind = "process") {
|
|
|
1558
662
|
return outcome;
|
|
1559
663
|
}
|
|
1560
664
|
|
|
1561
|
-
async function processRead(args, owner
|
|
1562
|
-
const [, task] = ownedProcess(args[0], owner
|
|
665
|
+
async function processRead(args, owner) {
|
|
666
|
+
const [, task] = ownedProcess(args[0], owner);
|
|
1563
667
|
return task.next();
|
|
1564
668
|
}
|
|
1565
669
|
|
|
1566
|
-
async function processStop(args, owner
|
|
670
|
+
async function processStop(args, owner) {
|
|
1567
671
|
const handle = processHandle(args[0]);
|
|
1568
672
|
const task = processHandles.get(handle);
|
|
1569
673
|
if (!task) return { result: null, error: null };
|
|
1570
|
-
if (task.kind !==
|
|
674
|
+
if (task.kind !== "process") throw new Error("Desktop process handle is unknown or already released");
|
|
1571
675
|
if (task.owner !== owner) throw new Error("Desktop process handle belongs to another document generation");
|
|
1572
676
|
task.stop();
|
|
1573
677
|
const outcome = await waitForTask(task);
|
|
@@ -1576,10 +680,10 @@ async function processStop(args, owner, kind = "process") {
|
|
|
1576
680
|
return { result: outcome.result, error: outcome.error };
|
|
1577
681
|
}
|
|
1578
682
|
|
|
1579
|
-
function ownedProcess(value, owner
|
|
683
|
+
function ownedProcess(value, owner) {
|
|
1580
684
|
const handle = processHandle(value);
|
|
1581
685
|
const task = processHandles.get(handle);
|
|
1582
|
-
if (!task || task.kind !==
|
|
686
|
+
if (!task || task.kind !== "process") throw new Error("Desktop process handle is unknown or already released");
|
|
1583
687
|
if (task.owner !== owner) throw new Error("Desktop process handle belongs to another document generation");
|
|
1584
688
|
return [handle, task];
|
|
1585
689
|
}
|
|
@@ -1606,15 +710,6 @@ function retireOwner(owner) {
|
|
|
1606
710
|
if (task.owner === owner) releaseFileWatcher(task, new Error("Desktop document generation retired"));
|
|
1607
711
|
}
|
|
1608
712
|
for (const activity of activeRequests.values()) if (activity.owner === owner) cancelActivity(activity);
|
|
1609
|
-
for (const task of languageServers.values()) {
|
|
1610
|
-
if (task.owner === owner) releaseLanguageServer(task, new Error("Desktop document generation retired"));
|
|
1611
|
-
}
|
|
1612
|
-
for (const task of projectChangeHandles.values()) {
|
|
1613
|
-
if (task.owner === owner) releaseProjectChangeHandle(task, new Error("Desktop document generation retired"));
|
|
1614
|
-
}
|
|
1615
|
-
for (const task of terminals.values()) {
|
|
1616
|
-
if (task.owner === owner) releaseTerminal(task, new Error("Desktop document generation retired"));
|
|
1617
|
-
}
|
|
1618
713
|
for (const [handle, task] of processHandles) {
|
|
1619
714
|
if (task.owner === owner) retainRetiredProcess(handle, task);
|
|
1620
715
|
}
|
|
@@ -1632,9 +727,6 @@ async function fatalDrain() {
|
|
|
1632
727
|
reader.removeAllListeners("line");
|
|
1633
728
|
reader.close();
|
|
1634
729
|
for (const task of fileWatchers.values()) releaseFileWatcher(task, new Error("Desktop capability host failed"));
|
|
1635
|
-
for (const task of languageServers.values()) releaseLanguageServer(task, new Error("Desktop capability host failed"));
|
|
1636
|
-
retireProjectTransactionOwner(new Error("Desktop capability host failed"));
|
|
1637
|
-
for (const task of terminals.values()) releaseTerminal(task, new Error("Desktop capability host failed"));
|
|
1638
730
|
const tasks = Array.from(processHandles.values());
|
|
1639
731
|
for (const task of tasks) task.stop();
|
|
1640
732
|
for (const request of httpHandles.values()) request.controller.abort(new Error("Desktop capability host failed"));
|
|
@@ -1665,19 +757,19 @@ function processError(value) {
|
|
|
1665
757
|
return new Error(value.message);
|
|
1666
758
|
}
|
|
1667
759
|
|
|
1668
|
-
function
|
|
760
|
+
function processCompletionOutcome(task) {
|
|
1669
761
|
return task.result.then(
|
|
1670
762
|
(result) => ({ result, error: null, retained: false }),
|
|
1671
763
|
(failure) => ({ result: null, error: processErrorRecord(failure), retained: false }),
|
|
1672
764
|
);
|
|
1673
765
|
}
|
|
1674
766
|
|
|
1675
|
-
async function processConfirmationOutcome(
|
|
767
|
+
async function processConfirmationOutcome(completion) {
|
|
1676
768
|
let timer = null;
|
|
1677
769
|
const confirmationFailure = new Error(`Process termination could not be confirmed within ${PROCESS_STOP_CONFIRMATION_TIMEOUT_MS} milliseconds`);
|
|
1678
770
|
try {
|
|
1679
771
|
return await Promise.race([
|
|
1680
|
-
|
|
772
|
+
completion,
|
|
1681
773
|
new Promise((resolveOutcome) => {
|
|
1682
774
|
timer = setTimeout(
|
|
1683
775
|
() => resolveOutcome({ result: null, error: processErrorRecord(confirmationFailure), retained: true }),
|
|
@@ -1691,27 +783,27 @@ async function processConfirmationOutcome(terminal) {
|
|
|
1691
783
|
}
|
|
1692
784
|
|
|
1693
785
|
async function waitForTask(task) {
|
|
1694
|
-
const
|
|
786
|
+
const completion = processCompletionOutcome(task);
|
|
1695
787
|
if (!task.terminationRequested) {
|
|
1696
|
-
const first = await Promise.race([
|
|
788
|
+
const first = await Promise.race([completion, task.termination.then(() => processTerminationMarker)]);
|
|
1697
789
|
if (first !== processTerminationMarker) return first;
|
|
1698
790
|
}
|
|
1699
791
|
if (task.rootExited && !task.stopping) {
|
|
1700
|
-
const afterExit = await Promise.race([
|
|
792
|
+
const afterExit = await Promise.race([completion, task.stopRequest.then(() => processStopMarker)]);
|
|
1701
793
|
if (afterExit !== processStopMarker) return afterExit;
|
|
1702
|
-
return await processConfirmationOutcome(
|
|
794
|
+
return await processConfirmationOutcome(completion);
|
|
1703
795
|
}
|
|
1704
|
-
const confirmation = processConfirmationOutcome(
|
|
796
|
+
const confirmation = processConfirmationOutcome(completion);
|
|
1705
797
|
const first = await Promise.race([
|
|
1706
|
-
|
|
798
|
+
completion,
|
|
1707
799
|
confirmation,
|
|
1708
800
|
task.stopping ? new Promise(() => {}) : task.rootExit.then(() => processRootExitMarker),
|
|
1709
801
|
]);
|
|
1710
802
|
if (first !== processRootExitMarker) return first;
|
|
1711
803
|
if (task.stopping) return await confirmation;
|
|
1712
|
-
const afterExit = await Promise.race([
|
|
804
|
+
const afterExit = await Promise.race([completion, task.stopRequest.then(() => processStopMarker)]);
|
|
1713
805
|
if (afterExit !== processStopMarker) return afterExit;
|
|
1714
|
-
return await processConfirmationOutcome(
|
|
806
|
+
return await processConfirmationOutcome(completion);
|
|
1715
807
|
}
|
|
1716
808
|
|
|
1717
809
|
function processHandle(value) {
|
|
@@ -2213,8 +1305,6 @@ function respond(value) {
|
|
|
2213
1305
|
|
|
2214
1306
|
process.once("exit", () => {
|
|
2215
1307
|
for (const task of processHandles.values()) signalTree(task.child, "SIGKILL");
|
|
2216
|
-
for (const task of languageServers.values()) signalTree(task.child, "SIGKILL");
|
|
2217
|
-
for (const task of terminals.values()) signalTerminalTree(task, "SIGKILL");
|
|
2218
1308
|
});
|
|
2219
1309
|
process.on("uncaughtException", () => { void fatalDrain(); });
|
|
2220
1310
|
process.on("unhandledRejection", () => { void fatalDrain(); });
|