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 parsed command origin, request admission options, queue progress messages, and typed admission failures.",
"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",
"comment": "Classify parsed built-in commands and admit phased and global requests through bounded workspace and graph scheduling.",
"type": "minor"
}
],
"packageName": "@rushstack/rush-daemon",
"email": "mojazayeri@users.noreply.github.com"
}
41 changes: 39 additions & 2 deletions common/reviews/api/rush-daemon-protocol.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ export const DAEMON_CONTROL_MESSAGE_KINDS: readonly [
'error',
'setRawMode',
'rawModeChanged',
'terminalPolicy'
'terminalPolicy',
'queuePosition'
];

// @beta
Expand Down Expand Up @@ -52,11 +53,14 @@ export const DAEMON_INTERACTIVE_IO_PROTOCOL_MINOR: number;
// @beta
export const DAEMON_PROTOCOL_VERSION: IDaemonProtocolVersion;

// @beta
export const DAEMON_REQUEST_ADMISSION_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;
export type DaemonControlMessage = IDaemonHelloMessage | IDaemonHelloAckMessage | IDaemonSubscribeMessage | IDaemonUnsubscribeMessage | IDaemonPingMessage | IDaemonPongMessage | IDaemonErrorMessage | IDaemonSetRawModeMessage | IDaemonRawModeChangedMessage | IDaemonTerminalPolicyMessage | IDaemonRequestQueuePositionMessage;

