@velarscript/desktop 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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, kind = "process") {
1551
- const [handle, task] = ownedProcess(args[0], owner, kind);
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, kind = "process") {
1562
- const [, task] = ownedProcess(args[0], owner, kind);
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, kind = "process") {
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 !== kind) throw new Error(`Desktop ${kind === "process" ? "process" : "project task"} handle is unknown or already released`);
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, kind = "process") {
683
+ function ownedProcess(value, owner) {
1580
684
  const handle = processHandle(value);
1581
685
  const task = processHandles.get(handle);
1582
- if (!task || task.kind !== kind) throw new Error(`Desktop ${kind === "process" ? "process" : "project task"} handle is unknown or already released`);
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 terminalProcessOutcome(task) {
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(terminal) {
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
- terminal,
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 terminal = terminalProcessOutcome(task);
786
+ const completion = processCompletionOutcome(task);
1695
787
  if (!task.terminationRequested) {
1696
- const first = await Promise.race([terminal, task.termination.then(() => processTerminationMarker)]);
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([terminal, task.stopRequest.then(() => processStopMarker)]);
792
+ const afterExit = await Promise.race([completion, task.stopRequest.then(() => processStopMarker)]);
1701
793
  if (afterExit !== processStopMarker) return afterExit;
1702
- return await processConfirmationOutcome(terminal);
794
+ return await processConfirmationOutcome(completion);
1703
795
  }
1704
- const confirmation = processConfirmationOutcome(terminal);
796
+ const confirmation = processConfirmationOutcome(completion);
1705
797
  const first = await Promise.race([
1706
- terminal,
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([terminal, task.stopRequest.then(() => processStopMarker)]);
804
+ const afterExit = await Promise.race([completion, task.stopRequest.then(() => processStopMarker)]);
1713
805
  if (afterExit !== processStopMarker) return afterExit;
1714
- return await processConfirmationOutcome(terminal);
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(); });