From c69cc287f29df2634ea0f4ddf48b1d852275547a Mon Sep 17 00:00:00 2001 From: Will Fuqua Date: Sun, 28 Jun 2026 16:16:56 +0700 Subject: [PATCH] Breaking Change: Rename CLI option 'inspect' to 'connect' --- .claude/skills/csharp-eval/SKILL.md | 12 +- .../SKILL.md | 56 ++++---- AGENTS.md | 38 +++--- ARCHITECTURE.md | 48 +++---- CHANGELOG.md | 4 + .../CSharpRepl.Services.csproj | 2 +- .../ReplaceMethodCompletionProvider.cs | 8 +- CSharpRepl.Services/Configuration.cs | 14 +- CSharpRepl.Services/PlainText.cs | 2 +- ...cessor.cs => ConnectorCommandProcessor.cs} | 48 +++---- ...andResult.cs => ConnectorCommandResult.cs} | 16 +-- ...pectorCommands.cs => ConnectorCommands.cs} | 18 +-- ...{InspectorClient.cs => ConnectorClient.cs} | 28 ++-- .../Remote/RemoteEditorContext.cs | 14 +- CSharpRepl.Services/Remote/RemoteSession.cs | 14 +- .../Remote/RemoteValueRenderer.cs | 4 +- .../References/AssemblyReferenceReadme.md | 8 +- .../Roslyn/References/ReplAssemblyLoader.cs | 2 +- CSharpRepl.Services/Roslyn/RoslynServices.cs | 20 +-- .../Roslyn/Scripting/ScriptRunner.cs | 2 +- .../Roslyn/WorkspaceManager.cs | 2 +- CSharpRepl/CSharpRepl.csproj | 16 +-- CSharpRepl/CSharpReplPromptCallbacks.cs | 20 +-- CSharpRepl/Commands/CommandLine.cs | 120 +++++++++--------- CSharpRepl/Commands/ShellDetector.cs | 2 +- CSharpRepl/Program.cs | 30 ++--- ...er.cs => ConnectorCommandResultPrinter.cs} | 24 ++-- CSharpRepl/Repls/RemotePipedInputEvaluator.cs | 16 +-- CSharpRepl/Repls/RemoteReadEvalPrintLoop.cs | 24 ++-- .../CSharpRepl.InjectedHook.Contracts.csproj | 2 +- ...nspectorGlobals.cs => ConnectorGlobals.cs} | 8 +- ...ctorTransport.cs => ConnectorTransport.cs} | 24 ++-- ...InspectorEngine.cs => IConnectorEngine.cs} | 2 +- .../MessageChannel.cs | 8 +- .../WireMessages.cs | 10 +- ...{InspectorEngine.cs => ConnectorEngine.cs} | 12 +- ...nspectorPatcher.cs => ConnectorPatcher.cs} | 2 +- ...{InspectorServer.cs => ConnectorServer.cs} | 34 ++--- .../CSharpRepl.InjectedHook/EngineHost.cs | 12 +- .../CSharpRepl.InjectedHook/HostCapture.cs | 8 +- .../CSharpRepl.InjectedHook/StartupHook.cs | 6 +- InjectedHook/InjectedHookReadme.md | 18 +-- README.md | 20 +-- .../CSharpRepl.Tests/CSharpRepl.Tests.csproj | 6 +- Tests/CSharpRepl.Tests/CommandLineTests.cs | 80 ++++++------ Tests/CSharpRepl.Tests/CompletionTests.cs | 16 +-- ...Tests.cs => ConnectorCancellationTests.cs} | 4 +- ...eryTests.cs => ConnectorDiscoveryTests.cs} | 30 ++--- ...EngineTests.cs => ConnectorEngineTests.cs} | 14 +- ...ripTests.cs => ConnectorRoundTripTests.cs} | 14 +- ...sts.cs => ConnectorServerProtocolTests.cs} | 16 +-- ...TestSupport.cs => ConnectorTestSupport.cs} | 24 ++-- ...ortTests.cs => ConnectorTransportTests.cs} | 18 +-- .../CSharpRepl.InjectedHook.TestTarget.csproj | 6 +- .../Program.cs | 12 +- .../Data/Directory.Packages.props | 2 +- .../RemoteEditorServicesTests.cs | 16 +-- .../RemotePipedInputEvaluatorTests.cs | 8 +- .../RemoteReadEvalPrintLoopTests.cs | 16 +-- .../RemoteValueRendererTests.cs | 2 +- 60 files changed, 533 insertions(+), 529 deletions(-) rename .claude/skills/{csharprepl-inspect => csharprepl-connect}/SKILL.md (59%) rename CSharpRepl.Services/Remote/Commands/{InspectorCommandProcessor.cs => ConnectorCommandProcessor.cs} (66%) rename CSharpRepl.Services/Remote/Commands/{InspectorCommandResult.cs => ConnectorCommandResult.cs} (61%) rename CSharpRepl.Services/Remote/Commands/{InspectorCommands.cs => ConnectorCommands.cs} (77%) rename CSharpRepl.Services/Remote/{InspectorClient.cs => ConnectorClient.cs} (86%) rename CSharpRepl/Repls/Common/{InspectorCommandResultPrinter.cs => ConnectorCommandResultPrinter.cs} (71%) rename InjectedHook/CSharpRepl.InjectedHook.Contracts/{InspectorGlobals.cs => ConnectorGlobals.cs} (90%) rename InjectedHook/CSharpRepl.InjectedHook.Contracts/{InspectorTransport.cs => ConnectorTransport.cs} (95%) rename InjectedHook/CSharpRepl.InjectedHook.Contracts/{IInspectorEngine.cs => IConnectorEngine.cs} (98%) rename InjectedHook/CSharpRepl.InjectedHook.ScriptEngine/{InspectorEngine.cs => ConnectorEngine.cs} (98%) rename InjectedHook/CSharpRepl.InjectedHook.ScriptEngine/{InspectorPatcher.cs => ConnectorPatcher.cs} (99%) rename InjectedHook/CSharpRepl.InjectedHook/{InspectorServer.cs => ConnectorServer.cs} (92%) rename Tests/CSharpRepl.Tests/{InspectorCancellationTests.cs => ConnectorCancellationTests.cs} (96%) rename Tests/CSharpRepl.Tests/{InspectorDiscoveryTests.cs => ConnectorDiscoveryTests.cs} (78%) rename Tests/CSharpRepl.Tests/{InspectorEngineTests.cs => ConnectorEngineTests.cs} (98%) rename Tests/CSharpRepl.Tests/{InspectorRoundTripTests.cs => ConnectorRoundTripTests.cs} (96%) rename Tests/CSharpRepl.Tests/{InspectorServerProtocolTests.cs => ConnectorServerProtocolTests.cs} (91%) rename Tests/CSharpRepl.Tests/{InspectorTestSupport.cs => ConnectorTestSupport.cs} (79%) rename Tests/CSharpRepl.Tests/{InspectorTransportTests.cs => ConnectorTransportTests.cs} (95%) diff --git a/.claude/skills/csharp-eval/SKILL.md b/.claude/skills/csharp-eval/SKILL.md index 19a60ecb..aff36c37 100644 --- a/.claude/skills/csharp-eval/SKILL.md +++ b/.claude/skills/csharp-eval/SKILL.md @@ -1,6 +1,6 @@ --- name: csharp-eval -description: Run / execute C# snippets non-interactively with the csharprepl CLI to observe real runtime behavior — return values, exceptions, serialized output — and to probe how a NuGet package actually behaves when called. The complement to dotnet-inspect; that tool inspects static API surface without executing; this one runs code. Use whenever you need to know what C# *does*, not just what an API *looks like*. (To evaluate C# *inside* a running process and read its live state, see the csharprepl-inspect skill.) +description: Run / execute C# snippets non-interactively with the csharprepl CLI to observe real runtime behavior — return values, exceptions, serialized output — and to probe how a NuGet package actually behaves when called. The complement to dotnet-inspect; that tool inspects static API surface without executing; this one runs code. Use whenever you need to know what C# *does*, not just what an API *looks like*. (To evaluate C# *inside* a running process and read its live state, see the csharprepl-connect skill.) --- # csharp-eval @@ -75,15 +75,15 @@ csharprepl -e 'JsonConvert.SerializeObject(new[] { 1, 2, 3 })' -r 'nuget: Newton ## Evaluating inside a running process The same CLI can attach to a *separate, already-running* .NET process and evaluate C# **inside it**, against -its live state (`csharprepl inspect `). That's a distinct workflow (the target must be launched with the -inspector enabled, state persists across calls, and you can detour live methods) with its own safety caveats -— see the **csharprepl-inspect** skill. +its live state (`csharprepl connect `). That's a distinct workflow (the target must be launched with the +connector enabled, state persists across calls, and you can detour live methods) with its own safety caveats +— see the **csharprepl-connect** skill. ## Gotchas - **No state across calls.** Each invocation is a fresh process — variables, `using`s, and references do not - carry over between runs. Make every snippet self-contained (include its own `#r` / `using`). (Inspect mode - is the exception — see the csharprepl-inspect skill.) + carry over between runs. Make every snippet self-contained (include its own `#r` / `using`). (Connect mode + is the exception — see the csharprepl-connect skill.) - **First restore is slow.** The first time a package is referenced it's downloaded; later runs are fast (cached under `~/.csharprepl/packages`). - **Errors go to stderr with a nonzero exit code.** Compilation and runtime errors are written to diff --git a/.claude/skills/csharprepl-inspect/SKILL.md b/.claude/skills/csharprepl-connect/SKILL.md similarity index 59% rename from .claude/skills/csharprepl-inspect/SKILL.md rename to .claude/skills/csharprepl-connect/SKILL.md index 9e186d3d..9603da51 100644 --- a/.claude/skills/csharprepl-inspect/SKILL.md +++ b/.claude/skills/csharprepl-connect/SKILL.md @@ -1,64 +1,64 @@ --- -name: csharprepl-inspect -description: Attach to a running, inspector-enabled .NET process with `csharprepl inspect ` and evaluate C# *inside* it — read and modify its live objects, statics, and DI services, and detour live methods (#replace/#wrap). Use to debug or probe a real running app's in-memory state, not static API surface (that's dotnet-inspect) or a throwaway snippet in a fresh process (that's csharp-eval). Dev/diagnostics only — code runs with the target's full privileges, never point it at production. +name: csharprepl-connect +description: Connect to a running, connector-enabled .NET process with `csharprepl connect ` and evaluate C# *inside* it — read and modify its live objects, statics, and DI services, and detour live methods (#replace/#wrap). Use to debug or probe a real running app's in-memory state, not static API surface (that's dotnet-inspect) or a throwaway snippet in a fresh process (that's csharp-eval). Dev/diagnostics only — code runs with the target's full privileges, never point it at production. --- -# csharprepl-inspect +# csharprepl-connect Evaluate C# **inside a separate, already-running .NET process** and see/modify its live state. `csharprepl` -injects a real Roslyn engine into a target you launched with the inspector enabled; you then send code +injects a real Roslyn engine into a target you launched with the connector enabled; you then send code non-interactively (same flags and output as the local REPL) and it runs in that process against its actual in-memory objects. ## When to use this vs. csharp-eval vs. dotnet-inspect - **"What's the live state of my running app?"** / **"Change a method's behavior in the running process"** - → **this skill** (`csharprepl inspect `). Code runs *inside* the target. + → **this skill** (`csharprepl connect `). Code runs *inside* the target. - **"What does this code do?"** (a self-contained snippet, fresh throwaway process) → **csharp-eval**. - **"What does this API look like?"** (signatures, members, docs — no execution) → **dotnet-inspect**. The eval mechanics here — `-e` / `--eval-file`, piped stdin, quoting, `-r "nuget: ..."`, the clean-stdout / errors-to-stderr / nonzero-exit contract — are **identical to csharp-eval**; see that skill for those -details. This skill covers only what's different about attaching to a live process. +details. This skill covers only what's different about connecting to a live process. ## ⚠️ Safety Evaluated code runs with the **target process's full privileges** — it's RCE-equivalent for same-user code. -Only attach to a process **you control** for development/diagnostics. **Never enable the inspector on, or -attach to, a production process.** +Only connect to a process **you control** for development/diagnostics. **Never enable the connector on, or +connect to, a production process.** ## 1. Enable the target (one-time, at launch) -The target only accepts connections if it was *started* with the inspector hook — you cannot enable an -already-running process. `inspect init` prints the env vars to set in the shell that launches it: +The target only accepts connections if it was *started* with the connector hook — you cannot enable an +already-running process. `connect init` prints the env vars to set in the shell that launches it: ``` -csharprepl inspect init # prints DOTNET_STARTUP_HOOKS=... and ASPNETCORE_HOSTINGSTARTUPASSEMBLIES=... +csharprepl connect init # prints DOTNET_STARTUP_HOOKS=... and ASPNETCORE_HOSTINGSTARTUPASSEMBLIES=... # auto-detects your shell; override with --shell bash|pwsh|cmd|fish ``` Set those env vars in the launching shell only (not system- or user-wide), then start the app normally -(e.g. `dotnet run`). It's now attachable for the life of that process. +(e.g. `dotnet run`). It's now connectable for the life of that process. -## 2. Attach and evaluate +## 2. Connect and evaluate ``` -csharprepl inspect list # list inspector-enabled processes + their PIDs -csharprepl inspect -e 'System.Environment.ProcessId' # -> the target's PID; confirms code runs in the target -csharprepl inspect --eval-file probe.csx # multi-line, same as local -echo 'SomeApp.Program.SomeStatic' | csharprepl inspect # piped stdin works too +csharprepl connect list # list connector-enabled processes + their PIDs +csharprepl connect -e 'System.Environment.ProcessId' # -> the target's PID; confirms code runs in the target +csharprepl connect --eval-file probe.csx # multi-line, same as local +echo 'SomeApp.Program.SomeStatic' | csharprepl connect # piped stdin works too ``` - **Reach the target's state** by fully-qualified name (`MyApp.Program.SomeStatic`), or, when its DI provider was captured, via `services.GetRequiredService()` / `Get()` (the connect banner reports whether the DI provider was captured). - **State persists across calls** (unlike local `csharp-eval`, where each run is a fresh process): the target - holds the script-state chain, so a `var` declared in one `inspect -e` invocation is usable in the + holds the script-state chain, so a `var` declared in one `connect -e` invocation is usable in the next. This lets you build up state with one-shot calls. ## 3. Live method replacement -While attached you can detour a live method to a REPL-defined delegate, changing the running app's behavior +While connected you can detour a live method to a REPL-defined delegate, changing the running app's behavior immediately: - `#replace with ` — swap the implementation. @@ -69,27 +69,27 @@ Instance methods take the instance as the first delegate parameter; a static met helper in one call, then `#replace` in the next — they share state across calls: ``` -csharprepl inspect -e 'decimal Half(MyApp.OrderService svc, int qty, decimal unit) => qty * unit * 0.5m;' -csharprepl inspect -e '#replace MyApp.OrderService.CalculatePrice with Half' -csharprepl inspect -e '#patches' # list active patches -csharprepl inspect -e '#revert all' # undo them +csharprepl connect -e 'decimal Half(MyApp.OrderService svc, int qty, decimal unit) => qty * unit * 0.5m;' +csharprepl connect -e '#replace MyApp.OrderService.CalculatePrice with Half' +csharprepl connect -e '#patches' # list active patches +csharprepl connect -e '#revert all' # undo them ``` - A command (`#replace`/`#wrap`/`#patches`/`#revert`) must be the **whole** submission — don't combine a definition and a command in one `-e` or one piped block, since collected stdin is sent as a single C# submission (so `#replace` would be compiled as invalid C#). To do it in one pipe, use `--streamPipedInput`, which evaluates line by line. -- Patches **persist in the target until reverted** (or it exits) — they outlive your detach, so revert when +- Patches **persist in the target until reverted** (or it exits) — they outlive your disconnect, so revert when done. - Not supported: generic methods, pointer params, and `#wrap` with by-ref parameters. ## Gotchas -- **Can't attach if not enabled.** `inspect ` fails unless the target was launched with the env vars - from `inspect init` (step 1). Use `inspect list` to see what's actually attachable. +- **Can't connect if not enabled.** `connect ` fails unless the target was launched with the env vars + from `connect init` (step 1). Use `connect list` to see what's actually connectable. - **Self-contained single-file targets are rejected** — their assemblies are bundled in memory with no - on-disk path, so the engine can't compile against them. Inspect a framework-dependent build instead. (A + on-disk path, so the engine can't compile against them. Connect to a framework-dependent build instead. (A *framework-dependent* single-file app connects but can only reach its own types via reflection.) -- **Detach leaves the target running.** `inspect` exits cleanly; the process keeps going and you can +- **Disconnecting leaves the target running.** `connect` exits cleanly; the process keeps going and you can reconnect — but any patches you applied stay in effect until reverted. - Everything else (quoting, NuGet refs, errors→stderr, exit codes) works as in **csharp-eval**. diff --git a/AGENTS.md b/AGENTS.md index e7ac86e3..f86c5ee1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -31,7 +31,7 @@ The test runner is **Microsoft.Testing.Platform** with the **xUnit v3** runner ( - Heavy Roslyn/integration tests share `[Collection(nameof(RoslynServices))]` and run **serially** on purpose: the loader's `AssemblyLoadContext.Resolving` hooks (attached to the process-global Default ALC by `AssemblyLoadContextHook` and never detached) are process-global, not per-`RoslynServices`. The full suite is ~2 minutes. (`MSBuildLocator.RegisterDefaults()` is also process-global, but a `[ModuleInitializer]` in `TestAssemblyInitializer` runs it once at assembly load, so it's no longer the reason for the collection — and tests that only needed MSBuildLocator, like `NugetPackageInstallerTests`, can now run in isolation.) - Some tests spawn `dotnet build` / MSBuild subprocesses (solution/project references) and a few touch the network (NuGet install). These are the slow ones and can occasionally be flaky. -- The inspect-feature integration tests (`InspectorRoundTripTests`, `InspectorCancellationTests`, `RemoteEditorServicesTests`, `InspectorServerProtocolTests`, `RemoteReadEvalPrintLoopTests`, `RemotePipedInputEvaluatorTests`) **launch a real hooked child process** — the interactive PrettyPrompt loop itself cannot be driven without a TTY, so `RemoteReadEvalPrintLoopTests` stubs `IPrompt` (like `ReadEvalPrintLoopTests`) and everything below it is real. The **non-interactive** inspect path (`inspect --eval`/`--eval-file`/piped stdin) needs no TTY, so `RemotePipedInputEvaluatorTests` drives the real `RemotePipedInputEvaluator` against a real hooked child directly. Two more inspect suites run in-process without a child: `InspectorEngineTests` hosts the real engine + Roslyn inside the test process, and `InspectorTransportTests` exercises the real OS pipe/socket transport (note: the Windows pipe uses zero-byte buffers, so a write rendezvouses with the peer's read — keep the read pending while writing). +- The connect-feature integration tests (`ConnectorRoundTripTests`, `ConnectorCancellationTests`, `RemoteEditorServicesTests`, `ConnectorServerProtocolTests`, `RemoteReadEvalPrintLoopTests`, `RemotePipedInputEvaluatorTests`) **launch a real hooked child process** — the interactive PrettyPrompt loop itself cannot be driven without a TTY, so `RemoteReadEvalPrintLoopTests` stubs `IPrompt` (like `ReadEvalPrintLoopTests`) and everything below it is real. The **non-interactive** connect path (`connect --eval`/`--eval-file`/piped stdin) needs no TTY, so `RemotePipedInputEvaluatorTests` drives the real `RemotePipedInputEvaluator` against a real hooked child directly. Two more connect suites run in-process without a child: `ConnectorEngineTests` hosts the real engine + Roslyn inside the test process, and `ConnectorTransportTests` exercises the real OS pipe/socket transport (note: the Windows pipe uses zero-byte buffers, so a write rendezvouses with the peer's read — keep the read pending while writing). ### Benchmarks @@ -44,10 +44,10 @@ dotnet run -c Release --project Tests/CSharpRepl.Benchmarks -- --filter *Allocat ## Solution layout - **`CSharpRepl/`** — the executable / global tool. `Program.cs` (CLI args, help, the read-eval-print loop), `CommandLine.cs` (argument parsing), `CSharpReplPromptCallbacks.cs` (wires PrettyPrompt callbacks to Roslyn services), `ReadEvalPrintLoop.cs`. -- **`CSharpRepl.Services/`** — the bulk of the logic. `Roslyn/` (scripting + workspace, see below), `Completion/`, `SyntaxHighlighting/`, `Theming/`, `Nuget/`, `SymbolExploration/` (Source Link), `CodeTransformation/` (IL disassembly + ILSpy lowering), and `Remote/` (controller side of the inspect feature). -- **`InjectedHook/`** — the three projects for the "inspect a running process" feature (see below). +- **`CSharpRepl.Services/`** — the bulk of the logic. `Roslyn/` (scripting + workspace, see below), `Completion/`, `SyntaxHighlighting/`, `Theming/`, `Nuget/`, `SymbolExploration/` (Source Link), `CodeTransformation/` (IL disassembly + ILSpy lowering), and `Remote/` (controller side of the connect feature). +- **`InjectedHook/`** — the three projects for the "connect a running process" feature (see below). - **`Tests/`** — `CSharpRepl.Tests` and `CSharpRepl.Benchmarks`. -- **`ARCHITECTURE.md`** — the authoritative deep-dive on design, including sequence/class diagrams for the inspect feature. Read it before substantial changes. +- **`ARCHITECTURE.md`** — the authoritative deep-dive on design, including sequence/class diagrams for the connect feature. Read it before substantial changes. - **`CSharpRepl.Services/Roslyn/References/AssemblyReferenceReadme.md`** — the authoritative deep-dive on the `#r`/NuGet/assembly-load-context machinery: compile-time-vs-run-time resolution, reference-vs-implementation assemblies, NuGet restore + version unification, and the dedicated load context. Read it before touching reference/NuGet/loading code. ## Core architecture @@ -65,52 +65,52 @@ csharprepl is an intermediary between **Roslyn** and **PrettyPrompt**. The singl Per-keystroke latency is dominated by syntax highlighting, which historically scaled linearly with submission count. The fix lives in `WorkspaceManager.UpdateCurrentDocumentAsync`: after setting the current document it `await`s `GetCompilationAsync()` and **discards the result**, forcing Roslyn's compilation tracker to its strongly-held final state so per-keystroke forks reuse it. Removing that line reintroduces an O(depth) regression — don't. -## Inspect-a-running-process feature (`InjectedHook/` + `CSharpRepl.Services/Remote/`) +## Connect-a-running-process feature (`InjectedHook/` + `CSharpRepl.Services/Remote/`) -csharprepl can attach to a *separate*, already-running .NET app and evaluate C# inside it, reading/writing its live state with full local-REPL parity. CLI: `csharprepl inspect init` (prints the env vars to launch the target with) then `csharprepl inspect `. It is **cooperative** (a real Roslyn engine is injected via a `DOTNET_STARTUP_HOOKS` startup hook — not a debugger) and **opt-in** only. +csharprepl can attach to a *separate*, already-running .NET app and evaluate C# inside it, reading/writing its live state with full local-REPL parity. CLI: `csharprepl connect init` (prints the env vars to launch the target with) then `csharprepl connect `. It is **cooperative** (a real Roslyn engine is injected via a `DOTNET_STARTUP_HOOKS` startup hook — not a debugger) and **opt-in** only. ### Naming — read this before searching The feature was renamed during development; **names are inconsistent across layers**, so search by the right token: - **Folder / projects:** `InjectedHook/` containing `CSharpRepl.InjectedHook`, `CSharpRepl.InjectedHook.Contracts`, and `CSharpRepl.InjectedHook.ScriptEngine`. - - Note: earlier planning docs use `CSharpRepl.Inspector.*` or `CSharpRepl.InjectedHook.Engine`; those names no longer exist. -- **Classes:** still prefixed **`Inspector*`** (`InspectorServer`, `InspectorEngine`, `IInspectorEngine`, `InspectorClient`, `InspectorTransport`, `InspectorRoots`, `InspectorGlobals`). -- **User-facing verb:** **`inspect`**. + - Note: earlier planning docs use `CSharpRepl.Connector.*` or `CSharpRepl.InjectedHook.Engine`; those names no longer exist. +- **Classes:** still prefixed **`Connector*`** (`ConnectorServer`, `ConnectorEngine`, `IConnectorEngine`, `ConnectorClient`, `ConnectorTransport`, `ConnectorRoots`, `ConnectorGlobals`). +- **User-facing verb:** **`connect`**. ### The three injected projects and their assembly-load-context (ALC) roles This is the crux of the design — the target may already load its own Roslyn, so the injected Roslyn must be isolated: -- **`CSharpRepl.InjectedHook`** (bootstrap) — injected into the target's **default ALC**. References **no Roslyn**. `StartupHook.Initialize()` (no namespace, `public static void`, must never throw and runs before the target's `Main`) installs an `AssemblyLoadContext.Default.Resolving` handler, creates the isolated engine ALC (`EngineHost.cs`), and starts the transport server (`InspectorServer.cs`). -- **`CSharpRepl.InjectedHook.ScriptEngine`** — loaded into a dedicated **isolated ALC** with its own Roslyn closure (`Microsoft.CodeAnalysis.CSharp.Scripting`). `InspectorEngine.cs` hosts `CSharpScript`, builds compilation references **lazily on first eval** from the *target's* loaded assemblies (so submissions bind to the target's real live objects), and projects results into a serializable `RemoteValue` tree. -- **`CSharpRepl.InjectedHook.Contracts`** — the shared boundary, loaded **once** in the default ALC and resolved to that same instance from the isolated ALC, so these types are **type-identical across the boundary**: `IInspectorEngine`, `InspectorGlobals`/`InspectorRoots`, `RemoteValue`, the `WireMessages` hierarchy, and `InspectorTransport`/`MessageChannel`. Also references no Roslyn. +- **`CSharpRepl.InjectedHook`** (bootstrap) — injected into the target's **default ALC**. References **no Roslyn**. `StartupHook.Initialize()` (no namespace, `public static void`, must never throw and runs before the target's `Main`) installs an `AssemblyLoadContext.Default.Resolving` handler, creates the isolated engine ALC (`EngineHost.cs`), and starts the transport server (`ConnectorServer.cs`). +- **`CSharpRepl.InjectedHook.ScriptEngine`** — loaded into a dedicated **isolated ALC** with its own Roslyn closure (`Microsoft.CodeAnalysis.CSharp.Scripting`). `ConnectorEngine.cs` hosts `CSharpScript`, builds compilation references **lazily on first eval** from the *target's* loaded assemblies (so submissions bind to the target's real live objects), and projects results into a serializable `RemoteValue` tree. +- **`CSharpRepl.InjectedHook.Contracts`** — the shared boundary, loaded **once** in the default ALC and resolved to that same instance from the isolated ALC, so these types are **type-identical across the boundary**: `IConnectorEngine`, `ConnectorGlobals`/`ConnectorRoots`, `RemoteValue`, the `WireMessages` hierarchy, and `ConnectorTransport`/`MessageChannel`. Also references no Roslyn. ### Controller side (`CSharpRepl.Services/Remote/` + `CSharpRepl/RemoteReadEvalPrintLoop.cs`) -When attached, csharprepl is a thin **controller**: it compiles nothing for evaluation, sends code strings, and renders the returned `RemoteValue` through the *same* theme/formatting pipeline as local output (`RemoteValueRenderer`). The **scripting world lives in the target** (the engine), but the **workspace world (completion/highlighting) stays in the controller** against a second, remote-configured `RoslynServices` seeded with the target's assembly paths + `InspectorGlobals` — so editor features need no per-keystroke round-trip. The controller advances that remote workspace only when an `EvalResponse` reports `Committed == true`. +When attached, csharprepl is a thin **controller**: it compiles nothing for evaluation, sends code strings, and renders the returned `RemoteValue` through the *same* theme/formatting pipeline as local output (`RemoteValueRenderer`). The **scripting world lives in the target** (the engine), but the **workspace world (completion/highlighting) stays in the controller** against a second, remote-configured `RoslynServices` seeded with the target's assembly paths + `ConnectorGlobals` — so editor features need no per-keystroke round-trip. The controller advances that remote workspace only when an `EvalResponse` reports `Committed == true`. -Inspect mode also runs **non-interactively** (so agents/scripts can use it without a TTY), mirroring the local REPL's `PipedInputEvaluator`: `inspect --eval`/`--eval-file` or piped stdin route to `RemotePipedInputEvaluator` instead of the interactive `RemoteReadEvalPrintLoop`. It evaluates against the same `RemoteSession`, auto-prints the final value as plain, uncolored, unwrapped text on stdout (errors to stderr, nonzero exit), and honors the same `#replace`/`#wrap`/`#patches`/`#revert` commands (the command-result wording is shared via `InspectorCommandResultPrinter`). It skips the editor-workspace seeding (completion/highlighting are interactive-only) and the connection chatter. Because the engine's state chain lives in the target, separate one-shot `--eval` invocations **accumulate state** across reconnects. +Connect mode also runs **non-interactively** (so agents/scripts can use it without a TTY), mirroring the local REPL's `PipedInputEvaluator`: `connect --eval`/`--eval-file` or piped stdin route to `RemotePipedInputEvaluator` instead of the interactive `RemoteReadEvalPrintLoop`. It evaluates against the same `RemoteSession`, auto-prints the final value as plain, uncolored, unwrapped text on stdout (errors to stderr, nonzero exit), and honors the same `#replace`/`#wrap`/`#patches`/`#revert` commands (the command-result wording is shared via `ConnectorCommandResultPrinter`). It skips the editor-workspace seeding (completion/highlighting are interactive-only) and the connection chatter. Because the engine's state chain lives in the target, separate one-shot `--eval` invocations **accumulate state** across reconnects. ### Wire protocol -A single duplex connection (named pipe on Windows, Unix domain socket elsewhere, **current-user only**). Every frame is a 4-byte little-endian length prefix + UTF-8 JSON body; messages are a `System.Text.Json` **polymorphic** `WireMessage` hierarchy keyed on a `$kind` discriminator (`MessageChannel`). Security model mirrors the .NET diagnostic port — OS access control, no secret. An inspector-enabled process is RCE-equivalent for same-user code and must never run in production. +A single duplex connection (named pipe on Windows, Unix domain socket elsewhere, **current-user only**). Every frame is a 4-byte little-endian length prefix + UTF-8 JSON body; messages are a `System.Text.Json` **polymorphic** `WireMessage` hierarchy keyed on a `$kind` discriminator (`MessageChannel`). Security model mirrors the .NET diagnostic port — OS access control, no secret. A connector-enabled process is RCE-equivalent for same-user code and must never run in production. ### Live method replacement `#replace`/`#wrap`/`#patches`/`#revert` (controller commands in `RemoteReadEvalPrintLoop`) detour a live method in the target to a REPL-defined delegate. -- Engine side: `InspectorPatcher` (in `CSharpRepl.InjectedHook.ScriptEngine`) over `MonoMod.RuntimeDetour`. Resolves the target by name, matches the overload from the delegate's signature, coerces a bare method group via a generated cast, applies a `Hook`, tracks it by id for revert. -- Wire (protocol v2): `ReplaceRequest`/`ReplaceResponse`, `PatchListRequest`/`PatchListResponse`, `RevertRequest`/`RevertResponse`, plus three `IInspectorEngine` methods served in `InspectorServer`. +- Engine side: `ConnectorPatcher` (in `CSharpRepl.InjectedHook.ScriptEngine`) over `MonoMod.RuntimeDetour`. Resolves the target by name, matches the overload from the delegate's signature, coerces a bare method group via a generated cast, applies a `Hook`, tracks it by id for revert. +- Wire (protocol v2): `ReplaceRequest`/`ReplaceResponse`, `PatchListRequest`/`PatchListResponse`, `RevertRequest`/`RevertResponse`, plus three `IConnectorEngine` methods served in `ConnectorServer`. - Replacement shape: instance methods take the instance as the first parameter; `#wrap` prepends an `orig` delegate the body can call. - Detours repoint native code, so they cross the engine-ALC/target-ALC boundary: the replacement compiles in the engine ALC, the target method lives in the default ALC. - `ref`/`out`/`in` work in Replace mode: `BuildCastDelegate` emits a delegate type carrying the modifiers (Func can't), which the engine prepends to the probe submission before the method-group cast. Not supported: generic methods, pointer parameters, Wrap + by-ref (the `orig` delegate can't be a shared type). - Limits: JIT-inlined call sites keep old behavior. Patches persist after detach until reverted. -- Tests: `InspectorEngineTests.ReplaceMethod_*` (in-process engine) and `RemoteReadEvalPrintLoopTests` (cross-process, real hooked child). +- Tests: `ConnectorEngineTests.ReplaceMethod_*` (in-process engine) and `RemoteReadEvalPrintLoopTests` (cross-process, real hooked child). ### Packaging -`CSharpRepl.csproj`'s `IncludeInspectorPayload` target stages the full inspector payload (bootstrap + contracts + engine + Roslyn closure + `MonoMod.RuntimeDetour` closure + `.deps.json`) into an **`inspector/` subdirectory** next to the tool (both on `dotnet run` and in the packed global tool), isolated so the engine's Roslyn never shadows the tool's. `inspect init` points `DOTNET_STARTUP_HOOKS` at `inspector/CSharpRepl.InjectedHook.dll`. The bootstrap is a `ProjectReference` with `ReferenceOutputAssembly="false"` (the tool must not link it). +`CSharpRepl.csproj`'s `IncludeConnectorPayload` target stages the full connector payload (bootstrap + contracts + engine + Roslyn closure + `MonoMod.RuntimeDetour` closure + `.deps.json`) into an **`connector/` subdirectory** next to the tool (both on `dotnet run` and in the packed global tool), isolated so the engine's Roslyn never shadows the tool's. `connect init` points `DOTNET_STARTUP_HOOKS` at `connector/CSharpRepl.InjectedHook.dll`. The bootstrap is a `ProjectReference` with `ReferenceOutputAssembly="false"` (the tool must not link it). ## Gotchas diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index d6782f46..612b28f5 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -38,7 +38,7 @@ As previously described, csharprepl serves as an intermediary between the above - Assembly reference management - Management of [Shared Frameworks](https://natemcmaster.com/blog/2017/12/21/netcore-primitives/), [implementation vs reference assemblies](https://docs.microsoft.com/en-us/dotnet/standard/assembly/reference-assemblies), and adding references dynamically are handled in the `AssemblyReferenceService.cs`. This class is called by our MetadataReferenceResolver implementation, which is an extension point provided by Roslyn. - This MetadataReferenceResolver is responsible for evaluating `#r` statements, and delegates for assembly references, nuget package installation, csproj/sln references, and shared framework loading. The implementation is in `CompositeMetadataReferenceResolver.cs` -## Process inspector (attaching to a running app) +## Process connector (attaching to a running app) In addition to the normal REPL — which evaluates code in csharprepl's *own* process — csharprepl can attach to a *separate*, already-running .NET application to evaluate expressions inside it and read/write its live state (statics, DI singletons). The design goal is full local-REPL parity against the other process: declare a `var` or a helper method on one line and reuse it on the next, with the values being the target's real, live objects. @@ -46,13 +46,13 @@ This is a *cooperative in-process* approach rather than a debugger. A debugger e ### Cooperative Model via Startup Hooks -The target's source is never modified. Attachment is opt-in via a runtime **startup hook** set through an environment variable when the app is launched: the hook runs before the app's entry point and brings up the inspector. No startup hook means there is nothing to connect to. Because connecting to the inspector is equivalent to running arbitrary code in the target with its privileges, the transport is a per-process, OS-access-controlled channel (a named pipe on Windows, a Unix domain socket elsewhere) scoped to the current user — the same security model as the .NET diagnostic port. An inspector-enabled process must never run in production. +The target's source is never modified. Attachment is opt-in via a runtime **startup hook** set through an environment variable when the app is launched: the hook runs before the app's entry point and brings up the connector. No startup hook means there is nothing to connect to. Because connecting to the connector is equivalent to running arbitrary code in the target with its privileges, the transport is a per-process, OS-access-controlled channel (a named pipe on Windows, a Unix domain socket elsewhere) scoped to the current user — the same security model as the .NET diagnostic port. A connector-enabled process must never run in production. -csharprepl drives this whole flow itself: one subcommand prints the exact environment variables to launch the app with (so the user doesn't hand-assemble them), and another attaches to a chosen process by its id. To make that work wherever csharprepl is installed, the inspector's pieces — the bootstrap, the engine, and the engine's private Roslyn closure — are packaged *alongside* the csharprepl tool in an isolated subdirectory, kept separate from the tool's own assemblies so neither set shadows the other. The printed startup-hook variable simply points at the bootstrap in that subdirectory. +csharprepl drives this whole flow itself: one subcommand prints the exact environment variables to launch the app with (so the user doesn't hand-assemble them), and another attaches to a chosen process by its id. To make that work wherever csharprepl is installed, the connector's pieces — the bootstrap, the engine, and the engine's private Roslyn closure — are packaged *alongside* the csharprepl tool in an isolated subdirectory, kept separate from the tool's own assemblies so neither set shadows the other. The printed startup-hook variable simply points at the bootstrap in that subdirectory. ### Assembly load context isolation -The hard problem is that the target may already use Roslyn itself (many apps do, transitively), at a different version. Loading the inspector's Roslyn naively would risk colliding with the target's. The design solves this with three projects mapped deliberately onto **assembly load contexts (ALCs)**: +The hard problem is that the target may already use Roslyn itself (many apps do, transitively), at a different version. Loading the connector's Roslyn naively would risk colliding with the target's. The design solves this with three projects mapped deliberately onto **assembly load contexts (ALCs)**: - **`CSharpRepl.InjectedHook`** (the bootstrap) is injected into the target's **default ALC**. It deliberately references *no* Roslyn, so nothing heavy leaks into the target's main context. Its job is plumbing: create an isolated context for the engine, run the transport server, and forward requests. - **`CSharpRepl.InjectedHook.ScriptEngine`** is loaded into a dedicated **isolated ALC** along with its own Roslyn closure. Because that Roslyn lives in its own context, it coexists with whatever Roslyn the target already loaded — neither disturbs the other. @@ -60,20 +60,20 @@ The hard problem is that the target may already use Roslyn itself (many apps do, The engine builds its compilation references from the **target's own loaded assemblies**, so submissions compile against the target's types and, at runtime, bind to the already-loaded live instances. The upshot: code typed in the REPL sees the target's real objects, and writes are observed by the target's own code. -### Controller and inspector: two halves of one REPL +### Controller and connector: two halves of one REPL When attached, the csharprepl process becomes a thin **controller**. It compiles nothing for evaluation; it sends code strings over the transport and renders the projected results that come back. Crucially, the engine ships only theme-agnostic data — the engine in the target does the work of turning a live object into a depth-limited, serializable projection, and the controller renders that projection through the *same* theme and value-formatting pipeline the local REPL uses, so remote output is styled identically to local output. The engine inside the target is the source of truth for the submission chain — it holds the persisted script state that gives the line-to-line parity described above, and reports back on each result whether the submission committed to that chain. This mirrors the local REPL's "two Roslyn worlds kept in sync" idea (see *Roslyn libraries* above), but split across two processes: - The **scripting world** (executing code, holding script state) lives in the **target**, inside the engine. This is what evaluation and result-rendering already use. -- The **workspace world** (syntax highlighting, autocompletion, symbol lookup) stays in the **controller**, because those are metadata operations that need the target's *types* and prior submission text, not its live object values — so they run locally without a per-keystroke round-trip over the transport. On connect the controller asks the inspector for the target's loaded-assembly paths and constructs a second, remote-configured `RoslynServices` seeded with them plus the inspector globals (so `services`/`Get()` resolve); the ordinary prompt callbacks then drive that target-aware workspace. It advances only when the engine reports a submission committed — the cross-process analogue of how the local REPL keeps its scripting and workspace APIs consistent. +- The **workspace world** (syntax highlighting, autocompletion, symbol lookup) stays in the **controller**, because those are metadata operations that need the target's *types* and prior submission text, not its live object values — so they run locally without a per-keystroke round-trip over the transport. On connect the controller asks the connector for the target's loaded-assembly paths and constructs a second, remote-configured `RoslynServices` seeded with them plus the connector globals (so `services`/`Get()` resolve); the ordinary prompt callbacks then drive that target-aware workspace. It advances only when the engine reports a submission committed — the cross-process analogue of how the local REPL keeps its scripting and workspace APIs consistent. -**Non-interactive inspect.** The interactive prompt above (`RemoteReadEvalPrintLoop`) needs a TTY, so for agents and scripts inspect mode also has a headless path that parallels the local REPL's `PipedInputEvaluator`. `inspect --eval`/`--eval-file` or piped stdin route to `RemotePipedInputEvaluator`, which drives the same `RemoteSession`, auto-prints the final value as plain/uncolored/unwrapped text on stdout (errors to stderr, nonzero exit), and honors the same `#replace`/`#wrap`/`#patches`/`#revert` commands (wording shared with the interactive loop via `InspectorCommandResultPrinter`). It skips the editor-workspace seeding above — completion/highlighting are interactive-only, so the reference round-trip would be wasted — and emits no connection chatter. Since the persisted script state lives in the target, separate one-shot `--eval` invocations accumulate state across reconnects. +**Non-interactive connect.** The interactive prompt above (`RemoteReadEvalPrintLoop`) needs a TTY, so for agents and scripts connect mode also has a headless path that parallels the local REPL's `PipedInputEvaluator`. `connect --eval`/`--eval-file` or piped stdin route to `RemotePipedInputEvaluator`, which drives the same `RemoteSession`, auto-prints the final value as plain/uncolored/unwrapped text on stdout (errors to stderr, nonzero exit), and honors the same `#replace`/`#wrap`/`#patches`/`#revert` commands (wording shared with the interactive loop via `ConnectorCommandResultPrinter`). It skips the editor-workspace seeding above — completion/highlighting are interactive-only, so the reference round-trip would be wasted — and emits no connection chatter. Since the persisted script state lives in the target, separate one-shot `--eval` invocations accumulate state across reconnects. ### Wire protocol and message flow -Controller and inspector talk over a single duplex connection — a named pipe on Windows, a Unix domain socket elsewhere, scoped to the current user. Every message is a `WireMessage` framed by `MessageChannel` as a **4-byte little-endian length prefix followed by a UTF-8 JSON body**; the JSON is a `System.Text.Json` polymorphic hierarchy keyed on a `$kind` discriminator, so a single read returns the right concrete message. Frame lengths are bounded and malformed frames surface as a catchable exception rather than crashing either process. All of these types live in the shared `CSharpRepl.InjectedHook.Contracts` assembly, so they're type-identical on both sides. +Controller and connector talk over a single duplex connection — a named pipe on Windows, a Unix domain socket elsewhere, scoped to the current user. Every message is a `WireMessage` framed by `MessageChannel` as a **4-byte little-endian length prefix followed by a UTF-8 JSON body**; the JSON is a `System.Text.Json` polymorphic hierarchy keyed on a `$kind` discriminator, so a single read returns the right concrete message. Frame lengths are bounded and malformed frames surface as a catchable exception rather than crashing either process. All of these types live in the shared `CSharpRepl.InjectedHook.Contracts` assembly, so they're type-identical on both sides. The exchange over a session's lifetime — connect, capture references for the editor workspace, then a request/response pair per submission (with optional cooperative cancellation), and finally a detach that leaves the target running: @@ -82,14 +82,14 @@ sequenceDiagram actor User participant Repl as RemoteReadEvalPrintLoop participant Session as RemoteSession - participant Server as InspectorServer - participant Engine as InspectorEngine + participant Server as ConnectorServer + participant Engine as ConnectorEngine - Note over Session: RemoteSession wraps InspectorClient + MessageChannel + Note over Session: RemoteSession wraps ConnectorClient + MessageChannel Note over Server,Engine: target process — Server in the default ALC, Engine in the isolated ALC Note over Session,Server: named pipe / Unix domain socket, current-user only
frames = 4-byte length prefix + JSON WireMessage with a kind discriminator - Session->>Server: connect via InspectorTransport + Session->>Server: connect via ConnectorTransport Server-->>Session: HandshakeMessage {pid, runtime, DI captured, assembly availability, protocol} Session->>Server: ReferencesRequest @@ -177,24 +177,24 @@ classDiagram +WriteAsync(WireMessage) +ReadAsync() WireMessage } - class InspectorClient { + class ConnectorClient { +EvalAsync(code, detailed) EvalResponse +GetReferencePathsAsync() IReadOnlyList~string~ +SendDisconnectAsync() } - class IInspectorEngine { + class IConnectorEngine { <> +EvalAsync(code, detailed, ct) EvalResponse +GetReferencePathsAsync(ct) IReadOnlyList~string~ } - class InspectorServer - class InspectorEngine - - RemoteSession --> InspectorClient - InspectorClient --> MessageChannel : controller side - InspectorServer --> MessageChannel : target side - InspectorServer --> IInspectorEngine : forwards Eval / GetReferencePaths - IInspectorEngine <|.. InspectorEngine + class ConnectorServer + class ConnectorEngine + + RemoteSession --> ConnectorClient + ConnectorClient --> MessageChannel : controller side + ConnectorServer --> MessageChannel : target side + ConnectorServer --> IConnectorEngine : forwards Eval / GetReferencePaths + IConnectorEngine <|.. ConnectorEngine MessageChannel ..> WireMessage : frames / unframes ``` @@ -202,13 +202,13 @@ classDiagram Statics and any singletons stored in statics are reachable for free — a submission just names them like ordinary code. For dependency-injected services, the bootstrap cooperatively captures the application's root service provider and exposes it to submissions (as `services` and `Get()`), so a script can resolve the app's real services. -The capture mechanism: before the target's `Main` runs, the bootstrap subscribes to the `"Microsoft.Extensions.Hosting"` `DiagnosticListener` — the same effectively-public contract `HostFactoryResolver` and the EF Core design-time tools rely on. Both `HostBuilder.Build()` and `HostApplicationBuilder.Build()` write a `HostBuilt` event whose payload is the `IHost`; the bootstrap reflects the host's `Services` property off the payload (reflection rather than a typed reference, so the bootstrap carries no hosting dependency and no version coupling) and stores it in the shared `InspectorRoots`. This covers the non-web Generic Host and modern ASP.NET Core (`WebApplication.CreateBuilder` builds through `HostApplicationBuilder`); the first host built wins, and apps that don't use the hosting abstractions simply report "no provider captured" — statics remain reachable. +The capture mechanism: before the target's `Main` runs, the bootstrap subscribes to the `"Microsoft.Extensions.Hosting"` `DiagnosticListener` — the same effectively-public contract `HostFactoryResolver` and the EF Core design-time tools rely on. Both `HostBuilder.Build()` and `HostApplicationBuilder.Build()` write a `HostBuilt` event whose payload is the `IHost`; the bootstrap reflects the host's `Services` property off the payload (reflection rather than a typed reference, so the bootstrap carries no hosting dependency and no version coupling) and stores it in the shared `ConnectorRoots`. This covers the non-web Generic Host and modern ASP.NET Core (`WebApplication.CreateBuilder` builds through `HostApplicationBuilder`); the first host built wins, and apps that don't use the hosting abstractions simply report "no provider captured" — statics remain reachable. ### Live method replacement When attached, the controller can detour a live method in the target to a method the user defines in the REPL, so the running application's behavior changes immediately. - The user defines a delegate, then runs `#replace` (supplant the original) or `#wrap` (call the original via a leading `orig` delegate). `#patches` lists active patches; `#revert` undoes them. -- The engine's `InspectorPatcher` detours the method via MonoMod.RuntimeDetour: it resolves the target, matches the overload from the delegate's signature, applies a reversible `Hook`, and tracks it by id. +- The engine's `ConnectorPatcher` detours the method via MonoMod.RuntimeDetour: it resolves the target, matches the overload from the delegate's signature, applies a reversible `Hook`, and tracks it by id. - The detour repoints native code, so it works across the engine-ALC/target-ALC split: the replacement compiles in the engine ALC and patches a method in the target's default ALC. - Patches persist until reverted or the process exits, so they outlive a controller detach. `ref`/`out`/`in` parameters work in Replace mode (the patcher emits a delegate type carrying the modifiers, since `Func` can't). Generic methods, pointer parameters, `#wrap` with by-ref, and already-inlined call sites are out of scope. diff --git a/CHANGELOG.md b/CHANGELOG.md index 93195b9c..71e1e499 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## Unreleased + +- **Breaking:** the "inspect a running process" command has been renamed from `inspect` to `connect`, to avoid confusion with the `dotnet-inspect` tool. `csharprepl inspect `, `inspect init`, and `inspect list` are now `csharprepl connect `, `connect init`, and `connect list`. Relaunch your target with the env vars from the new `connect init` (the startup-hook payload moved from the `inspector/` directory to `connector/`). + ## Release 0.9.1 - Simple variable declarations no longer require a trailing semicolon: typing `int i = 0` and pressing Enter now auto-inserts a semicolon ([#499](https://github.com/waf/CSharpRepl/pull/499)). diff --git a/CSharpRepl.Services/CSharpRepl.Services.csproj b/CSharpRepl.Services/CSharpRepl.Services.csproj index 0436d172..4d81268b 100644 --- a/CSharpRepl.Services/CSharpRepl.Services.csproj +++ b/CSharpRepl.Services/CSharpRepl.Services.csproj @@ -69,7 +69,7 @@ - + diff --git a/CSharpRepl.Services/Completion/ReplaceMethodCompletionProvider.cs b/CSharpRepl.Services/Completion/ReplaceMethodCompletionProvider.cs index 90757e88..de8dc234 100644 --- a/CSharpRepl.Services/Completion/ReplaceMethodCompletionProvider.cs +++ b/CSharpRepl.Services/Completion/ReplaceMethodCompletionProvider.cs @@ -17,7 +17,7 @@ namespace CSharpRepl.Services.Completion; /// -/// Completion for the inspect-mode REPL commands #replace / #wrap. Roslyn doesn't understand them +/// Completion for the connect-mode REPL commands #replace / #wrap. Roslyn doesn't understand them /// (they're bad preprocessor directives in Script mode), so the built-in providers produce nothing useful on /// such a line. This provider detects the command, rewrites the argument under the caret into a snippet Roslyn /// CAN complete, delegates to the normal on a throwaway document, and re-emits @@ -28,15 +28,15 @@ namespace CSharpRepl.Services.Completion; /// - Replacement position (#replace X.M with expr): the expression is completed as ordinary script code, /// against the submission chain (so the user's just-defined delegate/method is offered). /// -/// Registered only in the inspect-mode workspace (see WorkspaceManager), so the local REPL is unaffected. +/// Registered only in the connect-mode workspace (see WorkspaceManager), so the local REPL is unaffected. /// [ExportCompletionProvider(nameof(ReplaceMethodCompletionProvider), LanguageNames.CSharp), Shared] internal sealed class ReplaceMethodCompletionProvider : CompletionProvider { // The argument-taking commands, each with the trailing space that precedes its argument. private static readonly string[] Commands = - [InspectorCommands.Replace.Token + " ", InspectorCommands.Wrap.Token + " "]; - private const string WithSeparator = InspectorCommands.WithSeparator; + [ConnectorCommands.Replace.Token + " ", ConnectorCommands.Wrap.Token + " "]; + private const string WithSeparator = ConnectorCommands.WithSeparator; // Carried on each re-emitted item so GetDescriptionAsync can rebuild the synthetic document on demand. private const string SyntheticTextProperty = "CSharpRepl.SyntheticText"; diff --git a/CSharpRepl.Services/Configuration.cs b/CSharpRepl.Services/Configuration.cs index 93474791..b20878db 100644 --- a/CSharpRepl.Services/Configuration.cs +++ b/CSharpRepl.Services/Configuration.cs @@ -80,14 +80,14 @@ internal static string GetApplicationDirectory(string roamingDirectory, string l public string? EvaluateInput { get; } /// - /// When set (via csharprepl inspect <pid>), the REPL connects to the inspector hosted in that + /// When set (via csharprepl connect <pid>), the REPL connects to the connector hosted in that /// target process and evaluates submissions there instead of constructing a local script engine. /// - public int? InspectProcessId { get; } + public int? ConnectProcessId { get; } public string? LoadScript { get; } public string[] LoadScriptArgs { get; } /// - /// Output to render before exiting (help, version, usage, inspect init exports, ...). Spectre + /// Output to render before exiting (help, version, usage, connect init exports, ...). Spectre /// word-wraps to the console width; for machine-consumable output that must not wrap, supply a /// . /// @@ -110,7 +110,7 @@ public Configuration( bool usePrereleaseNugets = false, bool streamPipedInput = false, string? evaluateInput = null, - int? inspectProcessId = null, + int? connectProcessId = null, int tabSize = 4, string? loadScript = null, string[]? loadScriptArgs = null, @@ -159,11 +159,11 @@ public Configuration( } } - InspectProcessId = inspectProcessId; + ConnectProcessId = connectProcessId; - // In inspect mode, default the prompt to the target's pid (e.g. "1234> ") so it's obvious submissions + // In connect mode, default the prompt to the target's pid (e.g. "1234> ") so it's obvious submissions // run remotely. A user-supplied --prompt still wins. - if (inspectProcessId is { } pid && promptMarkup == PromptDefault) + if (connectProcessId is { } pid && promptMarkup == PromptDefault) { promptMarkup = $"{pid}> "; } diff --git a/CSharpRepl.Services/PlainText.cs b/CSharpRepl.Services/PlainText.cs index f9380a91..e6c55770 100644 --- a/CSharpRepl.Services/PlainText.cs +++ b/CSharpRepl.Services/PlainText.cs @@ -9,7 +9,7 @@ namespace CSharpRepl.Services; /// /// An that emits its text verbatim — ignoring the console width, so no word-wrapping, -/// cropping, or markup parsing. Use for machine-consumable output (e.g. the inspect init shell exports) +/// cropping, or markup parsing. Use for machine-consumable output (e.g. the connect init shell exports) /// where wrapping a long path would corrupt a copy-paste or a pipe. Contrast with , /// whose only overflow modes (Fold/Crop/Ellipsis) are all width-driven. /// diff --git a/CSharpRepl.Services/Remote/Commands/InspectorCommandProcessor.cs b/CSharpRepl.Services/Remote/Commands/ConnectorCommandProcessor.cs similarity index 66% rename from CSharpRepl.Services/Remote/Commands/InspectorCommandProcessor.cs rename to CSharpRepl.Services/Remote/Commands/ConnectorCommandProcessor.cs index b51bae07..3de09da6 100644 --- a/CSharpRepl.Services/Remote/Commands/InspectorCommandProcessor.cs +++ b/CSharpRepl.Services/Remote/Commands/ConnectorCommandProcessor.cs @@ -11,82 +11,82 @@ namespace CSharpRepl.Services.Remote.Commands; /// -/// Parses inspect-mode command lines (#replace/#wrap/#patches/#revert) and runs them +/// Parses connect-mode command lines (#replace/#wrap/#patches/#revert) and runs them /// against the , returning the raw engine response for the REPL to render. Commands /// are recognized case-insensitively while the casing of method names and replacement expressions is preserved. /// -public sealed class InspectorCommandProcessor +public sealed class ConnectorCommandProcessor { private readonly RemoteSession session; - public InspectorCommandProcessor(RemoteSession session) => this.session = session; + public ConnectorCommandProcessor(RemoteSession session) => this.session = session; /// - /// Runs if it's an inspect command, returning its result; returns null when + /// Runs if it's a connect command, returning its result; returns null when /// the text is not one of the commands, so the caller can fall through to normal evaluation. May surface an /// / from the underlying session for the /// caller to handle. /// - public async Task TryExecuteAsync(string commandText, CancellationToken cancellationToken) + public async Task TryExecuteAsync(string commandText, CancellationToken cancellationToken) { - if (TryMatchArgument(commandText, InspectorCommands.Replace, out var replaceArgs)) + if (TryMatchArgument(commandText, ConnectorCommands.Replace, out var replaceArgs)) { return await ReplaceAsync(replaceArgs, PatchMode.Replace, cancellationToken).ConfigureAwait(false); } - if (TryMatchArgument(commandText, InspectorCommands.Wrap, out var wrapArgs)) + if (TryMatchArgument(commandText, ConnectorCommands.Wrap, out var wrapArgs)) { return await ReplaceAsync(wrapArgs, PatchMode.Wrap, cancellationToken).ConfigureAwait(false); } - if (commandText.Equals(InspectorCommands.Patches.Token, StringComparison.OrdinalIgnoreCase)) + if (commandText.Equals(ConnectorCommands.Patches.Token, StringComparison.OrdinalIgnoreCase)) { - return new InspectorCommandResult.Listed(await session.ListPatchesAsync(cancellationToken).ConfigureAwait(false)); + return new ConnectorCommandResult.Listed(await session.ListPatchesAsync(cancellationToken).ConfigureAwait(false)); } - if (commandText.Equals(InspectorCommands.Revert.Token, StringComparison.OrdinalIgnoreCase) || - commandText.StartsWith(InspectorCommands.Revert.Token + " ", StringComparison.OrdinalIgnoreCase)) + if (commandText.Equals(ConnectorCommands.Revert.Token, StringComparison.OrdinalIgnoreCase) || + commandText.StartsWith(ConnectorCommands.Revert.Token + " ", StringComparison.OrdinalIgnoreCase)) { - return await RevertAsync(commandText[InspectorCommands.Revert.Token.Length..].Trim(), cancellationToken).ConfigureAwait(false); + return await RevertAsync(commandText[ConnectorCommands.Revert.Token.Length..].Trim(), cancellationToken).ConfigureAwait(false); } return null; } - private async Task ReplaceAsync(string arguments, PatchMode mode, CancellationToken cancellationToken) + private async Task ReplaceAsync(string arguments, PatchMode mode, CancellationToken cancellationToken) { - var info = mode == PatchMode.Wrap ? InspectorCommands.Wrap : InspectorCommands.Replace; - var separator = arguments.IndexOf(InspectorCommands.WithSeparator, StringComparison.OrdinalIgnoreCase); + var info = mode == PatchMode.Wrap ? ConnectorCommands.Wrap : ConnectorCommands.Replace; + var separator = arguments.IndexOf(ConnectorCommands.WithSeparator, StringComparison.OrdinalIgnoreCase); if (separator < 0) { - return new InspectorCommandResult.UsageError(info); + return new ConnectorCommandResult.UsageError(info); } var target = arguments[..separator].Trim(); - var replacement = arguments[(separator + InspectorCommands.WithSeparator.Length)..].Trim(); + var replacement = arguments[(separator + ConnectorCommands.WithSeparator.Length)..].Trim(); if (target.Length == 0 || replacement.Length == 0) { - return new InspectorCommandResult.UsageError(info); + return new ConnectorCommandResult.UsageError(info); } var response = await session.ReplaceAsync(target, replacement, mode, cancellationToken).ConfigureAwait(false); - return new InspectorCommandResult.Replaced(mode, target, replacement, response); + return new ConnectorCommandResult.Replaced(mode, target, replacement, response); } - private async Task RevertAsync(string argument, CancellationToken cancellationToken) + private async Task RevertAsync(string argument, CancellationToken cancellationToken) { if (argument.Equals("all", StringComparison.OrdinalIgnoreCase)) { - return new InspectorCommandResult.Reverted(All: true, RequestedId: 0, await session.RevertAsync(0, all: true, cancellationToken).ConfigureAwait(false)); + return new ConnectorCommandResult.Reverted(All: true, RequestedId: 0, await session.RevertAsync(0, all: true, cancellationToken).ConfigureAwait(false)); } if (int.TryParse(argument, out var id)) { - return new InspectorCommandResult.Reverted(All: false, RequestedId: id, await session.RevertAsync(id, all: false, cancellationToken).ConfigureAwait(false)); + return new ConnectorCommandResult.Reverted(All: false, RequestedId: id, await session.RevertAsync(id, all: false, cancellationToken).ConfigureAwait(false)); } - return new InspectorCommandResult.UsageError(InspectorCommands.Revert); + return new ConnectorCommandResult.UsageError(ConnectorCommands.Revert); } /// /// Matches an argument-taking command, requiring the trailing space before the argument (so a bare /// #replace with no argument falls through to evaluation, preserving the original dispatch behavior). /// - private static bool TryMatchArgument(string commandText, InspectorCommandInfo info, out string arguments) + private static bool TryMatchArgument(string commandText, ConnectorCommandInfo info, out string arguments) { var prefix = info.Token + " "; if (commandText.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) diff --git a/CSharpRepl.Services/Remote/Commands/InspectorCommandResult.cs b/CSharpRepl.Services/Remote/Commands/ConnectorCommandResult.cs similarity index 61% rename from CSharpRepl.Services/Remote/Commands/InspectorCommandResult.cs rename to CSharpRepl.Services/Remote/Commands/ConnectorCommandResult.cs index 617f870a..59af34f0 100644 --- a/CSharpRepl.Services/Remote/Commands/InspectorCommandResult.cs +++ b/CSharpRepl.Services/Remote/Commands/ConnectorCommandResult.cs @@ -7,22 +7,22 @@ namespace CSharpRepl.Services.Remote.Commands; /// -/// The outcome of running an inspect-mode command. A closed hierarchy carrying the raw engine responses so the +/// The outcome of running a connect-mode command. A closed hierarchy carrying the raw engine responses so the /// REPL — not this service — decides how to render them. /// -public abstract record InspectorCommandResult +public abstract record ConnectorCommandResult { - private InspectorCommandResult() { } + private ConnectorCommandResult() { } /// The argument was missing or malformed; the REPL should show 's usage. - public sealed record UsageError(InspectorCommandInfo Command) : InspectorCommandResult; + public sealed record UsageError(ConnectorCommandInfo Command) : ConnectorCommandResult; - /// A #replace/#wrap completed; inspect .Ok for success vs failure. - public sealed record Replaced(PatchMode Mode, string Target, string Replacement, ReplaceResponse Response) : InspectorCommandResult; + /// A #replace/#wrap completed; connect .Ok for success vs failure. + public sealed record Replaced(PatchMode Mode, string Target, string Replacement, ReplaceResponse Response) : ConnectorCommandResult; /// A #patches listing. - public sealed record Listed(PatchListResponse Response) : InspectorCommandResult; + public sealed record Listed(PatchListResponse Response) : ConnectorCommandResult; /// A #revert result. is meaningful only when not . - public sealed record Reverted(bool All, int RequestedId, RevertResponse Response) : InspectorCommandResult; + public sealed record Reverted(bool All, int RequestedId, RevertResponse Response) : ConnectorCommandResult; } diff --git a/CSharpRepl.Services/Remote/Commands/InspectorCommands.cs b/CSharpRepl.Services/Remote/Commands/ConnectorCommands.cs similarity index 77% rename from CSharpRepl.Services/Remote/Commands/InspectorCommands.cs rename to CSharpRepl.Services/Remote/Commands/ConnectorCommands.cs index f7f4d253..9a63a5c4 100644 --- a/CSharpRepl.Services/Remote/Commands/InspectorCommands.cs +++ b/CSharpRepl.Services/Remote/Commands/ConnectorCommands.cs @@ -7,41 +7,41 @@ namespace CSharpRepl.Services.Remote.Commands; /// -/// Static metadata for one inspect-mode REPL command. The single source of truth for its token, whether it +/// Static metadata for one connect-mode REPL command. The single source of truth for its token, whether it /// takes an argument, its usage line, and its help/completion description — consumed by the command processor, /// the completion provider, and the prompt's completion items. /// -public sealed record InspectorCommandInfo(string Token, bool RequiresArgument, string Usage, string Description); +public sealed record ConnectorCommandInfo(string Token, bool RequiresArgument, string Usage, string Description); -/// The inspect-mode commands (#replace/#wrap/#patches/#revert) and their canonical text. -public static class InspectorCommands +/// The connect-mode commands (#replace/#wrap/#patches/#revert) and their canonical text. +public static class ConnectorCommands { /// Separates the target method from the replacement expression in #replace/#wrap. public const string WithSeparator = " with "; - public static readonly InspectorCommandInfo Replace = new( + public static readonly ConnectorCommandInfo Replace = new( Token: "#replace", RequiresArgument: true, Usage: "#replace with ", Description: "Replace a live method in the target with a method you define in the REPL. Signature must match. Usage: #replace with "); - public static readonly InspectorCommandInfo Wrap = new( + public static readonly ConnectorCommandInfo Wrap = new( Token: "#wrap", RequiresArgument: true, Usage: "#wrap with ", Description: "Wrap a live method: the replacement's first parameter is an 'orig' delegate (Func/Action) that invokes the original. Usage: #wrap with "); - public static readonly InspectorCommandInfo Patches = new( + public static readonly ConnectorCommandInfo Patches = new( Token: "#patches", RequiresArgument: false, Usage: "#patches", Description: "List the method replacements currently applied to the target."); - public static readonly InspectorCommandInfo Revert = new( + public static readonly ConnectorCommandInfo Revert = new( Token: "#revert", RequiresArgument: false, Usage: "#revert | #revert all", Description: "Undo a method replacement, restoring the original. Usage: #revert or #revert all"); - public static readonly IReadOnlyList All = [Replace, Wrap, Patches, Revert]; + public static readonly IReadOnlyList All = [Replace, Wrap, Patches, Revert]; } diff --git a/CSharpRepl.Services/Remote/InspectorClient.cs b/CSharpRepl.Services/Remote/ConnectorClient.cs similarity index 86% rename from CSharpRepl.Services/Remote/InspectorClient.cs rename to CSharpRepl.Services/Remote/ConnectorClient.cs index a668b2ec..3c6f9b76 100644 --- a/CSharpRepl.Services/Remote/InspectorClient.cs +++ b/CSharpRepl.Services/Remote/ConnectorClient.cs @@ -12,40 +12,40 @@ namespace CSharpRepl.Services.Remote; /// -/// Controller-side transport for an inspector session: connects to the per-process pipe/socket, reads the -/// handshake, and exchanges framed s with the inspector. This is the thin wire +/// Controller-side transport for a connector session: connects to the per-process pipe/socket, reads the +/// handshake, and exchanges framed s with the connector. This is the thin wire /// layer; adds the eval/disconnect semantics on top. /// -public sealed class InspectorClient : IAsyncDisposable +public sealed class ConnectorClient : IAsyncDisposable { private readonly Stream stream; private readonly MessageChannel channel; - private InspectorClient(Stream stream, MessageChannel channel, HandshakeMessage handshake) + private ConnectorClient(Stream stream, MessageChannel channel, HandshakeMessage handshake) { this.stream = stream; this.channel = channel; Handshake = handshake; } - /// Identity/version details the inspector sent on connect. + /// Identity/version details the connector sent on connect. public HandshakeMessage Handshake { get; } /// - /// Connects to the inspector listening for and consumes its handshake. - /// Retries until the inspector's listener exists or elapses. + /// Connects to the connector listening for and consumes its handshake. + /// Retries until the connector's listener exists or elapses. /// - public static async Task ConnectAsync(int processId, TimeSpan timeout, CancellationToken cancellationToken) + public static async Task ConnectAsync(int processId, TimeSpan timeout, CancellationToken cancellationToken) { - var stream = await InspectorTransport.ConnectAsync(processId, timeout, cancellationToken).ConfigureAwait(false); + var stream = await ConnectorTransport.ConnectAsync(processId, timeout, cancellationToken).ConfigureAwait(false); try { var channel = new MessageChannel(stream); var first = await channel.ReadAsync(cancellationToken).ConfigureAwait(false) - ?? throw new IOException("The inspector closed the connection before sending a handshake."); + ?? throw new IOException("The connector closed the connection before sending a handshake."); return first is not HandshakeMessage handshake - ? throw new IOException($"Expected a handshake from the inspector but received {first.GetType().Name}.") - : new InspectorClient(stream, channel, handshake); + ? throw new IOException($"Expected a handshake from the connector but received {first.GetType().Name}.") + : new ConnectorClient(stream, channel, handshake); } catch { @@ -106,9 +106,9 @@ private async Task ReadResponseAsync(string requestNoun, s where TResponse : WireMessage { var response = await channel.ReadAsync(cancellationToken).ConfigureAwait(false) - ?? throw new IOException($"The inspector closed the connection without responding to the {requestNoun}."); + ?? throw new IOException($"The connector closed the connection without responding to the {requestNoun}."); return response as TResponse - ?? throw new IOException($"Expected {responseNoun} from the inspector but received {response.GetType().Name}."); + ?? throw new IOException($"Expected {responseNoun} from the connector but received {response.GetType().Name}."); } internal async Task SendDisconnectAsync(CancellationToken cancellationToken) diff --git a/CSharpRepl.Services/Remote/RemoteEditorContext.cs b/CSharpRepl.Services/Remote/RemoteEditorContext.cs index 0af4cd32..b3902af9 100644 --- a/CSharpRepl.Services/Remote/RemoteEditorContext.cs +++ b/CSharpRepl.Services/Remote/RemoteEditorContext.cs @@ -12,9 +12,9 @@ namespace CSharpRepl.Services.Remote; /// -/// Seeds a for inspect mode: the editor services (completion, semantic +/// Seeds a for connect mode: the editor services (completion, semantic /// highlighting, overloads, formatting) run controller-side but must see the target's types and the -/// inspector globals, not the local REPL's. This carries the two things that differ from a local session — +/// connector globals, not the local REPL's. This carries the two things that differ from a local session — /// the target's loaded-assembly paths and the globals type whose members are in scope for every submission. /// /// @@ -30,19 +30,19 @@ public RemoteEditorContext(IReadOnlyList referencePaths, Type globalsTyp GlobalsType = globalsType; } - // The default Roslyn MEF host plus this assembly, so the inspect-only completion provider + // The default Roslyn MEF host plus this assembly, so the connect-only completion provider // (ReplaceMethodCompletionProvider, which powers #replace/#wrap autocomplete) is discovered. A process-wide // singleton built on first use: reconnects don't recompose the MEF catalog, and the local REPL (which uses the // cached MefHostServices.DefaultHost) never pays for it. private static readonly Lazy editorHostServices = new(() => MefHostServices.Create(MefHostServices.DefaultAssemblies.Add(typeof(ReplaceMethodCompletionProvider).Assembly))); - /// The target's loaded-assembly file paths, as reported by the inspector engine. + /// The target's loaded-assembly file paths, as reported by the connector engine. public IReadOnlyList ReferencePaths { get; } /// - /// The Roslyn workspace host the editor services should use in inspect mode: the default providers plus the - /// inspect-only command-completion provider. Supplied to the WorkspaceManager; local sessions pass no + /// The Roslyn workspace host the editor services should use in connect mode: the default providers plus the + /// connect-only command-completion provider. Supplied to the WorkspaceManager; local sessions pass no /// host and fall back to . /// internal HostServices EditorHostServices => editorHostServices.Value; @@ -50,7 +50,7 @@ public RemoteEditorContext(IReadOnlyList referencePaths, Type globalsTyp /// /// The globals type whose members (e.g. services, Get<T>()) are in scope for every /// submission. Set as the workspace's host object type so the editor resolves those members; its assembly - /// must be reachable from (the target loads 's + /// must be reachable from (the target loads 's /// Contracts assembly in its default ALC, so it is). /// public Type GlobalsType { get; } diff --git a/CSharpRepl.Services/Remote/RemoteSession.cs b/CSharpRepl.Services/Remote/RemoteSession.cs index 5631d703..704ca775 100644 --- a/CSharpRepl.Services/Remote/RemoteSession.cs +++ b/CSharpRepl.Services/Remote/RemoteSession.cs @@ -11,22 +11,22 @@ namespace CSharpRepl.Services.Remote; /// -/// A controller-side remote inspector session: send a code line, receive its , +/// A controller-side remote connector session: send a code line, receive its , /// and end the session cleanly when the controller exits (leaving the target running). The REPL drives this -/// in place of the local RoslynServices when running in inspect mode. +/// in place of the local RoslynServices when running in connect mode. /// public sealed class RemoteSession : IAsyncDisposable { - private readonly InspectorClient client; + private readonly ConnectorClient client; private bool disconnected; - private RemoteSession(InspectorClient client) => this.client = client; + private RemoteSession(ConnectorClient client) => this.client = client; - /// Identity/version details the inspector sent on connect (pid, runtime, DI-captured, ...). + /// Identity/version details the connector sent on connect (pid, runtime, DI-captured, ...). public HandshakeMessage Handshake => client.Handshake; public static async Task ConnectAsync(int processId, TimeSpan timeout, CancellationToken cancellationToken) - => new RemoteSession(await InspectorClient.ConnectAsync(processId, timeout, cancellationToken).ConfigureAwait(false)); + => new RemoteSession(await ConnectorClient.ConnectAsync(processId, timeout, cancellationToken).ConfigureAwait(false)); /// /// Evaluates a single submission in the target and returns its result. selects @@ -58,7 +58,7 @@ public Task ListPatchesAsync(CancellationToken cancellationTo public Task RevertAsync(int patchId, bool all, CancellationToken cancellationToken) => client.RevertAsync(patchId, all, cancellationToken); - /// Asks the inspector to end the session. The target process keeps running. + /// Asks the connector to end the session. The target process keeps running. public async Task DisconnectAsync(CancellationToken cancellationToken) { if (disconnected) return; diff --git a/CSharpRepl.Services/Remote/RemoteValueRenderer.cs b/CSharpRepl.Services/Remote/RemoteValueRenderer.cs index 573651ca..54c73be1 100644 --- a/CSharpRepl.Services/Remote/RemoteValueRenderer.cs +++ b/CSharpRepl.Services/Remote/RemoteValueRenderer.cs @@ -14,7 +14,7 @@ namespace CSharpRepl.Services.Remote; /// -/// Renders a / produced by the inspector (in the +/// Renders a / produced by the connector (in the /// target process) into a themed Spectre on the controller. The engine carries /// theme-agnostic data (formatted scalar text + a hint, or a structured /// member/element breakdown); this applies the user's theme via the controller's , @@ -42,7 +42,7 @@ internal sealed class RemoteValueRenderer /// /// Renders a to plain, uncolored, unwrapped text for non-interactive output - /// (inspect <pid> --eval / piped input): the same layout as , with styling + /// (connect <pid> --eval / piped input): the same layout as , with styling /// and width-wrapping stripped so the value is safe to capture or pipe. /// public string RenderToPlainText(RemoteValue value, Level level) => ToPlainText(Render(value, level)); diff --git a/CSharpRepl.Services/Roslyn/References/AssemblyReferenceReadme.md b/CSharpRepl.Services/Roslyn/References/AssemblyReferenceReadme.md index b76b633c..0b6a4876 100644 --- a/CSharpRepl.Services/Roslyn/References/AssemblyReferenceReadme.md +++ b/CSharpRepl.Services/Roslyn/References/AssemblyReferenceReadme.md @@ -238,8 +238,8 @@ identity no matter which path loads it. > that isolated island. (The shared framework is the deliberate exception — `System.*` / `Microsoft.NETCore.App` > resolve from Default, which is why they're never put in the registry.) -> **The opposite choice, on purpose — the inspect engine.** csharprepl's inspect-a-running-process feature -> (`InjectedHook/.../InspectorEngine.cs`) faces the same isolate-vs-share decision and resolves it the other +> **The opposite choice, on purpose — the connect engine.** csharprepl's connect-a-running-process feature +> (`InjectedHook/.../ConnectorEngine.cs`) faces the same isolate-vs-share decision and resolves it the other > way. Injected into a target process, it builds its compilation references from the *target's* live, > already-loaded assemblies (the target's Default ALC) and pins them with `RegisterDependency`, so the > submissions bind to the target's **real live objects** — there the whole point is *not* to isolate. (Its @@ -289,8 +289,8 @@ This is necessary in addition to the `Resolving` fallback, not redundant with it its own copy of a submission's **direct** reference, and `Resolving` only fires on a load *miss* — which never happens for a direct reference — so pinning is the only lever that makes IAL agree on our instance. Only the nuget closure is pinned; project/solution references are bound correctly by the existing -machinery and are served, if needed, by the by-name resolve fallback, never pinned. (The inspector engine, -`InjectedHook/.../InspectorEngine.cs`, uses the same `RegisterDependency` primitive to pin the target +machinery and are served, if needed, by the by-name resolve fallback, never pinned. (The connector engine, +`InjectedHook/.../ConnectorEngine.cs`, uses the same `RegisterDependency` primitive to pin the target process's live assemblies.) ### Shadow copying — load-bearing beyond file-unlock diff --git a/CSharpRepl.Services/Roslyn/References/ReplAssemblyLoader.cs b/CSharpRepl.Services/Roslyn/References/ReplAssemblyLoader.cs index 1b1cedf2..11fa3466 100644 --- a/CSharpRepl.Services/Roslyn/References/ReplAssemblyLoader.cs +++ b/CSharpRepl.Services/Roslyn/References/ReplAssemblyLoader.cs @@ -37,7 +37,7 @@ namespace CSharpRepl.Services.Roslyn.References; /// Compile-time metadata resolution lives in ; /// this type is the runtime half. /// -/// Elsewhere, the injected inspector engine has its own analogous loader+pinning (InspectorEngine.cs), but it +/// Elsewhere, the injected connector engine has its own analogous loader+pinning (ConnectorEngine.cs), but it /// runs in the target process, and is separate. /// internal sealed class ReplAssemblyLoader diff --git a/CSharpRepl.Services/Roslyn/RoslynServices.cs b/CSharpRepl.Services/Roslyn/RoslynServices.cs index 1c99670f..b1019428 100644 --- a/CSharpRepl.Services/Roslyn/RoslynServices.cs +++ b/CSharpRepl.Services/Roslyn/RoslynServices.cs @@ -81,15 +81,15 @@ public sealed partial class RoslynServices // classification) of the per-keystroke pipeline. Null until Initialization completes. internal Document? CurrentDocumentForProfiling => workspaceManager?.CurrentDocument; - // In inspect mode this seeds the editor services with the target's references + the inspector globals, so + // In connect mode this seeds the editor services with the target's references + the connector globals, so // completion/highlighting are target-aware. Null for a normal local REPL session. private readonly RemoteEditorContext? remoteEditor; /// - /// True when this is an inspect-mode session (editor services target a remote process). Used to offer the - /// inspect-only commands (#replace/#wrap/#patches/#revert) in completion only when they're applicable. + /// True when this is a connect-mode session (editor services target a remote process). Used to offer the + /// connect-only commands (#replace/#wrap/#patches/#revert) in completion only when they're applicable. /// - public bool IsInspectMode => remoteEditor is not null; + public bool IsConnectMode => remoteEditor is not null; public RoslynServices(IConsoleService console, Configuration config, ITraceLogger logger, RemoteEditorContext? remoteEditor = null) { @@ -109,8 +109,8 @@ public RoslynServices(IConsoleService console, Configuration config, ITraceLogge this.referenceService = new AssemblyReferenceService(config, parseOptions, logger); - // Inspect mode: fold the target's loaded-assembly paths into the reference set so the editor - // workspace sees the target's own types (and the inspector globals' Contracts assembly), on top of + // Connect mode: fold the target's loaded-assembly paths into the reference set so the editor + // workspace sees the target's own types (and the connector globals' Contracts assembly), on top of // the local framework references. EnsureReferenceAssemblyWithDocumentation maps the target's // framework implementation assemblies to the local reference assemblies and passes the app's own // assemblies through, which is exactly the reference set we want for completion/highlighting. @@ -134,8 +134,8 @@ public RoslynServices(IConsoleService console, Configuration config, ITraceLogge // the script runner is used to actually execute the scripts, and the workspace manager // is updated alongside. The workspace is a datamodel used in "editor services" like - // syntax highlighting, autocompletion, and roslyn symbol queries. In inspect mode the host object - // type is the inspector globals, so `services`/`Get()` resolve in the editor (the engine in the + // syntax highlighting, autocompletion, and roslyn symbol queries. In connect mode the host object + // type is the connector globals, so `services`/`Get()` resolve in the editor (the engine in the // target — not this ScriptRunner — performs the actual evaluation). this.workspaceManager = new WorkspaceManager(compilationOptions, referenceService, logger, hostObjectType: remoteGlobalsType, hostServices: remoteEditor?.EditorHostServices); this.scriptRunner = new ScriptRunner(workspaceManager, parseOptions, compilationOptions, referenceService, console, config, globalsType: remoteGlobalsType); @@ -190,14 +190,14 @@ public async Task EvaluateAsync(string input, string[]? args = private RemoteValueRenderer? remoteValueRenderer; /// - /// Renders a produced by an inspector session (in a target process) using the + /// Renders a produced by a connector session (in a target process) using the /// user's theme. Unlike this needs no live object and no /// Roslyn initialization — only the theme/highlighter, which is ready as soon as the service is constructed. /// public IRenderable RenderRemoteValue(RemoteValue value, Level level) => (remoteValueRenderer ??= new RemoteValueRenderer(highlighter)).Render(value, level); - /// Plain-text analogue of for non-interactive inspect output. + /// Plain-text analogue of for non-interactive connect output. public string RenderRemoteValueToPlainText(RemoteValue value, Level level) => (remoteValueRenderer ??= new RemoteValueRenderer(highlighter)).RenderToPlainText(value, level); diff --git a/CSharpRepl.Services/Roslyn/Scripting/ScriptRunner.cs b/CSharpRepl.Services/Roslyn/Scripting/ScriptRunner.cs index 1ae6c691..f05f00ee 100644 --- a/CSharpRepl.Services/Roslyn/Scripting/ScriptRunner.cs +++ b/CSharpRepl.Services/Roslyn/Scripting/ScriptRunner.cs @@ -49,7 +49,7 @@ public ScriptRunner( this.parseOptions = parseOptions; this.referenceAssemblyService = referenceAssemblyService; // The globals type backs CompileTransient's semantic model (used for symbol exploration). It defaults - // to the local ScriptGlobals; the inspector's remote editor passes InspectorGlobals so symbol lookups + // to the local ScriptGlobals; the connector's remote editor passes ConnectorGlobals so symbol lookups // over `services` resolve. The live-evaluation path below always uses ScriptGlobals (it's local-only). this.globalsType = globalsType ?? typeof(ScriptGlobals); this.assemblyLoader = new InteractiveAssemblyLoader(new MetadataShadowCopyProvider()); diff --git a/CSharpRepl.Services/Roslyn/WorkspaceManager.cs b/CSharpRepl.Services/Roslyn/WorkspaceManager.cs index db4a485d..2483f121 100644 --- a/CSharpRepl.Services/Roslyn/WorkspaceManager.cs +++ b/CSharpRepl.Services/Roslyn/WorkspaceManager.cs @@ -37,7 +37,7 @@ internal sealed class WorkspaceManager /// /// The script globals type whose members are in scope for every submission (e.g. services / - /// Get<T>() from the inspector's globals). Null for the local REPL, which doesn't surface its + /// Get<T>() from the connector's globals). Null for the local REPL, which doesn't surface its /// globals in completion. When set, it's applied as each submission project's host object type, so editor /// services resolve the globals members. The type's assembly must be among the project's references. /// diff --git a/CSharpRepl/CSharpRepl.csproj b/CSharpRepl/CSharpRepl.csproj index 73b7fdf9..1325e27a 100644 --- a/CSharpRepl/CSharpRepl.csproj +++ b/CSharpRepl/CSharpRepl.csproj @@ -42,25 +42,25 @@ - + - <_InspectorPayload Include="$(MSBuildThisFileDirectory)..\InjectedHook\CSharpRepl.InjectedHook\bin\$(Configuration)\$(TargetFramework)\**\*.*" /> - + <_ConnectorPayload Include="$(MSBuildThisFileDirectory)..\InjectedHook\CSharpRepl.InjectedHook\bin\$(Configuration)\$(TargetFramework)\**\*.*" /> + PreserveNewest diff --git a/Tests/CSharpRepl.Tests/CommandLineTests.cs b/Tests/CSharpRepl.Tests/CommandLineTests.cs index a6f692ed..92f36c91 100644 --- a/Tests/CSharpRepl.Tests/CommandLineTests.cs +++ b/Tests/CSharpRepl.Tests/CommandLineTests.cs @@ -181,18 +181,18 @@ public void ParseArguments_DotNetSuggestUsingValue_IsAutocompleted() } [Fact] - public void ParseArguments_DotNetSuggestInspectSubcommand_IsAutocompleted() + public void ParseArguments_DotNetSuggestConnectSubcommand_IsAutocompleted() { - var result = Parse(new[] { "[suggest:8]", "inspect " }); + var result = Parse(new[] { "[suggest:8]", "connect " }); var output = Render(result.OutputForEarlyExit); Assert.Contains("init", output); Assert.Contains("list", output); } [Fact] - public void ParseArguments_DotNetSuggestInspectShellValue_IsAutocompleted() + public void ParseArguments_DotNetSuggestConnectShellValue_IsAutocompleted() { - var result = Parse(new[] { "[suggest:21]", "inspect init --shell " }); + var result = Parse(new[] { "[suggest:21]", "connect init --shell " }); var output = Render(result.OutputForEarlyExit); Assert.Contains("pwsh", output); Assert.Contains("bash", output); @@ -200,68 +200,68 @@ public void ParseArguments_DotNetSuggestInspectShellValue_IsAutocompleted() } [Fact] - public void ParseArguments_InspectPid_SetsInspectProcessId() + public void ParseArguments_ConnectPid_SetsConnectProcessId() { - var result = Parse("inspect 1234"); + var result = Parse("connect 1234"); Assert.NotNull(result); - Assert.Equal(1234, result.InspectProcessId); + Assert.Equal(1234, result.ConnectProcessId); } [Fact] - public void ParseArguments_InspectPidWithReplOption_BindsRecursiveOption() + public void ParseArguments_ConnectPidWithReplOption_BindsRecursiveOption() { - // The REPL render options are recursive, so they still bind after `inspect ` and remote + // The REPL render options are recursive, so they still bind after `connect ` and remote // results render with the user's theme. - var result = Parse("inspect 1234 --theme Data/theme.json"); - Assert.Equal(1234, result.InspectProcessId); + var result = Parse("connect 1234 --theme Data/theme.json"); + Assert.Equal(1234, result.ConnectProcessId); Assert.True(result.Theme.TryGetSyntaxHighlightingAnsiColor("struct name", out var color)); Assert.Equal("Yellow", color.ToString()); } [Theory] - [InlineData("inspect")] // missing pid - [InlineData("inspect 0")] // non-positive - [InlineData("inspect -5")] // non-positive - [InlineData("inspect abc")] // non-numeric - public void ParseArguments_InspectWithInvalidPid_ThrowsUsage(string commandline) + [InlineData("connect")] // missing pid + [InlineData("connect 0")] // non-positive + [InlineData("connect -5")] // non-positive + [InlineData("connect abc")] // non-numeric + public void ParseArguments_ConnectWithInvalidPid_ThrowsUsage(string commandline) { var ex = Assert.Throws(() => Parse(commandline)); - Assert.Contains("Usage: csharprepl inspect", ex.Message); + Assert.Contains("Usage: csharprepl connect", ex.Message); } [Fact] - public void ParseArguments_InspectInit_PrintsExportsAndExitsWithoutInspecting() + public void ParseArguments_ConnectInit_PrintsExportsAndExitsWithoutConnecting() { - var result = Parse("inspect init"); + var result = Parse("connect init"); Assert.NotNull(result.OutputForEarlyExit); var output = Render(result.OutputForEarlyExit); Assert.Contains("DOTNET_STARTUP_HOOKS", output); Assert.Contains("ASPNETCORE_HOSTINGSTARTUPASSEMBLIES", output); - Assert.Null(result.InspectProcessId); + Assert.Null(result.ConnectProcessId); } [Fact] - public void ParseArguments_InspectList_ProducesEarlyExitWithoutInspecting() + public void ParseArguments_ConnectList_ProducesEarlyExitWithoutConnecting() { - // `inspect list` reports the attachable processes and exits, like `inspect init` — it never enters - // inspect mode (no pid). The rendered content depends on what's running, so only assert the shape. - var result = Parse("inspect list"); + // `connect list` reports the attachable processes and exits, like `connect init` — it never enters + // connect mode (no pid). The rendered content depends on what's running, so only assert the shape. + var result = Parse("connect list"); Assert.NotNull(result.OutputForEarlyExit); - Assert.Null(result.InspectProcessId); + Assert.Null(result.ConnectProcessId); } [Fact] - public void RenderInspectList_WithNoProcesses_ShowsTheInitHint() + public void RenderConnectList_WithNoProcesses_ShowsTheInitHint() { - var output = Render(CommandLine.RenderInspectList([])); - Assert.Contains("No inspector-enabled processes found", output); - Assert.Contains("csharprepl inspect init", output); + var output = Render(CommandLine.RenderConnectList([])); + Assert.Contains("No connector-enabled processes found", output); + Assert.Contains("csharprepl connect init", output); } [Fact] - public void RenderInspectList_WithProcesses_ListsEachPidAndNameAndAConnectHint() + public void RenderConnectList_WithProcesses_ListsEachPidAndNameAndAConnectHint() { - var output = Render(CommandLine.RenderInspectList([(1234, "MyApp"), (5678, "dotnet")])); + var output = Render(CommandLine.RenderConnectList([(1234, "MyApp"), (5678, "dotnet")])); Assert.Contains("PID", output); Assert.Contains("Process", output); @@ -270,26 +270,26 @@ public void RenderInspectList_WithProcesses_ListsEachPidAndNameAndAConnectHint() Assert.Contains("5678", output); Assert.Contains("dotnet", output); // the "connect with" hint - Assert.Contains("csharprepl inspect", output); + Assert.Contains("csharprepl connect", output); Assert.Contains("Hint: you most likely want to connect to the 'MyApp' process (PID 1234).", output); } [Fact] - public void RenderInspectList_WithMarkupCharsInName_RendersThemLiterally() + public void RenderConnectList_WithMarkupCharsInName_RendersThemLiterally() { // A process name with '[' must not be interpreted as Spectre markup (it would otherwise throw or be // swallowed). The cells use Text, not markup, so the brackets survive verbatim. - var output = Render(CommandLine.RenderInspectList([(42, "weird[name]")])); + var output = Render(CommandLine.RenderConnectList([(42, "weird[name]")])); Assert.Contains("weird[name]", output); } [Theory] - [InlineData("inspect init --shell bash", "export DOTNET_STARTUP_HOOKS=")] - [InlineData("inspect init --shell=bash", "export DOTNET_STARTUP_HOOKS=")] - [InlineData("inspect init --shell cmd", "set \"DOTNET_STARTUP_HOOKS=")] - [InlineData("inspect init --shell fish", "set -gx DOTNET_STARTUP_HOOKS ")] - [InlineData("inspect init --shell pwsh", "$env:DOTNET_STARTUP_HOOKS = ")] - public void ParseArguments_InspectInitWithShell_EmitsShellSpecificSyntax(string commandline, string expectedFragment) + [InlineData("connect init --shell bash", "export DOTNET_STARTUP_HOOKS=")] + [InlineData("connect init --shell=bash", "export DOTNET_STARTUP_HOOKS=")] + [InlineData("connect init --shell cmd", "set \"DOTNET_STARTUP_HOOKS=")] + [InlineData("connect init --shell fish", "set -gx DOTNET_STARTUP_HOOKS ")] + [InlineData("connect init --shell pwsh", "$env:DOTNET_STARTUP_HOOKS = ")] + public void ParseArguments_ConnectInitWithShell_EmitsShellSpecificSyntax(string commandline, string expectedFragment) { var result = Parse(commandline); Assert.NotNull(result.OutputForEarlyExit); diff --git a/Tests/CSharpRepl.Tests/CompletionTests.cs b/Tests/CSharpRepl.Tests/CompletionTests.cs index 3eafeefe..67482b33 100644 --- a/Tests/CSharpRepl.Tests/CompletionTests.cs +++ b/Tests/CSharpRepl.Tests/CompletionTests.cs @@ -46,9 +46,9 @@ public async Task Complete_GivenCode_ReturnsCompletions() } [Fact] - public async Task Complete_ReplaceCommand_IsInspectModeOnly() + public async Task Complete_ReplaceCommand_IsConnectModeOnly() { - // #replace is an inspect-mode command. Its completion provider is registered only in the remote + // #replace is a connect-mode command. Its completion provider is registered only in the remote // workspace, so a #replace line in the local REPL yields no type/member completions. const string text = "#replace System.Console."; var completions = await this.services.CompleteAsync(text, text.Length, TestContext.Current.CancellationToken); @@ -56,9 +56,9 @@ public async Task Complete_ReplaceCommand_IsInspectModeOnly() } [Fact] - public async Task Complete_InspectCommandNames_AreInspectModeOnly() + public async Task Complete_ConnectCommandNames_AreConnectModeOnly() { - // The local REPL isn't inspect mode, so #replace/#wrap/etc. aren't offered as command-name completions. + // The local REPL isn't connect mode, so #replace/#wrap/etc. aren't offered as command-name completions. var completions = await promptCallbacks.GetCompletionItemsCoreAsync("#re", 3, TestContext.Current.CancellationToken); Assert.DoesNotContain(completions, c => c.DisplayText == "#replace"); } @@ -67,9 +67,9 @@ public async Task Complete_InspectCommandNames_AreInspectModeOnly() [InlineData("#re", 3, 0, 3)] [InlineData(" #rep", 6, 2, 4)] [InlineData("#replace", 8, 0, 8)] - public void InspectorCommandSpan_CoversTheHashToken(string text, int caret, int expectedStart, int expectedLength) + public void ConnectorCommandSpan_CoversTheHashToken(string text, int caret, int expectedStart, int expectedLength) { - Assert.True(CSharpReplPromptCallbacks.TryGetInspectorCommandSpan(text, caret, out var span)); + Assert.True(CSharpReplPromptCallbacks.TryGetConnectorCommandSpan(text, caret, out var span)); Assert.Equal(expectedStart, span.Start); Assert.Equal(expectedLength, span.Length); } @@ -78,9 +78,9 @@ public void InspectorCommandSpan_CoversTheHashToken(string text, int caret, int [InlineData("#replace Foo.", 13)] // a space → argument position, handled by the completion provider, not the command list [InlineData("hello", 5)] // no leading '#' [InlineData(" ", 3)] // whitespace only - public void InspectorCommandSpan_RejectsNonCommandLines(string text, int caret) + public void ConnectorCommandSpan_RejectsNonCommandLines(string text, int caret) { - Assert.False(CSharpReplPromptCallbacks.TryGetInspectorCommandSpan(text, caret, out _)); + Assert.False(CSharpReplPromptCallbacks.TryGetConnectorCommandSpan(text, caret, out _)); } [Fact] diff --git a/Tests/CSharpRepl.Tests/InspectorCancellationTests.cs b/Tests/CSharpRepl.Tests/ConnectorCancellationTests.cs similarity index 96% rename from Tests/CSharpRepl.Tests/InspectorCancellationTests.cs rename to Tests/CSharpRepl.Tests/ConnectorCancellationTests.cs index 95df170f..626a722d 100644 --- a/Tests/CSharpRepl.Tests/InspectorCancellationTests.cs +++ b/Tests/CSharpRepl.Tests/ConnectorCancellationTests.cs @@ -19,12 +19,12 @@ namespace CSharpRepl.Tests; /// a sleeping/CPU-bound submission isn't actually interrupted; the point under test is that the channel does /// not desync and a subsequent evaluation still works. /// -public class InspectorCancellationTests +public class ConnectorCancellationTests { [Fact(Timeout = 120_000)] public async Task Cancelling_AnInFlightEval_KeepsTheChannelInSyncAndTheSessionAlive() { - using var process = InspectorTestSupport.StartHookedTarget(); + using var process = ConnectorTestSupport.StartHookedTarget(); var stdoutTask = process.StandardOutput.ReadToEndAsync(TestContext.Current.CancellationToken); var stderrTask = process.StandardError.ReadToEndAsync(TestContext.Current.CancellationToken); diff --git a/Tests/CSharpRepl.Tests/InspectorDiscoveryTests.cs b/Tests/CSharpRepl.Tests/ConnectorDiscoveryTests.cs similarity index 78% rename from Tests/CSharpRepl.Tests/InspectorDiscoveryTests.cs rename to Tests/CSharpRepl.Tests/ConnectorDiscoveryTests.cs index 4992d89f..e9e664cf 100644 --- a/Tests/CSharpRepl.Tests/InspectorDiscoveryTests.cs +++ b/Tests/CSharpRepl.Tests/ConnectorDiscoveryTests.cs @@ -15,12 +15,12 @@ namespace CSharpRepl.Tests; /// -/// Tests for the `inspect list` discovery layer ( and -/// ). +/// Tests for the `connect list` discovery layer ( and +/// ). /// A mix of unit tests (parsing) and integration tests (launch a real hooked target and proves the /// target's pid is discovered while a non-hooked process (this test runner) is not. /// -public class InspectorDiscoveryTests +public class ConnectorDiscoveryTests { [Theory] [InlineData(1)] @@ -29,11 +29,11 @@ public class InspectorDiscoveryTests public void TryParseProcessId_RoundTripsTheEndpointNamesItBuilds(int processId) { // Round-trip through the real builders so the parser and PipeName/SocketPath can't drift apart. - Assert.True(InspectorTransport.TryParseProcessId(InspectorTransport.PipeName(processId), out var fromPipe)); + Assert.True(ConnectorTransport.TryParseProcessId(ConnectorTransport.PipeName(processId), out var fromPipe)); Assert.Equal(processId, fromPipe); - var socketName = Path.GetFileName(InspectorTransport.SocketPath(processId)); - Assert.True(InspectorTransport.TryParseProcessId(socketName, out var fromSocket)); + var socketName = Path.GetFileName(ConnectorTransport.SocketPath(processId)); + Assert.True(ConnectorTransport.TryParseProcessId(socketName, out var fromSocket)); Assert.Equal(processId, fromSocket); } @@ -46,26 +46,26 @@ public void TryParseProcessId_RoundTripsTheEndpointNamesItBuilds(int processId) [InlineData("CSharpRepl.InjectedHook.0")] // pids are positive [InlineData("CSharpRepl.InjectedHook.12.3")] // not an integer [InlineData("dotnet-diagnostic-1234")] // a real .NET diagnostic-port pipe must NOT be claimed as ours - [InlineData("inspector-1234.txt")] // wrong suffix - [InlineData("inspector-.sock")] // socket form but no number - [InlineData("inspector-abc.sock")] // socket form, not numeric + [InlineData("connector-1234.txt")] // wrong suffix + [InlineData("connector-.sock")] // socket form but no number + [InlineData("connector-abc.sock")] // socket form, not numeric [InlineData("some-unrelated-pipe")] public void TryParseProcessId_RejectsAnythingThatIsNotOurEndpoint(string? endpointName) { - Assert.False(InspectorTransport.TryParseProcessId(endpointName!, out _)); + Assert.False(ConnectorTransport.TryParseProcessId(endpointName!, out _)); } [Fact] public void EnumerateListeningProcessIds_DoesNotListThisNonHookedProcess() { - // The test runner wasn't launched with the inspector hook, so it must never appear as attachable. - Assert.DoesNotContain(Environment.ProcessId, InspectorTransport.EnumerateListeningProcessIds()); + // The test runner wasn't launched with the connector hook, so it must never appear as attachable. + Assert.DoesNotContain(Environment.ProcessId, ConnectorTransport.EnumerateListeningProcessIds()); } [Fact(Timeout = 120_000)] public async Task EnumerateListeningProcessIds_FindsAHookedTarget() { - using var process = InspectorTestSupport.StartHookedTarget(); + using var process = ConnectorTestSupport.StartHookedTarget(); // Drain the child's output so a full pipe buffer can't block it; the content isn't needed. var stdoutTask = process.StandardOutput.ReadToEndAsync(TestContext.Current.CancellationToken); @@ -77,7 +77,7 @@ public async Task EnumerateListeningProcessIds_FindsAHookedTarget() // so poll rather than assuming it's immediately present. var found = await WaitUntilListedAsync(process.Id, TimeSpan.FromSeconds(30)); Assert.True(found, - $"The hooked target (pid {process.Id}) should be discoverable via its inspector endpoint."); + $"The hooked target (pid {process.Id}) should be discoverable via its connector endpoint."); } finally { @@ -92,7 +92,7 @@ private static async Task WaitUntilListedAsync(int processId, TimeSpan tim var stopwatch = Stopwatch.StartNew(); while (stopwatch.Elapsed < timeout) { - if (InspectorTransport.EnumerateListeningProcessIds().Contains(processId)) + if (ConnectorTransport.EnumerateListeningProcessIds().Contains(processId)) return true; await Task.Delay(200, TestContext.Current.CancellationToken); } diff --git a/Tests/CSharpRepl.Tests/InspectorEngineTests.cs b/Tests/CSharpRepl.Tests/ConnectorEngineTests.cs similarity index 98% rename from Tests/CSharpRepl.Tests/InspectorEngineTests.cs rename to Tests/CSharpRepl.Tests/ConnectorEngineTests.cs index 621e9fe8..d4e15643 100644 --- a/Tests/CSharpRepl.Tests/InspectorEngineTests.cs +++ b/Tests/CSharpRepl.Tests/ConnectorEngineTests.cs @@ -19,7 +19,7 @@ namespace CSharpRepl.Tests; /// assemblies and bind to its live objects (see ). The cross-process transport /// and server around the engine are covered by the hooked-child-process tests. /// -public class InspectorEngineTests : IClassFixture +public class ConnectorEngineTests : IClassFixture { /// /// One engine for the whole class: its submissions share a persisted state chain (like a real session), @@ -28,12 +28,12 @@ public class InspectorEngineTests : IClassFixture public sealed class EngineFixture { - public InspectorEngine Engine { get; } = new(); + public ConnectorEngine Engine { get; } = new(); } - private readonly InspectorEngine engine; + private readonly ConnectorEngine engine; - public InspectorEngineTests(EngineFixture fixture) => this.engine = fixture.Engine; + public ConnectorEngineTests(EngineFixture fixture) => this.engine = fixture.Engine; private Task EvalAsync(string code, bool detailed = false) => engine.EvalAsync(code, detailed, TestContext.Current.CancellationToken); @@ -347,7 +347,7 @@ public async Task ReplaceMethod_CoercesOverloadedMethodGroups_ViaGeneratedDelega // A *single* named method has a natural delegate type (C# 10+), so it evaluates as a value and takes the // delegate-value path. An *overloaded* method group has no natural type, so it can only be coerced by // casting it to the delegate each candidate expects — the engine's method-group path, which builds the - // cast (InspectorPatcher.BuildCastDelegate / BuildParameterList / ParameterText / DelegateTypeText). + // cast (ConnectorPatcher.BuildCastDelegate / BuildParameterList / ParameterText / DelegateTypeText). var ct = TestContext.Current.CancellationToken; try { @@ -454,13 +454,13 @@ public async Task GetReferencePaths_ReportsTheHostProcessesLoadedAssemblies() Assert.NotEmpty(paths); Assert.Contains(typeof(object).Assembly.Location, paths); - Assert.Contains(typeof(InspectorEngineTests).Assembly.Location, paths); + Assert.Contains(typeof(ConnectorEngineTests).Assembly.Location, paths); Assert.All(paths, p => Assert.True(File.Exists(p), $"Reported reference path does not exist: '{p}'")); } } /// -/// Mutable static state in the test assembly for submissions to bind to — +/// Mutable static state in the test assembly for submissions to bind to — /// the in-process stand-in for the hooked target's Program.WriteProbe-style statics. /// public static class EngineTestProbe diff --git a/Tests/CSharpRepl.Tests/InspectorRoundTripTests.cs b/Tests/CSharpRepl.Tests/ConnectorRoundTripTests.cs similarity index 96% rename from Tests/CSharpRepl.Tests/InspectorRoundTripTests.cs rename to Tests/CSharpRepl.Tests/ConnectorRoundTripTests.cs index 240daf02..bfaa6e80 100644 --- a/Tests/CSharpRepl.Tests/InspectorRoundTripTests.cs +++ b/Tests/CSharpRepl.Tests/ConnectorRoundTripTests.cs @@ -15,20 +15,20 @@ namespace CSharpRepl.Tests; /// -/// End-to-end test for the cooperative in-process Roslyn inspector. Launches the unmodified test target -/// as a separate process with the inspector injected via DOTNET_STARTUP_HOOKS, connects over the transport, +/// End-to-end test for the cooperative in-process Roslyn connector. Launches the unmodified test target +/// as a separate process with the connector injected via DOTNET_STARTUP_HOOKS, connects over the transport, /// and exercises the full round-trip: handshake, value evaluation, live static read, REPL parity (a var and /// a declared method reused across submissions binding to the live object), exception survival, cross-process /// write-back, and a graceful disconnect leaving the target running. /// -public class InspectorRoundTripTests +public class ConnectorRoundTripTests { private const string TargetType = "CSharpRepl.InjectedHook.TestTarget.Program"; [Fact(Timeout = 120_000)] public async Task RoundTrip_AgainstHookedProcess_EvaluatesReadsWritesAndDisconnects() { - using var process = InspectorTestSupport.StartHookedTarget(); + using var process = ConnectorTestSupport.StartHookedTarget(); // Drain the child's output so a full pipe buffer can't block it; results are only needed for diagnostics. var stdoutTask = process.StandardOutput.ReadToEndAsync(TestContext.Current.CancellationToken); @@ -40,7 +40,7 @@ public async Task RoundTrip_AgainstHookedProcess_EvaluatesReadsWritesAndDisconne // --- Handshake --- Assert.Equal(process.Id, session.Handshake.ProcessId); - Assert.Equal(InspectorTransport.ProtocolVersion, session.Handshake.ProtocolVersion); + Assert.Equal(ConnectorTransport.ProtocolVersion, session.Handshake.ProtocolVersion); Assert.False(string.IsNullOrEmpty(session.Handshake.SessionId)); // A normal `dotnet App` launch has its assemblies on disk (not a single-file bundle). Assert.Equal(TargetAssemblyAvailability.Normal, session.Handshake.AssemblyAvailability); @@ -126,7 +126,7 @@ public async Task RoundTrip_AgainstHookedProcess_EvaluatesReadsWritesAndDisconne // --- DI-root capture: the bootstrap captures the host's root provider from the HostBuilt // DiagnosticListener event, and Get() resolves the target's real live singleton. // The capture races with connecting (the transport is up before Main builds the host), - // so poll: the globals read InspectorRoots live, no reconnect needed. --- + // so poll: the globals read ConnectorRoots live, no reconnect needed. --- await WaitForDiCaptureAsync(session); var resolved = await EvalAsync(session, "Get().Value"); Assert.Equal(ResultKind.Value, resolved.Kind); @@ -172,7 +172,7 @@ private static async Task ConnectAsync(Process process) catch (Exception ex) when (process.HasExited) { throw new InvalidOperationException( - $"The target process exited (code {process.ExitCode}) before the inspector connection succeeded.", ex); + $"The target process exited (code {process.ExitCode}) before the connector connection succeeded.", ex); } } diff --git a/Tests/CSharpRepl.Tests/InspectorServerProtocolTests.cs b/Tests/CSharpRepl.Tests/ConnectorServerProtocolTests.cs similarity index 91% rename from Tests/CSharpRepl.Tests/InspectorServerProtocolTests.cs rename to Tests/CSharpRepl.Tests/ConnectorServerProtocolTests.cs index 50271447..584a20a4 100644 --- a/Tests/CSharpRepl.Tests/InspectorServerProtocolTests.cs +++ b/Tests/CSharpRepl.Tests/ConnectorServerProtocolTests.cs @@ -16,19 +16,19 @@ namespace CSharpRepl.Tests; /// -/// Drives the injected InspectorServer at the raw wire-protocol level (a hand-rolled MessageChannel -/// instead of InspectorClient) against a real hooked child process, covering server behaviors a well-behaved +/// Drives the injected ConnectorServer at the raw wire-protocol level (a hand-rolled MessageChannel +/// instead of ConnectorClient) against a real hooked child process, covering server behaviors a well-behaved /// controller never triggers: a stray cancel with no evaluation in flight, a malformed frame tearing down /// only that one connection (the accept loop then serves a fresh controller on the same live instance), and /// the per-process-instance session id surviving across connections. /// -public class InspectorServerProtocolTests +public class ConnectorServerProtocolTests { [Fact(Timeout = 120_000)] public async Task Server_SurvivesProtocolErrors_AndKeepsServingNewConnections() { var cancellationToken = TestContext.Current.CancellationToken; - using var process = InspectorTestSupport.StartHookedTarget(); + using var process = ConnectorTestSupport.StartHookedTarget(); // Drain the child's output so a full pipe buffer can't block it; results are only needed for diagnostics. var stdoutTask = process.StandardOutput.ReadToEndAsync(cancellationToken); @@ -37,11 +37,11 @@ public async Task Server_SurvivesProtocolErrors_AndKeepsServingNewConnections() try { // --- First connection: handshake fields, a stray cancel, then a normal evaluation --- - await using var stream = await InspectorTransport.ConnectAsync(process.Id, TimeSpan.FromSeconds(30), cancellationToken); + await using var stream = await ConnectorTransport.ConnectAsync(process.Id, TimeSpan.FromSeconds(30), cancellationToken); var channel = new MessageChannel(stream); var handshake = Assert.IsType(await channel.ReadAsync(cancellationToken)); - Assert.Equal(InspectorTransport.ProtocolVersion, handshake.ProtocolVersion); - Assert.NotEmpty(handshake.InspectorVersion); + Assert.Equal(ConnectorTransport.ProtocolVersion, handshake.ProtocolVersion); + Assert.NotEmpty(handshake.ConnectorVersion); Assert.NotEmpty(handshake.RuntimeVersion); Assert.NotEmpty(handshake.ProcessName); @@ -77,7 +77,7 @@ public async Task Server_SurvivesProtocolErrors_AndKeepsServingNewConnections() $"Expected a closed connection, got: {dropError?.GetType().Name}"); // --- The accept loop recovers: a fresh controller is served by the same live process instance --- - await using var secondStream = await InspectorTransport.ConnectAsync(process.Id, TimeSpan.FromSeconds(30), cancellationToken); + await using var secondStream = await ConnectorTransport.ConnectAsync(process.Id, TimeSpan.FromSeconds(30), cancellationToken); var secondChannel = new MessageChannel(secondStream); var secondHandshake = Assert.IsType(await secondChannel.ReadAsync(cancellationToken)); Assert.Equal(handshake.SessionId, secondHandshake.SessionId); // confirms a non-stale, same-instance reconnect diff --git a/Tests/CSharpRepl.Tests/InspectorTestSupport.cs b/Tests/CSharpRepl.Tests/ConnectorTestSupport.cs similarity index 79% rename from Tests/CSharpRepl.Tests/InspectorTestSupport.cs rename to Tests/CSharpRepl.Tests/ConnectorTestSupport.cs index de28dc0b..9a3d375c 100644 --- a/Tests/CSharpRepl.Tests/InspectorTestSupport.cs +++ b/Tests/CSharpRepl.Tests/ConnectorTestSupport.cs @@ -11,16 +11,16 @@ namespace CSharpRepl.Tests; /// -/// Shared helpers for the inspector integration tests: locate the built bootstrap, build the test-target -/// fixture, and launch the unmodified target as a separate process with the inspector injected via -/// DOTNET_STARTUP_HOOKS. The target knows nothing about the inspector; the hook brings it up before Main. +/// Shared helpers for the connector integration tests: locate the built bootstrap, build the test-target +/// fixture, and launch the unmodified target as a separate process with the connector injected via +/// DOTNET_STARTUP_HOOKS. The target knows nothing about the connector; the hook brings it up before Main. /// -internal static class InspectorTestSupport +internal static class ConnectorTestSupport { private const string TestTargetName = "CSharpRepl.InjectedHook.TestTarget"; // The test target is a fixture project under Data\ (copied next to the test via Content), built on demand - // like the solution fixtures. Built once per test process and shared across the inspector test classes. + // like the solution fixtures. Built once per test process and shared across the connector test classes. private static readonly Lazy builtTestTarget = new(BuildTestTarget); public static Process StartHookedTarget() @@ -32,20 +32,20 @@ public static Process StartHookedTarget() UseShellExecute = false, CreateNoWindow = true, }; - startInfo.Environment["DOTNET_STARTUP_HOOKS"] = ResolveInspectorBootstrap(); + startInfo.Environment["DOTNET_STARTUP_HOOKS"] = ResolveConnectorBootstrap(); startInfo.Environment["NO_COLOR"] = "1"; return Process.Start(startInfo) - ?? throw new InvalidOperationException("Failed to start the inspector test target process."); + ?? throw new InvalidOperationException("Failed to start the connector test target process."); } - public static string ResolveInspectorBootstrap() + public static string ResolveConnectorBootstrap() { var dir = AppContext.BaseDirectory.Replace( Path.Combine("Tests", "CSharpRepl.Tests", "bin"), Path.Combine("InjectedHook", "CSharpRepl.InjectedHook", "bin")); var bootstrap = Path.Combine(dir, "CSharpRepl.InjectedHook.dll"); - Assert.True(File.Exists(bootstrap), $"Could not find the built inspector bootstrap at '{bootstrap}'."); + Assert.True(File.Exists(bootstrap), $"Could not find the built connector bootstrap at '{bootstrap}'."); return bootstrap; } @@ -56,18 +56,18 @@ private static string BuildTestTarget() // The fixture is copied next to the test by Content Include="Data\**"; build it in place. var projectDirectory = Path.Combine(AppContext.BaseDirectory, "Data", TestTargetName); Assert.True(Directory.Exists(projectDirectory), - $"Inspector test target fixture not found at '{projectDirectory}'. Is it copied via Content Include=\"Data\\**\"?"); + $"Connector test target fixture not found at '{projectDirectory}'. Is it copied via Content Include=\"Data\\**\"?"); var (console, _) = FakeConsole.CreateStubbedOutput(); var (exitCode, output) = new DotnetBuilder(console).Build(projectDirectory); Assert.True(exitCode == 0, - $"Building the inspector test target failed (exit {exitCode}):{Environment.NewLine}{string.Join(Environment.NewLine, output)}"); + $"Building the connector test target failed (exit {exitCode}):{Environment.NewLine}{string.Join(Environment.NewLine, output)}"); // DotnetBuilder doesn't pass a configuration, so it always produces the Debug apphost (matching the // other on-demand-built fixtures, e.g. DemoProject3). var executable = Path.Combine(projectDirectory, "bin", "Debug", "net10.0", OperatingSystem.IsWindows() ? TestTargetName + ".exe" : TestTargetName); - Assert.True(File.Exists(executable), $"Built inspector test target not found at '{executable}'."); + Assert.True(File.Exists(executable), $"Built connector test target not found at '{executable}'."); return executable; } } diff --git a/Tests/CSharpRepl.Tests/InspectorTransportTests.cs b/Tests/CSharpRepl.Tests/ConnectorTransportTests.cs similarity index 95% rename from Tests/CSharpRepl.Tests/InspectorTransportTests.cs rename to Tests/CSharpRepl.Tests/ConnectorTransportTests.cs index 1e78d9e7..38afdb22 100644 --- a/Tests/CSharpRepl.Tests/InspectorTransportTests.cs +++ b/Tests/CSharpRepl.Tests/ConnectorTransportTests.cs @@ -20,7 +20,7 @@ namespace CSharpRepl.Tests; /// -/// Integration tests for the inspector's wire layer over the real OS transport (a named pipe on Windows, a +/// Integration tests for the connector's wire layer over the real OS transport (a named pipe on Windows, a /// Unix domain socket elsewhere): listener/client connection with the production security options, /// length-prefixed polymorphic JSON framing, write serialization, reconnect after a controller leaves, and /// the defensive handling of torn/oversized/malformed frames from a buggy or hostile peer. @@ -28,7 +28,7 @@ namespace CSharpRepl.Tests; /// The Windows pipe is created with zero-byte buffers, so a write rendezvouses with the peer's read; like the /// production server/controller loops, these tests always have the read pending while the write is in flight. /// -public class InspectorTransportTests +public class ConnectorTransportTests { // Each test gets its own endpoint: pipe/socket names are keyed by "process id", so derive unique fake ids // from this process's real pid to avoid colliding with concurrent tests or a genuinely hooked process. @@ -39,7 +39,7 @@ public async Task TransportAndChannel_RoundTripMessages_AndAcceptAReconnect() { var cancellationToken = TestContext.Current.CancellationToken; var processId = FakeProcessId(1); - using var listener = new InspectorTransportListener(processId); + using var listener = new ConnectorTransportListener(processId); var (clientStream, serverStream) = await ConnectPairAsync(listener, processId, cancellationToken); await using (clientStream) @@ -53,7 +53,7 @@ public async Task TransportAndChannel_RoundTripMessages_AndAcceptAReconnect() { ProcessId = processId, ProcessName = "transport-test", - ProtocolVersion = InspectorTransport.ProtocolVersion, + ProtocolVersion = ConnectorTransport.ProtocolVersion, AssemblyAvailability = TargetAssemblyAvailability.FrameworkDependentSingleFile, SessionId = "session-1", }, cancellationToken)); @@ -140,7 +140,7 @@ public async Task Channel_RejectsTornOversizedAndMalformedFrames() { var cancellationToken = TestContext.Current.CancellationToken; var processId = FakeProcessId(2); - using var listener = new InspectorTransportListener(processId); + using var listener = new ConnectorTransportListener(processId); // Torn frame: the header promises 16 bytes but the peer vanishes after 4 — never silently truncated. { @@ -198,9 +198,9 @@ static byte[] FrameHeader(int declaredLength) } [Fact(Timeout = 60_000)] - public async Task Connect_TimesOut_WhenNoInspectorIsListening() + public async Task Connect_TimesOut_WhenNoConnectorIsListening() { - var exception = await Record.ExceptionAsync(() => InspectorTransport.ConnectAsync( + var exception = await Record.ExceptionAsync(() => ConnectorTransport.ConnectAsync( FakeProcessId(3), TimeSpan.FromMilliseconds(300), TestContext.Current.CancellationToken)); // TimeoutException from the Windows named pipe; SocketException once the Unix retry deadline elapses. @@ -218,10 +218,10 @@ public async Task Connect_TimesOut_WhenNoInspectorIsListening() } private static async Task<(Stream Client, Stream Server)> ConnectPairAsync( - InspectorTransportListener listener, int processId, CancellationToken cancellationToken) + ConnectorTransportListener listener, int processId, CancellationToken cancellationToken) { var acceptTask = listener.AcceptAsync(cancellationToken); - var client = await InspectorTransport.ConnectAsync(processId, TimeSpan.FromSeconds(30), cancellationToken); + var client = await ConnectorTransport.ConnectAsync(processId, TimeSpan.FromSeconds(30), cancellationToken); return (client, await acceptTask); } } diff --git a/Tests/CSharpRepl.Tests/Data/CSharpRepl.InjectedHook.TestTarget/CSharpRepl.InjectedHook.TestTarget.csproj b/Tests/CSharpRepl.Tests/Data/CSharpRepl.InjectedHook.TestTarget/CSharpRepl.InjectedHook.TestTarget.csproj index 02e3bf50..b60577df 100644 --- a/Tests/CSharpRepl.Tests/Data/CSharpRepl.InjectedHook.TestTarget/CSharpRepl.InjectedHook.TestTarget.csproj +++ b/Tests/CSharpRepl.Tests/Data/CSharpRepl.InjectedHook.TestTarget/CSharpRepl.InjectedHook.TestTarget.csproj @@ -1,10 +1,10 @@ Exe diff --git a/Tests/CSharpRepl.Tests/Data/CSharpRepl.InjectedHook.TestTarget/Program.cs b/Tests/CSharpRepl.Tests/Data/CSharpRepl.InjectedHook.TestTarget/Program.cs index 6d54661b..27bb27f3 100644 --- a/Tests/CSharpRepl.Tests/Data/CSharpRepl.InjectedHook.TestTarget/Program.cs +++ b/Tests/CSharpRepl.Tests/Data/CSharpRepl.InjectedHook.TestTarget/Program.cs @@ -13,18 +13,18 @@ namespace CSharpRepl.InjectedHook.TestTarget; /// -/// An ordinary app with mutable static state. It knows nothing about the inspector — the integration -/// test injects the inspector via DOTNET_STARTUP_HOOKS and reads/writes this live state across the process -/// boundary. Stays alive for a bounded time so the test can connect and evaluate; the test kills it when done. +/// An ordinary app with mutable static state. It knows nothing about the connector — the integration +/// test injects the connector via DOTNET_STARTUP_HOOKS and reads/writes this live state across the process +/// boundary. Stays alive for a bounded time so the test ca connect and evaluate; the test kills it when done. /// It builds (but doesn't run) a Generic Host with a singleton so the tests can also prove the bootstrap's /// DI-root capture: building the host emits the hosting DiagnosticListener's HostBuilt event. /// public static class Program { - /// Climbs over time so the inspector can observe live, changing state. + /// Climbs over time so the connector can observe live, changing state. public static int Counter; - /// Written by the inspector and re-read to prove cross-process writes land on the real static. + /// Written by the connector and re-read to prove cross-process writes land on the real static. public static int WriteProbe; /// A live object instance for REPL-parity tests (bind a var to it, reuse across submissions). @@ -50,7 +50,7 @@ public static void Main() } } -/// A small domain type the inspector binds a local to, to prove cross-submission parity. +/// A small domain type the connector binds a local to, to prove cross-submission parity. public sealed class Service { public int Value { get; set; } = 41; diff --git a/Tests/CSharpRepl.Tests/Data/Directory.Packages.props b/Tests/CSharpRepl.Tests/Data/Directory.Packages.props index 0d80104b..cc1d640a 100644 --- a/Tests/CSharpRepl.Tests/Data/Directory.Packages.props +++ b/Tests/CSharpRepl.Tests/Data/Directory.Packages.props @@ -8,7 +8,7 @@ This file is copied alongside the fixtures into bin\...\Data\ via the test project's Content Include="Data\**", so it also covers the copies built in-place at test time - (e.g. InspectorTestSupport.BuildTestTarget against the TestTarget under bin\...\Data\). + (e.g. ConnectorTestSupport.BuildTestTarget against the TestTarget under bin\...\Data\). --> false diff --git a/Tests/CSharpRepl.Tests/RemoteEditorServicesTests.cs b/Tests/CSharpRepl.Tests/RemoteEditorServicesTests.cs index 510b1b50..e411de06 100644 --- a/Tests/CSharpRepl.Tests/RemoteEditorServicesTests.cs +++ b/Tests/CSharpRepl.Tests/RemoteEditorServicesTests.cs @@ -22,9 +22,9 @@ namespace CSharpRepl.Tests; /// /// End-to-end test for the controller-side remote editor services. Launches the unmodified test target with -/// the inspector injected, fetches the target's loaded-assembly paths over the wire, and builds a +/// the connector injected, fetches the target's loaded-assembly paths over the wire, and builds a /// remote-configured from them. Then asserts the editor services are target-aware: -/// the inspector globals (services/Get) complete, the target's own types complete and highlight +/// the connector globals (services/Get) complete, the target's own types complete and highlight /// as types, and a declared var from a committed submission is usable on the next line — the /// cross-process analogue of the local REPL's submission-chain parity. /// @@ -39,7 +39,7 @@ public class RemoteEditorServicesTests [Fact(Timeout = 120_000)] public async Task RemoteEditor_AgainstHookedProcess_IsTargetAware() { - using var process = InspectorTestSupport.StartHookedTarget(); + using var process = ConnectorTestSupport.StartHookedTarget(); var stdoutTask = process.StandardOutput.ReadToEndAsync(TestContext.Current.CancellationToken); var stderrTask = process.StandardError.ReadToEndAsync(TestContext.Current.CancellationToken); @@ -52,11 +52,11 @@ public async Task RemoteEditor_AgainstHookedProcess_IsTargetAware() Assert.Contains(referencePaths, p => p.EndsWith($"{TargetNamespace}.dll", StringComparison.OrdinalIgnoreCase)); var (console, _) = FakeConsole.CreateStubbedOutput(); - var remoteEditor = new RemoteEditorContext(referencePaths, typeof(InspectorGlobals)); + var remoteEditor = new RemoteEditorContext(referencePaths, typeof(ConnectorGlobals)); var services = new RoslynServices(console, new Configuration(theme: "Data/theme.json"), new TestTraceLogger(), remoteEditor); await services.WarmUpAsync([]); // awaits background initialization and warms the editor path - // --- Inspector globals are in scope via the workspace's host object type --- + // --- Connector globals are in scope via the workspace's host object type --- Assert.Contains("services", await CompletionDisplayTextsAsync(services, "ser")); Assert.Contains("Get", await CompletionDisplayTextsAsync(services, "Ge")); @@ -101,7 +101,7 @@ public async Task RemoteEditor_AgainstHookedProcess_IsTargetAware() var afterLetter = $"#replace {TargetNamespace}.Servi"; Assert.True(await services.ShouldOpenCompletionWindowAsync(afterLetter, afterLetter.Length, letterKey, TestContext.Current.CancellationToken)); - // A non-command line: the inspect provider declines (TryRewrite false), leaving the decision to others. + // A non-command line: the connect provider declines (TryRewrite false), leaving the decision to others. var ordinaryLine = $"{TargetNamespace}.Servi"; await services.ShouldOpenCompletionWindowAsync(ordinaryLine, ordinaryLine.Length, letterKey, TestContext.Current.CancellationToken); @@ -113,7 +113,7 @@ public async Task RemoteEditor_AgainstHookedProcess_IsTargetAware() var description = await computeItem.GetDescriptionAsync(TestContext.Current.CancellationToken); Assert.Contains("Compute", description.Text); - // --- The inspect commands themselves complete (with help text), via the prompt callbacks --- + // --- The connect commands themselves complete (with help text), via the prompt callbacks --- var promptCallbacks = new CSharpReplPromptCallbacks(console, services, new Configuration(theme: "Data/theme.json")); var commandItems = await promptCallbacks.GetCompletionItemsCoreAsync("#re", 3, TestContext.Current.CancellationToken); var commandTexts = commandItems.Select(c => c.ReplacementText).ToList(); @@ -155,7 +155,7 @@ private static async Task ConnectAsync(Process process) catch (Exception ex) when (process.HasExited) { throw new InvalidOperationException( - $"The target process exited (code {process.ExitCode}) before the inspector connection succeeded.", ex); + $"The target process exited (code {process.ExitCode}) before the connector connection succeeded.", ex); } } } diff --git a/Tests/CSharpRepl.Tests/RemotePipedInputEvaluatorTests.cs b/Tests/CSharpRepl.Tests/RemotePipedInputEvaluatorTests.cs index 947fadaa..922071bb 100644 --- a/Tests/CSharpRepl.Tests/RemotePipedInputEvaluatorTests.cs +++ b/Tests/CSharpRepl.Tests/RemotePipedInputEvaluatorTests.cs @@ -14,9 +14,9 @@ namespace CSharpRepl.Tests; /// -/// End-to-end test for the non-interactive inspect path: drives against a +/// End-to-end test for the non-interactive connect path: drives against a /// real hooked child via a real . Verifies clean plain-text stdout, errors to stderr -/// with a nonzero exit, inspect commands, and that engine state persists across separate evaluations. +/// with a nonzero exit, connect commands, and that engine state persists across separate evaluations. /// public class RemotePipedInputEvaluatorTests { @@ -26,7 +26,7 @@ public class RemotePipedInputEvaluatorTests public async Task EvaluatesNonInteractively_AgainstAHookedProcess() { var cancellationToken = TestContext.Current.CancellationToken; - using var process = InspectorTestSupport.StartHookedTarget(); + using var process = ConnectorTestSupport.StartHookedTarget(); var stdoutTask = process.StandardOutput.ReadToEndAsync(cancellationToken); var stderrTask = process.StandardError.ReadToEndAsync(cancellationToken); @@ -73,7 +73,7 @@ public async Task EvaluatesNonInteractively_AgainstAHookedProcess() Assert.Contains("boom", stderr.ToString()); } - // --- An inspect command (#patches) is honored non-interactively and renders to stdout --- + // --- A connect command (#patches) is honored non-interactively and renders to stdout --- { var (console, stdout, _) = FakeConsole.CreateStubbedOutputAndError(); var evaluator = NewEvaluator(console, session); diff --git a/Tests/CSharpRepl.Tests/RemoteReadEvalPrintLoopTests.cs b/Tests/CSharpRepl.Tests/RemoteReadEvalPrintLoopTests.cs index 503588b5..3e74647f 100644 --- a/Tests/CSharpRepl.Tests/RemoteReadEvalPrintLoopTests.cs +++ b/Tests/CSharpRepl.Tests/RemoteReadEvalPrintLoopTests.cs @@ -17,7 +17,7 @@ namespace CSharpRepl.Tests; /// -/// End-to-end test for the inspect-mode REPL loop. Launches the unmodified test target with the inspector +/// End-to-end test for the connect-mode REPL loop. Launches the unmodified test target with the connector /// injected, builds the same controller stack Program.cs wires up (a real plus a /// remote-configured ), and drives /// with a scripted prompt: banner, commands, value/exception rendering through the themed renderer, the @@ -30,10 +30,10 @@ public class RemoteReadEvalPrintLoopTests private const string ServiceType = "CSharpRepl.InjectedHook.TestTarget.Service"; [Fact(Timeout = 180_000)] - public async Task RunAsync_DrivesAFullInspectSession_AgainstAHookedProcess() + public async Task RunAsync_DrivesAFullConnectSession_AgainstAHookedProcess() { var cancellationToken = TestContext.Current.CancellationToken; - using var process = InspectorTestSupport.StartHookedTarget(); + using var process = ConnectorTestSupport.StartHookedTarget(); var stdoutTask = process.StandardOutput.ReadToEndAsync(cancellationToken); var stderrTask = process.StandardError.ReadToEndAsync(cancellationToken); @@ -41,12 +41,12 @@ public async Task RunAsync_DrivesAFullInspectSession_AgainstAHookedProcess() { await using var session = await RemoteSession.ConnectAsync(process.Id, TimeSpan.FromSeconds(30), cancellationToken); - // The same controller stack Program.cs builds for inspect mode: editor services seeded with the - // target's references and the inspector globals, rendering through the user's theme. + // The same controller stack Program.cs builds for connect mode: editor services seeded with the + // target's references and the connector globals, rendering through the user's theme. var referencePaths = await session.GetReferencePathsAsync(cancellationToken); var (console, capturedStdout, _) = FakeConsole.CreateStubbedOutputAndError(); var roslyn = new RoslynServices(console, new Configuration(), new TestTraceLogger(), - new RemoteEditorContext(referencePaths, typeof(InspectorGlobals))); + new RemoteEditorContext(referencePaths, typeof(ConnectorGlobals))); await roslyn.WarmUpAsync([]); var prompt = Substitute.For(); @@ -86,9 +86,9 @@ public async Task RunAsync_DrivesAFullInspectSession_AgainstAHookedProcess() var output = console.AnsiConsole.Output; - // Banner built from the live handshake, plus the help command's inspect-mode text. + // Banner built from the live handshake, plus the help command's connect-mode text. Assert.Contains($"(pid {process.Id})", output); - Assert.Contains("Inspect mode", output); + Assert.Contains("Connect mode", output); console.Received().Clear(); // The live target object rendered at both levels: its value, and its members in the detailed tree. diff --git a/Tests/CSharpRepl.Tests/RemoteValueRendererTests.cs b/Tests/CSharpRepl.Tests/RemoteValueRendererTests.cs index 2b43e1d5..17915df9 100644 --- a/Tests/CSharpRepl.Tests/RemoteValueRendererTests.cs +++ b/Tests/CSharpRepl.Tests/RemoteValueRendererTests.cs @@ -18,7 +18,7 @@ namespace CSharpRepl.Tests; /// /// Unit tests for the controller-side — it turns a theme-agnostic -/// (produced by the inspector in the target process) into themed console output, +/// (produced by the connector in the target process) into themed console output, /// mirroring the local REPL's simple-vs-detailed behavior. These need no running target: they feed /// hand-built projections straight through the renderer. ///