Repository navigation
Expand file tree
/
Copy pathdaemonGraph.ts
More file actions
146 lines (139 loc) · 5.6 KB
/
Copy pathdaemonGraph.ts
File metadata and controls
146 lines (139 loc) · 5.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import * as path from 'node:path';
import {
captureDaemonRequest,
type DaemonClient,
type DaemonClientOutcome,
type IConnectOrStartDaemonOptions
} from '@rushstack/rush-client-core';
import {
DAEMON_GRAPH_GENERATION_PROTOCOL_MINOR,
isDaemonControlRecord,
RUSHD_GRAPH_SNAPSHOT,
type IDaemonRequestEnvelope
} from '@rushstack/rush-daemon-protocol';
import type { IDaemonCommandOptions } from './daemonCommands';
import { getDaemonConnectionOptions } from './daemonConnectionOptions';
import { writeStreamAsync } from './writeStreamAsync';
import {
parseGraphGenerationControls,
readGraphGenerationAsync,
type IGraphGenerationControls
} from './GraphGenerationControls';
import { GraphRequestCancelledError, GraphRequestContext } from './GraphRequestContext';
/** The reference graph client has no renderer, native graph imports, or in-process fallback. */
export async function executeDaemonGraphCommandAsync(options: IDaemonCommandOptions): Promise<void> {
try {
await runGraphAsync(options);
} catch (error) {
process.exitCode = error instanceof GraphRequestCancelledError ? 130 : 1;
await writeJsonAsync({
kind: 'graphError',
message: error instanceof Error ? error.message : String(error)
});
}
}
async function runGraphAsync(options: IDaemonCommandOptions): Promise<void> {
if (options.environment.RUSH_DAEMON_EXPERIMENTAL !== '1') {
throw new Error('Graph commands require RUSH_DAEMON_EXPERIMENTAL=1.');
}
if (!options.rushJsonPath) throw new Error('Graph commands require a repository containing rush.json.');
const connection: IConnectOrStartDaemonOptions = getDaemonConnectionOptions(
path.dirname(options.rushJsonPath),
options.rushVersion,
options.environment,
false
);
const controls: IGraphGenerationControls = parseGraphGenerationControls(options.argv);
if (!options.admission) throw new Error('Graph admission options must be resolved before dispatch.');
const context: GraphRequestContext = new GraphRequestContext(options.admission);
try {
const expectedWorkspaceGeneration: string | undefined = controls.mutation
? (controls.generation ?? (await readGraphGenerationAsync(connection, options.environment, context)))
: undefined;
// Graph inspection connects only; starting a daemon is a separate management command.
const client: DaemonClient = await context.connectAsync({
socketPath: connection.paths.socketPath,
capabilities: { isTTY: false, colorLevel: 0 }
});
try {
if (controls.mutation && client.protocolVersion.minor < DAEMON_GRAPH_GENERATION_PROTOCOL_MINOR) {
throw new Error('Graph mutations require generation-aware protocol 0.9 or newer.');
}
const request: IDaemonRequestEnvelope = captureDaemonRequest({
argv: ['daemon', ...controls.argv],
commandName: 'daemon',
commandOrigin: 'built-in',
cwd: process.cwd(),
environment: options.environment,
admission: context.admission,
expectedWorkspaceGeneration,
terminal: { isTTY: false, supportsColor: false, acceptsStdin: false }
});
await executeGraphRequestAsync(client, request, context.signal);
} finally {
await client.closeAsync();
}
} finally {
context[Symbol.dispose]();
}
}
async function executeGraphRequestAsync(
client: DaemonClient,
request: IDaemonRequestEnvelope,
abortSignal: AbortSignal
): Promise<void> {
const { expectedWorkspaceGeneration } = request;
let sawSnapshot: boolean = false;
const outcome: DaemonClientOutcome = await client.executeAsync({
request,
abortSignal,
onEventAsync: async (event) => {
const payload: unknown = event.payload;
if (
event.type !== 'extension' ||
!isDaemonControlRecord(payload) ||
payload.name !== RUSHD_GRAPH_SNAPSHOT ||
!isDaemonControlRecord(payload.data) ||
payload.data.requestId !== request.requestId ||
!isDaemonControlRecord(payload.data.snapshot) ||
typeof payload.data.snapshot.initialized !== 'boolean'
) {
throw new Error('The daemon sent an invalid graph snapshot event.');
}
if (
expectedWorkspaceGeneration !== undefined &&
payload.data.snapshot.workspaceGeneration !== expectedWorkspaceGeneration
)
throw new Error('The graph mutation response belongs to a different workspace generation.');
sawSnapshot = true;
await writeJsonAsync(event);
},
onQueuePositionAsync: (position, restartReason) =>
writeJsonAsync({
kind: 'queuePosition',
payload: { requestId: request.requestId, position, ...(restartReason && { restartReason }) }
})
});
if (outcome.kind === 'fallback') {
throw new Error(
`The daemon does not support graph requests: ${outcome.message ?? outcome.reason}. No native fallback was attempted.`
);
}
if (outcome.kind === 'rejected') {
process.exitCode = 1;
await writeJsonAsync({ kind: 'requestRejected', payload: outcome.rejection });
return;
}
if (outcome.result.outcome === 'success' && !sawSnapshot) {
throw new Error(
'The daemon completed without a graph snapshot; graph support is required. No native fallback was attempted.'
);
}
process.exitCode = outcome.result.exitCode;
await writeJsonAsync({ kind: 'requestResult', payload: outcome.result });
}
function writeJsonAsync(value: object): Promise<void> {
return writeStreamAsync(process.stdout, Buffer.from(`${JSON.stringify(value)}\n`));
}