// @beta
export type DaemonControlMessageKind = (typeof DAEMON_CONTROL_MESSAGE_KINDS)[number];
Expand Down Expand Up @@ -121,6 +125,12 @@ export class DaemonProtocolError extends Error {
// @beta
export type DaemonProtocolErrorCode = 'frameTooLarge' | 'unknownFrameType' | 'malformedPayload' | 'malformedControlMessage' | 'protocolVersionMismatch';

// @beta
export type DaemonRequestAdmissionErrorCode = 'aborted' | 'no-wait' | 'wait-timeout';

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

// @beta
export type DaemonTerminalPolicyDecision = 'runInDaemon' | 'requiresInProcess';

Expand Down Expand Up @@ -181,12 +191,14 @@ export interface IDaemonClientCaps {
readonly columns?: number;
readonly isTTY: boolean;
readonly supportsInteractiveIO?: boolean;
readonly supportsRequestAdmission?: boolean;
readonly verbosity?: DaemonVerbosity;
}

// @beta
export interface IDaemonCommandResult {
readonly aborted: boolean;
readonly admissionErrorCode?: DaemonRequestAdmissionErrorCode;
readonly errorMessage?: string;
readonly exitCode: number;
readonly outcome: DaemonCommandOutcome;
Expand Down Expand Up @@ -337,7 +349,9 @@ export interface IDaemonPhasedOperationSelection {
// @beta
export interface IDaemonPhasedRequest {
readonly acceptsStdin?: boolean;
readonly admission?: IDaemonRequestAdmissionOptions;
readonly commandName: string;
readonly commandOrigin?: DaemonRushCommandOrigin;
readonly engineShape: IDaemonPhasedEngineShape;
readonly environment: Readonly<Record<string, string>>;
readonly operationSelection: ReadonlyArray<IDaemonPhasedOperationSelection>;
Expand Down Expand Up @@ -394,6 +408,23 @@ export interface IDaemonRawModeChangedMessage {
};
}

// @beta
export interface IDaemonRequestAdmissionOptions {
readonly noWait?: boolean;
readonly waitTimeoutMs?: number;
}

// @beta
export interface IDaemonRequestQueuePositionMessage {
// (undocumented)
readonly kind: 'queuePosition';
// (undocumented)
readonly payload: {
readonly position: number;
readonly requestId: string;
};
}

// @beta
export interface IDaemonSetRawModeMessage {
// (undocumented)
Expand Down Expand Up @@ -478,6 +509,9 @@ export const LENGTH_FIELD_BYTES: number;
// @beta
export const LENGTH_FIELD_OFFSET: number;

// @beta
export const MAX_DAEMON_REQUEST_WAIT_TIMEOUT_MS: number;

// @beta
export const MAX_OPERATION_ID_BYTES: number;

Expand Down Expand Up @@ -536,4 +570,7 @@ export function validateDaemonControlMessage(value: unknown): void;
// @beta
export function validateDaemonEventEnvelope(value: unknown): IDaemonEventEnvelope;

// @beta
export function validateDaemonRequestAdmissionOptions(options: IDaemonRequestAdmissionOptions | undefined): void;

```
15 changes: 15 additions & 0 deletions common/reviews/api/rush-daemon.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@
/// <reference types="node" />

import * as childProcess from 'node:child_process';
import type { DaemonRushCommandOrigin } from '@rushstack/rush-daemon-protocol';
import type { DaemonTerminalRequirement } from '@rushstack/rush-daemon-protocol';
import type { GetInputsSnapshotAsyncFn } from '@microsoft/rush-lib';
import type { IDaemonCommandResult } from '@rushstack/rush-daemon-protocol';
import type { IDaemonEventEnvelope } from '@rushstack/rush-daemon-protocol';
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 { IDaemonRequestQueuePositionMessage } from '@rushstack/rush-daemon-protocol';
import type { IDaemonSetRawModeMessage } from '@rushstack/rush-daemon-protocol';
import type { IDaemonTerminalPolicyResult } from '@rushstack/rush-daemon-protocol';
import type { IInputsSnapshot } from '@microsoft/rush-lib';
Expand Down Expand Up @@ -138,6 +141,8 @@ export interface IGlobalCommandExecutionResult {
export interface IGlobalCommandRequestClient {
readonly abortSignal: AbortSignal;
readonly interactiveSession?: IInteractiveRequestSession;
readonly supportsRequestAdmission?: boolean;
writeQueuePositionAsync?(message: IDaemonRequestQueuePositionMessage): Promise<void>;
writeResultAsync(result: IDaemonCommandResult): Promise<void>;
writeTerminalChunkAsync(stream: 'stdout' | 'stderr', chunk: Uint8Array): Promise<void>;
writeTerminalPolicyAsync(result: IDaemonTerminalPolicyResult): Promise<void>;
Expand Down Expand Up @@ -248,8 +253,10 @@ export interface IPhasedRequestClient {
readonly interactiveInputSink?: IInteractiveRequestInputSink;
readonly interactiveSession?: IInteractiveRequestSession;
readonly sessionId: string;
readonly supportsRequestAdmission?: boolean;
writeEventAsync(event: IDaemonEventEnvelope): Promise<void>;
writeLogChunkAsync(operationId: string, stream: 'stdout' | 'stderr', chunk: Uint8Array): Promise<void>;
writeQueuePositionAsync?(message: IDaemonRequestQueuePositionMessage): Promise<void>;
writeResultAsync(result: IDaemonPhasedRequestResult): Promise<void>;
writeTerminalPolicyAsync(result: IDaemonTerminalPolicyResult): Promise<void>;
}
Expand All @@ -273,9 +280,13 @@ export interface IRequestSchedulerAcquireOptions {

// @beta
export interface IResolvedGlobalCommandRequest {
// (undocumented)
readonly admission: IDaemonRequestAdmissionOptions | undefined;
// (undocumented)
readonly commandName: string;
// (undocumented)
readonly commandOrigin: DaemonRushCommandOrigin;
// (undocumented)
readonly cwd: string;
// (undocumented)
readonly environment: IGlobalCommandEnvironment;
Expand All @@ -287,9 +298,13 @@ export interface IResolvedGlobalCommandRequest {

// @beta
export interface IResolveGlobalCommandRequestOptions {
// (undocumented)
readonly admission?: IDaemonRequestAdmissionOptions;
// (undocumented)
readonly commandName: string;
// (undocumented)
readonly commandOrigin: DaemonRushCommandOrigin;
// (undocumented)
readonly cwd: string;
// (undocumented)
readonly environment: Readonly<NodeJS.ProcessEnv>;
Expand Down
6 changes: 4 additions & 2 deletions libraries/rush-daemon-protocol/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ The engine-agnostic **wire layer** spoken by every client of the Rush daemon (`r
- **Per-subscription verbosity** — a pure filter applied at event serialization so each
client receives its own verbosity subset without mutating shared engine state.
- **Resolved phased-request contracts** — engine-agnostic request, enabled-state selection,
and client-scoped result types for integrations that have already parsed a command and
resolved it against a real warm operation graph.
parsed built-in/custom command origin, and client-scoped result types for integrations that
have already parsed a command and resolved it against a real warm operation graph.
- **Final command result contract** — one typed success, warning, failure, or abort outcome
with the authoritative Rush-compatible exit code, delivered after request output drains.
- **Interactive request contracts** — request-tagged stdin frames preserve arbitrary bytes, while
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.

Part of the Rush 6 / rushd re-architecture:
[microsoft/rushstack#5894](https://github.com/microsoft/rushstack/issues/5894).
Expand Down
15 changes: 7 additions & 8 deletions libraries/rush-daemon-protocol/src/ControlMessageValidation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ 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;
Expand All @@ -33,43 +37,37 @@ function requireNumberField(record: Record<string, unknown>, field: string): voi
fail(`Control message field "${field}" must be a number.`);
}
}

function requireVersion(payload: Record<string, unknown>): void {
const version: Record<string, unknown> = requireRecordField(payload, 'protocolVersion');
requireNumberField(version, 'major');
requireNumberField(version, 'minor');
}

function validateHelloAck(payload: Record<string, unknown>): void {
requireVersion(payload);
requireStringField(payload, 'sessionId');
}

function validatePong(payload: Record<string, unknown>): void {
if (payload.daemonVersion !== undefined) requireStringField(payload, 'daemonVersion');
if (payload.protocolVersion !== undefined) requireVersion(payload);
requireNumberField(payload, 'uptimeMs');
}

function validateSubscribe(payload: Record<string, unknown>): void {
if (typeof payload.isTTY !== 'boolean') {
fail('Subscribe message payload.isTTY must be a boolean.');
}
validateInteractiveCapability(payload);
validateRequestAdmissionCapability(payload);
requireSubscribeVerbosity(payload);
}

function requireSubscribeVerbosity(payload: Record<string, unknown>): void {
if (payload.verbosity !== undefined && !isDaemonVerbosity(payload.verbosity)) {
fail('Subscribe message payload.verbosity is not a known verbosity level.');
}
}

function validateError(payload: Record<string, unknown>): void {
requireStringField(payload, 'code');
requireStringField(payload, 'message');
}

type ControlValidator = (payload: Record<string, unknown>) => void;

const noopValidator: ControlValidator = () => undefined;
Expand All @@ -84,7 +82,8 @@ const VALIDATORS_BY_KIND: Record<string, ControlValidator> = {
error: validateError,
setRawMode: validateRawModeControl,
rawModeChanged: validateRawModeControl,
terminalPolicy: validateTerminalPolicyControl
terminalPolicy: validateTerminalPolicyControl,
queuePosition: validateRequestQueuePositionControl
};

/** Structurally validates a parsed control message. @beta */
Expand Down
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 @@ -18,6 +18,8 @@ export interface IDaemonClientCaps {
readonly isTTY: boolean;
/** Whether the client supports request-scoped stdin and acknowledged raw-mode control. */
readonly supportsInteractiveIO?: boolean;
/** Whether the client supports request admission progress controls and typed failures. */
readonly supportsRequestAdmission?: boolean;
/** The verbosity subset this client receives. Defaults to `normal`. */
readonly verbosity?: DaemonVerbosity;
/** The client's terminal width in columns, when known. */
Expand Down
4 changes: 4 additions & 0 deletions libraries/rush-daemon-protocol/src/DaemonCommandResult.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

import type { DaemonRequestAdmissionErrorCode } from './DaemonRequestAdmission';

/**
* The semantic outcome of a daemon command.
*
Expand All @@ -16,6 +18,8 @@ export type DaemonCommandOutcome = 'success' | 'success-with-warning' | 'failure
export interface IDaemonCommandResult {
/** Whether cancellation or disconnect was observed, even if a cleanup failure determines the outcome. */
readonly aborted: boolean;
/** The typed admission failure, when execution never started. */
readonly admissionErrorCode?: DaemonRequestAdmissionErrorCode;
/** The process exit code a compatible in-process Rush invocation would return. */
readonly exitCode: number;
/** A failure description for execution or cleanup failures that were not already operation-scoped. */
Expand Down
5 changes: 3 additions & 2 deletions libraries/rush-daemon-protocol/src/DaemonControlKinds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@ export const DAEMON_CONTROL_MESSAGE_KINDS: readonly [
'error',
'setRawMode',
'rawModeChanged',
'terminalPolicy'
'terminalPolicy',
'queuePosition'
] = [
'hello', 'helloAck', 'subscribe', 'unsubscribe', 'ping', 'pong', 'error',
'setRawMode', 'rawModeChanged', 'terminalPolicy'
'setRawMode', 'rawModeChanged', 'terminalPolicy', 'queuePosition'
];

/** The union of control message `kind` discriminants. @beta */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {
import type { IDaemonPongMessage } from './DaemonPongMessage';
import type { DaemonProtocolErrorCode } from './DaemonProtocolError';
import type { IDaemonProtocolVersion } from './DaemonProtocolVersion';
import type { IDaemonRequestQueuePositionMessage } from './DaemonRequestAdmission';

/** The empty payload of control messages that carry no data. @beta */
export type DaemonEmptyPayload = Record<string, never>;
Expand Down Expand Up @@ -74,4 +75,5 @@ export type DaemonControlMessage =
| IDaemonErrorMessage
| IDaemonSetRawModeMessage
| IDaemonRawModeChangedMessage
| IDaemonTerminalPolicyMessage;
| IDaemonTerminalPolicyMessage
| IDaemonRequestQueuePositionMessage;
6 changes: 6 additions & 0 deletions libraries/rush-daemon-protocol/src/DaemonPhasedRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// See LICENSE in the project root for license information.

import type { IDaemonCommandResult } from './DaemonCommandResult';
import type { IDaemonRequestAdmissionOptions } from './DaemonRequestAdmission';
import type { DaemonRushCommandOrigin } from './DaemonRushCommand';
import type { DaemonTerminalRequirement } from './DaemonTerminalPolicy';

/**
Expand Down Expand Up @@ -45,10 +47,14 @@ export interface IDaemonPhasedEngineShape {
* @beta
*/
export interface IDaemonPhasedRequest {
/** Queue-and-wait behavior resolved by the client integration. */
readonly admission?: IDaemonRequestAdmissionOptions;
/** Whether the command accepts request-scoped stdin bytes. */
readonly acceptsStdin?: boolean;
/** The parsed phased command name. */
readonly commandName: string;
/** The parsed action's origin. Omission and custom actions fail closed to exclusive admission. */
readonly commandOrigin?: DaemonRushCommandOrigin;
/** The exact warm engine shape against which the selection was resolved. */
readonly engineShape: IDaemonPhasedEngineShape;
/** The request environment used for Rush command policy without mutating the daemon process environment. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
/** The first additive protocol minor that supports request-scoped interactive I/O. @beta */
export const DAEMON_INTERACTIVE_IO_PROTOCOL_MINOR: number = 3;

/** The first additive protocol minor that supports request admission. @beta */
export const DAEMON_REQUEST_ADMISSION_PROTOCOL_MINOR: number = 4;

/**
* A rushd wire protocol version.
*
Expand Down Expand Up @@ -37,7 +40,7 @@ export interface IDaemonProtocolVersion {
*/
export const DAEMON_PROTOCOL_VERSION: IDaemonProtocolVersion = {
major: 0,
minor: DAEMON_INTERACTIVE_IO_PROTOCOL_MINOR
minor: DAEMON_REQUEST_ADMISSION_PROTOCOL_MINOR
};

/**
Expand Down
Loading