Skip to content

Latest commit

 

History

History

@rushstack/rush-daemon

The long-lived Rush workspace daemon host, including workspace-keyed listener bootstrap, protocol handshake and liveness control, a warm WorkspaceSession, and explicit serve/shutdown lifecycle APIs.

The package provides an opt-in rushd executable. Run it from a Rush workspace to start the host for the nearest rush.json; it does not change the default behavior of rush, rushx, or rush-pnpm.

The host loads RushConfiguration once before signaling readiness and keeps a headless file watcher active for the daemon lifetime. Its invalidation tracker retains changes while no clients are connected so a later request can reconcile them. The tracker starts with a conservative unknown invalidation covering session startup, and excessive distinct paths are compacted into the same full-workspace signal.

WorkspaceEngineComponentFactory provides the opt-in seam for a command integration to supply a real all-project operation graph, its RushSession, and a refreshable inputs snapshot. The integration must declare the complete phase and plugin shape because Rush plugins can currently vary that shape by command. The factory validates graph ownership, serializes retained invalidation reconciliation, and maps path-specific changes through the integration. The engine owner must supply one deterministic async disposer because IOperationGraph does not yet expose an operation that both stops the lifetime and awaits runner cleanup. After the initial conservative startup reconciliation, changes to Rush configuration, project package manifests, or integration-classified plugin graph inputs fail closed with WorkspaceEngineRecreationRequiredError before the input baseline advances or the invalidation is acknowledged. The startup watcher-registration boundary has no paths to classify and therefore remains a full invalidation. The routing layer must replace the complete workspace session rather than run a stale graph. The default daemon executable does not construct or route this graph while the command-independent plugin shape and per-iteration runner lifetime tracked by rushstack#5895 remain incomplete.

PhasedRequestRouter is the opt-in execution boundary once an integration has supplied that real warm graph. The integration parses the command and supplies its built-in/custom origin, an explicit phase/plugin shape, and operation enabled-state selection; the router validates both, reconciles retained invalidations, applies the selection with IOperationGraph.setEnabledStates, and runs at most one scheduled iteration. A workspace-wide RequestScheduler admits phased and global routes using the static built-in command policy (SHARED-BUILD, SHARED-READ, or EXCLUSIVE); custom-origin commands and unknown built-in names fail closed to EXCLUSIVE, including plugin replacements of built-in names. Queued clients receive ordered, one-based position controls and can request fail-fast or bounded waiting. One absolute deadline and progress channel cover both workspace admission and the temporary phased graph-execution gate. Cancellation, disconnect, or queue-output failure removes queued work before it can execute. A requesting client receives only its enabled dependency closure's WS1 raw chunks and structured events through backpressured, ordered callbacks, followed exactly once by a typed final command result after all preceding output drains. The result translates only that client's operation subset to Rush's success, warning, failure, or abort exit semantics. Warning-only builds honor the operation's configured allowWarningsInSuccessfulBuild state plus the request's immutable RUSH_ALLOW_WARNINGS_IN_SUCCESSFUL_BUILD environment override without mutating process.env. Compatible phased SHARED-BUILD requests admitted before the next graph iteration starts are coalesced at a deterministic event-loop-turn boundary. The router reconciles retained invalidations once, unions the clients' enabled dependency closures, and schedules one iteration. Shared operations execute once, while each client subscribes only to its own closure and derives its final result only from that subset. Requests admitted after scheduling starts form a later batch. Cancelling or disconnecting one client removes its subscription without aborting work needed by other clients; the graph iteration is aborted only after every client in that batch has stopped needing it.

This layer deliberately does not reconstruct PhasedScriptAction command/plugin 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.

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. Executors return their command exit code; the router preserves that code, translates thrown or cleanup failures to Rush's failure exit code, drains terminal output, and delivers one final result.

RushDaemonHost now owns one DaemonRequestDispatcher for the complete warm workspace lifecycle and passes it to every DaemonControlSession. After hello and capability subscription, each connection validates unique request identifiers, accepts presentation-free request envelopes, routes request-tagged stdin and cancellation, and serializes queue progress, raw-mode controls, binary output, structured events, and the terminal result through one backpressured wire queue. A connection runs at most one request at a time so binary operation output remains unambiguous; concurrent requests use separate connections. Each connection accepts at most 256 distinct request identifiers before the client must reconnect, allowing the lifecycle and stdin routers to retain every identifier for deterministic duplicate and late-frame handling without unbounded growth. Disconnect and host shutdown abort every connection-owned active or queued request before the resolver and warm workspace are disposed. Separate connections still share the workspace scheduler and phased batch coordinator, so compatible selections can execute in one iteration.

The dispatcher accepts an integration-owned IDaemonRequestResolver that maps the validated envelope to the existing typed phased request or isolated global executor contracts. Resolvers receive the request abort signal and must settle when cancellation, disconnect, or host shutdown aborts it. Without that resolver, the standalone executable continues to start, answer ping, and reject request execution with the typed unsupported outcome; it never constructs an empty graph or reports a false success. A retained invalidation that throws WorkspaceEngineRecreationRequiredError is reported as workspaceRecreationRequired before scheduling. Replacing the warm session is intentionally deferred to WS3.

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 engine/action prerequisite work. InteractiveRequestInputRouter supplies the opt-in WS2.7 boundary for connection-scoped input. The WS1 stdin frame carries a request identifier plus untouched raw bytes; frames are serialized per request through an injected sink while separate requests remain isolated. Global command integrations can bind that sink directly to a spawned child process. Both global and phased routes stop accepting input on abort/disconnect and await input drain plus an acknowledged cooked-mode restoration before publishing the exact-once command result. The daemon never reads or mutates its own stdin or raw-mode state.

Terminal width remains the immutable request-start value established by WS2.5. The thin client owns resize and rendering, so this layer does not forward SIGWINCH. Commands declaring a real controlling-terminal requirement receive a typed requiresInProcess policy result and are not executed by rushd; no pseudo-terminal is allocated or emulated. The future WS4 client will perform the actual in-process fallback and parse --no-wait / --wait-timeout.