Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "@rushstack/rush-daemon-protocol",
"comment": "Add validated request start, cancellation, rejection, and terminal result wire controls.",
"type": "minor"
}
],
"packageName": "@rushstack/rush-daemon-protocol",
"email": "mojazayeri@users.noreply.github.com"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "@rushstack/rush-daemon-transport",
"comment": "Add an internal abortive close path for stalled daemon connection shutdown.",
"type": "patch"
}
],
"packageName": "@rushstack/rush-daemon-transport",
"email": "mojazayeri@users.noreply.github.com"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "@rushstack/rush-daemon",
"comment": "Wire validated request lifecycles through shared warm host sessions and typed request resolvers.",
"type": "minor"
}
],
"packageName": "@rushstack/rush-daemon",
"email": "mojazayeri@users.noreply.github.com"
}
74 changes: 72 additions & 2 deletions common/reviews/api/rush-daemon-protocol.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ export const DAEMON_CONTROL_MESSAGE_KINDS: readonly [
'setRawMode',
'rawModeChanged',
'terminalPolicy',
'queuePosition'
'queuePosition',
'requestStart',
'requestCancel',
'requestRejected',
'requestResult'
];

// @beta
Expand Down Expand Up @@ -56,11 +60,14 @@ export const DAEMON_PROTOCOL_VERSION: IDaemonProtocolVersion;
// @beta
export const DAEMON_REQUEST_ADMISSION_PROTOCOL_MINOR: number;

// @beta
export const DAEMON_REQUEST_LIFECYCLE_PROTOCOL_MINOR: number;

// @beta
export type DaemonCommandOutcome = 'success' | 'success-with-warning' | 'failure' | 'aborted';

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

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

// @beta
export type DaemonRequestRejectionCode = 'invalidRequest' | 'routingFailed' | 'unsupported' | 'workspaceRecreationRequired';

// @beta
export type DaemonRushCommandOrigin = 'built-in' | 'custom';

Expand Down Expand Up @@ -192,6 +202,7 @@ export interface IDaemonClientCaps {
readonly isTTY: boolean;
readonly supportsInteractiveIO?: boolean;
readonly supportsRequestAdmission?: boolean;
readonly supportsRequestLifecycle?: boolean;
readonly verbosity?: DaemonVerbosity;
}

Expand Down Expand Up @@ -414,6 +425,28 @@ export interface IDaemonRequestAdmissionOptions {
readonly waitTimeoutMs?: number;
}

// @beta
export interface IDaemonRequestCancelMessage {
// (undocumented)
readonly kind: 'requestCancel';
// (undocumented)
readonly payload: {
readonly requestId: string;
};
}

// @beta
export interface IDaemonRequestEnvelope {
readonly admission?: IDaemonRequestAdmissionOptions;
readonly argv: ReadonlyArray<string>;
readonly commandName: string;
readonly commandOrigin: DaemonRushCommandOrigin;
readonly cwd: string;
readonly environment: Readonly<Record<string, string>>;
readonly requestId: string;
readonly terminal: IDaemonRequestTerminal;
}

// @beta
export interface IDaemonRequestQueuePositionMessage {
// (undocumented)
Expand All @@ -425,6 +458,43 @@ export interface IDaemonRequestQueuePositionMessage {
};
}

// @beta
export interface IDaemonRequestRejectedMessage {
// (undocumented)
readonly kind: 'requestRejected';
// (undocumented)
readonly payload: {
readonly code: DaemonRequestRejectionCode;
readonly message: string;
readonly requestId: string;
};
}

// @beta
export interface IDaemonRequestResultMessage {
// (undocumented)
readonly kind: 'requestResult';
// (undocumented)
readonly payload: IDaemonCommandResult | IDaemonPhasedRequestResult;
}

// @beta
export interface IDaemonRequestStartMessage {
// (undocumented)
readonly kind: 'requestStart';
// (undocumented)
readonly payload: IDaemonRequestEnvelope;
}

// @beta
export interface IDaemonRequestTerminal {
readonly acceptsStdin?: boolean;
readonly columns?: number;
readonly isTTY: boolean;
readonly supportsColor: boolean;
readonly terminalRequirement?: DaemonTerminalRequirement;
}

