@hydranium/core 1.0.0-next.266 → 1.0.0-next.268
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/lib/documents/ast-document-manager.d.ts.map +1 -1
- package/lib/documents/ast-document-manager.js +4 -3
- package/lib/documents/ast-document-manager.js.map +1 -1
- package/lib/documents/document-release-handler.d.ts +2 -2
- package/lib/documents/document-release-handler.d.ts.map +1 -1
- package/lib/documents/document-release-handler.js +14 -19
- package/lib/documents/document-release-handler.js.map +1 -1
- package/lib/documents/hydranium-text-documents.d.ts.map +1 -1
- package/lib/documents/hydranium-text-documents.js +7 -10
- package/lib/documents/hydranium-text-documents.js.map +1 -1
- package/lib/documents/language-client-shadow.d.ts.map +1 -1
- package/lib/documents/language-client-shadow.js +2 -1
- package/lib/documents/language-client-shadow.js.map +1 -1
- package/lib/documents/version-sync-service.d.ts +18 -14
- package/lib/documents/version-sync-service.d.ts.map +1 -1
- package/lib/documents/version-sync-service.js +15 -13
- package/lib/documents/version-sync-service.js.map +1 -1
- package/lib/langium/diagnostics/lsp-logger.d.ts.map +1 -1
- package/lib/langium/diagnostics/lsp-logger.js +2 -2
- package/lib/langium/diagnostics/lsp-logger.js.map +1 -1
- package/lib/langium/document-builder/document-builder.d.ts +2 -2
- package/lib/langium/document-builder/document-builder.d.ts.map +1 -1
- package/lib/langium/document-builder/document-builder.js +23 -10
- package/lib/langium/document-builder/document-builder.js.map +1 -1
- package/lib/langium/integrity/integrity-service.d.ts.map +1 -1
- package/lib/langium/integrity/integrity-service.js +5 -1
- package/lib/langium/integrity/integrity-service.js.map +1 -1
- package/lib/langium/model-service/client-session.d.ts.map +1 -1
- package/lib/langium/model-service/client-session.js +20 -3
- package/lib/langium/model-service/client-session.js.map +1 -1
- package/lib/langium/model-service/model-service.d.ts +4 -3
- package/lib/langium/model-service/model-service.d.ts.map +1 -1
- package/lib/langium/model-service/model-service.js +49 -19
- package/lib/langium/model-service/model-service.js.map +1 -1
- package/lib/langium/project/abstract-project-manager.d.ts.map +1 -1
- package/lib/langium/project/abstract-project-manager.js +4 -4
- package/lib/langium/project/abstract-project-manager.js.map +1 -1
- package/lib/langium/scope/hydranium-scope-computation.d.ts +6 -0
- package/lib/langium/scope/hydranium-scope-computation.d.ts.map +1 -1
- package/lib/langium/scope/hydranium-scope-computation.js +6 -10
- package/lib/langium/scope/hydranium-scope-computation.js.map +1 -1
- package/lib/langium/validation/document-validator.d.ts.map +1 -1
- package/lib/langium/validation/document-validator.js +5 -3
- package/lib/langium/validation/document-validator.js.map +1 -1
- package/lib/langium/workspace/hydranium-workspace-lock.d.ts +1 -1
- package/lib/langium/workspace/hydranium-workspace-lock.js +1 -1
- package/lib/langium/workspace/hydranium-workspace-lock.js.map +1 -1
- package/lib/langium/workspace/hydranium-workspace-manager.d.ts.map +1 -1
- package/lib/langium/workspace/hydranium-workspace-manager.js +4 -5
- package/lib/langium/workspace/hydranium-workspace-manager.js.map +1 -1
- package/lib/langium/workspace/persistent-file-system-provider.d.ts.map +1 -1
- package/lib/langium/workspace/persistent-file-system-provider.js +2 -1
- package/lib/langium/workspace/persistent-file-system-provider.js.map +1 -1
- package/lib/launcher/integrated-server.d.ts +3 -1
- package/lib/launcher/integrated-server.d.ts.map +1 -1
- package/lib/lsp/hydranium-document-update-handler.d.ts.map +1 -1
- package/lib/lsp/hydranium-document-update-handler.js +5 -5
- package/lib/lsp/hydranium-document-update-handler.js.map +1 -1
- package/lib/node/event-loop-monitor.d.ts.map +1 -1
- package/lib/node/event-loop-monitor.js +2 -1
- package/lib/node/event-loop-monitor.js.map +1 -1
- package/lib/node/log-preamble-node.d.ts +5 -3
- package/lib/node/log-preamble-node.d.ts.map +1 -1
- package/lib/node/log-preamble-node.js +13 -10
- package/lib/node/log-preamble-node.js.map +1 -1
- package/lib/node/memory-monitor.d.ts +2 -2
- package/lib/node/memory-monitor.d.ts.map +1 -1
- package/lib/node/memory-monitor.js +3 -5
- package/lib/node/memory-monitor.js.map +1 -1
- package/lib/node/node-file-system-provider.d.ts +5 -2
- package/lib/node/node-file-system-provider.d.ts.map +1 -1
- package/lib/node/node-file-system-provider.js +5 -2
- package/lib/node/node-file-system-provider.js.map +1 -1
- package/lib/node/server-diagnostics.js +1 -1
- package/lib/node/server-diagnostics.js.map +1 -1
- package/lib/node/socket-launcher.d.ts.map +1 -1
- package/lib/node/socket-launcher.js +4 -4
- package/lib/node/socket-launcher.js.map +1 -1
- package/lib/node/stdio-launcher.d.ts.map +1 -1
- package/lib/node/stdio-launcher.js +12 -3
- package/lib/node/stdio-launcher.js.map +1 -1
- package/lib/testing/playwright/server-log-capture.d.ts +7 -0
- package/lib/testing/playwright/server-log-capture.d.ts.map +1 -1
- package/lib/testing/playwright/server-log-capture.js +12 -0
- package/lib/testing/playwright/server-log-capture.js.map +1 -1
- package/lib/testing/playwright/server-log-fixture.d.ts +16 -0
- package/lib/testing/playwright/server-log-fixture.d.ts.map +1 -1
- package/lib/testing/playwright/server-log-fixture.js +25 -6
- package/lib/testing/playwright/server-log-fixture.js.map +1 -1
- package/lib/testing/playwright/server-log-region.d.ts +15 -0
- package/lib/testing/playwright/server-log-region.d.ts.map +1 -0
- package/lib/testing/playwright/server-log-region.js +30 -0
- package/lib/testing/playwright/server-log-region.js.map +1 -0
- package/lib/util/connection-liveness.d.ts.map +1 -1
- package/lib/util/connection-liveness.js +3 -2
- package/lib/util/connection-liveness.js.map +1 -1
- package/package.json +5 -5
- package/src/documents/ast-document-manager.ts +7 -3
- package/src/documents/document-release-handler.ts +15 -21
- package/src/documents/hydranium-text-documents.ts +13 -12
- package/src/documents/language-client-shadow.ts +2 -1
- package/src/documents/version-sync-service.ts +32 -24
- package/src/langium/diagnostics/lsp-logger.ts +2 -1
- package/src/langium/document-builder/document-builder.ts +30 -11
- package/src/langium/integrity/integrity-service.ts +4 -1
- package/src/langium/model-service/client-session.ts +20 -3
- package/src/langium/model-service/model-service.ts +55 -24
- package/src/langium/project/abstract-project-manager.ts +4 -4
- package/src/langium/scope/hydranium-scope-computation.ts +12 -12
- package/src/langium/validation/document-validator.ts +5 -3
- package/src/langium/workspace/hydranium-workspace-lock.ts +2 -2
- package/src/langium/workspace/hydranium-workspace-manager.ts +4 -5
- package/src/langium/workspace/persistent-file-system-provider.ts +2 -1
- package/src/launcher/integrated-server.ts +3 -1
- package/src/lsp/hydranium-document-update-handler.ts +15 -7
- package/src/node/event-loop-monitor.ts +2 -1
- package/src/node/log-preamble-node.ts +20 -10
- package/src/node/memory-monitor.ts +4 -5
- package/src/node/node-file-system-provider.ts +5 -2
- package/src/node/server-diagnostics.ts +1 -1
- package/src/node/socket-launcher.ts +4 -4
- package/src/node/stdio-launcher.ts +11 -3
- package/src/testing/playwright/server-log-capture.ts +14 -0
- package/src/testing/playwright/server-log-fixture.ts +46 -6
- package/src/testing/playwright/server-log-region.ts +32 -0
- package/src/util/connection-liveness.ts +4 -2
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
* primarily by tests waiting for tidy teardown.
|
|
21
21
|
*
|
|
22
22
|
* Either promise may reject if startup or shutdown encounters an error
|
|
23
|
-
* the head can't recover from.
|
|
23
|
+
* the head can't recover from. A launcher given a logger logs its own startup
|
|
24
|
+
* failure, so a rejected `started` is control flow: the caller stops what
|
|
25
|
+
* depends on it without logging the failure again.
|
|
24
26
|
*/
|
|
25
27
|
export interface IntegratedServer {
|
|
26
28
|
readonly started: Promise<void>;
|
|
@@ -7,7 +7,15 @@
|
|
|
7
7
|
* SPDX-License-Identifier: MIT
|
|
8
8
|
********************************************************************************/
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
Debouncer,
|
|
12
|
+
Deferred,
|
|
13
|
+
type Logger,
|
|
14
|
+
ObservableValue,
|
|
15
|
+
type MaybeObservableValue,
|
|
16
|
+
TIMED_OUT,
|
|
17
|
+
describeErrorWithStack
|
|
18
|
+
} from '@hydranium/protocol';
|
|
11
19
|
import { URI, UriUtils } from '@hydranium/langium';
|
|
12
20
|
import { DefaultDocumentUpdateHandler } from '@hydranium/langium/lsp';
|
|
13
21
|
import { HYDRANIUM_BUILD_REASONS, type HydraniumDocumentBuilder } from '../langium/document-builder/document-builder.js';
|
|
@@ -221,7 +229,9 @@ export class HydraniumDocumentUpdateHandler extends DefaultDocumentUpdateHandler
|
|
|
221
229
|
for (const change of filtered.changes) {
|
|
222
230
|
this.textDocuments
|
|
223
231
|
.reloadDiskBaseline(change.uri)
|
|
224
|
-
.catch((err: unknown) =>
|
|
232
|
+
.catch((err: unknown) =>
|
|
233
|
+
this.logger.error(`Disk baseline not reloaded for ${change.uri}. ${describeErrorWithStack(err)}`)
|
|
234
|
+
);
|
|
225
235
|
}
|
|
226
236
|
// External file-system changes are authoritative — overwrite any
|
|
227
237
|
// pending reason (typically empty here; could be `didChangeContent`
|
|
@@ -238,7 +248,7 @@ export class HydraniumDocumentUpdateHandler extends DefaultDocumentUpdateHandler
|
|
|
238
248
|
// `unhandledRejection` listener, which reports neither the URIs nor
|
|
239
249
|
// the fact that a watched-file rebuild was dropped — leaving the
|
|
240
250
|
// workspace diverged from disk with no diagnostic and no retry.
|
|
241
|
-
const detail =
|
|
251
|
+
const detail = describeErrorWithStack(err);
|
|
242
252
|
const uris = params.changes.map(change => change.uri).join(', ');
|
|
243
253
|
this.logger.error(`Watched-file update dropped for ${uris}. ${detail}`);
|
|
244
254
|
});
|
|
@@ -293,9 +303,7 @@ export class HydraniumDocumentUpdateHandler extends DefaultDocumentUpdateHandler
|
|
|
293
303
|
});
|
|
294
304
|
} catch (err) {
|
|
295
305
|
cap.dispose();
|
|
296
|
-
this.logger.error(
|
|
297
|
-
`willSaveWaitUntil for ${uri} answered without waiting. ${err instanceof Error ? (err.stack ?? err.message) : String(err)}`
|
|
298
|
-
);
|
|
306
|
+
this.logger.error(`willSaveWaitUntil for ${uri} answered without waiting. ${describeErrorWithStack(err)}`);
|
|
299
307
|
release();
|
|
300
308
|
resolve([]);
|
|
301
309
|
}
|
|
@@ -417,7 +425,7 @@ export class HydraniumDocumentUpdateHandler extends DefaultDocumentUpdateHandler
|
|
|
417
425
|
if (isConnectionGoneError(err)) {
|
|
418
426
|
return;
|
|
419
427
|
}
|
|
420
|
-
const detail =
|
|
428
|
+
const detail = describeErrorWithStack(err);
|
|
421
429
|
this.logger.error(`Workspace initialization failed. Could not perform document update. ${detail}`);
|
|
422
430
|
});
|
|
423
431
|
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
********************************************************************************/
|
|
9
9
|
|
|
10
10
|
import { monitorEventLoopDelay } from 'node:perf_hooks';
|
|
11
|
+
import { describeErrorWithStack } from '@hydranium/protocol';
|
|
11
12
|
import { formatMemory } from '../langium/diagnostics/logger.js';
|
|
12
13
|
import { type ServerSharedServicesMinimal } from '../langium/shared-services.js';
|
|
13
14
|
import { formatServerState } from './server-state-snapshot.js';
|
|
@@ -74,7 +75,7 @@ export function startEventLoopMonitor(shared: ServerSharedServicesMinimal, optio
|
|
|
74
75
|
try {
|
|
75
76
|
onStallSnapshot(maxMs);
|
|
76
77
|
} catch (error) {
|
|
77
|
-
logger.warn(`Stall snapshot failed: ${
|
|
78
|
+
logger.warn(`Stall snapshot failed: ${describeErrorWithStack(error)}`);
|
|
78
79
|
}
|
|
79
80
|
}
|
|
80
81
|
}
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
} from './event-loop-monitor.js';
|
|
18
18
|
import { MEMORY_DELTA_BYTES } from '../langium/diagnostics/logger.js';
|
|
19
19
|
import { DEFAULT_MEMORY_CRITICAL_THRESHOLD, DEFAULT_MEMORY_WARN_THRESHOLD } from './memory-monitor.js';
|
|
20
|
+
import { type ServerDiagnosticsOptions } from './server-diagnostics.js';
|
|
20
21
|
import { type LogPreambleOptions } from '../langium/diagnostics/log-preamble.js';
|
|
21
22
|
|
|
22
23
|
/**
|
|
@@ -37,19 +38,25 @@ export function nodeSystemInfoLines(): string[] {
|
|
|
37
38
|
* Node monitor + memory-suffix convention lines for
|
|
38
39
|
* {@link LogPreambleOptions.conventionLines}. These describe the `[EventLoop]`
|
|
39
40
|
* / `[Memory]` monitors and the heap/rss suffix — all Node-only behaviour, so
|
|
40
|
-
* they're omitted from the portable preamble.
|
|
41
|
+
* they're omitted from the portable preamble. Pass the options the monitors
|
|
42
|
+
* were started with, or the lines state the defaults.
|
|
41
43
|
*/
|
|
42
|
-
export function nodeConventionLines(
|
|
44
|
+
export function nodeConventionLines(
|
|
45
|
+
phaseDetailThresholdMs = 50,
|
|
46
|
+
monitors: Pick<ServerDiagnosticsOptions, 'eventLoop' | 'memory'> = {}
|
|
47
|
+
): string[] {
|
|
43
48
|
const memoryDeltaMb = MEMORY_DELTA_BYTES / (1024 * 1024);
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
const
|
|
49
|
+
const thresholdMs = monitors.eventLoop?.thresholdMs ?? DEFAULT_EVENT_LOOP_THRESHOLD_MS;
|
|
50
|
+
const intervalSeconds = (monitors.eventLoop?.intervalMs ?? DEFAULT_EVENT_LOOP_INTERVAL_MS) / 1000;
|
|
51
|
+
const snapshotAboveMs = monitors.eventLoop?.snapshotAboveMs ?? DEFAULT_EVENT_LOOP_SNAPSHOT_ABOVE_MS;
|
|
52
|
+
const warnPercent = Math.round((monitors.memory?.warnThreshold ?? DEFAULT_MEMORY_WARN_THRESHOLD) * 100);
|
|
53
|
+
const criticalPercent = Math.round((monitors.memory?.criticalThreshold ?? DEFAULT_MEMORY_CRITICAL_THRESHOLD) * 100);
|
|
47
54
|
return [
|
|
48
55
|
`• Memory suffix: heap/rss included on [done] only when heap moved >${memoryDeltaMb}MB since last log,`,
|
|
49
56
|
` always included on rebuilds >=${phaseDetailThresholdMs}ms and on [EventLoop] stall warnings`,
|
|
50
|
-
`• [EventLoop]: warns when a stall >=${
|
|
51
|
-
` stalls >=${
|
|
52
|
-
`• [Memory]: warns >=${warnPercent}% of V8 heap_size_limit,
|
|
57
|
+
`• [EventLoop]: warns when a stall >=${thresholdMs}ms is observed within a ${intervalSeconds}s window;`,
|
|
58
|
+
` stalls >=${snapshotAboveMs}ms also trigger a [Process] server-state snapshot`,
|
|
59
|
+
`• [Memory]: warns >=${warnPercent}% of V8 heap_size_limit, again as imminent OOM >=${criticalPercent}%, info on recovery`
|
|
53
60
|
];
|
|
54
61
|
}
|
|
55
62
|
|
|
@@ -58,6 +65,9 @@ export function nodeConventionLines(phaseDetailThresholdMs = 50): string[] {
|
|
|
58
65
|
* the portable `logPreamble`'s options on a Node host:
|
|
59
66
|
* `logPreamble(logger, { productName, version, ...nodeLogPreambleSections() })`.
|
|
60
67
|
*/
|
|
61
|
-
export function nodeLogPreambleSections(
|
|
62
|
-
|
|
68
|
+
export function nodeLogPreambleSections(
|
|
69
|
+
phaseDetailThresholdMs = 50,
|
|
70
|
+
monitors: Pick<ServerDiagnosticsOptions, 'eventLoop' | 'memory'> = {}
|
|
71
|
+
): Pick<LogPreambleOptions, 'systemInfoLines' | 'conventionLines'> {
|
|
72
|
+
return { systemInfoLines: nodeSystemInfoLines(), conventionLines: nodeConventionLines(phaseDetailThresholdMs, monitors) };
|
|
63
73
|
}
|
|
@@ -13,7 +13,7 @@ import { type ServerSharedServicesMinimal } from '../langium/shared-services.js'
|
|
|
13
13
|
|
|
14
14
|
/** Heap usage ratio (heapUsed / heap_size_limit) above which we emit a warning. */
|
|
15
15
|
export const DEFAULT_MEMORY_WARN_THRESHOLD = 0.85;
|
|
16
|
-
/** Heap usage ratio above which
|
|
16
|
+
/** Heap usage ratio above which the warning says out-of-memory is imminent. */
|
|
17
17
|
export const DEFAULT_MEMORY_CRITICAL_THRESHOLD = 0.95;
|
|
18
18
|
/** Cooldown between repeated warnings at the same level, in ms. */
|
|
19
19
|
export const DEFAULT_COOLDOWN_MS = 30_000;
|
|
@@ -25,7 +25,7 @@ type Severity = 'ok' | 'warn' | 'critical';
|
|
|
25
25
|
export interface MemoryMonitorOptions {
|
|
26
26
|
/** Heap-usage ratio (0..1) above which a warning is emitted. Default {@link DEFAULT_MEMORY_WARN_THRESHOLD}. */
|
|
27
27
|
warnThreshold?: number;
|
|
28
|
-
/** Heap-usage ratio above which
|
|
28
|
+
/** Heap-usage ratio above which the warning says out-of-memory is imminent. Default {@link DEFAULT_MEMORY_CRITICAL_THRESHOLD}. */
|
|
29
29
|
criticalThreshold?: number;
|
|
30
30
|
/** Sample interval, in ms. Default {@link DEFAULT_MEMORY_INTERVAL_MS}. */
|
|
31
31
|
intervalMs?: number;
|
|
@@ -86,9 +86,8 @@ export function startMemoryMonitor(shared: ServerSharedServicesMinimal, options:
|
|
|
86
86
|
: severity === 'warn'
|
|
87
87
|
? ' - garbage collection pressure may slow operations'
|
|
88
88
|
: ' - recovered below warning threshold');
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
} else if (severity === 'warn') {
|
|
89
|
+
// Critical is still a warning: nothing has failed yet.
|
|
90
|
+
if (severity !== 'ok') {
|
|
92
91
|
logger.warn(message);
|
|
93
92
|
} else {
|
|
94
93
|
logger.info(message);
|
|
@@ -23,7 +23,7 @@ import { NO_SUCH_FILE, NO_SUCH_PATH, UNSUPPORTED_WRITE } from '../langium/worksp
|
|
|
23
23
|
/**
|
|
24
24
|
* Node-based default {@link WritableFileSystemProvider}. Extends Langium's
|
|
25
25
|
* {@link NodeFileSystemProvider} with an async {@link writeFile} that
|
|
26
|
-
* creates parent directories on demand, replaces the target
|
|
26
|
+
* creates parent directories on demand, replaces the target whole unless
|
|
27
27
|
* that would break a hard link, and registers the resulting mtime with the
|
|
28
28
|
* {@link SelfSaveRegistry} bound at `services.workspace.SelfSaveRegistry` — so
|
|
29
29
|
* consumers can suppress the `didChangeWatchedFiles` echo for their own writes.
|
|
@@ -143,7 +143,10 @@ export class DefaultFileSystemProvider extends NodeFileSystemProvider implements
|
|
|
143
143
|
* overwrite cannot offer that, because the target is truncated before the
|
|
144
144
|
* first byte lands, so a reader in that window gets a short or empty file —
|
|
145
145
|
* and a model file that fails to parse is worse than one holding a previous
|
|
146
|
-
* good revision.
|
|
146
|
+
* good revision. On Windows the replacing rename also leaves the name
|
|
147
|
+
* unresolvable for a moment, so a reader there can find the file missing
|
|
148
|
+
* (`ENOENT`) or refused (`EPERM`), never torn, and must not take either for
|
|
149
|
+
* a deletion. It does NOT serialise writers: two writers still race, and
|
|
147
150
|
* the loser's content is replaced wholesale rather than interleaved.
|
|
148
151
|
*
|
|
149
152
|
* The destination is the RESOLVED path, via {@link realpath}: a `rename`
|
|
@@ -49,7 +49,7 @@ export function startServerDiagnostics(shared: ServerSharedServicesMinimal, opti
|
|
|
49
49
|
productName,
|
|
50
50
|
version,
|
|
51
51
|
phaseDetailThresholdMs,
|
|
52
|
-
...nodeLogPreambleSections(phaseDetailThresholdMs)
|
|
52
|
+
...nodeLogPreambleSections(phaseDetailThresholdMs, options)
|
|
53
53
|
});
|
|
54
54
|
if (options.logInitialMemory ?? true) {
|
|
55
55
|
shared.Tracer.for('Process').memory('Initial Memory');
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
********************************************************************************/
|
|
9
9
|
|
|
10
10
|
import * as net from 'node:net';
|
|
11
|
-
import { Deferred, type Disposable, type Logger } from '@hydranium/protocol';
|
|
11
|
+
import { Deferred, describeErrorWithStack, type Disposable, type Logger } from '@hydranium/protocol';
|
|
12
12
|
import { createMessageConnection, SocketMessageReader, SocketMessageWriter } from 'vscode-jsonrpc/node';
|
|
13
13
|
import type { MessageConnection } from 'vscode-jsonrpc';
|
|
14
14
|
import type { IntegratedServer } from '../launcher/integrated-server.js';
|
|
@@ -131,7 +131,7 @@ export function startSocketServer(options: SocketServerOptions, onClientConnecti
|
|
|
131
131
|
try {
|
|
132
132
|
adopterDisposable.dispose();
|
|
133
133
|
} catch (err) {
|
|
134
|
-
logger?.warn(`[${tag}] Adopter disposable threw on cleanup: ${(err
|
|
134
|
+
logger?.warn(`[${tag}] Adopter disposable threw on cleanup: ${describeErrorWithStack(err)}`);
|
|
135
135
|
}
|
|
136
136
|
connection.dispose();
|
|
137
137
|
const idx = activeConnections.indexOf(entry);
|
|
@@ -164,7 +164,7 @@ export function startSocketServer(options: SocketServerOptions, onClientConnecti
|
|
|
164
164
|
started.resolve();
|
|
165
165
|
});
|
|
166
166
|
netServer.on('error', err => {
|
|
167
|
-
logger?.error(`[${tag}] Error: ${err
|
|
167
|
+
logger?.error(`[${tag}] Error: ${describeErrorWithStack(err)}`);
|
|
168
168
|
started.reject(err);
|
|
169
169
|
});
|
|
170
170
|
netServer.on('close', () => {
|
|
@@ -174,7 +174,7 @@ export function startSocketServer(options: SocketServerOptions, onClientConnecti
|
|
|
174
174
|
try {
|
|
175
175
|
entry.adopterDisposable.dispose();
|
|
176
176
|
} catch (err) {
|
|
177
|
-
logger?.warn(`[${tag}] Adopter disposable threw on shutdown: ${(err
|
|
177
|
+
logger?.warn(`[${tag}] Adopter disposable threw on shutdown: ${describeErrorWithStack(err)}`);
|
|
178
178
|
}
|
|
179
179
|
entry.connection.dispose();
|
|
180
180
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* SPDX-License-Identifier: MIT
|
|
8
8
|
********************************************************************************/
|
|
9
9
|
|
|
10
|
-
import { Deferred, type Disposable, type Logger } from '@hydranium/protocol';
|
|
10
|
+
import { Deferred, type Disposable, type Logger, describeError, describeErrorWithStack } from '@hydranium/protocol';
|
|
11
11
|
import type { MessageConnection } from 'vscode-jsonrpc';
|
|
12
12
|
import { createMessageConnection, StreamMessageReader, StreamMessageWriter } from 'vscode-jsonrpc/node';
|
|
13
13
|
import type { IntegratedServer } from '../launcher/integrated-server.js';
|
|
@@ -115,17 +115,21 @@ export function startStdioServer(
|
|
|
115
115
|
try {
|
|
116
116
|
adopterDisposable.dispose();
|
|
117
117
|
} catch (err: unknown) {
|
|
118
|
-
logger?.warn(`[${tag}] Adopter disposable threw on cleanup: ${
|
|
118
|
+
logger?.warn(`[${tag}] Adopter disposable threw on cleanup: ${describeErrorWithStack(err)}`);
|
|
119
119
|
}
|
|
120
120
|
connection.dispose();
|
|
121
121
|
stopped.resolve();
|
|
122
122
|
};
|
|
123
123
|
connection.onClose(() => close());
|
|
124
124
|
|
|
125
|
+
// The workspace manager logs a failed initialization itself.
|
|
126
|
+
let initializing = false;
|
|
125
127
|
const begin = async (): Promise<void> => {
|
|
126
128
|
if (options.workspace !== undefined) {
|
|
127
129
|
logger?.info(`[${tag}] Initializing workspace`);
|
|
130
|
+
initializing = true;
|
|
128
131
|
await initializeWorkspaceProgrammatically(options.shared, options.workspace);
|
|
132
|
+
initializing = false;
|
|
129
133
|
}
|
|
130
134
|
// Only now attach the reader — see the ordering note in the doc above.
|
|
131
135
|
connection.listen();
|
|
@@ -138,7 +142,11 @@ export function startStdioServer(
|
|
|
138
142
|
// A failed startup must not leave a half-live head: report it on
|
|
139
143
|
// `started` and tear the connection down rather than listening over an
|
|
140
144
|
// uninitialized workspace.
|
|
141
|
-
|
|
145
|
+
if (initializing) {
|
|
146
|
+
logger?.debug(`[${tag}] Startup failed: the workspace did not initialize. ${describeError(err)}`);
|
|
147
|
+
} else {
|
|
148
|
+
logger?.error(`[${tag}] Startup failed: ${describeErrorWithStack(err)}`);
|
|
149
|
+
}
|
|
142
150
|
started.reject(err);
|
|
143
151
|
close();
|
|
144
152
|
}
|
|
@@ -415,6 +415,20 @@ export function forwardBrowserConsole(
|
|
|
415
415
|
});
|
|
416
416
|
}
|
|
417
417
|
|
|
418
|
+
/** The label a `Warn` or `Error` entry starts with; continuation lines of a stack never do. */
|
|
419
|
+
const WARN_OR_ERROR_LINE = /^\[(Warn|Error)\s*-/;
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* The `Warn` and `Error` lines of `text` that no pattern of `allowlist`
|
|
423
|
+
* matches; its keys are the reasons the lines are expected. A clean run logs
|
|
424
|
+
* neither: `warn` is an anomaly that was handled and `error` is work that
|
|
425
|
+
* failed, so either one in a passing spec is a defect or a missing allowance.
|
|
426
|
+
*/
|
|
427
|
+
export function findUnexpectedServerLogLines(text: string, allowlist: Readonly<Record<string, RegExp>> = {}): string[] {
|
|
428
|
+
const patterns = Object.values(allowlist);
|
|
429
|
+
return text.split('\n').filter(line => WARN_OR_ERROR_LINE.test(line) && !patterns.some(pattern => pattern.test(line)));
|
|
430
|
+
}
|
|
431
|
+
|
|
418
432
|
/**
|
|
419
433
|
* Per-test server-log capture, called from a spec's `afterEach` with the test's
|
|
420
434
|
* workspace path:
|
|
@@ -11,7 +11,14 @@
|
|
|
11
11
|
// The plain functions in `server-log-capture.ts` stay playwright-free; only
|
|
12
12
|
// adopters that use this fixture need `@playwright/test` installed.
|
|
13
13
|
import { test as base, type Fixtures, type PlaywrightTestArgs, type PlaywrightTestOptions } from '@playwright/test';
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
attachServerLog,
|
|
16
|
+
DEFAULT_SERVER_LOG_ATTACH_ENV,
|
|
17
|
+
findUnexpectedServerLogLines,
|
|
18
|
+
markServerLog,
|
|
19
|
+
resolveServerLogPath
|
|
20
|
+
} from './server-log-capture.js';
|
|
21
|
+
import { readServerLogRegion } from './server-log-region.js';
|
|
15
22
|
|
|
16
23
|
export interface ServerLogFixtures {
|
|
17
24
|
/**
|
|
@@ -22,6 +29,21 @@ export interface ServerLogFixtures {
|
|
|
22
29
|
* `attachServerLog` in `beforeEach` / `afterEach` instead.
|
|
23
30
|
*/
|
|
24
31
|
serverLogWorkspace: string;
|
|
32
|
+
/**
|
|
33
|
+
* Fail a test whose part of the server log holds a `Warn` or `Error` line
|
|
34
|
+
* that {@link serverLogAllowlist} does not match. Off by default; reads
|
|
35
|
+
* nothing when capture is off.
|
|
36
|
+
*/
|
|
37
|
+
serverLogClean: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* The `Warn` and `Error` lines {@link serverLogClean} lets through: each key
|
|
40
|
+
* is why the line is expected, and its pattern is matched against the whole
|
|
41
|
+
* line, label and timestamp included. A pattern takes neither the `g` nor
|
|
42
|
+
* the `y` flag, which make `test` resume from its last match. An object
|
|
43
|
+
* rather than a list, because Playwright reads an array whose second entry
|
|
44
|
+
* is an object as a fixture's `[value, options]`.
|
|
45
|
+
*/
|
|
46
|
+
serverLogAllowlist: Readonly<Record<string, RegExp>>;
|
|
25
47
|
}
|
|
26
48
|
|
|
27
49
|
/** Full fixture set including the internal side-effect-only auto fixture. */
|
|
@@ -36,17 +58,35 @@ interface ServerLogTestFixtures extends ServerLogFixtures {
|
|
|
36
58
|
* (teardown) — i.e. the `beforeEach` + `afterEach` pair fused into one auto
|
|
37
59
|
* fixture. Compose onto any test base, or use the pre-extended {@link test}.
|
|
38
60
|
* Override `serverLogWorkspace` to wire the workspace; otherwise it's inert.
|
|
61
|
+
* Set `serverLogClean` to also fail a test on an unexpected `Warn` or `Error`.
|
|
39
62
|
*/
|
|
40
63
|
export const serverLogFixtures: Fixtures<ServerLogTestFixtures, object, PlaywrightTestArgs & PlaywrightTestOptions> = {
|
|
41
64
|
serverLogWorkspace: ['', { option: true }],
|
|
65
|
+
serverLogClean: [false, { option: true }],
|
|
66
|
+
serverLogAllowlist: [{}, { option: true }],
|
|
42
67
|
serverLogCapture: [
|
|
43
|
-
async ({ serverLogWorkspace }, use): Promise<void> => {
|
|
44
|
-
if (serverLogWorkspace) {
|
|
45
|
-
|
|
68
|
+
async ({ serverLogWorkspace, serverLogClean, serverLogAllowlist }, use): Promise<void> => {
|
|
69
|
+
if (!serverLogWorkspace) {
|
|
70
|
+
await use(undefined);
|
|
71
|
+
return;
|
|
46
72
|
}
|
|
73
|
+
markServerLog(base.info(), serverLogWorkspace);
|
|
47
74
|
await use(undefined);
|
|
48
|
-
|
|
49
|
-
|
|
75
|
+
const file = resolveServerLogPath(serverLogWorkspace);
|
|
76
|
+
const region = file ? readServerLogRegion(file) : '';
|
|
77
|
+
let problem: string | undefined;
|
|
78
|
+
if (serverLogClean) {
|
|
79
|
+
const unexpected = findUnexpectedServerLogLines(region, serverLogAllowlist);
|
|
80
|
+
if (unexpected.length > 0) {
|
|
81
|
+
problem = `The server logged ${unexpected.length} unexpected Warn or Error line(s):\n${unexpected.join('\n')}`;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// The failure-only policy reads the test's status, which failing here
|
|
85
|
+
// does not change; a run set to attach nothing still attaches nothing.
|
|
86
|
+
const attachOn = process.env[DEFAULT_SERVER_LOG_ATTACH_ENV] === 'never' ? 'never' : 'always';
|
|
87
|
+
await attachServerLog(base.info(), serverLogWorkspace, problem ? { attachOn } : {});
|
|
88
|
+
if (problem) {
|
|
89
|
+
throw new Error(problem);
|
|
50
90
|
}
|
|
51
91
|
},
|
|
52
92
|
{ auto: true }
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2026 EclipseSource and others.
|
|
3
|
+
*
|
|
4
|
+
* This program and the accompanying materials are made available under the
|
|
5
|
+
* terms of the MIT License which is available in the project root.
|
|
6
|
+
*
|
|
7
|
+
* SPDX-License-Identifier: MIT
|
|
8
|
+
********************************************************************************/
|
|
9
|
+
|
|
10
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* How far each log has been read. A suite starts its app before its first
|
|
14
|
+
* test's marker, so reading from the marker would leave startup unread.
|
|
15
|
+
*/
|
|
16
|
+
const readUpTo = new Map<string, number>();
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The part of `file` written since the last read of it, up to its last complete
|
|
20
|
+
* line, since the server may be mid-write. Every test reads, checked or not, so
|
|
21
|
+
* a test that is not checked leaves no lines to the next one.
|
|
22
|
+
*/
|
|
23
|
+
export function readServerLogRegion(file: string): string {
|
|
24
|
+
if (!existsSync(file)) {
|
|
25
|
+
return '';
|
|
26
|
+
}
|
|
27
|
+
const log = readFileSync(file);
|
|
28
|
+
const end = log.lastIndexOf('\n') + 1;
|
|
29
|
+
const text = log.subarray(readUpTo.get(file) ?? 0, end).toString('utf-8');
|
|
30
|
+
readUpTo.set(file, end);
|
|
31
|
+
return text;
|
|
32
|
+
}
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* SPDX-License-Identifier: MIT
|
|
8
8
|
********************************************************************************/
|
|
9
9
|
|
|
10
|
+
import { describeError } from '@hydranium/protocol';
|
|
11
|
+
|
|
10
12
|
/**
|
|
11
13
|
* Phrases that mean a call lost its connection. Matched on the message because
|
|
12
14
|
* the condition surfaces in shapes that share no discriminator:
|
|
@@ -64,7 +66,7 @@ export function isConnectionGoneError(err: unknown): boolean {
|
|
|
64
66
|
if (isDestroyedStreamError(err)) {
|
|
65
67
|
return true;
|
|
66
68
|
}
|
|
67
|
-
const message =
|
|
69
|
+
const message = describeError(err);
|
|
68
70
|
const normalized = message.toLowerCase();
|
|
69
71
|
return CONNECTION_GONE_PATTERNS.some(pattern => pattern.test(normalized));
|
|
70
72
|
}
|
|
@@ -92,6 +94,6 @@ export function isClientGoneError(err: unknown): boolean {
|
|
|
92
94
|
if (err instanceof Error && 'code' in err && typeof err.code === 'string' && SOCKET_GONE_CODES.has(err.code)) {
|
|
93
95
|
return true;
|
|
94
96
|
}
|
|
95
|
-
const message =
|
|
97
|
+
const message = describeError(err);
|
|
96
98
|
return SOCKET_GONE_PATTERN.test(message.toLowerCase());
|
|
97
99
|
}
|