ttsc 0.17.4 → 0.18.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/cmd/ttscserver/main.go +8 -0
- package/driver/lsp.go +14 -0
- package/internal/graphsymbols/provider.go +514 -0
- package/internal/lspserver/lsp_proxy.go +32 -0
- package/internal/lspserver/lsp_server.go +5 -0
- package/internal/lspserver/lsp_symbols.go +124 -0
- package/lib/plugin/internal/buildSourcePlugin.js +156 -12
- package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
- package/package.json +8 -8
- package/src/plugin/internal/buildSourcePlugin.ts +242 -26
|
@@ -104,6 +104,9 @@ const CONTRIB_DIRNAME = "contrib";
|
|
|
104
104
|
// runtimeHooks.ts.
|
|
105
105
|
const PLUGIN_BUILD_LOCK_STEAL_MS = 600_000;
|
|
106
106
|
const PLUGIN_BUILD_LOCK_POLL_MS = 50;
|
|
107
|
+
const PLUGIN_BUILD_LOCK_LEGACY_STALE_MS = 30_000;
|
|
108
|
+
const PLUGIN_BUILD_LOCK_STATUS_MS = 30_000;
|
|
109
|
+
const PLUGIN_BUILD_LOCK_OWNER_FILE = "owner.json";
|
|
107
110
|
// The default cache lives INSIDE the workspace, at
|
|
108
111
|
// `<workspaceRoot>/node_modules/.cache/ttsc`, so `rm -rf node_modules` (or
|
|
109
112
|
// deleting the repo) reclaims every compiled plugin binary and Go object file.
|
|
@@ -192,22 +195,28 @@ export function buildSourcePlugin(opts: {
|
|
|
192
195
|
return binaryPath;
|
|
193
196
|
}
|
|
194
197
|
fs.mkdirSync(cacheDir, { recursive: true });
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
198
|
+
const label = opts.label ?? "source plugin";
|
|
199
|
+
const quiet = opts.quiet === true;
|
|
200
|
+
return buildUnderPluginLock(
|
|
201
|
+
cacheDir,
|
|
202
|
+
binaryPath,
|
|
203
|
+
{ label, pluginName: opts.pluginName, quiet },
|
|
204
|
+
() =>
|
|
205
|
+
compileSourcePlugin({
|
|
206
|
+
binaryPath,
|
|
207
|
+
cacheDir,
|
|
208
|
+
contributors,
|
|
209
|
+
dir,
|
|
210
|
+
entry,
|
|
211
|
+
goBinary,
|
|
212
|
+
key,
|
|
213
|
+
label,
|
|
214
|
+
goBuildCacheRoot: paths.goBuildRoot,
|
|
215
|
+
overlayDirs,
|
|
216
|
+
pluginName: opts.pluginName,
|
|
217
|
+
quiet,
|
|
218
|
+
source,
|
|
219
|
+
}),
|
|
211
220
|
);
|
|
212
221
|
}
|
|
213
222
|
|
|
@@ -235,7 +244,8 @@ function compileSourcePlugin(opts: {
|
|
|
235
244
|
.map((c) => c.name)
|
|
236
245
|
.join(", ")}`;
|
|
237
246
|
process.stderr.write(
|
|
238
|
-
`ttsc: building ${opts.label} "${opts.pluginName}" from ${opts.source}${extra}
|
|
247
|
+
`ttsc: building ${opts.label} "${opts.pluginName}" from ${opts.source}${extra} ` +
|
|
248
|
+
`(this runs once per cache key and can take several minutes on a cold Go cache)\n`,
|
|
239
249
|
);
|
|
240
250
|
}
|
|
241
251
|
|
|
@@ -294,6 +304,11 @@ function compileSourcePlugin(opts: {
|
|
|
294
304
|
function buildUnderPluginLock(
|
|
295
305
|
cacheDir: string,
|
|
296
306
|
binaryPath: string,
|
|
307
|
+
lockInfo: {
|
|
308
|
+
label: string;
|
|
309
|
+
pluginName: string;
|
|
310
|
+
quiet: boolean;
|
|
311
|
+
},
|
|
297
312
|
build: () => string,
|
|
298
313
|
): string {
|
|
299
314
|
const lockDir = `${cacheDir}.lock`;
|
|
@@ -308,6 +323,7 @@ function buildUnderPluginLock(
|
|
|
308
323
|
// exists, so a single-level mkdir is all that is needed and its EEXIST
|
|
309
324
|
// is exactly the "someone else holds the lock" signal.
|
|
310
325
|
fs.mkdirSync(lockDir);
|
|
326
|
+
writePluginBuildLockOwner(lockDir);
|
|
311
327
|
} catch (error) {
|
|
312
328
|
if ((error as NodeJS.ErrnoException).code !== "EEXIST") {
|
|
313
329
|
// A lock dir we cannot create for an unexpected reason (e.g. a
|
|
@@ -315,14 +331,17 @@ function buildUnderPluginLock(
|
|
|
315
331
|
// unlocked build — correctness is preserved by the atomic publish.
|
|
316
332
|
return build();
|
|
317
333
|
}
|
|
318
|
-
const waited = waitForPluginBinary(
|
|
334
|
+
const waited = waitForPluginBinary({
|
|
319
335
|
binaryPath,
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
336
|
+
lockDir,
|
|
337
|
+
lockInfo,
|
|
338
|
+
timeoutMs: PLUGIN_BUILD_LOCK_STEAL_MS,
|
|
339
|
+
});
|
|
340
|
+
if (waited.published) {
|
|
323
341
|
touchCacheEntry(cacheDir);
|
|
324
342
|
return binaryPath;
|
|
325
343
|
}
|
|
344
|
+
reportPluginLockSteal(lockDir, binaryPath, lockInfo, waited.reason);
|
|
326
345
|
// Builder appears to have crashed: steal the abandoned lock and retry.
|
|
327
346
|
fs.rmSync(lockDir, { force: true, recursive: true });
|
|
328
347
|
continue;
|
|
@@ -341,19 +360,216 @@ function buildUnderPluginLock(
|
|
|
341
360
|
}
|
|
342
361
|
|
|
343
362
|
/** Poll for the locked builder to publish its binary, up to `timeoutMs`. */
|
|
344
|
-
function waitForPluginBinary(
|
|
363
|
+
function waitForPluginBinary(opts: {
|
|
364
|
+
binaryPath: string;
|
|
365
|
+
lockDir: string;
|
|
366
|
+
lockInfo: {
|
|
367
|
+
label: string;
|
|
368
|
+
pluginName: string;
|
|
369
|
+
quiet: boolean;
|
|
370
|
+
};
|
|
371
|
+
timeoutMs: number;
|
|
372
|
+
}): { published: boolean; reason?: string } {
|
|
345
373
|
const startedAt = Date.now();
|
|
374
|
+
let nextStatusAt = startedAt + PLUGIN_BUILD_LOCK_STATUS_MS;
|
|
346
375
|
for (;;) {
|
|
347
|
-
if (fs.existsSync(binaryPath)) {
|
|
348
|
-
return true;
|
|
376
|
+
if (fs.existsSync(opts.binaryPath)) {
|
|
377
|
+
return { published: true };
|
|
349
378
|
}
|
|
350
|
-
|
|
351
|
-
|
|
379
|
+
const now = Date.now();
|
|
380
|
+
const lockStatus = inspectPluginBuildLock(opts.lockDir, now);
|
|
381
|
+
if (lockStatus.abandoned) {
|
|
382
|
+
return {
|
|
383
|
+
published: false,
|
|
384
|
+
reason: lockStatus.reason,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
if (now - startedAt > opts.timeoutMs) {
|
|
388
|
+
return {
|
|
389
|
+
published: false,
|
|
390
|
+
reason: `timed out after ${formatDuration(now - startedAt)}`,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
if (!opts.lockInfo.quiet && now >= nextStatusAt) {
|
|
394
|
+
reportPluginLockWait({
|
|
395
|
+
binaryPath: opts.binaryPath,
|
|
396
|
+
elapsedMs: now - startedAt,
|
|
397
|
+
lockDir: opts.lockDir,
|
|
398
|
+
lockInfo: opts.lockInfo,
|
|
399
|
+
owner: lockStatus.owner,
|
|
400
|
+
});
|
|
401
|
+
nextStatusAt = now + PLUGIN_BUILD_LOCK_STATUS_MS;
|
|
352
402
|
}
|
|
353
403
|
sleepSync(PLUGIN_BUILD_LOCK_POLL_MS);
|
|
354
404
|
}
|
|
355
405
|
}
|
|
356
406
|
|
|
407
|
+
function writePluginBuildLockOwner(lockDir: string): void {
|
|
408
|
+
try {
|
|
409
|
+
fs.writeFileSync(
|
|
410
|
+
path.join(lockDir, PLUGIN_BUILD_LOCK_OWNER_FILE),
|
|
411
|
+
`${JSON.stringify(
|
|
412
|
+
{
|
|
413
|
+
hostname: os.hostname(),
|
|
414
|
+
pid: process.pid,
|
|
415
|
+
startedAt: new Date().toISOString(),
|
|
416
|
+
},
|
|
417
|
+
null,
|
|
418
|
+
2,
|
|
419
|
+
)}\n`,
|
|
420
|
+
"utf8",
|
|
421
|
+
);
|
|
422
|
+
} catch {
|
|
423
|
+
// Best-effort metadata: the mkdir lock still serializes the build.
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function inspectPluginBuildLock(
|
|
428
|
+
lockDir: string,
|
|
429
|
+
now: number,
|
|
430
|
+
): {
|
|
431
|
+
abandoned: boolean;
|
|
432
|
+
owner: string;
|
|
433
|
+
reason?: string;
|
|
434
|
+
} {
|
|
435
|
+
const owner = readPluginBuildLockOwner(lockDir);
|
|
436
|
+
if (owner !== null) {
|
|
437
|
+
const label = describePluginBuildLockOwner(owner);
|
|
438
|
+
if (isLocalHostName(owner.hostname) && !isProcessAlive(owner.pid)) {
|
|
439
|
+
return {
|
|
440
|
+
abandoned: true,
|
|
441
|
+
owner: label,
|
|
442
|
+
reason: `${label} is no longer running`,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
return {
|
|
446
|
+
abandoned: false,
|
|
447
|
+
owner: label,
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const ageMs = pluginBuildLockAgeMs(lockDir, now);
|
|
452
|
+
if (ageMs > PLUGIN_BUILD_LOCK_LEGACY_STALE_MS) {
|
|
453
|
+
return {
|
|
454
|
+
abandoned: true,
|
|
455
|
+
owner: `legacy lock with no ${PLUGIN_BUILD_LOCK_OWNER_FILE}`,
|
|
456
|
+
reason:
|
|
457
|
+
`legacy lock has no ${PLUGIN_BUILD_LOCK_OWNER_FILE} and is ` +
|
|
458
|
+
`${formatDuration(ageMs)} old`,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
return {
|
|
462
|
+
abandoned: false,
|
|
463
|
+
owner: `legacy lock with no ${PLUGIN_BUILD_LOCK_OWNER_FILE}`,
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function readPluginBuildLockOwner(
|
|
468
|
+
lockDir: string,
|
|
469
|
+
): { hostname: string; pid: number; startedAt?: string } | null {
|
|
470
|
+
try {
|
|
471
|
+
const parsed = JSON.parse(
|
|
472
|
+
fs.readFileSync(path.join(lockDir, PLUGIN_BUILD_LOCK_OWNER_FILE), "utf8"),
|
|
473
|
+
) as Record<string, unknown>;
|
|
474
|
+
if (
|
|
475
|
+
typeof parsed.hostname !== "string" ||
|
|
476
|
+
!Number.isInteger(parsed.pid) ||
|
|
477
|
+
typeof parsed.pid !== "number" ||
|
|
478
|
+
parsed.pid <= 0
|
|
479
|
+
) {
|
|
480
|
+
return null;
|
|
481
|
+
}
|
|
482
|
+
return {
|
|
483
|
+
hostname: parsed.hostname,
|
|
484
|
+
pid: parsed.pid,
|
|
485
|
+
startedAt:
|
|
486
|
+
typeof parsed.startedAt === "string" ? parsed.startedAt : undefined,
|
|
487
|
+
};
|
|
488
|
+
} catch {
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function pluginBuildLockAgeMs(lockDir: string, now: number): number {
|
|
494
|
+
try {
|
|
495
|
+
return Math.max(0, now - fs.statSync(lockDir).mtimeMs);
|
|
496
|
+
} catch {
|
|
497
|
+
return Number.POSITIVE_INFINITY;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function isLocalHostName(hostname: string): boolean {
|
|
502
|
+
return hostname.toLowerCase() === os.hostname().toLowerCase();
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function isProcessAlive(pid: number): boolean {
|
|
506
|
+
try {
|
|
507
|
+
process.kill(pid, 0);
|
|
508
|
+
return true;
|
|
509
|
+
} catch (error) {
|
|
510
|
+
return (error as NodeJS.ErrnoException).code === "EPERM";
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function describePluginBuildLockOwner(owner: {
|
|
515
|
+
hostname: string;
|
|
516
|
+
pid: number;
|
|
517
|
+
startedAt?: string;
|
|
518
|
+
}): string {
|
|
519
|
+
const started =
|
|
520
|
+
owner.startedAt === undefined ? "" : ` started at ${owner.startedAt}`;
|
|
521
|
+
return `pid ${owner.pid} on ${owner.hostname}${started}`;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function reportPluginLockWait(opts: {
|
|
525
|
+
binaryPath: string;
|
|
526
|
+
elapsedMs: number;
|
|
527
|
+
lockDir: string;
|
|
528
|
+
lockInfo: {
|
|
529
|
+
label: string;
|
|
530
|
+
pluginName: string;
|
|
531
|
+
quiet: boolean;
|
|
532
|
+
};
|
|
533
|
+
owner: string;
|
|
534
|
+
}): void {
|
|
535
|
+
process.stderr.write(
|
|
536
|
+
`ttsc: waiting for ${opts.lockInfo.label} "${opts.lockInfo.pluginName}" ` +
|
|
537
|
+
`cache lock after ${formatDuration(opts.elapsedMs)}; ` +
|
|
538
|
+
`lock=${opts.lockDir}; binary=${opts.binaryPath}; owner=${opts.owner}\n`,
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function reportPluginLockSteal(
|
|
543
|
+
lockDir: string,
|
|
544
|
+
binaryPath: string,
|
|
545
|
+
lockInfo: {
|
|
546
|
+
label: string;
|
|
547
|
+
pluginName: string;
|
|
548
|
+
quiet: boolean;
|
|
549
|
+
},
|
|
550
|
+
reason: string | undefined,
|
|
551
|
+
): void {
|
|
552
|
+
if (lockInfo.quiet) return;
|
|
553
|
+
const suffix = reason === undefined ? "" : ` (${reason})`;
|
|
554
|
+
process.stderr.write(
|
|
555
|
+
`ttsc: reclaiming abandoned ${lockInfo.label} "${lockInfo.pluginName}" ` +
|
|
556
|
+
`cache lock at ${lockDir}; binary=${binaryPath}${suffix}\n`,
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function formatDuration(ms: number): string {
|
|
561
|
+
if (ms < 1_000) {
|
|
562
|
+
return `${Math.max(0, Math.round(ms))}ms`;
|
|
563
|
+
}
|
|
564
|
+
const seconds = Math.floor(ms / 1_000);
|
|
565
|
+
const minutes = Math.floor(seconds / 60);
|
|
566
|
+
const remainder = seconds % 60;
|
|
567
|
+
if (minutes === 0) {
|
|
568
|
+
return `${seconds}s`;
|
|
569
|
+
}
|
|
570
|
+
return `${minutes}m ${remainder}s`;
|
|
571
|
+
}
|
|
572
|
+
|
|
357
573
|
/** Block the current (synchronous) thread for `ms` without busy-spinning. */
|
|
358
574
|
function sleepSync(ms: number): void {
|
|
359
575
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|