// @beta
export interface IDaemonSetRawModeMessage {
// (undocumented)
Expand Down
2 changes: 2 additions & 0 deletions common/reviews/api/rush-daemon-transport.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export function connectDaemonAsync(socketPath: string, options?: IDaemonConnecto
// @beta
export class DaemonFrameConnection {
constructor(socket: net.Socket);
// @internal
abort(error: Error): void;
closeAsync(): Promise<void>;
onClosed(handler: (error: Error | undefined) => void): void;
onFrame(handler: (frame: IDaemonFrame) => void | Promise<void>): void;
Expand Down
87 changes: 86 additions & 1 deletion common/reviews/api/rush-daemon.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type { IDaemonPaths } from '@rushstack/rush-daemon-transport';
import type { IDaemonPhasedRequest } from '@rushstack/rush-daemon-protocol';
import type { IDaemonPhasedRequestResult } from '@rushstack/rush-daemon-protocol';
import type { IDaemonRequestAdmissionOptions } from '@rushstack/rush-daemon-protocol';
import type { IDaemonRequestEnvelope } from '@rushstack/rush-daemon-protocol';
import type { IDaemonRequestQueuePositionMessage } from '@rushstack/rush-daemon-protocol';
import type { IDaemonSetRawModeMessage } from '@rushstack/rush-daemon-protocol';
import type { IDaemonTerminalPolicyResult } from '@rushstack/rush-daemon-protocol';
Expand All @@ -33,6 +34,25 @@ export type CreateWorkspaceEngineComponentsAsync = (options: ICreateWorkspaceEng
// @beta
export type CreateWorkspaceSessionComponentsAsync = (options: ICreateWorkspaceSessionComponentsOptions) => Promise<IWorkspaceSessionComponents>;

// @beta
export class DaemonRequestDispatcher implements AsyncDisposable {
// (undocumented)
[Symbol.asyncDispose](): Promise<void>;
constructor(workspaceSession: IWorkspaceSession, resolver?: IDaemonRequestResolver);
// (undocumented)
dispatchAsync(envelope: IDaemonRequestEnvelope, client: IDaemonRequestDispatchClient): Promise<void>;
}

// @beta
export class DaemonRequestDispatchError extends Error {
constructor(code: DaemonRequestDispatchErrorCode, message: string, options?: ErrorOptions);
// (undocumented)
readonly code: DaemonRequestDispatchErrorCode;
}

// @beta
export type DaemonRequestDispatchErrorCode = 'invalidRequest' | 'routingFailed' | 'unsupported';

// @beta
export class DaemonRequiresInProcessError extends Error {
constructor(policy: IDaemonTerminalPolicyResult);
Expand Down Expand Up @@ -100,6 +120,40 @@ export interface IDaemonInteractiveRequestOptions {
readonly requestId: string;
}

// @beta
export interface IDaemonRequestDispatchClient {
// (undocumented)
readonly abortSignal: AbortSignal;
// (undocumented)
getNextEventSequence(): number;
// (undocumented)
readonly interactiveSession: IInteractiveRequestSession;
// (undocumented)
readonly sessionId: string;
// (undocumented)
readonly supportsRequestAdmission: boolean;
// (undocumented)
writeEventAsync(event: IDaemonEventEnvelope): Promise<void>;
// (undocumented)
writeLogChunkAsync(operationId: string, stream: 'stdout' | 'stderr', chunk: Uint8Array): Promise<void>;
// (undocumented)
writeQueuePositionAsync(message: IDaemonRequestQueuePositionMessage): Promise<void>;
// (undocumented)
writeResultAsync(result: IDaemonCommandResult | IDaemonPhasedRequestResult): Promise<void>;
// (undocumented)
writeTerminalChunkAsync(stream: 'stdout' | 'stderr', chunk: Uint8Array): Promise<void>;
// (undocumented)
writeTerminalPolicyAsync(result: IDaemonTerminalPolicyResult): Promise<void>;
}

// @beta
export interface IDaemonRequestResolver {
// (undocumented)
readonly [Symbol.asyncDispose]?: () => Promise<void>;
// (undocumented)
resolveRequestAsync(options: IResolveDaemonRequestOptions): Promise<ResolvedDaemonRequest>;
}

// @beta
export interface IGlobalCommandEnvironment {
// (undocumented)
Expand Down Expand Up @@ -236,10 +290,12 @@ export class InteractiveInputRoutingError extends Error {
}

// @beta
export type InteractiveInputRoutingErrorCode = 'duplicateRequest' | 'unknownRequest' | 'completedRequest' | 'nonInteractiveRequest';
export type InteractiveInputRoutingErrorCode = 'duplicateRequest' | 'unknownRequest' | 'completedRequest' | 'nonInteractiveRequest' | 'requestLimitExceeded';

// @beta
export class InteractiveRequestInputRouter {
// @internal
markRequestCompleted(requestId: string): void;
// (undocumented)
register(options: IInteractiveRequestRegistrationOptions): IInteractiveRequestSession;
// (undocumented)
Expand Down Expand Up @@ -278,6 +334,31 @@ export interface IRequestSchedulerAcquireOptions {
waitTimeoutMs?: number;
}

// @beta
export interface IResolveDaemonRequestOptions {
readonly abortSignal: AbortSignal;
// (undocumented)
readonly envelope: IDaemonRequestEnvelope;
// (undocumented)
readonly workspaceSession: IWorkspaceSession;
}

// @beta
export interface IResolvedDaemonGlobalRequest {
// (undocumented)
readonly executor: GlobalCommandExecutor;
// (undocumented)
readonly kind: 'global';
}

// @beta
export interface IResolvedDaemonPhasedRequest {
// (undocumented)
readonly kind: 'phased';
// (undocumented)
readonly request: IDaemonPhasedRequest;
}

// @beta
export interface IResolvedGlobalCommandRequest {
// (undocumented)
Expand Down Expand Up @@ -321,6 +402,7 @@ export interface IRushDaemonHostOptions {
readonly onError?: (error: Error) => void;
readonly onInteractiveConnection?: (connection: IDaemonInteractiveConnection) => void;
readonly repoRoot: string;
readonly requestResolver?: IDaemonRequestResolver;
readonly rushVersion: string;
readonly startupOptions?: Readonly<Record<string, unknown>>;
}
Expand Down Expand Up @@ -497,6 +579,9 @@ export enum RequestSchedulerErrorCode {
WaitTimeout = "WAIT_TIMEOUT"
}

// @beta
export type ResolvedDaemonRequest = IResolvedDaemonPhasedRequest | IResolvedDaemonGlobalRequest;

// @beta
export class RushDaemonHost {
closeAsync(): Promise<void>;
Expand Down
3 changes: 3 additions & 0 deletions libraries/rush-daemon-protocol/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ The engine-agnostic **wire layer** spoken by every client of the Rush daemon (`r
acknowledged raw-mode controls and typed terminal-policy results remain scoped to one request.
- **Request admission contracts** — resolved no-wait and bounded-timeout options, typed admission
failure codes, and capability-gated one-based queue-position control messages.
- **Request lifecycle contracts** — a validated presentation-free command envelope, cancellation,
typed routing rejection/fallback, and one authoritative terminal result control. Command parsing
and Rush action construction remain outside the protocol.

Part of the Rush 6 / rushd re-architecture:
[microsoft/rushstack#5894](https://github.com/microsoft/rushstack/issues/5894).
Expand Down
20 changes: 10 additions & 10 deletions libraries/rush-daemon-protocol/src/ControlMessageValidation.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

import { isDaemonControlRecord } from './ControlRecord';
import { isDaemonControlMessageKind } from './DaemonControlKinds';
import { DaemonProtocolError } from './DaemonProtocolError';
import { isDaemonVerbosity } from './DaemonVerbosity';
Expand All @@ -9,14 +10,9 @@ import {
validateRawModeControl,
validateTerminalPolicyControl
} from './InteractiveControlValidation';
import {
validateRequestAdmissionCapability,
validateRequestQueuePositionControl
} from './RequestAdmissionControlValidation';
/** Returns `true` when `value` is a non-null control record. @beta */
export function isDaemonControlRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null;
}
import { validateRequestAdmissionCapability, validateRequestQueuePositionControl } from './RequestAdmissionControlValidation';
import { validateRequestCancelControl, validateRequestRejectedControl, validateRequestResultControl, validateRequestStartControl } from './RequestControlValidation';
import { validateRequestLifecycleCapability } from './RequestLifecycleCapabilityValidation';
function fail(reason: string): never {
throw new DaemonProtocolError('malformedControlMessage', reason);
}
Expand Down Expand Up @@ -57,6 +53,7 @@ function validateSubscribe(payload: Record<string, unknown>): void {
}
validateInteractiveCapability(payload);
validateRequestAdmissionCapability(payload);
validateRequestLifecycleCapability(payload);
requireSubscribeVerbosity(payload);
}
function requireSubscribeVerbosity(payload: Record<string, unknown>): void {
Expand All @@ -69,7 +66,6 @@ function validateError(payload: Record<string, unknown>): void {
requireStringField(payload, 'message');
}
type ControlValidator = (payload: Record<string, unknown>) => void;

const noopValidator: ControlValidator = () => undefined;

const VALIDATORS_BY_KIND: Record<string, ControlValidator> = {
Expand All @@ -83,7 +79,11 @@ const VALIDATORS_BY_KIND: Record<string, ControlValidator> = {
setRawMode: validateRawModeControl,
rawModeChanged: validateRawModeControl,
terminalPolicy: validateTerminalPolicyControl,
queuePosition: validateRequestQueuePositionControl
queuePosition: validateRequestQueuePositionControl,
requestStart: validateRequestStartControl,
requestCancel: validateRequestCancelControl,
requestRejected: validateRequestRejectedControl,
requestResult: validateRequestResultControl
};

/** Structurally validates a parsed control message. @beta */
Expand Down
7 changes: 7 additions & 0 deletions libraries/rush-daemon-protocol/src/ControlRecord.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

/** Returns `true` when `value` is a non-null control record. @beta */
export function isDaemonControlRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null;
}
2 changes: 2 additions & 0 deletions libraries/rush-daemon-protocol/src/DaemonClientCaps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export interface IDaemonClientCaps {
readonly supportsInteractiveIO?: boolean;
/** Whether the client supports request admission progress controls and typed failures. */
readonly supportsRequestAdmission?: boolean;
/** Whether the client supports the request start, cancellation, and terminal outcome controls. */
readonly supportsRequestLifecycle?: boolean;
/** The verbosity subset this client receives. Defaults to `normal`. */
readonly verbosity?: DaemonVerbosity;
/** The client's terminal width in columns, when known. */
Expand Down
Loading