Skip to content
Draft
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,10 @@
{
"changes": [
{
"packageName": "@rushstack/rush-daemon",
"comment": "Add an opt-in isolated execution context for caller-resolved global commands.",
"type": "minor"
}
],
"packageName": "@rushstack/rush-daemon"
}
106 changes: 106 additions & 0 deletions common/reviews/api/rush-daemon.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@

/// <reference types="node" />

import * as childProcess from 'node:child_process';
import type { GetInputsSnapshotAsyncFn } from '@microsoft/rush-lib';
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 { IInputsSnapshot } from '@microsoft/rush-lib';
import type { IOperationGraph } from '@microsoft/rush-lib';
import type { ITerminal } from '@rushstack/terminal';
import type { Operation } from '@microsoft/rush-lib';
import { RushConfiguration } from '@microsoft/rush-lib';
import type { RushConfigurationProject } from '@microsoft/rush-lib';
Expand All @@ -24,6 +26,16 @@ export type CreateWorkspaceEngineComponentsAsync = (options: ICreateWorkspaceEng
// @beta
export type CreateWorkspaceSessionComponentsAsync = (options: ICreateWorkspaceSessionComponentsOptions) => Promise<IWorkspaceSessionComponents>;

// @beta
export type GlobalCommandExecutor = (context: IGlobalCommandExecutionContext) => Promise<void>;

// @beta
export class GlobalCommandRequestRouter {
constructor(workspaceSession: IWorkspaceSession);
executeAsync(request: IResolvedGlobalCommandRequest, executor: GlobalCommandExecutor, client: IGlobalCommandRequestClient): Promise<IGlobalCommandRequestResult>;
resolveRequest(options: IResolveGlobalCommandRequestOptions): IResolvedGlobalCommandRequest;
}

// @beta
export interface IClassifyWorkspaceInvalidationsOptions {
// (undocumented)
Expand All @@ -49,6 +61,72 @@ export interface ICreateWorkspaceSessionComponentsOptions {
readonly rushConfiguration: RushConfiguration;
}

// @beta
export interface IGlobalCommandEnvironment {
// (undocumented)
get(name: string): string | undefined;
// (undocumented)
getNames(): ReadonlyArray<string>;
// (undocumented)
toObject(): NodeJS.ProcessEnv;
}

// @beta
export interface IGlobalCommandExecutionContext {
// (undocumented)
readonly abortSignal: AbortSignal;
// (undocumented)
readonly cwd: string;
// (undocumented)
readonly environment: IGlobalCommandEnvironment;
// (undocumented)
registerDisposable(disposable: AsyncDisposable): void;
// (undocumented)
spawnChild(command: string, args: ReadonlyArray<string>, options?: IGlobalCommandSpawnOptions): childProcess.ChildProcessWithoutNullStreams;
// (undocumented)
readonly terminal: ITerminal;
// (undocumented)
readonly terminalProperties: IGlobalCommandTerminalProperties;
// (undocumented)
readonly workspaceSession: IWorkspaceSession;
}

// @beta
export interface IGlobalCommandRequestClient {
readonly abortSignal: AbortSignal;
writeTerminalChunkAsync(stream: 'stdout' | 'stderr', chunk: Uint8Array): Promise<void>;
}

// @beta
export interface IGlobalCommandRequestResult {
// (undocumented)
readonly aborted: boolean;
// (undocumented)
readonly requestId: string;
}

// @beta
export interface IGlobalCommandSpawnOptions {
// (undocumented)
readonly environmentOverlay?: Readonly<NodeJS.ProcessEnv>;
// (undocumented)
readonly forwardOutput?: boolean;
// (undocumented)
readonly shell?: boolean | string;
// (undocumented)
readonly windowsHide?: boolean;
}

// @beta
export interface IGlobalCommandTerminalProperties {
// (undocumented)
readonly columns: number | undefined;
// (undocumented)
readonly isTTY: boolean;
// (undocumented)
readonly supportsColor: boolean;
}

// @beta
export interface IMapWorkspaceInvalidationsOptions {
// (undocumented)
Expand Down Expand Up @@ -87,6 +165,34 @@ export interface IRequestSchedulerAcquireOptions {
waitTimeoutMs?: number;
}

// @beta
export interface IResolvedGlobalCommandRequest {
// (undocumented)
readonly commandName: string;
// (undocumented)
readonly cwd: string;
// (undocumented)
readonly environment: IGlobalCommandEnvironment;
// (undocumented)
readonly requestId: string;
// (undocumented)
readonly terminal: IGlobalCommandTerminalProperties;
}

// @beta
export interface IResolveGlobalCommandRequestOptions {
// (undocumented)
readonly commandName: string;
// (undocumented)
readonly cwd: string;
// (undocumented)
readonly environment: Readonly<NodeJS.ProcessEnv>;
// (undocumented)
readonly requestId: string;
// (undocumented)
readonly terminal: IGlobalCommandTerminalProperties;
}

// @beta
export interface IRushDaemonHostOptions {
readonly createWorkspaceSessionAsync?: WorkspaceSessionFactory;
Expand Down
16 changes: 16 additions & 0 deletions libraries/rush-daemon/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,19 @@ This layer deliberately does not add control-frame admission or reconstruct `Pha
initialization. The typed phased request contract begins after an integration has produced a validated selection for
the exact warm engine shape; full command parsing remains blocked by
[rushstack#5895](https://github.com/microsoft/rushstack/issues/5895).

`GlobalCommandRequestRouter` is the corresponding opt-in boundary for caller-resolved global command logic. It
canonicalizes and confines the request working directory to the workspace, snapshots its environment, creates a
request-scoped terminal with explicit columns/color/TTY properties, and tracks child processes and async resources
through cancellation or disconnect. Concurrent requests never change `process.cwd()`, `process.env`, or daemon
stdin/stdout/stderr; child commands receive cwd, environment, cancellation, and output routing through the injected
execution context.
Executors must cooperatively observe the context abort signal and settle before cancellation completes, ensuring no
caller-owned logic can outlive its request resources.

The existing `RushCommandLineParser`, `BaseRushAction`, and some built-in/global action helpers still consult or mutate
process-global state. This layer therefore does not pretend that arbitrary existing actions are daemon-safe: the
integration must supply already resolved command logic that consumes `IGlobalCommandExecutionContext`, including
`spawnChild()` for command-local subprocesses. Adapting the complete action surface remains bounded by the open
[rushstack#5895](https://github.com/microsoft/rushstack/issues/5895) engine/action prerequisite work. Exit-code policy,
interactive stdin/raw-mode/PTY support, scheduling classification, and shared-build merging belong to later layers.
Loading