Skip to content

Commit a88b242

Browse files
mojazamojazayeriCopilot
authored
[rush-daemon][WS2][9/9] Wire host request lifecycle (#5973)
* Wire daemon host request lifecycle Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Adapt host lifecycle to upstream routing Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Address host lifecycle review feedback Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: mojaza <mojazayeri@users.noreply.github.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 9b4feb4 commit a88b242

41 files changed

Lines changed: 2625 additions & 134 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@rushstack/rush-daemon-protocol",
5+
"comment": "Add validated request start, cancellation, rejection, and terminal result wire controls.",
6+
"type": "minor"
7+
}
8+
],
9+
"packageName": "@rushstack/rush-daemon-protocol",
10+
"email": "mojazayeri@users.noreply.github.com"
11+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@rushstack/rush-daemon-transport",
5+
"comment": "Add an internal abortive close path for stalled daemon connection shutdown.",
6+
"type": "patch"
7+
}
8+
],
9+
"packageName": "@rushstack/rush-daemon-transport",
10+
"email": "mojazayeri@users.noreply.github.com"
11+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@rushstack/rush-daemon",
5+
"comment": "Wire validated request lifecycles through shared warm host sessions and typed request resolvers.",
6+
"type": "minor"
7+
}
8+
],
9+
"packageName": "@rushstack/rush-daemon",
10+
"email": "mojazayeri@users.noreply.github.com"
11+
}

common/reviews/api/rush-daemon-protocol.api.md

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,11 @@ export const DAEMON_CONTROL_MESSAGE_KINDS: readonly [
2525
'setRawMode',
2626
'rawModeChanged',
2727
'terminalPolicy',
28-
'queuePosition'
28+
'queuePosition',
29+
'requestStart',
30+
'requestCancel',
31+
'requestRejected',
32+
'requestResult'
2933
];
3034

3135
// @beta
@@ -56,11 +60,14 @@ export const DAEMON_PROTOCOL_VERSION: IDaemonProtocolVersion;
5660
// @beta
5761
export const DAEMON_REQUEST_ADMISSION_PROTOCOL_MINOR: number;
5862

63+
// @beta
64+
export const DAEMON_REQUEST_LIFECYCLE_PROTOCOL_MINOR: number;
65+
5966
// @beta
6067
export type DaemonCommandOutcome = 'success' | 'success-with-warning' | 'failure' | 'aborted';
6168

6269
// @beta
63-
export type DaemonControlMessage = IDaemonHelloMessage | IDaemonHelloAckMessage | IDaemonSubscribeMessage | IDaemonUnsubscribeMessage | IDaemonPingMessage | IDaemonPongMessage | IDaemonErrorMessage | IDaemonSetRawModeMessage | IDaemonRawModeChangedMessage | IDaemonTerminalPolicyMessage | IDaemonRequestQueuePositionMessage;
70+
export type DaemonControlMessage = IDaemonHelloMessage | IDaemonHelloAckMessage | IDaemonSubscribeMessage | IDaemonUnsubscribeMessage | IDaemonPingMessage | IDaemonPongMessage | IDaemonErrorMessage | IDaemonSetRawModeMessage | IDaemonRawModeChangedMessage | IDaemonTerminalPolicyMessage | IDaemonRequestQueuePositionMessage | IDaemonRequestStartMessage | IDaemonRequestCancelMessage | IDaemonRequestRejectedMessage | IDaemonRequestResultMessage;
6471

6572
// @beta
6673
export type DaemonControlMessageKind = (typeof DAEMON_CONTROL_MESSAGE_KINDS)[number];
@@ -128,6 +135,9 @@ export type DaemonProtocolErrorCode = 'frameTooLarge' | 'unknownFrameType' | 'ma
128135
// @beta
129136
export type DaemonRequestAdmissionErrorCode = 'aborted' | 'no-wait' | 'wait-timeout';
130137

138+
// @beta
139+
export type DaemonRequestRejectionCode = 'invalidRequest' | 'routingFailed' | 'unsupported' | 'workspaceRecreationRequired';
140+
131141
// @beta
132142
export type DaemonRushCommandOrigin = 'built-in' | 'custom';
133143

@@ -192,6 +202,7 @@ export interface IDaemonClientCaps {
192202
readonly isTTY: boolean;
193203
readonly supportsInteractiveIO?: boolean;
194204
readonly supportsRequestAdmission?: boolean;
205+
readonly supportsRequestLifecycle?: boolean;
195206
readonly verbosity?: DaemonVerbosity;
196207
}
197208

@@ -414,6 +425,28 @@ export interface IDaemonRequestAdmissionOptions {
414425
readonly waitTimeoutMs?: number;
415426
}
416427

428+
// @beta
429+
export interface IDaemonRequestCancelMessage {
430+
// (undocumented)
431+
readonly kind: 'requestCancel';
432+
// (undocumented)
433+
readonly payload: {
434+
readonly requestId: string;
435+
};
436+
}
437+
438+
// @beta
439+
export interface IDaemonRequestEnvelope {
440+
readonly admission?: IDaemonRequestAdmissionOptions;
441+
readonly argv: ReadonlyArray<string>;
442+
readonly commandName: string;
443+
readonly commandOrigin: DaemonRushCommandOrigin;
444+
readonly cwd: string;
445+
readonly environment: Readonly<Record<string, string>>;
446+
readonly requestId: string;
447+
readonly terminal: IDaemonRequestTerminal;
448+
}
449+
417450
// @beta
418451
export interface IDaemonRequestQueuePositionMessage {
419452
// (undocumented)
@@ -425,6 +458,43 @@ export interface IDaemonRequestQueuePositionMessage {
425458
};
426459
}
427460

461+
// @beta
462+
export interface IDaemonRequestRejectedMessage {
463+
// (undocumented)
464+
readonly kind: 'requestRejected';
465+
// (undocumented)
466+
readonly payload: {
467+
readonly code: DaemonRequestRejectionCode;
468+
readonly message: string;
469+
readonly requestId: string;
470+
};
471+
}
472+
473+
// @beta
474+
export interface IDaemonRequestResultMessage {
475+
// (undocumented)
476+
readonly kind: 'requestResult';
477+
// (undocumented)
478+
readonly payload: IDaemonCommandResult | IDaemonPhasedRequestResult;
479+
}
480+
481+
// @beta
482+
export interface IDaemonRequestStartMessage {
483+
// (undocumented)
484+
readonly kind: 'requestStart';
485+
// (undocumented)
486+
readonly payload: IDaemonRequestEnvelope;
487+
}
488+
489+
// @beta
490+
export interface IDaemonRequestTerminal {
491+
readonly acceptsStdin?: boolean;
492+
readonly columns?: number;
493+
readonly isTTY: boolean;
494+
readonly supportsColor: boolean;
495+
readonly terminalRequirement?: DaemonTerminalRequirement;
496+
}
497+
428498
// @beta
429499
export interface IDaemonSetRawModeMessage {
430500
// (undocumented)

common/reviews/api/rush-daemon-transport.api.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ export function connectDaemonAsync(socketPath: string, options?: IDaemonConnecto
1717
// @beta
1818
export class DaemonFrameConnection {
1919
constructor(socket: net.Socket);
20+
// @internal
21+
abort(error: Error): void;
2022
closeAsync(): Promise<void>;
2123
onClosed(handler: (error: Error | undefined) => void): void;
2224
onFrame(handler: (frame: IDaemonFrame) => void | Promise<void>): void;

common/reviews/api/rush-daemon.api.md

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import type { IDaemonPaths } from '@rushstack/rush-daemon-transport';
1616
import type { IDaemonPhasedRequest } from '@rushstack/rush-daemon-protocol';
1717
import type { IDaemonPhasedRequestResult } from '@rushstack/rush-daemon-protocol';
1818
import type { IDaemonRequestAdmissionOptions } from '@rushstack/rush-daemon-protocol';
19+
import type { IDaemonRequestEnvelope } from '@rushstack/rush-daemon-protocol';
1920
import type { IDaemonRequestQueuePositionMessage } from '@rushstack/rush-daemon-protocol';
2021
import type { IDaemonSetRawModeMessage } from '@rushstack/rush-daemon-protocol';
2122
import type { IDaemonTerminalPolicyResult } from '@rushstack/rush-daemon-protocol';
@@ -33,6 +34,25 @@ export type CreateWorkspaceEngineComponentsAsync = (options: ICreateWorkspaceEng
3334
// @beta
3435
export type CreateWorkspaceSessionComponentsAsync = (options: ICreateWorkspaceSessionComponentsOptions) => Promise<IWorkspaceSessionComponents>;
3536

37+
// @beta
38+
export class DaemonRequestDispatcher implements AsyncDisposable {
39+
// (undocumented)
40+
[Symbol.asyncDispose](): Promise<void>;
41+
constructor(workspaceSession: IWorkspaceSession, resolver?: IDaemonRequestResolver);
42+
// (undocumented)
43+
dispatchAsync(envelope: IDaemonRequestEnvelope, client: IDaemonRequestDispatchClient): Promise<void>;
44+
}
45+
46+
// @beta
47+
export class DaemonRequestDispatchError extends Error {
48+
constructor(code: DaemonRequestDispatchErrorCode, message: string, options?: ErrorOptions);
49+
// (undocumented)
50+
readonly code: DaemonRequestDispatchErrorCode;
51+
}
52+
53+
// @beta
54+
export type DaemonRequestDispatchErrorCode = 'invalidRequest' | 'routingFailed' | 'unsupported';
55+
3656
// @beta
3757
export class DaemonRequiresInProcessError extends Error {
3858
constructor(policy: IDaemonTerminalPolicyResult);
@@ -100,6 +120,40 @@ export interface IDaemonInteractiveRequestOptions {
100120
readonly requestId: string;
101121
}
102122

123+
// @beta
124+
export interface IDaemonRequestDispatchClient {
125+
// (undocumented)
126+
readonly abortSignal: AbortSignal;
127+
// (undocumented)
128+
getNextEventSequence(): number;
129+
// (undocumented)
130+
readonly interactiveSession: IInteractiveRequestSession;
131+
// (undocumented)
132+
readonly sessionId: string;
133+
// (undocumented)
134+
readonly supportsRequestAdmission: boolean;
135+
// (undocumented)
136+
writeEventAsync(event: IDaemonEventEnvelope): Promise<void>;
137+
// (undocumented)
138+
writeLogChunkAsync(operationId: string, stream: 'stdout' | 'stderr', chunk: Uint8Array): Promise<void>;
139+
// (undocumented)
140+
writeQueuePositionAsync(message: IDaemonRequestQueuePositionMessage): Promise<void>;
141+
// (undocumented)
142+
writeResultAsync(result: IDaemonCommandResult | IDaemonPhasedRequestResult): Promise<void>;
143+
// (undocumented)
144+
writeTerminalChunkAsync(stream: 'stdout' | 'stderr', chunk: Uint8Array): Promise<void>;
145+
// (undocumented)
146+
writeTerminalPolicyAsync(result: IDaemonTerminalPolicyResult): Promise<void>;
147+
}
148+
149+
// @beta
150+
export interface IDaemonRequestResolver {
151+
// (undocumented)
152+
readonly [Symbol.asyncDispose]?: () => Promise<void>;
153+
// (undocumented)
154+
resolveRequestAsync(options: IResolveDaemonRequestOptions): Promise<ResolvedDaemonRequest>;
155+
}
156+
103157
// @beta
104158
export interface IGlobalCommandEnvironment {
105159
// (undocumented)
@@ -236,10 +290,12 @@ export class InteractiveInputRoutingError extends Error {
236290
}
237291

238292
// @beta
239-
export type InteractiveInputRoutingErrorCode = 'duplicateRequest' | 'unknownRequest' | 'completedRequest' | 'nonInteractiveRequest';
293+
export type InteractiveInputRoutingErrorCode = 'duplicateRequest' | 'unknownRequest' | 'completedRequest' | 'nonInteractiveRequest' | 'requestLimitExceeded';
240294

241295
// @beta
242296
export class InteractiveRequestInputRouter {
297+
// @internal
298+
markRequestCompleted(requestId: string): void;
243299
// (undocumented)
244300
register(options: IInteractiveRequestRegistrationOptions): IInteractiveRequestSession;
245301
// (undocumented)
@@ -278,6 +334,31 @@ export interface IRequestSchedulerAcquireOptions {
278334
waitTimeoutMs?: number;
279335
}
280336

337+
// @beta
338+
export interface IResolveDaemonRequestOptions {
339+
readonly abortSignal: AbortSignal;
340+
// (undocumented)
341+
readonly envelope: IDaemonRequestEnvelope;
342+
// (undocumented)
343+
readonly workspaceSession: IWorkspaceSession;
344+
}
345+
346+
// @beta
347+
export interface IResolvedDaemonGlobalRequest {
348+
// (undocumented)
349+
readonly executor: GlobalCommandExecutor;
350+
// (undocumented)
351+
readonly kind: 'global';
352+
}
353+
354+
// @beta
355+
export interface IResolvedDaemonPhasedRequest {
356+
// (undocumented)
357+
readonly kind: 'phased';
358+
// (undocumented)
359+
readonly request: IDaemonPhasedRequest;
360+
}
361+
281362
// @beta
282363
export interface IResolvedGlobalCommandRequest {
283364
// (undocumented)
@@ -321,6 +402,7 @@ export interface IRushDaemonHostOptions {
321402
readonly onError?: (error: Error) => void;
322403
readonly onInteractiveConnection?: (connection: IDaemonInteractiveConnection) => void;
323404
readonly repoRoot: string;
405+
readonly requestResolver?: IDaemonRequestResolver;
324406
readonly rushVersion: string;
325407
readonly startupOptions?: Readonly<Record<string, unknown>>;
326408
}
@@ -497,6 +579,9 @@ export enum RequestSchedulerErrorCode {
497579
WaitTimeout = "WAIT_TIMEOUT"
498580
}
499581

582+
// @beta
583+
export type ResolvedDaemonRequest = IResolvedDaemonPhasedRequest | IResolvedDaemonGlobalRequest;
584+
500585
// @beta
501586
export class RushDaemonHost {
502587
closeAsync(): Promise<void>;

libraries/rush-daemon-protocol/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ The engine-agnostic **wire layer** spoken by every client of the Rush daemon (`r
2626
acknowledged raw-mode controls and typed terminal-policy results remain scoped to one request.
2727
- **Request admission contracts** — resolved no-wait and bounded-timeout options, typed admission
2828
failure codes, and capability-gated one-based queue-position control messages.
29+
- **Request lifecycle contracts** — a validated presentation-free command envelope, cancellation,
30+
typed routing rejection/fallback, and one authoritative terminal result control. Command parsing
31+
and Rush action construction remain outside the protocol.
2932

3033
Part of the Rush 6 / rushd re-architecture:
3134
[microsoft/rushstack#5894](https://github.com/microsoft/rushstack/issues/5894).

libraries/rush-daemon-protocol/src/ControlMessageValidation.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
22
// See LICENSE in the project root for license information.
33

4+
import { isDaemonControlRecord } from './ControlRecord';
45
import { isDaemonControlMessageKind } from './DaemonControlKinds';
56
import { DaemonProtocolError } from './DaemonProtocolError';
67
import { isDaemonVerbosity } from './DaemonVerbosity';
@@ -9,14 +10,9 @@ import {
910
validateRawModeControl,
1011
validateTerminalPolicyControl
1112
} from './InteractiveControlValidation';
12-
import {
13-
validateRequestAdmissionCapability,
14-
validateRequestQueuePositionControl
15-
} from './RequestAdmissionControlValidation';
16-
/** Returns `true` when `value` is a non-null control record. @beta */
17-
export function isDaemonControlRecord(value: unknown): value is Record<string, unknown> {
18-
return typeof value === 'object' && value !== null;
19-
}
13+
import { validateRequestAdmissionCapability, validateRequestQueuePositionControl } from './RequestAdmissionControlValidation';
14+
import { validateRequestCancelControl, validateRequestRejectedControl, validateRequestResultControl, validateRequestStartControl } from './RequestControlValidation';
15+
import { validateRequestLifecycleCapability } from './RequestLifecycleCapabilityValidation';
2016
function fail(reason: string): never {
2117
throw new DaemonProtocolError('malformedControlMessage', reason);
2218
}
@@ -57,6 +53,7 @@ function validateSubscribe(payload: Record<string, unknown>): void {
5753
}
5854
validateInteractiveCapability(payload);
5955
validateRequestAdmissionCapability(payload);
56+
validateRequestLifecycleCapability(payload);
6057
requireSubscribeVerbosity(payload);
6158
}
6259
function requireSubscribeVerbosity(payload: Record<string, unknown>): void {
@@ -69,7 +66,6 @@ function validateError(payload: Record<string, unknown>): void {
6966
requireStringField(payload, 'message');
7067
}
7168
type ControlValidator = (payload: Record<string, unknown>) => void;
72-
7369
const noopValidator: ControlValidator = () => undefined;
7470

7571
const VALIDATORS_BY_KIND: Record<string, ControlValidator> = {
@@ -83,7 +79,11 @@ const VALIDATORS_BY_KIND: Record<string, ControlValidator> = {
8379
setRawMode: validateRawModeControl,
8480
rawModeChanged: validateRawModeControl,
8581
terminalPolicy: validateTerminalPolicyControl,
86-
queuePosition: validateRequestQueuePositionControl
82+
queuePosition: validateRequestQueuePositionControl,
83+
requestStart: validateRequestStartControl,
84+
requestCancel: validateRequestCancelControl,
85+
requestRejected: validateRequestRejectedControl,
86+
requestResult: validateRequestResultControl
8787
};
8888

8989
/** Structurally validates a parsed control message. @beta */
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
2+
// See LICENSE in the project root for license information.
3+
4+
/** Returns `true` when `value` is a non-null control record. @beta */
5+
export function isDaemonControlRecord(value: unknown): value is Record<string, unknown> {
6+
return typeof value === 'object' && value !== null;
7+
}

libraries/rush-daemon-protocol/src/DaemonClientCaps.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ export interface IDaemonClientCaps {
2020
readonly supportsInteractiveIO?: boolean;
2121
/** Whether the client supports request admission progress controls and typed failures. */
2222
readonly supportsRequestAdmission?: boolean;
23+
/** Whether the client supports the request start, cancellation, and terminal outcome controls. */
24+
readonly supportsRequestLifecycle?: boolean;
2325
/** The verbosity subset this client receives. Defaults to `normal`. */
2426
readonly verbosity?: DaemonVerbosity;
2527
/** The client's terminal width in columns, when known. */

0 commit comments

Comments
 (0)