From 4d8ba504ed5ebfa52a3efbabeef786a093ae9318 Mon Sep 17 00:00:00 2001 From: Gregg Miskelly Date: Thu, 18 Jun 2026 13:53:23 -0700 Subject: [PATCH 01/25] Copilot instructions + test filtering fix (#1587) This PR: - Adds copilot-instructions.md with links to documentation - Adds a READMD.md to the various DebugEngineHost implementations to make that more clear - Fixes a bug with how `DependsOnTest` works which made it so that any test with that attribute couldn't be run with a test filter --- .github/copilot-instructions.md | 25 +++++ docs/Architecture-for-AI.md | 56 ++++++++++++ docs/Building-outside-of-VS-for-AI.md | 56 ++++++++++++ docs/CodingStandards-CSharp-for-AI.md | 42 +++++++++ docs/DebugEngineHost-for-AI.md | 76 ++++++++++++++++ docs/RunningCppTests-outside-of-VS-for-AI.md | 91 +++++++++++++++++++ docs/RunningUnitTests-outside-of-VS-for-AI.md | 47 ++++++++++ src/DebugEngineHost.Stub/README.md | 17 ++++ src/DebugEngineHost.VSCode/README.md | 18 ++++ src/DebugEngineHost/README.md | 17 ++++ src/MIDebugEngine-Unix.sln | 4 +- src/MIDebugEngine.sln | 21 ++++- .../Ordering/DependencyOrderer.cs | 45 +++++---- .../Ordering/DependencyTestOrderer.cs | 32 +++++++ 14 files changed, 527 insertions(+), 20 deletions(-) create mode 100644 .github/copilot-instructions.md create mode 100644 docs/Architecture-for-AI.md create mode 100644 docs/Building-outside-of-VS-for-AI.md create mode 100644 docs/CodingStandards-CSharp-for-AI.md create mode 100644 docs/DebugEngineHost-for-AI.md create mode 100644 docs/RunningCppTests-outside-of-VS-for-AI.md create mode 100644 docs/RunningUnitTests-outside-of-VS-for-AI.md create mode 100644 src/DebugEngineHost.Stub/README.md create mode 100644 src/DebugEngineHost.VSCode/README.md create mode 100644 src/DebugEngineHost/README.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 000000000..0fc89225d --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,25 @@ +# Conventions + +- Apply the code-formatting style defined in `.editorconfig`. +- This repo builds a debugger. Any `debugger_*` tool call will generally be **debugging the debugger** — `debugger_launch` will likely start another instance of Visual Studio (or a different debugger host process) where the model can drive the code in this repo. + +# Documentation to read + +⚠️ **CRITICAL**: Before proceeding with any task listed below, you MUST read the linked files as your FIRST action. Do NOT attempt the task until you have read and understood the documentation. + +**Mandatory workflow for AI:** +1. ✅ FIRST: read all required documentation files for the task +2. ✅ SECOND: follow the workflow specified there +3. ❌ NEVER: skip to code analysis, edits, or hypothesis formation before reading the docs + +| Task | Required documentation files | +| --- | --- | +| Understanding the project layout, how the layers fit together, or where new code belongs | [Architecture-for-AI.md](../docs/Architecture-for-AI.md) (and the project wiki: [Architecture-of-the-MIEngine](https://github.com/microsoft/MIEngine/wiki/Architecture-of-the-MIEngine), [Architecture-of-OpenDebugAD7](https://github.com/microsoft/MIEngine/wiki/Architecture-of-OpenDebugAD7), [Architecture-of-DebugAdapterRunner](https://github.com/microsoft/MIEngine/wiki/Architecture-of-DebugAdapterRunner)) | +| Writing or reviewing C# in this repo | [CodingStandards-CSharp-for-AI.md](../docs/CodingStandards-CSharp-for-AI.md) | +| Adding a new `DebugEngineHost` (host) API, or trying to understand what a `Host*` API actually does | [DebugEngineHost-for-AI.md](../docs/DebugEngineHost-for-AI.md) — `src/DebugEngineHost.Stub` is a **contract assembly only**; the real behavior lives in `src/DebugEngineHost` (VS) and `src/DebugEngineHost.VSCode` (VS Code), and a new API must be added to **all three** projects. | +| Building MIEngine **outside of Visual Studio** (CLI / CI / VS Code scenarios) | [Building-outside-of-VS-for-AI.md](../docs/Building-outside-of-VS-for-AI.md) | +| Running the in-process unit tests (`MICoreUnitTests`, `MIDebugEngineUnitTests`, `JDbgUnitTests`, `SSHDebugTests`) **outside of Visual Studio** | [RunningUnitTests-outside-of-VS-for-AI.md](../docs/RunningUnitTests-outside-of-VS-for-AI.md) | +| Running the end-to-end DAP tests in `test/CppTests` (against real `gdb` / `lldb-mi`) **outside of Visual Studio** | [RunningCppTests-outside-of-VS-for-AI.md](../docs/RunningCppTests-outside-of-VS-for-AI.md) | +| Capturing the MI traffic between MIEngine and `gdb`/`lldb` while reproducing a customer issue | wiki: [Logging](https://github.com/microsoft/MIEngine/wiki/Logging) (use the `Debug.MIDebugLog` Command Window verb in VS, or `src/MICore/SetMIDebugLogging.cmd on` for older flows) | + +⚠️ **When working inside Visual Studio**, do **not** follow the "outside of Visual Studio" docs above. VS already provides dedicated commands for these tasks. The `*-outside-of-VS-for-AI.md` docs are for command-line, CI, and VS Code scenarios where the IDE isn't hosting the conversation. diff --git a/docs/Architecture-for-AI.md b/docs/Architecture-for-AI.md new file mode 100644 index 000000000..66fecad7a --- /dev/null +++ b/docs/Architecture-for-AI.md @@ -0,0 +1,56 @@ +# MIEngine architecture for AI + +MIEngine is a Visual Studio **Debug Engine** that drives debuggers speaking the GDB **Machine Interface** ("MI") protocol — primarily GDB and LLDB-MI. The same code powers the `cppdbg` debug adapter for the VS Code C/C++ extension via `OpenDebugAD7`. + +## Layered design + +The engine is layered. New code almost always belongs in one of these projects; before adding a class, decide which layer owns the concern. + +| Layer | Project(s) | Responsibility | +| --- | --- | --- | +| Transport + MI parsing | `src/MICore` | Connections to the debuggee's debugger over local pty, SSH, pipe, serial. Owns the `MICommandFactory` family (`GdbMICommandFactory`, `LldbMICommandFactory`, `ClrdbgMICommandFactory`). All flavor-specific MI quirks live here as factory overrides. | +| AD7 implementation | `src/MIDebugEngine` | Implements the VS Core Debug Interfaces (`IDebugEngine2`, `IDebug*2`). `DebuggedProcess` is the central object owning the `MICore.Debugger`, breakpoint/threads/modules managers, and the event pump. AD7 wrapper classes (`AD7Engine`, `AD7Thread`, `AD7StackFrame`, `AD7BoundBreakpoint`, …) translate VS SDK COM calls into engine operations. | +| DAP shim | `src/OpenDebugAD7` | Hosts MIDebugEngine in-proc and exposes it as a Debug Adapter Protocol server. The entry point for the `cppdbg` adapter consumed by the VS Code C/C++ extension. | +| Host abstraction | `src/DebugEngineHost`, `src/DebugEngineHost.Common`, `src/DebugEngineHost.Stub`, `src/DebugEngineHost.VSCode` | The shim that lets MIDebugEngine avoid calling VS APIs directly. Goes through `HostLogger`, `HostMarshal`, `HostOutputWindow`, etc. The `DebugEngineHost.VSCode` variant is used by OpenDebugAD7; the in-VS build uses the COM-based host (`src/DebugEngineHost`). | +| Launchers | `src/AndroidDebugLauncher`, `src/IOSDebugLauncher`, `src/WindowsDebugLauncher` | Out-of-proc helpers spawned by the engine to start a debug session in a special environment (Android emulator, iOS device, Windows console). They communicate over stdio and implement `IPlatformAppLauncher`. | +| SSH port supplier | `src/SSHDebugPS` | A standalone VS "Port Supplier" for picking processes over SSH or Linux Docker. Independent of the engine flow. | + +## Hard rules + +- **No raw MI strings outside `MICore.MICommandFactory` / `Debugger.CmdAsync`.** If MIDebugEngine needs a new MI command, add a method on the factory and override per debugger flavor when behavior diverges. +- **MIDebugEngine does not depend on `Microsoft.VisualStudio.*` types directly.** Route everything through `DebugEngineHost`. This is what allows OpenDebugAD7 to host the engine on non-VS platforms. +- **Don't add `MIDebugEngine.sln`-only projects to `MIDebugEngine-Unix.sln`** (and vice versa). The split is intentional — the Unix solution must remain SDK-buildable without `msbuild`, the Windows extension workload, or COM PIAs. +- **AD7 surface goes on the `AD7*` partial class** for that interface. Keep VS-SDK COM concerns out of the core `Debugged*` classes (`DebuggedProcess`, `DebuggedThread`, `DebuggedModule`). + +## Inside MICore + +`MICore` itself decomposes into five concerns. When adding code there, place it in the right one rather than growing `Debugger` further: + +1. **`Debugger`** — central pump that processes text from GDB/LLDB and dispatches it to consumers. +2. **`MICommandFactory`** + flavor overrides (`Gdb`, `Lldb`, `Clrdbg`) — the abstraction for "send command X". *All* MI string construction goes through here. +3. **Result parser** (`ResultValue`, etc.) — parses MI result records into typed objects. +4. **Transports** — local pty, SSH, named-pipe, serial; set up the stdin/stdout connection to the debugger. +5. **Launch options** — XML deserialization driven by `LaunchOptions.xsd` (codegenned by `tools/LaunchOptionsGen` into `LaunchOptions.cs`); also loads custom launchers. + +The only enforced layering rule between MICore and MIDebugEngine is that **launchers depend on `MICore` only**. Any type a launcher needs must therefore live in MICore, not in MIDebugEngine. + +## Async / threading + +MIDebugEngine uses TPL `Task`s but AD7 callbacks must not block the dispatcher thread. The pattern is `Task.Run` for the work + post results back via the engine's `WorkerThread` / `EngineCallback`. Mirror existing call sites rather than inventing new threading; introducing a new pattern almost always causes deadlocks against `DebuggedProcess.WorkerThread`. + +## LaunchOptions + +The XML payload that `launch.json` (VS Code) or `vsdbg`/the IDE (VS) sends to the engine is described by `src/MICore/LaunchOptions.xsd`. The C# binding classes are **generated** by `tools/LaunchOptionsGen` — regenerate (`LaunchOptionsGen `) rather than hand-editing the generated `LaunchOptions.cs`. + +## Entry points worth knowing + +- VS extension entry: `MIDebugPackage` registers MIDebugEngine and pulls in `MIDebugEngine.dll`. +- VS Code adapter entry: `src/OpenDebugAD7/Program.cs` — main loop reads DAP messages from stdin and dispatches to `AD7DebugSession`. +- Engine creation: `AD7Engine.LaunchSuspended` / `AD7Engine.Attach` — both end at constructing a `DebuggedProcess`. + +## Tests at a glance + +- Pure managed unit tests live in `MICoreUnitTests`, `MIDebugEngineUnitTests`, `JDbgUnitTests`, `SSHDebugTests`. +- End-to-end DAP tests against a real debugger live in `test/CppTests` and use the `DebugAdapterRunner` framework. + +See [RunningUnitTests-outside-of-VS-for-AI.md](RunningUnitTests-outside-of-VS-for-AI.md) and [RunningCppTests-outside-of-VS-for-AI.md](RunningCppTests-outside-of-VS-for-AI.md). diff --git a/docs/Building-outside-of-VS-for-AI.md b/docs/Building-outside-of-VS-for-AI.md new file mode 100644 index 000000000..da19eccbc --- /dev/null +++ b/docs/Building-outside-of-VS-for-AI.md @@ -0,0 +1,56 @@ +# Building MIEngine outside of Visual Studio + +⚠️ **Do not use these instructions when working inside Visual Studio.** When VS is hosting the conversation (e.g. via the in-IDE Copilot chat / `debugger_*` tools), prefer the IDE's built-in build functions, or functions like `debugger_launch` which have an implicit build. Those commands set up the VS environment correctly and update the Error List for you. The scripts below are for command-line / CI / VS Code scenarios where no IDE is available. + +## Solutions + +There are two solutions, picked by what you are targeting: + +| Solution | Use when… | How to build | +| --- | --- | --- | +| `src/MIDebugEngine.sln` | Building the **Visual Studio extension** (full set of projects, including the VSIX, COM/PIA references, IOSDebugLauncher, MIDebugPackage, SSHDebugPS, …). | `msbuild` from a **VS Developer Command Prompt** (Dev 17+ with the *Visual Studio extension development* workload), via `eng/Scripts/CI-Build.ps1`. **Windows only.** | +| `src/MIDebugEngine-Unix.sln` | Building the subset that the **VS Code `cppdbg` adapter** uses (MICore, MIDebugEngine, OpenDebugAD7, DebugEngineHost.VSCode, the launchers, and the test projects). | `dotnet build` — works on Windows, Linux, and macOS. | + +Don't add an `.sln`-only project to the other solution; the split is intentional so the Unix/VS Code flavor remains buildable with just the .NET SDK. + +## Command-line build + +### Windows (full VS extension build) + +From a VS Developer Command Prompt or after putting `MSBuild.exe` on `PATH`: + +```powershell +# Debug, VS extension flavor (default) +eng\Scripts\CI-Build.ps1 -Configuration Debug -TargetPlatform vs + +# Debug, VS Code adapter flavor (also publishes OpenDebugAD7 + native deps to +# bin\DebugAdapterProtocolTests\Debug\extension\debugAdapters) +eng\Scripts\CI-Build.ps1 -Configuration Debug -TargetPlatform vscode +``` + +The script restores NuGet, builds `MIDebugEngine.sln` with `msbuild`, and (for `-TargetPlatform vscode`) `dotnet publish`es OpenDebugAD7 and stages the adapter under `bin\DebugAdapterProtocolTests\\extension\debugAdapters`. + +If `msbuild.exe` isn't on `PATH`, locate it with vswhere: + +```powershell +$msbuild = & "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" -latest -prerelease -requires Microsoft.Component.MSBuild -find "MSBuild\**\Bin\MSBuild.exe" +$env:Path = (Split-Path $msbuild) + ';' + $env:Path +``` + +### Linux / macOS + +```bash +eng/Scripts/CI-Build.sh +``` + +This runs `dotnet build src/MIDebugEngine-Unix.sln` and then `PublishOpenDebugAD7.sh -c Debug -o bin/DebugAdapterProtocolTests/Debug/extension/debugAdapters`. + +## Outputs + +- `bin//...` — primary binaries from `msbuild`. +- `bin//vscode/...` — the VS Code-flavor host (`Microsoft.DebugEngineHost.dll` for VSCode, `WindowsDebugLauncher.exe`, etc.). +- `bin/DebugAdapterProtocolTests//extension/debugAdapters/` — the staged VS Code debug adapter used by the CppTests. + +## Targeting + +The projects in this repo run on both .NET and .NET Framework, with most of the core projects targeting `netstandard2.0`. .NET is used for VS Code support while .NET Framework is used for VS. See `TargetFramework` in [OpenDebugAD7.csproj](../src/OpenDebugAD7/OpenDebugAD7.csproj) for the specific target framework moniker used for VS Code. \ No newline at end of file diff --git a/docs/CodingStandards-CSharp-for-AI.md b/docs/CodingStandards-CSharp-for-AI.md new file mode 100644 index 000000000..9791fd020 --- /dev/null +++ b/docs/CodingStandards-CSharp-for-AI.md @@ -0,0 +1,42 @@ +# C# coding conventions for MIEngine + +The `.editorconfig` at the repo root is authoritative — these are the conventions an AI assistant is most likely to get wrong if it isn't reminded. + +## File header + +Every `.cs` file starts with the MIT header (enforced by `dotnet_diagnostic` via `file_header_template` in `.editorconfig`): + +```csharp +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +``` + +## Naming + +Enforced as **warnings** by `.editorconfig`: + +| Symbol | Convention | Example | +| --- | --- | --- | +| `static` private/internal/private_protected fields | `s_camelCase` | `private static readonly object s_lock` | +| Other private/internal fields | `_camelCase` | `private readonly Debugger _debugger` | + +`readonly` is preferred where possible (`dotnet_style_readonly_field = true:warning`). + +## Usings + +`dotnet_sort_system_directives_first = false` — `using` directives are **not** auto-sorted. Leave the existing order alone; do not run an "organize usings" pass on otherwise unrelated files. + +## Indentation + +- 4 spaces for `.cs` (`indent_size = 4`). +- CRLF line endings (`end_of_line = crlf`) on Windows-tracked files. +- 2 spaces for XML formats (`.csproj`, `.props`, `.targets`, `.resx`, `.natvis`, `.vsct`, `.xsd`). + +## Engine-specific patterns + +These aren't in `.editorconfig` but show up everywhere — follow the existing call sites: + +- **MI commands go through `MICommandFactory`.** Don't build raw MI strings in MIDebugEngine. Add a method to the factory and override per debugger flavor when behavior diverges (`GdbMICommandFactory`, `LldbMICommandFactory`, …). +- **Host calls go through `DebugEngineHost`.** MIDebugEngine never references `Microsoft.VisualStudio.*` directly; use `HostLogger`, `HostMarshal`, `HostOutputWindow`, etc. +- **AD7 surface lives on `AD7*` partial classes.** Keep VS-SDK COM concerns out of the core `Debugged*` classes. +- **Worker thread discipline.** AD7 callbacks must not block. Use `Task.Run` for work and post results back via the engine's `WorkerThread` / `EngineCallback`. Mirror existing call sites; do not invent new threading patterns. diff --git a/docs/DebugEngineHost-for-AI.md b/docs/DebugEngineHost-for-AI.md new file mode 100644 index 000000000..e03f42d5e --- /dev/null +++ b/docs/DebugEngineHost-for-AI.md @@ -0,0 +1,76 @@ +# `Microsoft.DebugEngineHost` — contract assembly + two implementations + +`Microsoft.DebugEngineHost` is the shim that lets `MIDebugEngine` (and the +launchers) avoid calling `Microsoft.VisualStudio.*` directly. It is what +allows the same engine to run inside Visual Studio *and* inside the VS Code +`cppdbg` debug adapter (OpenDebugAD7). + +⚠️ The host is split across **four** projects on disk. Knowing which is +which is critical before you read or change anything host-related. + +| Project | What it is | When you build it | +| --- | --- | --- | +| `src/DebugEngineHost.Stub` | **Contract / reference assembly.** Defines the public shape of `Microsoft.DebugEngineHost` (types, method signatures, XML docs). Method bodies are stubs (`throw new NotImplementedException()`, `return null`, etc.) — **this assembly is never loaded at runtime by the engine.** Built only as a reference assembly so consumers can compile against the surface. | Always (part of `MIDebugEngine.sln`). | +| `src/DebugEngineHost` | **Visual Studio implementation.** The real `Microsoft.DebugEngineHost.dll` used when MIDebugEngine runs inside VS. Backed by `Microsoft.VisualStudio.*` (settings store, output window, COM marshaling, telemetry, wait dialog, …). | `MIDebugEngine.sln` only (Windows, VS Dev Cmd Prompt). | +| `src/DebugEngineHost.VSCode` | **VS Code / OpenDebugAD7 implementation.** The real `Microsoft.DebugEngineHost.dll` used when MIDebugEngine is hosted by `OpenDebugAD7` (the `cppdbg` DAP adapter). No VS / COM dependencies — pure .NET. | Both solutions (`MIDebugEngine.sln` and `MIDebugEngine-Unix.sln`). Output lands in `bin//vscode/`. | +| `src/DebugEngineHost.Common` | Shared source files (e.g. `HostLogChannel.cs`) that are linked into the two real implementations so they don't drift. Not an assembly the engine references on its own. | Linked into both implementations. | + +All three of `DebugEngineHost.Stub`, `DebugEngineHost`, and `DebugEngineHost.VSCode` emit an assembly +named **`Microsoft.DebugEngineHost.dll`** with the same `AssemblyVersion` +(`1.0.0`). That is intentional — the engine binds to one name and the +build wires up whichever real implementation matches the host. + +## Rules for AI working on host APIs + +1. **`DebugEngineHost.Stub` is a contract, not behavior.** If you want to + know what a host API actually *does*, do **not** read `.Stub`. Open the + matching file in `src/DebugEngineHost` (VS behavior) and/or + `src/DebugEngineHost.VSCode` (VS Code behavior). The `.Stub` body is + meaningless — it exists only so other projects can compile. + +2. **Adding a new host API means editing all three projects.** A new + method or type on a `Host*` class must be added to: + - `src/DebugEngineHost.Stub/DebugEngineHost.ref.cs` (or the right `Shared` file) — declare the signature with XML docs, stub the body. + - `src/DebugEngineHost/.cs` — implement it against the VS APIs. + - `src/DebugEngineHost.VSCode/.cs` — implement it against the VS Code / OpenDebugAD7 environment. + The three surfaces must stay **identical** (same namespace, type, name, + parameters, return type, generic arity, accessibility). If they drift, + one of the two hosts will fail to bind at runtime with a + `MissingMethodException` / `TypeLoadException`. + +3. **Shared helpers go in `DebugEngineHost.Common`.** If the two real + implementations would copy/paste the same code, put it in `.Common` and + link it into both — do **not** add it to `.Stub` (`.Stub` has no real + code). + +4. **No `Microsoft.VisualStudio.*` references from `.VSCode` or + `.Stub`'s public surface.** The whole point of the split is that + OpenDebugAD7 / VS Code can load the engine without any VS assemblies. + `.Stub` may reference `Microsoft.VisualStudio.Debugger.Interop` because + that is part of the contract (e.g. `HostMarshal` deals in `IDebug*` + interfaces), but the `.VSCode` implementation must provide its own + substitute behavior, not pull in VS. + +5. **MIDebugEngine itself only references the contract.** It compiles + against `.Stub`, then at runtime loads whichever real + `Microsoft.DebugEngineHost.dll` is next to it. This is why you can't + "just call a VS API" from MIDebugEngine — there is no VS API on the + `.Stub` surface to call. + +## Quick map of the `Host*` types + +Every `Host*` file has three copies (one in each project). Common ones: + +- `Host` — top-level host identity (`GetHostUIIdentifier()`). +- `HostConfigurationStore` — engine/launcher configuration lookup. VS reads from the settings store; VS Code reads from launch.json / registered options. +- `HostLogger` / `HostLogChannel` — logging plumbing (`Debug.MIDebugLog` in VS; file-based in VS Code). +- `HostMarshal` — marshals AD7 COM interface pointers (`IDebugDocumentPosition2`, etc.) across boundaries. Real COM in VS; an in-proc table in VS Code. +- `HostOutputWindow` — writes to the VS Debug Output pane vs. the DAP `output` event. +- `HostRunInTerminal` — launches a child process in an interactive terminal (VS terminal vs. DAP `runInTerminal` request). +- `HostWaitDialog` / `HostWaitLoop` — modal progress UI (VS dialog vs. DAP progress events / no-op). +- `HostTelemetry` — VS telemetry sink vs. a VS Code no-op. +- `HostNatvisProject` — natvis discovery from the loaded VS project vs. from launch.json. +- `HostDebugger` — only present in the VS implementation (it drives the VS debugger itself for nested scenarios); no `.VSCode` counterpart is needed. + +When in doubt, grep for the type name across all three project folders +and read **both** real implementations — they're the source of truth. diff --git a/docs/RunningCppTests-outside-of-VS-for-AI.md b/docs/RunningCppTests-outside-of-VS-for-AI.md new file mode 100644 index 000000000..8109de4ea --- /dev/null +++ b/docs/RunningCppTests-outside-of-VS-for-AI.md @@ -0,0 +1,91 @@ +# Running CppTests (end-to-end DAP tests) outside of Visual Studio + +⚠️ **Do not use these instructions when working inside Visual Studio.** Use VS **Test Explorer** (or the `debugger_run_tests` tool if available) — it sets up the working directory and lets you attach the debugger to both the test and the spawned `OpenDebugAD7`. The command-line flow below is for CI / VS Code / headless scenarios. + +`test/CppTests` drives the built `OpenDebugAD7` adapter against a real `gdb` (or `lldb-mi`) using the `DebugAdapterRunner` framework. Every test has an `[xunit.Theory]` parameterized by an `ITestSettings` discovered at runtime from `config.xml` — see [Filtering CppTests](#filtering-cpptests) below for an important quirk. + +## Prerequisites + +1. A VSCode-flavor build (see [Building-outside-of-VS-for-AI.md](Building-outside-of-VS-for-AI.md)): + + ```powershell + eng\Scripts\CI-Build.ps1 -Configuration Debug -TargetPlatform vscode # Windows + ``` + + ```bash + eng/Scripts/CI-Build.sh # Linux / macOS + ``` + +2. A native toolchain for the test debuggees: + - **Windows:** **MSYS2 + MinGW64** (`mingw-w64-x86_64-toolchain`, providing `g++` and `gdb`). **Cygwin is not supported.** Install with `winget install --id MSYS2.MSYS2 --silent` and then, in `C:\msys64\usr\bin\bash.exe -lc "..."`: + + ```bash + pacman -Syu --noconfirm + pacman -S --needed --noconfirm mingw-w64-x86_64-toolchain + ``` + + Put `C:\msys64\mingw64\bin;C:\msys64\usr\bin` on `PATH`, or run `dotnet test` from `msys2_shell.cmd -mingw64`. + - **Linux:** `gdb`, `g++`, plus `ptrace_scope=0` and a writable `core_pattern` if any test exercises core dumps: + + ```bash + sudo apt-get install -y gdb g++ + echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope + echo core | sudo tee /proc/sys/kernel/core_pattern + ``` + + - **macOS:** `lldb-mi` is downloaded by `tools/DownloadLldbMI.sh`; CI does this automatically inside `eng/Scripts/CI-Test.sh`. + +3. A `config.xml` next to `CppTests.dll`. Pick the right template from `bin/DebugAdapterProtocolTests//CppTests/TestConfigurations/` and copy it as `config.xml`: + + | Platform / debugger | Template | + | --- | --- | + | Windows + MSYS2 GDB | `config_msys_gdb.xml` | + | Linux + GDB | `config_gdb.xml` | + | macOS + LLDB-MI | `config_lldb.xml` | + | Windows + VsDbg | `config_vsdbg.xml` | + + ⚠️ The shipped `config_msys_gdb.xml` has hardcoded GitHub Actions paths (`D:\a\_temp\msys64\mingw64\bin\…`). Edit it to match your local install (e.g. `C:\msys64\mingw64\bin\g++.exe`, `C:\msys64\mingw64\bin\gdb.exe`) before running. + +`eng/Scripts/CI-Test.sh` performs steps 2-3 automatically on Linux/macOS. + +## Running + +```powershell +cd bin\DebugAdapterProtocolTests\Debug\CppTests +dotnet test CppTests.dll --logger "trx;LogFileName=results.trx" +``` + +```bash +cd bin/DebugAdapterProtocolTests/Debug/CppTests +dotnet test CppTests.dll --logger "trx;LogFileName=results.trx" +``` + +## Filtering CppTests + +Standard VSTest filters work: + +```powershell +dotnet test CppTests.dll --filter "FullyQualifiedName~SampleTests.TestArguments" +``` + +**Quirk to know about — dependency ordering vs. filtering.** Many CppTests use `[DependsOnTest("OtherTest")]` (e.g. a debuggee-compile step), and the type is decorated with `[TestCaseOrderer(DependencyTestOrderer.TypeName, DependencyTestOrderer.AssemblyName)]`. The orderer (`test/DebuggerTesting/Ordering/DependencyOrderer.cs`) runs **after** VSTest's `--filter`. Historically it removed any test whose dependency wasn't in the filtered set, so a single-test filter would silently produce *"No test matches the given testcase filter"*. The orderer was updated to instead ignore missing dependencies and run the test anyway, logging a warning through xUnit's diagnostic message sink (`IMessageSink` / `DiagnosticMessage`). The warning is only surfaced when xUnit diagnostic messages are enabled — see [Seeing xUnit diagnostic messages](#seeing-xunit-diagnostic-messages) below. When adding new dependency-ordered tests: + +- Don't rely on the orderer to *skip* a test when its dependency is filtered out — it will run, and may fail at runtime if the predecessor was genuinely required. +- If a predecessor produces a build artifact (e.g. a compiled debuggee), ensure the dependent test can find or rebuild that artifact when run in isolation. + +## Seeing xUnit diagnostic messages + +Diagnostics emitted via xUnit's `IMessageSink` (used by `DependencyTestOrderer` and other test infrastructure) are **suppressed by default**. To see them in `dotnet test` console output, in VS Test Explorer's Test output pane, or in TRX logs, drop an `xunit.runner.json` next to the test DLL with diagnostics enabled: + +```json +{ + "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", + "diagnosticMessages": true +} +``` + +For `CppTests`, place it next to `bin\DebugAdapterProtocolTests\\CppTests\CppTests.dll`. With `--logger "console;verbosity=detailed"`, diagnostic lines appear prefixed with `[xUnit.net …]` (e.g. `WARNING: Missing dependency for 'CppTests.Tests.SampleTests.TestArguments'; running anyway.`). + +## Test-data XML + +`config.xml` defines `` entries (compiler + debugger + architecture). Every `[RequiresTestSettings]` theory is invoked once per matching configuration. To run against multiple debuggers in one pass, add multiple `` entries. diff --git a/docs/RunningUnitTests-outside-of-VS-for-AI.md b/docs/RunningUnitTests-outside-of-VS-for-AI.md new file mode 100644 index 000000000..e3fc20ac4 --- /dev/null +++ b/docs/RunningUnitTests-outside-of-VS-for-AI.md @@ -0,0 +1,47 @@ +# Running the in-process unit tests outside of Visual Studio + +⚠️ **Do not use these instructions when working inside Visual Studio.** Use VS **Test Explorer** (or the `debugger_run_tests` tool if available) to discover, run, and debug these tests — that path attaches the VS debugger and updates the Test Explorer UI. The commands below are for command-line / CI scenarios. + +This page covers the four pure .NET unit-test assemblies. For end-to-end DAP tests against a real `gdb`/`lldb-mi`, see [RunningCppTests-outside-of-VS-for-AI.md](RunningCppTests-outside-of-VS-for-AI.md). + +## The unit-test assemblies + +| Project | What it tests | +| --- | --- | +| `MICoreUnitTests` | MI parser, transports, command factories. | +| `MIDebugEngineUnitTests` | AD7 wrappers, expression evaluation helpers, and other in-engine logic. | +| `JDbgUnitTests` | JDWP client used by `AndroidDebugLauncher`. | +| `SSHDebugTests` | SSHDebugPS port supplier and related SSH helpers. | + +All four are built by `MIDebugEngine.sln` (Windows) and most are also built by `MIDebugEngine-Unix.sln`. + +## Prerequisites + +- A successful build (see [Building-outside-of-VS-for-AI.md](Building-outside-of-VS-for-AI.md)). +- No native toolchain is required — these are managed-only tests. + +## Running the full suite (Windows / VSTest) + +This matches what GitHub Actions does. From a VS Developer Command Prompt: + +```powershell +vstest.console.exe ` + bin\Debug\MICoreUnitTests.dll ` + bin\Debug\JDbgUnitTests.dll ` + bin\Debug\SSHDebugTests.dll ` + bin\Debug\MIDebugEngineUnitTests.dll +``` + +## Running a single test + +```powershell +vstest.console.exe bin\Debug\MICoreUnitTests.dll /Tests:Namespace.ClassName.MethodName +``` + +`/Tests:` does substring matching, so you can pass just the method name if it is unique. + +For the projects that are also in the Unix solution, `dotnet test` works too: + +```powershell +dotnet test src\MICoreUnitTests\MICoreUnitTests.csproj --filter "FullyQualifiedName~Namespace.ClassName.MethodName" +``` diff --git a/src/DebugEngineHost.Stub/README.md b/src/DebugEngineHost.Stub/README.md new file mode 100644 index 000000000..be2e378bb --- /dev/null +++ b/src/DebugEngineHost.Stub/README.md @@ -0,0 +1,17 @@ +# DebugEngineHost.Stub + +⚠️ **This project is a contract / reference assembly. It is never loaded +at runtime.** The method bodies here are stubs — they do not describe +what the host actually does. + +If you want to know what a `Host*` API actually does, read the **real** +implementations instead: + +- `src/DebugEngineHost` — Visual Studio implementation. +- `src/DebugEngineHost.VSCode` — VS Code / OpenDebugAD7 implementation. + +If you are adding or changing a host API, you must update all three +projects (`DebugEngineHost.Stub`, `DebugEngineHost`, +`DebugEngineHost.VSCode`) so their public surfaces stay identical. See +[`docs/DebugEngineHost-for-AI.md`](../../docs/DebugEngineHost-for-AI.md) +for the full rules and rationale. diff --git a/src/DebugEngineHost.VSCode/README.md b/src/DebugEngineHost.VSCode/README.md new file mode 100644 index 000000000..1525538fd --- /dev/null +++ b/src/DebugEngineHost.VSCode/README.md @@ -0,0 +1,18 @@ +# DebugEngineHost.VSCode (VS Code / OpenDebugAD7 implementation) + +This project is the **VS Code** implementation of +`Microsoft.DebugEngineHost.dll`. It is one of two real implementations +of the host contract defined in +[`src/DebugEngineHost.Stub`](../DebugEngineHost.Stub/README.md); the +other is [`src/DebugEngineHost`](../DebugEngineHost/README.md) (the +Visual Studio implementation). + +Behavior here has **no `Microsoft.VisualStudio.*` dependencies** — it is +pure .NET so the engine can be hosted by `OpenDebugAD7` (the `cppdbg` +debug adapter) on Windows, Linux, and macOS. + +If you are adding or changing a host API, you must update all three +projects (`DebugEngineHost.Stub`, `DebugEngineHost`, +`DebugEngineHost.VSCode`) so their public surfaces stay identical. See +[`docs/DebugEngineHost-for-AI.md`](../../docs/DebugEngineHost-for-AI.md) +for the full rules and rationale. diff --git a/src/DebugEngineHost/README.md b/src/DebugEngineHost/README.md new file mode 100644 index 000000000..ff670b582 --- /dev/null +++ b/src/DebugEngineHost/README.md @@ -0,0 +1,17 @@ +# DebugEngineHost (Visual Studio implementation) + +This project is the **Visual Studio** implementation of +`Microsoft.DebugEngineHost.dll`. It is one of two real implementations +of the host contract defined in +[`src/DebugEngineHost.Stub`](../DebugEngineHost.Stub/README.md); the +other is [`src/DebugEngineHost.VSCode`](../DebugEngineHost.VSCode/README.md). + +Behavior here is backed by `Microsoft.VisualStudio.*` APIs (settings +store, output window, COM marshaling, telemetry, wait dialog, …) and is +loaded when MIDebugEngine runs inside Visual Studio. + +If you are adding or changing a host API, you must update all three +projects (`DebugEngineHost.Stub`, `DebugEngineHost`, +`DebugEngineHost.VSCode`) so their public surfaces stay identical. See +[`docs/DebugEngineHost-for-AI.md`](../../docs/DebugEngineHost-for-AI.md) +for the full rules and rationale. diff --git a/src/MIDebugEngine-Unix.sln b/src/MIDebugEngine-Unix.sln index 2fb7d689c..21a53b902 100644 --- a/src/MIDebugEngine-Unix.sln +++ b/src/MIDebugEngine-Unix.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.65535.65535.65535 +# Visual Studio Version 18 +VisualStudioVersion = 18.65535.65535.65535 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{CF02407C-BF37-4D51-83F4-845A7F36C101}" ProjectSection(SolutionItems) = preProject diff --git a/src/MIDebugEngine.sln b/src/MIDebugEngine.sln index ed5df3b55..88e672933 100755 --- a/src/MIDebugEngine.sln +++ b/src/MIDebugEngine.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.65535.65535.65535 +# Visual Studio Version 18 +VisualStudioVersion = 18.65535.65535.65535 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Platform Launchers", "Platform Launchers", "{B864C337-1AA8-42B3-BF01-90901F55DE70}" EndProject @@ -103,6 +103,21 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MakePIAPortableTool", "tool EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MIDebugEngineUnitTests", "MIDebugEngineUnitTests\MIDebugEngineUnitTests.csproj", "{7F98435A-526E-41DC-9F9A-BFD55CC991DE}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{617F1819-7755-445A-8F8E-7C96137BA449}" + ProjectSection(SolutionItems) = preProject + ..\.github\copilot-instructions.md = ..\.github\copilot-instructions.md + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{03A604B5-0964-4B2B-A7F1-CD89764B8BB1}" + ProjectSection(SolutionItems) = preProject + ..\docs\Architecture-for-AI.md = ..\docs\Architecture-for-AI.md + ..\docs\Building-outside-of-VS-for-AI.md = ..\docs\Building-outside-of-VS-for-AI.md + ..\docs\CodingStandards-CSharp-for-AI.md = ..\docs\CodingStandards-CSharp-for-AI.md + ..\docs\DebugEngineHost-for-AI.md = ..\docs\DebugEngineHost-for-AI.md + ..\docs\RunningCppTests-outside-of-VS-for-AI.md = ..\docs\RunningCppTests-outside-of-VS-for-AI.md + ..\docs\RunningUnitTests-outside-of-VS-for-AI.md = ..\docs\RunningUnitTests-outside-of-VS-for-AI.md + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -294,6 +309,8 @@ Global {6B26CBAE-38A1-47E6-BFF1-F4A626C9A5B5} = {9D97EF1A-BCD5-4932-BBCC-98194CF8A841} {4F96DA84-CCBA-4ADE-8D7D-BC15D566A329} = {CF02407C-BF37-4D51-83F4-845A7F36C101} {CC5BDD33-7EB1-4FB9-BC67-806773018989} = {20B91EF1-0CE0-4E6D-A122-319BF9B68E94} + {617F1819-7755-445A-8F8E-7C96137BA449} = {CF02407C-BF37-4D51-83F4-845A7F36C101} + {03A604B5-0964-4B2B-A7F1-CD89764B8BB1} = {CF02407C-BF37-4D51-83F4-845A7F36C101} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {6099AC71-DA1D-4578-A8A3-376EB3C602E6} diff --git a/test/DebuggerTesting/Ordering/DependencyOrderer.cs b/test/DebuggerTesting/Ordering/DependencyOrderer.cs index da5216dfc..f540ff96f 100644 --- a/test/DebuggerTesting/Ordering/DependencyOrderer.cs +++ b/test/DebuggerTesting/Ordering/DependencyOrderer.cs @@ -1,9 +1,9 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System; using System.Collections; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; namespace DebuggerTesting.Ordering @@ -15,6 +15,16 @@ namespace DebuggerTesting.Ordering /// public abstract class DependencyOrderer { + /// + /// Reports a diagnostic message produced while ordering items. The default + /// implementation discards the message; derived classes should override to + /// route the message to whatever logging facility is available in their + /// hosting environment (e.g. xUnit's IMessageSink). + /// + protected virtual void LogDiagnostic(string message) + { + } + protected IEnumerable OrderBasedOnDependencies(IEnumerable itemsEnumerable) { List items = new List(itemsEnumerable); @@ -26,32 +36,35 @@ protected IEnumerable OrderBasedOnDependencies(IEnumerable itemsEnumerable while (i < items.Count) { T currentItem = items[i]; - IEnumerable dependencyIndexes = GetDependencyIndexes(items, currentItem); - if (dependencyIndexes == null) - continue; - - if (dependencyIndexes.Any(x => x < 0)) + // Materialize once: GetDependencyIndexes returns a lazy Select(...), + // and we both filter it and count it below. Treat null (no dependency + // metadata) the same as an empty list so the loop always advances — + // otherwise `continue` would leave `i` unchanged and loop forever. + IList dependencyIndexes = (IList)GetDependencyIndexes(items, currentItem)?.ToList() ?? Array.Empty(); + + // Ignore dependencies that aren't present in the current item set + // (for example, when the user filtered the run with `dotnet test --filter` + // or via VS Test Explorer). The orderer's job is to order items, not to + // drop them — silently removing the dependent test makes filtered runs + // appear to match nothing. If the dependency is genuinely required at + // runtime, the test will fail with an actionable error rather than + // vanishing from the run. + IList presentDependencyIndexes = dependencyIndexes.Where(x => x >= 0).ToList(); + if (presentDependencyIndexes.Count < dependencyIndexes.Count) { - // Remove item if dependency cannot be resolved in this set of items - Debug.WriteLine("ERROR: Cannot find dependency for '{0}'.".FormatWithArgs(GetItemName(currentItem))); - - // Remove the item and start over - items.RemoveAt(i); - stallCount = 0; - i = 0; - continue; + LogDiagnostic("WARNING: Missing dependency for '{0}'; running anyway.".FormatWithArgs(GetItemName(currentItem))); } // Move the current test after any required dependencies. // Verify we aren't stuck in an infinite loop - int lastDependencyIndex = dependencyIndexes.Any() ? dependencyIndexes.Max() : -1; + int lastDependencyIndex = presentDependencyIndexes.Count > 0 ? presentDependencyIndexes.Max() : -1; if (lastDependencyIndex > i) { MoveAfter(items, i, lastDependencyIndex); stallCount++; if (stallCount > (items.Count - i)) { - Debug.WriteLine("ERROR: Circular test dependency found."); + LogDiagnostic("ERROR: Circular test dependency found."); // Based on the stall count, the items at the end of the list // have a circular reference. Remove them all. items.RemoveRange(i, items.Count - i); diff --git a/test/DebuggerTesting/Ordering/DependencyTestOrderer.cs b/test/DebuggerTesting/Ordering/DependencyTestOrderer.cs index a2206d26b..c378e489e 100644 --- a/test/DebuggerTesting/Ordering/DependencyTestOrderer.cs +++ b/test/DebuggerTesting/Ordering/DependencyTestOrderer.cs @@ -15,12 +15,44 @@ public class DependencyTestOrderer : DependencyOrderer, ITest public const string TypeName = nameof(DebuggerTesting) + "." + nameof(Ordering) + "." + nameof(DependencyTestOrderer); public const string AssemblyName = nameof(DebuggerTesting); + private readonly IMessageSink _diagnosticMessageSink; + + // xUnit will pick the constructor with the most parameters it can satisfy + // and inject the diagnostic message sink when available. The parameterless + // constructor is retained so the orderer still works in hosts that don't + // supply a sink. + public DependencyTestOrderer() + { + } + + public DependencyTestOrderer(IMessageSink diagnosticMessageSink) + { + _diagnosticMessageSink = diagnosticMessageSink; + } + public IEnumerable OrderTestCases(IEnumerable testCases) where TTestCase : ITestCase { return OrderBasedOnDependencies(testCases.Cast()).Cast(); } + protected override void LogDiagnostic(string message) + { + // Route through xUnit's diagnostic message sink so the message shows up + // in `dotnet test` output (with `true` + // in xunit.runner.json or `-diagnostics` on the runner) and in the VS + // test output pane. Fall back to Console.Error so the message isn't lost + // when no sink is available (e.g. unit-testing the orderer directly). + if (_diagnosticMessageSink != null) + { + _diagnosticMessageSink.OnMessage(new DiagnosticMessage(message)); + } + else + { + Console.Error.WriteLine(message); + } + } + #region Dependency Helpers protected override int GetIndexOfDependency(IList tests, string testName) From a832560e18dfd18f60549f6ce94f9970913d5084 Mon Sep 17 00:00:00 2001 From: Andrew Wang Date: Thu, 18 Jun 2026 15:58:55 -0700 Subject: [PATCH 02/25] Merge pull request #1588 from microsoft/dev/waan/stopVsDbgTest Disable NullPointerNotExpandable test for cppvsdbg --- test/CppTests/Tests/ExpressionTests.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/CppTests/Tests/ExpressionTests.cs b/test/CppTests/Tests/ExpressionTests.cs index 0ed1acc67..2fea57c13 100644 --- a/test/CppTests/Tests/ExpressionTests.cs +++ b/test/CppTests/Tests/ExpressionTests.cs @@ -519,6 +519,9 @@ public void SetExpressionOnVariable(ITestSettings settings) [Theory] [DependsOnTest(nameof(CompileKitchenSinkForExpressionTests))] [RequiresTestSettings] + // cppvsdbg allows expanding null pointers to support managed delegate expansion. + // Disabling this test for VsDbg since the assertion does not apply. + [UnsupportedDebugger(SupportedDebugger.VsDbg, SupportedArchitecture.x64 | SupportedArchitecture.x86)] public void NullPointerNotExpandable(ITestSettings settings) { this.TestPurpose("Verify that a null pointer is not expandable in the variables view."); From 729de5409924019b65b943c623903ea9cb4c85c6 Mon Sep 17 00:00:00 2001 From: Andrew Wang Date: Sat, 13 Jun 2026 18:12:47 -0600 Subject: [PATCH 03/25] Refactor Docker transport into shared base classes and strategy pattern (#1579) * Refactor Docker transport into shared base classes and strategy pattern Extract shared container transport infrastructure to enable additional container runtimes without duplicating Docker code. Key changes: - `ContainerTransportSettingsBase` shared abstract base eliminates duplication in transport settings (exe name, host flag, command format) - `IContainerDiscoveryStrategy` interface with Docker implementation keeps runtime-specific logic out of the ViewModel - `ContainerRuntimeType` enum threaded through port picker -> ConnectionManager -> dialog for future extensibility - XAML bindings changed from static resources to ViewModel properties so labels can vary per runtime No behavioral changes - Docker works exactly as before. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address review feedback: remove unused field, add type check - Remove unused _runtimeType field from ContainerPickerViewModel - Add explicit type check in DockerExecutionManager.CreateExecSettings instead of unsafe cast Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address review: seal classes, use OrdinalIgnoreCase, remove defaults - Seal DockerDiscoveryStrategy, DockerCommandSettings, DockerContainerTransportSettings, DockerExecSettings, DockerCopySettings - Use StringComparison.OrdinalIgnoreCase for Windows check in AssignPlatforms instead of ToTitleCase + Contains - Remove default parameter values for ContainerRuntimeType to force explicit runtime selection by callers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Hoist CultureInfo allocation outside loop Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/SSHDebugPS/ConnectionManager.cs | 7 +- src/SSHDebugPS/ContainerRuntimeType.cs | 14 ++ .../ContainerTransportSettingsBase.cs | 163 ++++++++++++++++++ src/SSHDebugPS/Docker/DockerConnection.cs | 2 +- .../Docker/DockerContainerInstance.cs | 12 +- .../Docker/DockerDiscoveryStrategy.cs | 78 +++++++++ .../Docker/DockerExecutionManager.cs | 15 +- src/SSHDebugPS/Docker/DockerHelper.cs | 10 +- src/SSHDebugPS/Docker/DockerPortPicker.cs | 3 +- .../DockerContainerTransportSettings.cs | 89 ++-------- .../DockerTransportSettings.cs | 70 +------- src/SSHDebugPS/IContainerDiscoveryStrategy.cs | 21 +++ .../UI/ContainerPickerDialogWindow.xaml | 10 +- .../UI/ContainerPickerDialogWindow.xaml.cs | 4 +- .../UI/ViewModels/ContainerPickerViewModel.cs | 90 +++++----- 15 files changed, 369 insertions(+), 219 deletions(-) create mode 100644 src/SSHDebugPS/ContainerRuntimeType.cs create mode 100644 src/SSHDebugPS/ContainerTransportSettingsBase.cs create mode 100644 src/SSHDebugPS/Docker/DockerDiscoveryStrategy.cs create mode 100644 src/SSHDebugPS/IContainerDiscoveryStrategy.cs diff --git a/src/SSHDebugPS/ConnectionManager.cs b/src/SSHDebugPS/ConnectionManager.cs index 0222926b0..3bd40dec3 100644 --- a/src/SSHDebugPS/ConnectionManager.cs +++ b/src/SSHDebugPS/ConnectionManager.cs @@ -38,7 +38,7 @@ public static DockerConnection GetDockerConnection(string name, bool supportSSHC { string connectionString; - bool success = ShowContainerPickerWindow(IntPtr.Zero, supportSSHConnections, out connectionString); + bool success = ShowContainerPickerWindow(IntPtr.Zero, supportSSHConnections, ContainerRuntimeType.Docker, out connectionString); if (success) { success = DockerConnection.TryConvertConnectionStringToSettings(connectionString, out settings, out remoteConnection); @@ -146,11 +146,12 @@ public static SSHConnection GetSSHConnection(string name) /// /// Parent hwnd or IntPtr.Zero /// SSHConnections are supported + /// Which container runtime to query /// [out] connection string obtained by the dialog - public static bool ShowContainerPickerWindow(IntPtr hwnd, bool supportSSHConnections, out string connectionString) + public static bool ShowContainerPickerWindow(IntPtr hwnd, bool supportSSHConnections, ContainerRuntimeType runtimeType, out string connectionString) { ThreadHelper.ThrowIfNotOnUIThread("Microsoft.SSHDebugPS.ShowContainerPickerWindow"); - ContainerPickerDialogWindow dialog = new ContainerPickerDialogWindow(supportSSHConnections); + ContainerPickerDialogWindow dialog = new ContainerPickerDialogWindow(supportSSHConnections, runtimeType); if (hwnd == IntPtr.Zero) // get the VS main window hwnd { diff --git a/src/SSHDebugPS/ContainerRuntimeType.cs b/src/SSHDebugPS/ContainerRuntimeType.cs new file mode 100644 index 000000000..1a8afa73a --- /dev/null +++ b/src/SSHDebugPS/ContainerRuntimeType.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.SSHDebugPS +{ + /// + /// Identifies the container runtime to query when discovering containers. + /// + public enum ContainerRuntimeType + { + Unknown, + Docker + } +} diff --git a/src/SSHDebugPS/ContainerTransportSettingsBase.cs b/src/SSHDebugPS/ContainerTransportSettingsBase.cs new file mode 100644 index 000000000..79462f4f9 --- /dev/null +++ b/src/SSHDebugPS/ContainerTransportSettingsBase.cs @@ -0,0 +1,163 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.SSHDebugPS.Utilities; +using System.Diagnostics; + +namespace Microsoft.SSHDebugPS +{ + internal abstract class ContainerTransportSettingsBase : IPipeTransportSettings + { + protected abstract string SubCommand { get; } + protected abstract string SubCommandArgs { get; } + + private readonly string _windowsExe; + private readonly string _unixExe; + private readonly string _hostnameFormat; + + internal string HostName { get; private set; } + internal bool HostIsUnix { get; private set; } + + public ContainerTransportSettingsBase(string hostname, bool hostIsUnix, string windowsExe, string unixExe, string hostnameFormat) + { + HostIsUnix = hostIsUnix; + _windowsExe = windowsExe; + _unixExe = unixExe; + _hostnameFormat = hostnameFormat; + if (!string.IsNullOrWhiteSpace(hostname)) + { + HostName = hostname; + } + else + { + HostName = string.Empty; + } + } + + public ContainerTransportSettingsBase(ContainerTransportSettingsBase settings) + : this(settings.HostName, settings.HostIsUnix, settings._windowsExe, settings._unixExe, settings._hostnameFormat) + { } + + // 0 = command parameters (e.g. --host/--url) + // 1 = subcommand (e.g. exec, cp, ps) + // 2 = subcommand parameters + private const string _baseCommandFormat = "{0} {1} {2}"; + + private string GenerateExeCommandArgs() + { + var hostnameArg = string.Empty; + if (!string.IsNullOrWhiteSpace(this.HostName)) + hostnameArg = _hostnameFormat.FormatInvariantWithArgs(this.HostName); + + return _baseCommandFormat.FormatInvariantWithArgs(hostnameArg, SubCommand, SubCommandArgs); + } + + #region IPipeTransportSettings + + public string CommandArgs => GenerateExeCommandArgs(); + + public string Command => HostIsUnix ? _unixExe : _windowsExe; + + #endregion + } + + internal abstract class ContainerTargetTransportSettings : ContainerTransportSettingsBase + { + internal string ContainerName { get; private set; } + + public ContainerTargetTransportSettings(string hostname, string containerName, bool hostIsUnix, string windowsExe, string unixExe, string hostnameFormat) + : base(hostname, hostIsUnix, windowsExe, unixExe, hostnameFormat) + { + ContainerName = containerName; + } + + public ContainerTargetTransportSettings(ContainerTargetTransportSettings settings) + : base(settings) + { + ContainerName = settings.ContainerName; + } + + protected override string SubCommand => throw new System.NotImplementedException(); + protected override string SubCommandArgs => throw new System.NotImplementedException(); + } + + internal abstract class ContainerExecSettings : ContainerTargetTransportSettings + { + private bool _runInShell; + private string _commandToExecute; + // 0 = container, 1 = command to execute + private const string _subCommandArgsFormat = "{0} {1}"; + private const string _subCommandArgsFormatWithShell = "{0} /bin/sh -c \"{1}\""; + private const string _subCommandArgsFormatWithShellLinuxHost = "{0} /bin/sh -c '{1}'"; + private const string _interactiveFlag = "-i "; + + private bool _makeInteractive; + + public ContainerExecSettings(ContainerTargetTransportSettings settings, string command, bool runInShell, bool makeInteractive = true) + : base(settings) + { + Debug.Assert(!string.IsNullOrWhiteSpace(command), "Exec command cannot be null"); + _runInShell = runInShell; + _commandToExecute = command; + _makeInteractive = makeInteractive; + } + + protected override string SubCommand => "exec"; + protected override string SubCommandArgs + { + get + { + string subCommandFormat = this.HostIsUnix ? _subCommandArgsFormatWithShellLinuxHost : _subCommandArgsFormatWithShell; + // Escape single quotes on Linux so variable resolution does not happen until it is in the container. + string command = this.HostIsUnix ? _commandToExecute.Replace("'", "'\\''") : _commandToExecute; + return (_makeInteractive ? _interactiveFlag : string.Empty) + + (_runInShell ? subCommandFormat : _subCommandArgsFormat).FormatInvariantWithArgs(ContainerName, command); + } + } + } + + internal abstract class ContainerCopySettings : ContainerTargetTransportSettings + { + // {0} = container, {1} = source, {2} = destination + private const string _copyFormatToContainer = "{1} {0}:{2}"; + + private string _sourcePath; + private string _destinationPath; + + public ContainerCopySettings(string hostname, string sourcePath, string destinationPath, string containerName, bool hostIsUnix, string windowsExe, string unixExe, string hostnameFormat) + : base(hostname, containerName, hostIsUnix, windowsExe, unixExe, hostnameFormat) + { + _sourcePath = sourcePath; + _destinationPath = destinationPath; + } + + public ContainerCopySettings(ContainerTargetTransportSettings settings, string sourcePath, string destinationPath) + : base(settings) + { + _sourcePath = sourcePath; + _destinationPath = destinationPath; + } + + protected override string SubCommand => "cp"; + protected override string SubCommandArgs => _copyFormatToContainer.FormatInvariantWithArgs(ContainerName, _sourcePath, _destinationPath); + } + + internal abstract class ContainerCommandSettings : ContainerTransportSettingsBase + { + private string _cmd; + private string _args; + + public ContainerCommandSettings(string hostname, bool hostIsUnix, string windowsExe, string unixExe, string hostnameFormat) + : base(hostname, hostIsUnix, windowsExe, unixExe, hostnameFormat) + { } + + public void SetCommand(string cmd, string args) + { + _cmd = cmd; + _args = args; + } + + protected override string SubCommand => _cmd; + protected override string SubCommandArgs => _args; + } +} diff --git a/src/SSHDebugPS/Docker/DockerConnection.cs b/src/SSHDebugPS/Docker/DockerConnection.cs index d152e61ac..846245dbf 100644 --- a/src/SSHDebugPS/Docker/DockerConnection.cs +++ b/src/SSHDebugPS/Docker/DockerConnection.cs @@ -198,7 +198,7 @@ private ICommandRunner GetExecCommandRunner(string commandText, bool handleRawOu return GetCommandRunner(execSettings, handleRawOutput: handleRawOutput); } - private ICommandRunner GetCommandRunner(DockerContainerTransportSettings settings, bool handleRawOutput = false) + private ICommandRunner GetCommandRunner(IPipeTransportSettings settings, bool handleRawOutput = false) { if (OuterConnection == null) { diff --git a/src/SSHDebugPS/Docker/DockerContainerInstance.cs b/src/SSHDebugPS/Docker/DockerContainerInstance.cs index 810975773..e0df057a6 100644 --- a/src/SSHDebugPS/Docker/DockerContainerInstance.cs +++ b/src/SSHDebugPS/Docker/DockerContainerInstance.cs @@ -37,7 +37,7 @@ public static bool TryCreate(string json, out DockerContainerInstance instance) return instance != null; } - private DockerContainerInstance() { } + protected DockerContainerInstance() { } #region JsonProperties @@ -48,19 +48,19 @@ private DockerContainerInstance() { } public override string Name { get; set; } [JsonProperty(nameof(Image))] - public string Image { get; private set; } + public virtual string Image { get; protected set; } [JsonProperty(nameof(Ports))] - public string Ports { get; set; } + public virtual string Ports { get; set; } [JsonProperty(nameof(Command))] - public string Command { get; private set; } + public virtual string Command { get; protected set; } [JsonProperty(nameof(Status))] - public string Status { get; private set; } + public virtual string Status { get; protected set; } [JsonProperty("CreatedAt")] - public string Created { get; private set; } + public virtual string Created { get; protected set; } [JsonIgnore] public string Platform { get; set; } diff --git a/src/SSHDebugPS/Docker/DockerDiscoveryStrategy.cs b/src/SSHDebugPS/Docker/DockerDiscoveryStrategy.cs new file mode 100644 index 000000000..d47aad0b6 --- /dev/null +++ b/src/SSHDebugPS/Docker/DockerDiscoveryStrategy.cs @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Microsoft.SSHDebugPS.Docker; +using Microsoft.SSHDebugPS.UI; + +namespace Microsoft.SSHDebugPS +{ + internal sealed class DockerDiscoveryStrategy : IContainerDiscoveryStrategy + { + private const string unknownOS = "Unknown"; + + public string ConnectionLabel => UIResources.ConnectionLabel; + public string HostnameLabel => UIResources.HostnameLabel; + public string HostnameTip => UIResources.HostnameTip; + public string ConnectionToolTip => UIResources.ConnectionToolTip; + public string HostnameAutomationName => UIResources.HostnameAutomationName; + + public IEnumerable GetLocalContainers(string hostname, out int totalContainers) + { + return DockerHelper.GetLocalDockerContainers(hostname, out totalContainers); + } + + public IEnumerable GetRemoteContainers(IConnection connection, string hostname, out int totalContainers) + { + return DockerHelper.GetRemoteDockerContainers(connection, hostname, out totalContainers); + } + + public void AssignPlatforms(IEnumerable containers, string hostname) + { + if (!containers.Any()) + return; + + string serverOS; + if (DockerHelper.TryGetServerOS(hostname, out serverOS)) + { + bool lcow; + DockerHelper.TryGetLCOW(hostname, out lcow); + TextInfo textInfo = new CultureInfo("en-US", false).TextInfo; + + if (lcow && serverOS.IndexOf("windows", StringComparison.OrdinalIgnoreCase) >= 0) + { + foreach (DockerContainerInstance container in containers) + { + string containerPlatform = string.Empty; + if (DockerHelper.TryGetContainerPlatform(hostname, container.Name, out containerPlatform)) + { + container.Platform = textInfo.ToTitleCase(containerPlatform); + } + else + { + container.Platform = unknownOS; + } + } + } + else + { + string platform = textInfo.ToTitleCase(serverOS); + foreach (DockerContainerInstance container in containers) + { + container.Platform = platform; + } + } + } + else + { + foreach (DockerContainerInstance container in containers) + { + container.Platform = unknownOS; + } + } + } + } +} diff --git a/src/SSHDebugPS/Docker/DockerExecutionManager.cs b/src/SSHDebugPS/Docker/DockerExecutionManager.cs index 4d36eb4cc..e2f018926 100644 --- a/src/SSHDebugPS/Docker/DockerExecutionManager.cs +++ b/src/SSHDebugPS/Docker/DockerExecutionManager.cs @@ -52,18 +52,27 @@ internal class DockerExecutionManager private PipeAsyncCommand _currentCommand; private Connection _outerConnection = null; - private DockerContainerTransportSettings _baseSettings; + private ContainerTargetTransportSettings _baseSettings; private readonly ManualResetEvent _commandCompleteEvent = new ManualResetEvent(false); - public DockerExecutionManager(DockerContainerTransportSettings baseSettings, Connection outerConnection) + public DockerExecutionManager(ContainerTargetTransportSettings baseSettings, Connection outerConnection) { _baseSettings = baseSettings; _outerConnection = outerConnection; } + protected virtual ContainerExecSettings CreateExecSettings(ContainerTargetTransportSettings baseSettings, string command, bool runInShell, bool makeInteractive) + { + if (!(baseSettings is DockerContainerTransportSettings dockerSettings)) + { + throw new ArgumentException($"Expected {nameof(DockerContainerTransportSettings)} but got {baseSettings.GetType().Name}", nameof(baseSettings)); + } + return new DockerExecSettings(dockerSettings, command, runInShell, makeInteractive); + } + private ICommandRunner GetExecCommandRunner(string command, bool runInShell, bool makeInteractive) { - var execSettings = new DockerExecSettings(_baseSettings, command, runInShell, makeInteractive); + var execSettings = CreateExecSettings(_baseSettings, command, runInShell, makeInteractive); if (_outerConnection == null) { diff --git a/src/SSHDebugPS/Docker/DockerHelper.cs b/src/SSHDebugPS/Docker/DockerHelper.cs index 5bbd7889c..f5a7a06ae 100644 --- a/src/SSHDebugPS/Docker/DockerHelper.cs +++ b/src/SSHDebugPS/Docker/DockerHelper.cs @@ -26,7 +26,7 @@ public class DockerHelper private const string dockerInspectArgs = "-f \"{{json .Platform}}\" "; private static char[] charsToTrim = { ' ', '\"' }; - private static void RunDockerCommand(DockerCommandSettings settings, Action callback) + internal static void RunContainerCommand(IPipeTransportSettings settings, Action callback) { LocalCommandRunner commandRunner = new LocalCommandRunner(settings); @@ -106,7 +106,7 @@ internal static bool TryGetLCOW(string hostname, out bool lcow) try { - RunDockerCommand(settings, delegate (string args) + RunContainerCommand(settings, delegate (string args) { if (args.Contains("lcow")) { @@ -134,7 +134,7 @@ internal static bool TryGetServerOS(string hostname, out string serverOS) try { - RunDockerCommand(settings, delegate (string args) + RunContainerCommand(settings, delegate (string args) { delegateServerOS = args; }); @@ -159,7 +159,7 @@ internal static bool TryGetContainerPlatform(string hostname, string containerNa try { - RunDockerCommand(settings, delegate (string args) + RunContainerCommand(settings, delegate (string args) { delegateContainerPlatform = args; }); @@ -183,7 +183,7 @@ internal static IEnumerable GetLocalDockerContainers(st DockerCommandSettings settings = new DockerCommandSettings(hostname, false); settings.SetCommand(dockerPSCommand, dockerPSArgs); - RunDockerCommand(settings, delegate (string args) + RunContainerCommand(settings, delegate (string args) { if (args.Trim()[0] == '{') { diff --git a/src/SSHDebugPS/Docker/DockerPortPicker.cs b/src/SSHDebugPS/Docker/DockerPortPicker.cs index d098bb7f3..4489e14c8 100644 --- a/src/SSHDebugPS/Docker/DockerPortPicker.cs +++ b/src/SSHDebugPS/Docker/DockerPortPicker.cs @@ -33,12 +33,13 @@ public class DockerWindowsPortPicker : DockerPortPickerBase public abstract class DockerPortPickerBase : IDebugPortPicker { internal abstract bool SupportSSHConnections { get; } + internal virtual ContainerRuntimeType RuntimeType => ContainerRuntimeType.Docker; int IDebugPortPicker.DisplayPortPicker(IntPtr hwndParentDialog, out string pbstrPortId) { ThreadHelper.ThrowIfNotOnUIThread(); // If this is null, then the PortPicker handler shows an error. Set to empty by default - return ConnectionManager.ShowContainerPickerWindow(hwndParentDialog, SupportSSHConnections, out pbstrPortId) ? + return ConnectionManager.ShowContainerPickerWindow(hwndParentDialog, SupportSSHConnections, RuntimeType, out pbstrPortId) ? VSConstants.S_OK : VSConstants.S_FALSE; } diff --git a/src/SSHDebugPS/Docker/TransportSettings/DockerContainerTransportSettings.cs b/src/SSHDebugPS/Docker/TransportSettings/DockerContainerTransportSettings.cs index 593eea60a..cf61e1c64 100644 --- a/src/SSHDebugPS/Docker/TransportSettings/DockerContainerTransportSettings.cs +++ b/src/SSHDebugPS/Docker/TransportSettings/DockerContainerTransportSettings.cs @@ -1,97 +1,38 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using Microsoft.SSHDebugPS.Utilities; -using System.Diagnostics; - namespace Microsoft.SSHDebugPS.Docker { - internal class DockerContainerTransportSettings : DockerTransportSettingsBase + internal sealed class DockerContainerTransportSettings : ContainerTargetTransportSettings { - internal string ContainerName { get; private set; } + internal const string WindowsExeName = "docker.exe"; + internal const string UnixExeName = "docker"; + internal const string HostFlag = "--host \"{0}\""; public DockerContainerTransportSettings(string hostname, string containerName, bool hostIsUnix) - : base(hostname, hostIsUnix) - { - ContainerName = containerName; - } + : base(hostname, containerName, hostIsUnix, WindowsExeName, UnixExeName, HostFlag) + { } public DockerContainerTransportSettings(DockerContainerTransportSettings settings) : base(settings) - { - ContainerName = settings.ContainerName; - } - - protected override string SubCommand => throw new System.NotImplementedException(); - protected override string SubCommandArgs => throw new System.NotImplementedException(); + { } } - internal class DockerExecSettings : DockerContainerTransportSettings + internal sealed class DockerExecSettings : ContainerExecSettings { - private bool _runInShell; - private string _commandToExecute; - // 0 = container, 1 = command to execute - private const string _subCommandArgsFormat = "{0} {1}"; - private const string _subCommandArgsFormatWithShell = "{0} /bin/sh -c \"{1}\""; - private const string _subCommandArgsFormatWithShellLinuxHost = "{0} /bin/sh -c '{1}'"; // Single quote the argument on Linux so variable resolution does not happen until it is in the container. - private const string _interactiveFlag = "-i "; - - private bool _makeInteractive; - public DockerExecSettings(DockerContainerTransportSettings settings, string command, bool runInShell, bool makeInteractive = true) - : base(settings) - { - Debug.Assert(!string.IsNullOrWhiteSpace(command), "Exec command cannot be null"); - _runInShell = runInShell; - _commandToExecute = command; - _makeInteractive = makeInteractive; - } - - protected override string SubCommand => "exec"; - protected override string SubCommandArgs - { - get - { - string subCommandFormat = this.HostIsUnix ? _subCommandArgsFormatWithShellLinuxHost : _subCommandArgsFormatWithShell; - // Because _subCommandArgsFormatWithShellLinuxHost single quotes the the subcommand arguments, we need to escape the command's single quotes - // by closing the single quotes and adding an escaped single quote and then reopening the single quote. - string command = this.HostIsUnix ? _commandToExecute.Replace("'", "'\\''") : _commandToExecute; - return (_makeInteractive ? _interactiveFlag : string.Empty) + - (_runInShell ? subCommandFormat : _subCommandArgsFormat).FormatInvariantWithArgs(ContainerName, command); - } - } + : base(settings, command, runInShell, makeInteractive) + { } } - internal class DockerCopySettings : DockerContainerTransportSettings + internal sealed class DockerCopySettings : ContainerCopySettings { - // {0} = container, {1} = source, {2} = destination - private string _copyFormatToContainer = "{1} {0}:{2}"; - - private string _sourcePath; - private string _destinationPath; - - /// - /// Settings to copy from host to the docker container - /// - /// Local path on host - /// Remote path within the docker container - /// Name of container - /// Host is Unix public DockerCopySettings(string hostname, string sourcePath, string destinationPath, string containerName, bool hostIsUnix) - : base(hostname, containerName, hostIsUnix) - { - _sourcePath = sourcePath; - _destinationPath = destinationPath; - } + : base(hostname, sourcePath, destinationPath, containerName, hostIsUnix, DockerContainerTransportSettings.WindowsExeName, DockerContainerTransportSettings.UnixExeName, DockerContainerTransportSettings.HostFlag) + { } public DockerCopySettings(DockerContainerTransportSettings settings, string sourcePath, string destinationPath) - : base(settings) - { - _sourcePath = sourcePath; - _destinationPath = destinationPath; - } - - protected override string SubCommand => "cp"; - protected override string SubCommandArgs => _copyFormatToContainer.FormatInvariantWithArgs(ContainerName, _sourcePath, _destinationPath); + : base(settings, sourcePath, destinationPath) + { } } } diff --git a/src/SSHDebugPS/Docker/TransportSettings/DockerTransportSettings.cs b/src/SSHDebugPS/Docker/TransportSettings/DockerTransportSettings.cs index 63e5f0c43..9b1024f85 100644 --- a/src/SSHDebugPS/Docker/TransportSettings/DockerTransportSettings.cs +++ b/src/SSHDebugPS/Docker/TransportSettings/DockerTransportSettings.cs @@ -1,78 +1,12 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using Microsoft.SSHDebugPS.Utilities; - namespace Microsoft.SSHDebugPS.Docker { - internal abstract class DockerTransportSettingsBase : IPipeTransportSettings - { - protected abstract string SubCommand { get; } - protected abstract string SubCommandArgs { get; } - - internal string HostName { get; private set; } - internal bool HostIsUnix { get; private set; } - - public DockerTransportSettingsBase(string hostname, bool hostIsUnix) - { - HostIsUnix = hostIsUnix; - if (!string.IsNullOrWhiteSpace(hostname)) - { - HostName = hostname; - } - else - { - HostName = string.Empty; - } - } - - public DockerTransportSettingsBase(DockerTransportSettingsBase settings) - : this(settings.HostName, settings.HostIsUnix) - { } - - private static string WindowsExe => "docker.exe"; - private static string UnixExe => "docker"; - - // 0 = docker command parameters - // 1 = docker subcommand - // 2 = docker subcommand parameters - private const string _baseCommandFormat = "{0} {1} {2}"; - // 0 = hostname property - private const string _hostnameFormat = "--host \"{0}\""; - private string GenerateExeCommandArgs() - { - var hostnameArg = string.Empty; - if (!string.IsNullOrWhiteSpace(this.HostName)) - hostnameArg = _hostnameFormat.FormatInvariantWithArgs(this.HostName); - - return _baseCommandFormat.FormatInvariantWithArgs(hostnameArg, SubCommand, SubCommandArgs); - } - - #region IPipeTransportSettings - - public string CommandArgs => GenerateExeCommandArgs(); - - public string Command => HostIsUnix ? UnixExe : WindowsExe; - #endregion - } - - internal class DockerCommandSettings : DockerTransportSettingsBase + internal sealed class DockerCommandSettings : ContainerCommandSettings { - private string _cmd; - private string _args; - public DockerCommandSettings(string hostname, bool hostIsUnix) - : base(hostname, hostIsUnix) + : base(hostname, hostIsUnix, DockerContainerTransportSettings.WindowsExeName, DockerContainerTransportSettings.UnixExeName, DockerContainerTransportSettings.HostFlag) { } - - public void SetCommand(string cmd, string args) - { - _cmd = cmd; - _args = args; - } - - protected override string SubCommand => _cmd; - protected override string SubCommandArgs => _args; } } - diff --git a/src/SSHDebugPS/IContainerDiscoveryStrategy.cs b/src/SSHDebugPS/IContainerDiscoveryStrategy.cs new file mode 100644 index 000000000..c7fd87bb7 --- /dev/null +++ b/src/SSHDebugPS/IContainerDiscoveryStrategy.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using Microsoft.SSHDebugPS.Docker; + +namespace Microsoft.SSHDebugPS +{ + internal interface IContainerDiscoveryStrategy + { + string ConnectionLabel { get; } + string HostnameLabel { get; } + string HostnameTip { get; } + string ConnectionToolTip { get; } + string HostnameAutomationName { get; } + + IEnumerable GetLocalContainers(string hostname, out int totalContainers); + IEnumerable GetRemoteContainers(IConnection connection, string hostname, out int totalContainers); + void AssignPlatforms(IEnumerable containers, string hostname); + } +} diff --git a/src/SSHDebugPS/UI/ContainerPickerDialogWindow.xaml b/src/SSHDebugPS/UI/ContainerPickerDialogWindow.xaml index cfdc1e8d6..81387168c 100644 --- a/src/SSHDebugPS/UI/ContainerPickerDialogWindow.xaml +++ b/src/SSHDebugPS/UI/ContainerPickerDialogWindow.xaml @@ -512,14 +512,14 @@ VerticalAlignment="Center" Style="{StaticResource MainLabelStyle}" Target="{Binding ElementName=ConnectionTypeComboBox}" - Content="{x:Static local:UIResources.ConnectionLabel}" /> + Content="{Binding ConnectionLabelText}" /> + Content="{Binding HostnameLabelText}" /> + ToolTip="{Binding HostnameTipText}"> diff --git a/src/SSHDebugPS/UI/ContainerPickerDialogWindow.xaml.cs b/src/SSHDebugPS/UI/ContainerPickerDialogWindow.xaml.cs index feec9e5b7..ee0aae59c 100644 --- a/src/SSHDebugPS/UI/ContainerPickerDialogWindow.xaml.cs +++ b/src/SSHDebugPS/UI/ContainerPickerDialogWindow.xaml.cs @@ -25,9 +25,9 @@ namespace Microsoft.SSHDebugPS.UI /// public partial class ContainerPickerDialogWindow : DialogWindow { - public ContainerPickerDialogWindow(bool supportSSHConnections) + public ContainerPickerDialogWindow(bool supportSSHConnections, ContainerRuntimeType runtimeType) { - _model = new ContainerPickerViewModel(supportSSHConnections); + _model = new ContainerPickerViewModel(supportSSHConnections, runtimeType); this.DataContext = _model; this.Loaded += OnWindowLoaded; diff --git a/src/SSHDebugPS/UI/ViewModels/ContainerPickerViewModel.cs b/src/SSHDebugPS/UI/ViewModels/ContainerPickerViewModel.cs index 2fefd0890..0f19c2f20 100644 --- a/src/SSHDebugPS/UI/ViewModels/ContainerPickerViewModel.cs +++ b/src/SSHDebugPS/UI/ViewModels/ContainerPickerViewModel.cs @@ -22,10 +22,11 @@ public class ContainerPickerViewModel : INotifyPropertyChanged { private Lazy _sshAvailable; - public ContainerPickerViewModel(bool supportSSHConnections) + public ContainerPickerViewModel(bool supportSSHConnections, ContainerRuntimeType runtimeType) { ThreadHelper.ThrowIfNotOnUIThread(); SupportSSHConnections = supportSSHConnections; + _discoveryStrategy = CreateDiscoveryStrategy(runtimeType); InitializeConnections(); ContainerInstances = new ObservableCollection(); @@ -142,7 +143,29 @@ private bool ComputeContainerConnectionString() // The formatted string for the ConnectionType dialog public string SelectedContainerConnectionString { get; private set; } - private const string unknownOS = "Unknown"; + private readonly IContainerDiscoveryStrategy _discoveryStrategy; + + private static IContainerDiscoveryStrategy CreateDiscoveryStrategy(ContainerRuntimeType runtimeType) + { + switch (runtimeType) + { + case ContainerRuntimeType.Docker: + return new DockerDiscoveryStrategy(); + default: + Debug.Fail($"Unsupported container runtime type: {runtimeType}"); + return null; + } + } + + public string ConnectionLabelText => _discoveryStrategy?.ConnectionLabel ?? UIResources.ConnectionLabel; + + public string HostnameLabelText => _discoveryStrategy?.HostnameLabel ?? UIResources.HostnameLabel; + + public string HostnameTipText => _discoveryStrategy?.HostnameTip ?? UIResources.HostnameTip; + + public string ConnectionToolTipText => _discoveryStrategy?.ConnectionToolTip ?? UIResources.ConnectionToolTip; + + public string HostnameAutomationNameText => _discoveryStrategy?.HostnameAutomationName ?? UIResources.HostnameAutomationName; private void RefreshContainersListInternal() { @@ -152,11 +175,19 @@ private void RefreshContainersListInternal() IContainerViewModel selectedContainer = SelectedContainerInstance; SelectedContainerInstance = null; + var viewModels = new List(); + + if (_discoveryStrategy == null) + { + UpdateStatusMessage(string.Format(CultureInfo.CurrentCulture, UIResources.ContainersFoundStatusText, 0), isError: true); + return; + } + IEnumerable containers; if (SelectedConnection is LocalConnectionViewModel) { - containers = DockerHelper.GetLocalDockerContainers(Hostname, out totalContainers); + containers = _discoveryStrategy.GetLocalContainers(Hostname, out totalContainers); } else { @@ -167,59 +198,16 @@ private void RefreshContainersListInternal() UpdateStatusMessage(UIResources.SSHConnectionFailedStatusText, isError: true); return; } - containers = DockerHelper.GetRemoteDockerContainers(connection, Hostname, out totalContainers); + containers = _discoveryStrategy.GetRemoteContainers(connection, Hostname, out totalContainers); } - if (containers.Any()) + if (containers != null) { - string serverOS; - - if (DockerHelper.TryGetServerOS(Hostname, out serverOS)) - { - bool lcow; - bool getLCOW = DockerHelper.TryGetLCOW(Hostname, out lcow); - TextInfo textInfo = new CultureInfo("en-US", false).TextInfo; - serverOS = textInfo.ToTitleCase(serverOS); - - /* Note: LCOW is the abbreviation for Linux Containers on Windows - * - * In LCOW, both Linux and Windows containers can run simultaneously in a Docker (Windows) Engine. - * Thus, the container platform must be queried directly. - * Otherwise, the container platform must match that of the server engine. - */ - if (lcow && serverOS.Contains("Windows")) - { - foreach (DockerContainerInstance container in containers) - { - string containerPlatform = string.Empty; - if (DockerHelper.TryGetContainerPlatform(Hostname, container.Name, out containerPlatform)) - { - container.Platform = textInfo.ToTitleCase(containerPlatform); - } - else - { - container.Platform = unknownOS; - } - } - } - else - { - foreach (DockerContainerInstance container in containers) - { - container.Platform = serverOS; - } - } - } - else - { - foreach (DockerContainerInstance container in containers) - { - container.Platform = unknownOS; - } - } + _discoveryStrategy.AssignPlatforms(containers, Hostname); + viewModels.AddRange(containers.Select(item => (IContainerViewModel)new DockerContainerViewModel(item))); } - ContainerInstances = new ObservableCollection(containers.Select(item => new DockerContainerViewModel(item)).ToList()); + ContainerInstances = new ObservableCollection(viewModels); OnPropertyChanged(nameof(ContainerInstances)); if (ContainerInstances.Count > 0) From cd8948942e33808eb8a886c261ad065b6ca8c473 Mon Sep 17 00:00:00 2001 From: Andrew Wang Date: Fri, 19 Jun 2026 10:03:02 -0700 Subject: [PATCH 04/25] Add Podman container support for Attach to Process (#1582) * Add Podman container support for Attach to Process Adds Podman as a container runtime option in the Attach to Process dialog, enabling developers to discover and debug processes inside Podman containers. - New PodmanConnection, PodmanContainerInstance, PodmanDiscoveryStrategy, PodmanExecutionManager, PodmanHelper, PodmanPortPicker, PodmanPortSupplier, and PodmanTransportSettings classes - Renamed DockerContainerInstance to ContainerInstance (shared by both runtimes) - Renamed DockerHostPrefixRegex/DockerHostPrefix to HostPrefixRegex/HostPrefix - Registered Podman port supplier and CLSID in pkgdef files - Added ContainerRuntimeType.Podman enum value - Added Podman case in ContainerPickerViewModel and ConnectionManager NOTE: The Podman port supplier also needs to be registered in vsdbg's VsIntegration.pkgdef for full VS integration. * Address PR comments --- .../Microsoft.MIDebugEngine.pkgdef | 4 + src/SSHDebugPS/ConnectionManager.cs | 41 +++++ src/SSHDebugPS/ContainerRuntimeType.cs | 4 +- ...tainerInstance.cs => ContainerInstance.cs} | 77 +++++++-- src/SSHDebugPS/Docker/DockerConnection.cs | 12 +- .../Docker/DockerDiscoveryStrategy.cs | 12 +- src/SSHDebugPS/Docker/DockerHelper.cs | 14 +- src/SSHDebugPS/IContainerDiscoveryStrategy.cs | 6 +- src/SSHDebugPS/Microsoft.SSHDebugPS.pkgdef | 17 ++ src/SSHDebugPS/Podman/PodmanConnection.cs | 155 ++++++++++++++++++ .../Podman/PodmanContainerInstance.cs | 63 +++++++ .../Podman/PodmanDiscoveryStrategy.cs | 37 +++++ .../Podman/PodmanExecutionManager.cs | 19 +++ src/SSHDebugPS/Podman/PodmanHelper.cs | 155 ++++++++++++++++++ src/SSHDebugPS/Podman/PodmanJsonConverter.cs | 52 ++++++ src/SSHDebugPS/Podman/PodmanPort.cs | 20 +++ src/SSHDebugPS/Podman/PodmanPortPicker.cs | 18 ++ src/SSHDebugPS/Podman/PodmanPortSupplier.cs | 58 +++++++ .../Podman/PodmanTransportSettings.cs | 46 ++++++ src/SSHDebugPS/StringResources.Designer.cs | 36 ++++ src/SSHDebugPS/StringResources.resx | 13 ++ src/SSHDebugPS/UI/ContainerInstance.cs | 70 +------- src/SSHDebugPS/UI/UIResources.Designer.cs | 45 +++++ src/SSHDebugPS/UI/UIResources.resx | 16 ++ .../UI/ViewModels/ContainerPickerViewModel.cs | 5 +- .../UI/ViewModels/ContainerViewModel.cs | 4 +- src/SSHDebugPS/Utilities/TelemetryHelper.cs | 1 + 27 files changed, 889 insertions(+), 111 deletions(-) rename src/SSHDebugPS/Docker/{DockerContainerInstance.cs => ContainerInstance.cs} (50%) create mode 100644 src/SSHDebugPS/Podman/PodmanConnection.cs create mode 100644 src/SSHDebugPS/Podman/PodmanContainerInstance.cs create mode 100644 src/SSHDebugPS/Podman/PodmanDiscoveryStrategy.cs create mode 100644 src/SSHDebugPS/Podman/PodmanExecutionManager.cs create mode 100644 src/SSHDebugPS/Podman/PodmanHelper.cs create mode 100644 src/SSHDebugPS/Podman/PodmanJsonConverter.cs create mode 100644 src/SSHDebugPS/Podman/PodmanPort.cs create mode 100644 src/SSHDebugPS/Podman/PodmanPortPicker.cs create mode 100644 src/SSHDebugPS/Podman/PodmanPortSupplier.cs create mode 100644 src/SSHDebugPS/Podman/PodmanTransportSettings.cs diff --git a/src/MIDebugEngine/Microsoft.MIDebugEngine.pkgdef b/src/MIDebugEngine/Microsoft.MIDebugEngine.pkgdef index ce626ec28..85c7fe331 100644 --- a/src/MIDebugEngine/Microsoft.MIDebugEngine.pkgdef +++ b/src/MIDebugEngine/Microsoft.MIDebugEngine.pkgdef @@ -54,6 +54,8 @@ "1"="{A2BBC114-47E4-473F-A49C-69EE89711243}" ; WSL Port supplier "2"="{267B1341-AC92-44DC-94DF-2EE4205DD17E}" +; Podman Port Supplier +"3"="{D4F2F3A5-6B7C-4E8D-9F0A-1B2C3D4E5F6A}" ; Registration to use lldb with the port suppliers [$RootKey$\AD7Metrics\Engine\{5D630903-189D-4837-9785-699B05BEC2A9}] @@ -83,6 +85,8 @@ "0"="{3FDDF14E-E758-4695-BE0C-7509920432C9}" ; WSL Port supplier "1"="{267B1341-AC92-44DC-94DF-2EE4205DD17E}" +; Podman Port Supplier +"2"="{D4F2F3A5-6B7C-4E8D-9F0A-1B2C3D4E5F6A}" [$RootKey$\AD7Metrics\Engine\{5D630903-189D-4837-9785-699B05BEC2A9}\IncompatibleList] "MI Debug Engine - gdb"="{91744D97-430F-42C1-9779-A5813EBD6AB2}" diff --git a/src/SSHDebugPS/ConnectionManager.cs b/src/SSHDebugPS/ConnectionManager.cs index 3bd40dec3..bc8be3627 100644 --- a/src/SSHDebugPS/ConnectionManager.cs +++ b/src/SSHDebugPS/ConnectionManager.cs @@ -14,6 +14,7 @@ using liblinux; using liblinux.Persistence; using Microsoft.SSHDebugPS.Docker; +using Microsoft.SSHDebugPS.Podman; using Microsoft.SSHDebugPS.SSH; using Microsoft.SSHDebugPS.UI; using Microsoft.SSHDebugPS.Utilities; @@ -65,6 +66,46 @@ public static DockerConnection GetDockerConnection(string name, bool supportSSHC } } + public static PodmanConnection GetPodmanConnection(string name, bool supportSSHConnections) + { + if (string.IsNullOrWhiteSpace(name)) + return null; + + PodmanContainerTransportSettings settings; + Connection remoteConnection; + + ThreadHelper.ThrowIfNotOnUIThread(); + if (!PodmanConnection.TryConvertConnectionStringToSettings(name, out settings, out remoteConnection) || settings == null) + { + string connectionString; + + bool success = ShowContainerPickerWindow(IntPtr.Zero, supportSSHConnections, ContainerRuntimeType.Podman, out connectionString); + if (success) + { + success = PodmanConnection.TryConvertConnectionStringToSettings(connectionString, out settings, out remoteConnection); + } + + if (!success || settings == null) + { + VSMessageBoxHelper.PostErrorMessage(StringResources.Error_ContainerConnectionStringInvalidTitle, StringResources.Error_ContainerConnectionStringInvalidMessage); + return null; + } + } + + string displayName = PodmanConnection.CreateConnectionString(settings.ContainerName, remoteConnection?.Name, settings.HostName); + if (PodmanHelper.IsContainerRunning(settings.HostName, settings.ContainerName, remoteConnection)) + { + return new PodmanConnection(settings, remoteConnection, displayName); + } + else + { + VSMessageBoxHelper.PostErrorMessage( + StringResources.Error_ContainerUnavailableTitle, + StringResources.Error_ContainerUnavailableMessage.FormatCurrentCultureWithArgs(settings.ContainerName)); + return null; + } + } + public static SSHConnection GetSSHConnection(string name) { ThreadHelper.ThrowIfNotOnUIThread(); diff --git a/src/SSHDebugPS/ContainerRuntimeType.cs b/src/SSHDebugPS/ContainerRuntimeType.cs index 1a8afa73a..612c91035 100644 --- a/src/SSHDebugPS/ContainerRuntimeType.cs +++ b/src/SSHDebugPS/ContainerRuntimeType.cs @@ -8,7 +8,7 @@ namespace Microsoft.SSHDebugPS /// public enum ContainerRuntimeType { - Unknown, - Docker + Docker, + Podman } } diff --git a/src/SSHDebugPS/Docker/DockerContainerInstance.cs b/src/SSHDebugPS/Docker/ContainerInstance.cs similarity index 50% rename from src/SSHDebugPS/Docker/DockerContainerInstance.cs rename to src/SSHDebugPS/Docker/ContainerInstance.cs index e0df057a6..1e830d4cb 100644 --- a/src/SSHDebugPS/Docker/DockerContainerInstance.cs +++ b/src/SSHDebugPS/Docker/ContainerInstance.cs @@ -11,18 +11,18 @@ namespace Microsoft.SSHDebugPS.Docker { - public class DockerContainerInstance : ContainerInstance + public class ContainerInstance : IContainerInstance { /// - /// Create a DockerContainerInstance from the results of docker ps in JSON format + /// Create a ContainerInstance from the results of docker ps in JSON format /// - public static bool TryCreate(string json, out DockerContainerInstance instance) + public static bool TryCreate(string json, out ContainerInstance instance) { instance = null; try { JObject obj = JObject.Parse(json); - instance = obj.ToObject(); + instance = obj.ToObject(); } catch (Exception e) { @@ -37,15 +37,15 @@ public static bool TryCreate(string json, out DockerContainerInstance instance) return instance != null; } - protected DockerContainerInstance() { } + protected ContainerInstance() { } #region JsonProperties [JsonProperty("ID")] - public override string Id { get; set; } + public virtual string Id { get; set; } [JsonProperty("Names")] - public override string Name { get; set; } + public virtual string Name { get; set; } [JsonProperty(nameof(Image))] public virtual string Image { get; protected set; } @@ -67,24 +67,71 @@ protected DockerContainerInstance() { } #endregion - // Docker container names: only [a-zA-Z0-9][a-zA-Z0-9_.-] are allowed. It is also case sensitive - protected override bool EqualsInternal(ContainerInstance instance) + #region IEquatable + + public static bool operator ==(ContainerInstance left, ContainerInstance right) { - if (instance is DockerContainerInstance other) + if (left is null || right is null) { - // the id can be a partial on a container - return String.Equals(Id, other.Id, StringComparison.Ordinal) || - Id.StartsWith(other.Id, StringComparison.Ordinal) || - other.Id.StartsWith(Id, StringComparison.Ordinal); + return ReferenceEquals(left, right); } + return left.Equals(right); + } + + public static bool operator !=(ContainerInstance left, ContainerInstance right) + { + return !(left == right); + } + + public bool Equals(IContainerInstance instance) + { + if (instance is ContainerInstance container) + { + return this.EqualsInternal(container); + } + + return false; + } + + public override bool Equals(object obj) + { + if (obj is IContainerInstance instance) + { + return this.Equals(instance); + } return false; } - protected override int GetHashCodeInternal() + public override int GetHashCode() + { + return GetHashCodeInternal(); + } + + #endregion + + #region Helper Methods + + // Container names: only [a-zA-Z0-9][a-zA-Z0-9_.-] are allowed. It is also case sensitive + protected virtual bool EqualsInternal(ContainerInstance instance) + { + if (GetType() != instance.GetType()) + { + return false; + } + + // the id can be a partial on a container + return String.Equals(Id, instance.Id, StringComparison.Ordinal) || + Id.StartsWith(instance.Id, StringComparison.Ordinal) || + instance.Id.StartsWith(Id, StringComparison.Ordinal); + } + + protected virtual int GetHashCodeInternal() { // Since IDs can be partial, we don't have a good way to get a good hash code. return string.IsNullOrWhiteSpace(Id) ? 0 : Id.Substring(0,1).GetHashCode(); } + + #endregion } } diff --git a/src/SSHDebugPS/Docker/DockerConnection.cs b/src/SSHDebugPS/Docker/DockerConnection.cs index 846245dbf..bf99504fb 100644 --- a/src/SSHDebugPS/Docker/DockerConnection.cs +++ b/src/SSHDebugPS/Docker/DockerConnection.cs @@ -20,8 +20,8 @@ internal class DockerConnection : PipeConnection internal const string SshPrefixRegex = @"^[Ss]{2}[Hh]\s*=\s*"; internal const string SshPrefix = "ssh="; - internal const string DockerHostPrefixRegex = @"^host\s*=\s*"; - internal const string DockerHostPrefix = "host="; + internal const string HostPrefixRegex = @"^host\s*=\s*"; + internal const string HostPrefix = "host="; internal const char Separator = ';'; internal static string CreateConnectionString(string containerName, string remoteConnectionName, string hostName) @@ -34,7 +34,7 @@ internal static string CreateConnectionString(string containerName, string remot if (!string.IsNullOrWhiteSpace(hostName)) { - connectionString += Separator + DockerHostPrefix + hostName; + connectionString += Separator + HostPrefix + hostName; } return connectionString; @@ -56,7 +56,7 @@ internal static bool TryConvertConnectionStringToSettings(string connectionStrin if (connectionStrings.Length <= 3 && connectionStrings.Length > 0) { Regex SshRegex = new Regex(SshPrefixRegex); - Regex dockerHostRegex = new Regex(DockerHostPrefixRegex); + Regex hostRegex = new Regex(HostPrefixRegex); foreach (var item in connectionStrings) { @@ -66,9 +66,9 @@ internal static bool TryConvertConnectionStringToSettings(string connectionStrin Match match = SshRegex.Match(segment); remoteConnection = ConnectionManager.GetSSHConnection(segment.Substring(match.Length)); } - else if (dockerHostRegex.IsMatch(segment)) + else if (hostRegex.IsMatch(segment)) { - Match match = dockerHostRegex.Match(segment); + Match match = hostRegex.Match(segment); hostName = segment.Substring(match.Length); } else if (segment.Contains("=")) diff --git a/src/SSHDebugPS/Docker/DockerDiscoveryStrategy.cs b/src/SSHDebugPS/Docker/DockerDiscoveryStrategy.cs index d47aad0b6..e6847967d 100644 --- a/src/SSHDebugPS/Docker/DockerDiscoveryStrategy.cs +++ b/src/SSHDebugPS/Docker/DockerDiscoveryStrategy.cs @@ -20,17 +20,17 @@ internal sealed class DockerDiscoveryStrategy : IContainerDiscoveryStrategy public string ConnectionToolTip => UIResources.ConnectionToolTip; public string HostnameAutomationName => UIResources.HostnameAutomationName; - public IEnumerable GetLocalContainers(string hostname, out int totalContainers) + public IEnumerable GetLocalContainers(string hostname, out int totalContainers) { return DockerHelper.GetLocalDockerContainers(hostname, out totalContainers); } - public IEnumerable GetRemoteContainers(IConnection connection, string hostname, out int totalContainers) + public IEnumerable GetRemoteContainers(IConnection connection, string hostname, out int totalContainers) { return DockerHelper.GetRemoteDockerContainers(connection, hostname, out totalContainers); } - public void AssignPlatforms(IEnumerable containers, string hostname) + public void AssignPlatforms(IEnumerable containers, string hostname) { if (!containers.Any()) return; @@ -44,7 +44,7 @@ public void AssignPlatforms(IEnumerable containers, str if (lcow && serverOS.IndexOf("windows", StringComparison.OrdinalIgnoreCase) >= 0) { - foreach (DockerContainerInstance container in containers) + foreach (ContainerInstance container in containers) { string containerPlatform = string.Empty; if (DockerHelper.TryGetContainerPlatform(hostname, container.Name, out containerPlatform)) @@ -60,7 +60,7 @@ public void AssignPlatforms(IEnumerable containers, str else { string platform = textInfo.ToTitleCase(serverOS); - foreach (DockerContainerInstance container in containers) + foreach (ContainerInstance container in containers) { container.Platform = platform; } @@ -68,7 +68,7 @@ public void AssignPlatforms(IEnumerable containers, str } else { - foreach (DockerContainerInstance container in containers) + foreach (ContainerInstance container in containers) { container.Platform = unknownOS; } diff --git a/src/SSHDebugPS/Docker/DockerHelper.cs b/src/SSHDebugPS/Docker/DockerHelper.cs index f5a7a06ae..c433cb9d6 100644 --- a/src/SSHDebugPS/Docker/DockerHelper.cs +++ b/src/SSHDebugPS/Docker/DockerHelper.cs @@ -174,11 +174,11 @@ internal static bool TryGetContainerPlatform(string hostname, string containerNa return true; } - internal static IEnumerable GetLocalDockerContainers(string hostname, out int totalContainers) + internal static IEnumerable GetLocalDockerContainers(string hostname, out int totalContainers) { totalContainers = 0; int containerCount = 0; - List containers = new List(); + List containers = new List(); DockerCommandSettings settings = new DockerCommandSettings(hostname, false); settings.SetCommand(dockerPSCommand, dockerPSArgs); @@ -187,7 +187,7 @@ internal static IEnumerable GetLocalDockerContainers(st { if (args.Trim()[0] == '{') { - if (DockerContainerInstance.TryCreate(args, out DockerContainerInstance containerInstance)) + if (ContainerInstance.TryCreate(args, out ContainerInstance containerInstance)) { containers.Add(containerInstance); } @@ -205,7 +205,7 @@ internal static IEnumerable GetLocalDockerContainers(st // Another fallback option would be to: docker inspect --format {{.State.Status}} which should return "running" internal static bool IsContainerRunning(string hostName, string containerName, Connection remoteConnection) { - IEnumerable containers; + IEnumerable containers; if (remoteConnection != null) { containers = GetRemoteDockerContainers(remoteConnection, hostName, out _); @@ -228,7 +228,7 @@ internal static bool IsContainerRunning(string hostName, string containerName, C return false; } - internal static IEnumerable GetRemoteDockerContainers(IConnection connection, string hostname, out int totalContainers) + internal static IEnumerable GetRemoteDockerContainers(IConnection connection, string hostname, out int totalContainers) { totalContainers = 0; SSHConnection sshConnection = connection as SSHConnection; @@ -239,7 +239,7 @@ internal static IEnumerable GetRemoteDockerContainers(I return null; } - List containers = new List(); + List containers = new List(); DockerCommandSettings settings = new DockerCommandSettings(hostname, true); settings.SetCommand(dockerPSCommand, dockerPSArgs); @@ -300,7 +300,7 @@ internal static IEnumerable GetRemoteDockerContainers(I foreach (var item in outputLines) { - if (DockerContainerInstance.TryCreate(item, out DockerContainerInstance containerInstance)) + if (ContainerInstance.TryCreate(item, out ContainerInstance containerInstance)) { containers.Add(containerInstance); } diff --git a/src/SSHDebugPS/IContainerDiscoveryStrategy.cs b/src/SSHDebugPS/IContainerDiscoveryStrategy.cs index c7fd87bb7..c64cff6ec 100644 --- a/src/SSHDebugPS/IContainerDiscoveryStrategy.cs +++ b/src/SSHDebugPS/IContainerDiscoveryStrategy.cs @@ -14,8 +14,8 @@ internal interface IContainerDiscoveryStrategy string ConnectionToolTip { get; } string HostnameAutomationName { get; } - IEnumerable GetLocalContainers(string hostname, out int totalContainers); - IEnumerable GetRemoteContainers(IConnection connection, string hostname, out int totalContainers); - void AssignPlatforms(IEnumerable containers, string hostname); + IEnumerable GetLocalContainers(string hostname, out int totalContainers); + IEnumerable GetRemoteContainers(IConnection connection, string hostname, out int totalContainers); + void AssignPlatforms(IEnumerable containers, string hostname); } } diff --git a/src/SSHDebugPS/Microsoft.SSHDebugPS.pkgdef b/src/SSHDebugPS/Microsoft.SSHDebugPS.pkgdef index 66c5f6034..bd613b575 100644 --- a/src/SSHDebugPS/Microsoft.SSHDebugPS.pkgdef +++ b/src/SSHDebugPS/Microsoft.SSHDebugPS.pkgdef @@ -7,6 +7,11 @@ "PortPickerCLSID"="{91BDF293-E6A0-49C4-B033-6F36CFC4FF98}" "Name"="Docker (Linux Container)" +[$RootKey$\AD7Metrics\PortSupplier\{D4F2F3A5-6B7C-4E8D-9F0A-1B2C3D4E5F6A}] +"CLSID"="{C9E1E1E4-3E5A-4F2B-8D1A-5C6F7A8B9D0E}" +"PortPickerCLSID"="{E2A3B4C5-6D7E-4F8A-9B0C-1D2E3F4A5B6C}" +"Name"="Podman (Linux Container)" + [$RootKey$\AD7Metrics\PortSupplier\{267B1341-AC92-44DC-94DF-2EE4205DD17E}] "CLSID"="{B8587A49-00BD-4DEE-94B9-6EBF49003E04}" "Name"="Windows Subsystem for Linux (WSL)" @@ -47,6 +52,18 @@ "InprocServer32"="$WinDir$\SYSTEM32\MSCOREE.DLL" "CodeBase"="$PackageFolder$\Microsoft.SSHDebugPS.dll" +[$RootKey$\CLSID\{C9E1E1E4-3E5A-4F2B-8D1A-5C6F7A8B9D0E}] +"Assembly"="Microsoft.SSHDebugPS" +"Class"="Microsoft.SSHDebugPS.Podman.PodmanPortSupplier" +"InprocServer32"="$WinDir$\SYSTEM32\MSCOREE.DLL" +"CodeBase"="$PackageFolder$\Microsoft.SSHDebugPS.dll" + +[$RootKey$\CLSID\{E2A3B4C5-6D7E-4F8A-9B0C-1D2E3F4A5B6C}] +"Assembly"="Microsoft.SSHDebugPS" +"Class"="Microsoft.SSHDebugPS.Podman.PodmanLinuxPortPicker" +"InprocServer32"="$WinDir$\SYSTEM32\MSCOREE.DLL" +"CodeBase"="$PackageFolder$\Microsoft.SSHDebugPS.dll" + [$RootKey$\RuntimeConfiguration\dependentAssembly\codeBase\{7E3052B2-FB42-4E38-B22C-1FD281BD4413}] "name"="Microsoft.SSHDebugPS" ; With local development workflow and release workflow, there are two publicKeyTokens but no way to specify both. diff --git a/src/SSHDebugPS/Podman/PodmanConnection.cs b/src/SSHDebugPS/Podman/PodmanConnection.cs new file mode 100644 index 000000000..839a776e3 --- /dev/null +++ b/src/SSHDebugPS/Podman/PodmanConnection.cs @@ -0,0 +1,155 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Threading; +using System.Diagnostics; +using Microsoft.SSHDebugPS.Docker; +using Microsoft.SSHDebugPS.Utilities; +using Microsoft.VisualStudio.Debugger.Interop.UnixPortSupplier; +using Microsoft.VisualStudio.Shell; + +namespace Microsoft.SSHDebugPS.Podman +{ + internal sealed class PodmanConnection : PipeConnection + { + #region Statics + + internal static string CreateConnectionString(string containerName, string remoteConnectionName, string hostName) + { + // Reuses the same format as Docker connection strings + return DockerConnection.CreateConnectionString(containerName, remoteConnectionName, hostName); + } + + internal static bool TryConvertConnectionStringToSettings(string connectionString, out PodmanContainerTransportSettings settings, out Connection remoteConnection) + { + ThreadHelper.ThrowIfNotOnUIThread(); + + if (DockerConnection.TryConvertConnectionStringToSettings(connectionString, out DockerContainerTransportSettings dockerSettings, out remoteConnection)) + { + settings = new PodmanContainerTransportSettings(dockerSettings.HostName, dockerSettings.ContainerName, remoteConnection != null); + return true; + } + + settings = null; + return false; + } + + #endregion + + private readonly string _containerName; + private readonly PodmanExecutionManager _executionManager; + private readonly PodmanContainerTransportSettings _settings; + + public PodmanConnection(PodmanContainerTransportSettings settings, Connection outerConnection, string name) + : base(outerConnection, name) + { + _settings = settings; + _containerName = settings.ContainerName; + _executionManager = new PodmanExecutionManager(settings, outerConnection); + } + + public override int ExecuteCommand(string commandText, int timeout, out string commandOutput, out string errorMessage) + { + return _executionManager.ExecuteCommand(commandText, timeout, out commandOutput, out errorMessage); + } + + /// + public override void BeginExecuteAsyncCommand(string commandText, bool runInShell, IDebugUnixShellCommandCallback callback, out IDebugUnixShellAsyncCommand asyncCommand) + { + if (IsClosed) + { + throw new ObjectDisposedException(nameof(PipeConnection)); + } + + var commandRunner = GetExecCommandRunner(commandText, handleRawOutput: runInShell == false); + asyncCommand = new PipeAsyncCommand(commandRunner, callback); + } + + public override void CopyFile(string sourcePath, string destinationPath) + { + PodmanCopySettings settings; + string tmpFile = null; + + if (!Directory.Exists(sourcePath) && !File.Exists(sourcePath)) + { + throw new ArgumentException(StringResources.Error_CopyFile_SourceNotFound.FormatCurrentCultureWithArgs(sourcePath), nameof(sourcePath)); + } + + if (OuterConnection != null) + { + tmpFile = "/tmp" + "/" + StringResources.CopyFile_TempFilePrefix + Guid.NewGuid(); + OuterConnection.CopyFile(sourcePath, tmpFile); + settings = new PodmanCopySettings(_settings, tmpFile, destinationPath); + } + else + { + settings = new PodmanCopySettings(_settings, sourcePath, destinationPath); + } + + ICommandRunner runner = GetCommandRunner(settings); + + ManualResetEvent resetEvent = new ManualResetEvent(false); + int exitCode = -1; + runner.Closed += (e, args) => + { + exitCode = args; + resetEvent.Set(); + try + { + if (OuterConnection != null && !string.IsNullOrEmpty(tmpFile)) + { + string output; + string errorMessage; + int exit = OuterConnection.ExecuteCommand("rm " + tmpFile, 5000, out output, out errorMessage); + Debug.Assert(exit == 0, FormattableString.Invariant($"Removing file exited with {exit} and message {output}. {errorMessage}")); + } + } + catch (Exception ex) + { + Debug.Fail("Exception thrown while cleaning up temp file. " + ex.Message); + } + }; + + runner.Start(); + + bool complete = resetEvent.WaitOne(Timeout.Infinite); + if (!complete || exitCode != 0) + { + throw new CommandFailedException(StringResources.Error_CopyFileFailed); + } + } + + public override string GetUserHomeDirectory() + { + return ExecuteCommand("eval echo '~'", Timeout.Infinite); + } + + private ICommandRunner GetExecCommandRunner(string commandText, bool handleRawOutput = false) + { + var execSettings = new PodmanExecSettings(this._settings, commandText, handleRawOutput); + return GetCommandRunner(execSettings, handleRawOutput: handleRawOutput); + } + + private ICommandRunner GetCommandRunner(IPipeTransportSettings settings, bool handleRawOutput = false) + { + if (OuterConnection == null) + { + return LocalCommandRunner.CreateInstance(handleRawOutput, settings); + } + else + { + return new RemoteCommandRunner(settings, OuterConnection, handleRawOutput); + } + } + + protected override string ProcFSErrorMessage + { + get + { + return String.Concat(base.ProcFSErrorMessage, Environment.NewLine, StringResources.Error_EnsurePodmanContainerIsLinux); + } + } + } +} diff --git a/src/SSHDebugPS/Podman/PodmanContainerInstance.cs b/src/SSHDebugPS/Podman/PodmanContainerInstance.cs new file mode 100644 index 000000000..72f004ee9 --- /dev/null +++ b/src/SSHDebugPS/Podman/PodmanContainerInstance.cs @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Microsoft.DebugEngineHost; +using Microsoft.SSHDebugPS.Docker; +using Microsoft.SSHDebugPS.Utilities; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Microsoft.SSHDebugPS.Podman +{ + public class PodmanContainerInstance : ContainerInstance + { + public static bool TryCreate(string json, out PodmanContainerInstance instance) + { + instance = null; + try + { + JObject obj = JObject.Parse(json); + instance = obj.ToObject(); + } + catch (Exception e) + { + HostTelemetry.SendEvent(TelemetryHelper.Event_PodmanPSParseFailure, new KeyValuePair[] { + new KeyValuePair(TelemetryHelper.Property_ExceptionName, e.GetType().Name) + }); + + string error = e.ToString(); + VsOutputWindowWrapper.WriteLine(StringResources.Error_PodmanPSParseFailed.FormatCurrentCultureWithArgs(json, error), StringResources.Podman_PSName); + Debug.Fail(error); + } + return instance != null; + } + + [JsonProperty("Command")] + [JsonConverter(typeof(PodmanJsonConverter))] + public override string Command { get; protected set; } + + [JsonProperty("Ports")] + [JsonConverter(typeof(PodmanJsonConverter))] + public override string Ports { get; set; } + + [JsonProperty("Names")] + [JsonConverter(typeof(PodmanJsonConverter))] + public override string Name { get; set; } + + protected override bool EqualsInternal(ContainerInstance instance) + { + if (instance is PodmanContainerInstance other) + { + return String.Equals(Id, other.Id, StringComparison.Ordinal) || + Id.StartsWith(other.Id, StringComparison.Ordinal) || + other.Id.StartsWith(Id, StringComparison.Ordinal); + } + + return false; + } + } +} diff --git a/src/SSHDebugPS/Podman/PodmanDiscoveryStrategy.cs b/src/SSHDebugPS/Podman/PodmanDiscoveryStrategy.cs new file mode 100644 index 000000000..6d4571e52 --- /dev/null +++ b/src/SSHDebugPS/Podman/PodmanDiscoveryStrategy.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using Microsoft.SSHDebugPS.Docker; +using Microsoft.SSHDebugPS.UI; + +namespace Microsoft.SSHDebugPS.Podman +{ + internal sealed class PodmanDiscoveryStrategy : IContainerDiscoveryStrategy + { + public string ConnectionLabel => UIResources.Podman_ConnectionLabel; + public string HostnameLabel => UIResources.Podman_HostnameLabel; + public string HostnameTip => UIResources.Podman_HostnameTip; + public string ConnectionToolTip => UIResources.Podman_ConnectionToolTip; + public string HostnameAutomationName => UIResources.Podman_HostnameAutomationName; + + public IEnumerable GetLocalContainers(string hostname, out int totalContainers) + { + return PodmanHelper.GetLocalPodmanContainers(hostname, out totalContainers); + } + + public IEnumerable GetRemoteContainers(IConnection connection, string hostname, out int totalContainers) + { + return PodmanHelper.GetRemotePodmanContainers(connection, hostname, out totalContainers); + } + + public void AssignPlatforms(IEnumerable containers, string hostname) + { + // Podman only supports Linux containers + foreach (ContainerInstance container in containers) + { + container.Platform = "Linux"; + } + } + } +} diff --git a/src/SSHDebugPS/Podman/PodmanExecutionManager.cs b/src/SSHDebugPS/Podman/PodmanExecutionManager.cs new file mode 100644 index 000000000..aef6ccc8f --- /dev/null +++ b/src/SSHDebugPS/Podman/PodmanExecutionManager.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.SSHDebugPS.Docker; + +namespace Microsoft.SSHDebugPS.Podman +{ + internal sealed class PodmanExecutionManager : DockerExecutionManager + { + public PodmanExecutionManager(PodmanContainerTransportSettings baseSettings, Connection outerConnection) + : base(baseSettings, outerConnection) + { } + + protected override ContainerExecSettings CreateExecSettings(ContainerTargetTransportSettings baseSettings, string command, bool runInShell, bool makeInteractive) + { + return new PodmanExecSettings((PodmanContainerTransportSettings)baseSettings, command, runInShell, makeInteractive); + } + } +} diff --git a/src/SSHDebugPS/Podman/PodmanHelper.cs b/src/SSHDebugPS/Podman/PodmanHelper.cs new file mode 100644 index 000000000..5d25de158 --- /dev/null +++ b/src/SSHDebugPS/Podman/PodmanHelper.cs @@ -0,0 +1,155 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading; +using Microsoft.SSHDebugPS.Docker; +using Microsoft.SSHDebugPS.SSH; +using Microsoft.SSHDebugPS.Utilities; + +namespace Microsoft.SSHDebugPS.Podman +{ + public class PodmanHelper + { + private const string podmanPSCommand = "ps"; + private const string podmanPSArgs = "-f status=running --no-trunc --format \"{{json .}}\""; + + + internal static IEnumerable GetLocalPodmanContainers(string hostname, out int totalContainers) + { + totalContainers = 0; + int containerCount = 0; + List containers = new List(); + + PodmanCommandSettings settings = new PodmanCommandSettings(hostname, false); + settings.SetCommand(podmanPSCommand, podmanPSArgs); + + DockerHelper.RunContainerCommand(settings, delegate (string args) + { + if (args.Trim()[0] == '{') + { + if (PodmanContainerInstance.TryCreate(args, out PodmanContainerInstance containerInstance)) + { + containers.Add(containerInstance); + } + containerCount++; + } + }); + + totalContainers = containerCount; + return containers; + } + + /// + /// Checks if the specified container is in the list of containers from the target host. + /// + internal static bool IsContainerRunning(string hostName, string containerName, Connection remoteConnection) + { + IEnumerable containers; + if (remoteConnection != null) + { + containers = GetRemotePodmanContainers(remoteConnection, hostName, out _); + } + else + { + containers = GetLocalPodmanContainers(hostName, out _); + } + + if (containers != null) + { + if (containers.Any(container => string.Equals(container.Name, containerName, StringComparison.Ordinal) + || container.Id.StartsWith(containerName, StringComparison.Ordinal))) + { + return true; + } + } + + return false; + } + + internal static IEnumerable GetRemotePodmanContainers(IConnection connection, string hostname, out int totalContainers) + { + totalContainers = 0; + SSHConnection sshConnection = connection as SSHConnection; + List outputLines = new List(); + StringBuilder errorSB = new StringBuilder(); + if (sshConnection == null) + { + return null; + } + + List containers = new List(); + + PodmanCommandSettings settings = new PodmanCommandSettings(hostname, true); + settings.SetCommand(podmanPSCommand, podmanPSArgs); + + RemoteCommandRunner commandRunner = new RemoteCommandRunner(settings, sshConnection, handleRawOutput: false); + + ManualResetEvent resetEvent = new ManualResetEvent(false); + int exitCode = 0; + commandRunner.ErrorOccured += ((sender, args) => + { + errorSB.Append(args); + }); + + commandRunner.Closed += ((sender, args) => + { + exitCode = args; + resetEvent.Set(); + }); + + commandRunner.OutputReceived += ((sender, line) => + { + if (!string.IsNullOrWhiteSpace(line)) + { + Debug.Assert(line.IndexOf('\n') < 0, "Why does `line` have embedded newline characters?"); + + if (line.Trim()[0] != '{') + { + errorSB.Append(line); + } + + outputLines.Add(line); + } + }); + + commandRunner.Start(); + + bool cancellationRequested = false; + VS.VSOperationWaiter.Wait(UIResources.QueryingForContainersMessage, false, (cancellationToken) => + { + while (!resetEvent.WaitOne(2000) && !cancellationToken.IsCancellationRequested) + { } + cancellationRequested = cancellationToken.IsCancellationRequested; + }); + + if (!cancellationRequested) + { + if (exitCode != 0) + { + string exceptionMessage = UIResources.CommandExecutionErrorWithExitCodeFormat.FormatCurrentCultureWithArgs( + "{0} {1}".FormatInvariantWithArgs(settings.Command, settings.CommandArgs), + exitCode, + errorSB.ToString()); + + throw new CommandFailedException(exceptionMessage); + } + + foreach (var item in outputLines) + { + if (PodmanContainerInstance.TryCreate(item, out PodmanContainerInstance containerInstance)) + { + containers.Add(containerInstance); + totalContainers++; + } + } + } + + return containers; + } + } +} diff --git a/src/SSHDebugPS/Podman/PodmanJsonConverter.cs b/src/SSHDebugPS/Podman/PodmanJsonConverter.cs new file mode 100644 index 000000000..c82819bba --- /dev/null +++ b/src/SSHDebugPS/Podman/PodmanJsonConverter.cs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Linq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Microsoft.SSHDebugPS.Podman +{ + // Handles JSON values that may be a string, array of strings, or array of objects (port mappings). + internal sealed class PodmanJsonConverter : JsonConverter + { + public override bool CanConvert(Type objectType) => objectType == typeof(string); + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + var token = JToken.Load(reader); + switch (token.Type) + { + case JTokenType.String: + return token.Value(); + case JTokenType.Array: + return string.Join(", ", token.Select(t => + { + if (t.Type == JTokenType.String) + return t.Value(); + if (t.Type == JTokenType.Object) + { + // Handle Podman port mapping objects: {"host_ip":"0.0.0.0","container_port":80,"host_port":8080,"range":1,"protocol":"tcp"} + var hostIp = t.Value("host_ip") ?? "0.0.0.0"; + var hostPort = t.Value("host_port"); + var containerPort = t.Value("container_port"); + var protocol = t.Value("protocol") ?? "tcp"; + if (hostPort.HasValue && containerPort.HasValue) + return $"{hostIp}:{hostPort}->{containerPort}/{protocol}"; + } + return t.ToString(); + })); + case JTokenType.Null: + return string.Empty; + default: + return token.ToString(); + } + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + writer.WriteValue(value?.ToString()); + } + } +} diff --git a/src/SSHDebugPS/Podman/PodmanPort.cs b/src/SSHDebugPS/Podman/PodmanPort.cs new file mode 100644 index 000000000..edda987a3 --- /dev/null +++ b/src/SSHDebugPS/Podman/PodmanPort.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.VisualStudio.Shell; + +namespace Microsoft.SSHDebugPS.Podman +{ + internal sealed class PodmanPort : AD7Port + { + public PodmanPort(AD7PortSupplier portSupplier, string name, bool isInAddPort) + : base(portSupplier, name, isInAddPort) + { } + + protected override Connection GetConnectionInternal() + { + ThreadHelper.ThrowIfNotOnUIThread(); + return ConnectionManager.GetPodmanConnection(Name, supportSSHConnections: true); + } + } +} diff --git a/src/SSHDebugPS/Podman/PodmanPortPicker.cs b/src/SSHDebugPS/Podman/PodmanPortPicker.cs new file mode 100644 index 000000000..cd4fd2868 --- /dev/null +++ b/src/SSHDebugPS/Podman/PodmanPortPicker.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Runtime.InteropServices; +using Microsoft.SSHDebugPS.Docker; +using Microsoft.VisualStudio.Shell; + +namespace Microsoft.SSHDebugPS.Podman +{ + [ComVisible(true)] + [Guid("E2A3B4C5-6D7E-4F8A-9B0C-1D2E3F4A5B6C")] + public class PodmanLinuxPortPicker : DockerPortPickerBase + { + internal override bool SupportSSHConnections => true; + internal override ContainerRuntimeType RuntimeType => ContainerRuntimeType.Podman; + } +} diff --git a/src/SSHDebugPS/Podman/PodmanPortSupplier.cs b/src/SSHDebugPS/Podman/PodmanPortSupplier.cs new file mode 100644 index 000000000..82031b256 --- /dev/null +++ b/src/SSHDebugPS/Podman/PodmanPortSupplier.cs @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Runtime.InteropServices; +using Microsoft.VisualStudio.Debugger.Interop; + +namespace Microsoft.SSHDebugPS.Podman +{ + [ComVisible(true)] + [Guid("C9E1E1E4-3E5A-4F2B-8D1A-5C6F7A8B9D0E")] + internal sealed class PodmanPortSupplier : AD7PortSupplier + { + private readonly Guid _Id = new Guid("D4F2F3A5-6B7C-4E8D-9F0A-1B2C3D4E5F6A"); + + protected override Guid Id { get { return _Id; } } + protected override string Name { get { return StringResources.Podman_PSName; } } + protected override string Description { get { return StringResources.Podman_PSDescription; } } + + public PodmanPortSupplier() : base() + { } + + public override int AddPort(IDebugPortRequest2 request, out IDebugPort2 port) + { + string name; + HR.Check(request.GetPortName(out name)); + + if (!string.IsNullOrWhiteSpace(name)) + { + AD7Port newPort = new PodmanPort(this, name, isInAddPort: true); + + if (newPort.IsConnected) + { + port = newPort; + return HR.S_OK; + } + } + + port = null; + return HR.E_REMOTE_CONNECT_USER_CANCELED; + } + + public override unsafe int EnumPersistedPorts(BSTR_ARRAY portNames, out IEnumDebugPorts2 portEnum) + { + IDebugPort2[] ports = new IDebugPort2[portNames.dwCount]; + for (int c = 0; c < portNames.dwCount; c++) + { + char* bstrPortName = ((char**)portNames.Members)[c]; + string name = new string(bstrPortName); + + ports[c] = new PodmanPort(this, name, isInAddPort: false); + } + + portEnum = new AD7PortEnum(ports); + return HR.S_OK; + } + } +} diff --git a/src/SSHDebugPS/Podman/PodmanTransportSettings.cs b/src/SSHDebugPS/Podman/PodmanTransportSettings.cs new file mode 100644 index 000000000..1dd1d1248 --- /dev/null +++ b/src/SSHDebugPS/Podman/PodmanTransportSettings.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.SSHDebugPS.Podman +{ + internal sealed class PodmanContainerTransportSettings : ContainerTargetTransportSettings + { + internal const string WindowsExeName = "podman.exe"; + internal const string UnixExeName = "podman"; + internal const string HostFlag = "--url \"{0}\""; + + public PodmanContainerTransportSettings(string hostname, string containerName, bool hostIsUnix) + : base(hostname, containerName, hostIsUnix, WindowsExeName, UnixExeName, HostFlag) + { } + + public PodmanContainerTransportSettings(PodmanContainerTransportSettings settings) + : base(settings) + { } + } + + internal sealed class PodmanExecSettings : ContainerExecSettings + { + public PodmanExecSettings(PodmanContainerTransportSettings settings, string command, bool runInShell, bool makeInteractive = true) + : base(settings, command, runInShell, makeInteractive) + { } + } + + internal sealed class PodmanCopySettings : ContainerCopySettings + { + public PodmanCopySettings(string hostname, string sourcePath, string destinationPath, string containerName, bool hostIsUnix) + : base(hostname, sourcePath, destinationPath, containerName, hostIsUnix, PodmanContainerTransportSettings.WindowsExeName, PodmanContainerTransportSettings.UnixExeName, PodmanContainerTransportSettings.HostFlag) + { } + + public PodmanCopySettings(PodmanContainerTransportSettings settings, string sourcePath, string destinationPath) + : base(settings, sourcePath, destinationPath) + { } + } + + internal sealed class PodmanCommandSettings : ContainerCommandSettings + { + public PodmanCommandSettings(string hostname, bool hostIsUnix) + : base(hostname, hostIsUnix, PodmanContainerTransportSettings.WindowsExeName, PodmanContainerTransportSettings.UnixExeName, PodmanContainerTransportSettings.HostFlag) + { } + } +} + diff --git a/src/SSHDebugPS/StringResources.Designer.cs b/src/SSHDebugPS/StringResources.Designer.cs index 78a19bb65..5706b852b 100644 --- a/src/SSHDebugPS/StringResources.Designer.cs +++ b/src/SSHDebugPS/StringResources.Designer.cs @@ -114,6 +114,33 @@ internal static string Docker_PSName { } } + /// + /// Looks up a localized string similar to Podman (Linux Container). + /// + internal static string Podman_PSName { + get { + return ResourceManager.GetString("Podman_PSName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The Podman (Linux Container) connection type allows Visual Studio to connect to Podman containers running locally or remotely (using SSH).. + /// + internal static string Podman_PSDescription { + get { + return ResourceManager.GetString("Podman_PSDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Failed to parse output of '{0}': {1}. + /// + internal static string Error_PodmanPSParseFailed { + get { + return ResourceManager.GetString("Error_PodmanPSParseFailed", resourceCulture); + } + } + /// /// Looks up a localized string similar to Command failed to execute. /// @@ -195,6 +222,15 @@ internal static string Error_EnsureDockerContainerIsLinux { } } + /// + /// Looks up a localized string similar to Ensure the selected Podman Connection target is a Linux container.. + /// + internal static string Error_EnsurePodmanContainerIsLinux { + get { + return ResourceManager.GetString("Error_EnsurePodmanContainerIsLinux", resourceCulture); + } + } + /// /// Looks up a localized string similar to Unable to parse exit code.. /// diff --git a/src/SSHDebugPS/StringResources.resx b/src/SSHDebugPS/StringResources.resx index af44a77c5..571c143fb 100644 --- a/src/SSHDebugPS/StringResources.resx +++ b/src/SSHDebugPS/StringResources.resx @@ -139,6 +139,16 @@ Docker (Linux Container) + + Podman (Linux Container) + + + The Podman (Linux Container) connection type allows Visual Studio to connect to Podman containers running locally or remotely (using SSH). + + + Failed to parse output of '{0}': {1} + {0} is the JSON line that failed to parse. {1} is the exception details. + Command failed to execute @@ -165,6 +175,9 @@ Ensure the selected Docker Connection target is a Linux container. + + Ensure the selected Podman Connection target is a Linux container. + Failed to parse json '{0}'.\r\nError: '{1}' {0} is a json output item from the output 'docker ps' and {1} is the error message diff --git a/src/SSHDebugPS/UI/ContainerInstance.cs b/src/SSHDebugPS/UI/ContainerInstance.cs index 4ec33929e..fa182104a 100644 --- a/src/SSHDebugPS/UI/ContainerInstance.cs +++ b/src/SSHDebugPS/UI/ContainerInstance.cs @@ -1,79 +1,11 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Diagnostics; -using System.Globalization; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Text; -using System.Threading.Tasks; - namespace Microsoft.SSHDebugPS.Docker { - public interface IContainerInstance : IEquatable + public interface IContainerInstance : System.IEquatable { string Id { get; } string Name { get; } } - - public abstract class ContainerInstance : IContainerInstance - { - public abstract string Id { get; set; } - public abstract string Name { get; set; } - - #region IEquatable - - public static bool operator ==(ContainerInstance left, ContainerInstance right) - { - if (left is null || right is null) - { - return ReferenceEquals(left, right); - } - - return left.Equals(right); - } - - public static bool operator !=(ContainerInstance left, ContainerInstance right) - { - return !(left == right); - } - - public bool Equals(IContainerInstance instance) - { - if (!ReferenceEquals(null, instance) && instance is ContainerInstance container) - { - return this.EqualsInternal(container); - } - - return false; - } - - public override bool Equals(object obj) - { - if (obj is IContainerInstance instance) - { - return this.Equals(instance); - } - return false; - } - - public override int GetHashCode() - { - return GetHashCodeInternal(); - } - - #endregion - - #region Helper Methods - - protected abstract bool EqualsInternal(ContainerInstance instance); - protected abstract int GetHashCodeInternal(); - - #endregion - } } diff --git a/src/SSHDebugPS/UI/UIResources.Designer.cs b/src/SSHDebugPS/UI/UIResources.Designer.cs index 12e90b5c2..fce792a7a 100644 --- a/src/SSHDebugPS/UI/UIResources.Designer.cs +++ b/src/SSHDebugPS/UI/UIResources.Designer.cs @@ -267,6 +267,51 @@ public static string HostnameAutomationName { } } + /// + /// Looks up a localized string similar to Podman _CLI host:. + /// + public static string Podman_ConnectionLabel { + get { + return ResourceManager.GetString("Podman_ConnectionLabel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Podman _host (Optional):. + /// + public static string Podman_HostnameLabel { + get { + return ResourceManager.GetString("Podman_HostnameLabel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Specify a URL for connecting to a different Podman host. . + /// + public static string Podman_HostnameTip { + get { + return ResourceManager.GetString("Podman_HostnameTip", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Location from which to run the Podman CLI.... + /// + public static string Podman_ConnectionToolTip { + get { + return ResourceManager.GetString("Podman_ConnectionToolTip", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Optional Podman Host name. + /// + public static string Podman_HostnameAutomationName { + get { + return ResourceManager.GetString("Podman_HostnameAutomationName", resourceCulture); + } + } + /// /// Looks up a localized string similar to Docker _host (Optional):. /// diff --git a/src/SSHDebugPS/UI/UIResources.resx b/src/SSHDebugPS/UI/UIResources.resx index 7ce103459..e06689608 100644 --- a/src/SSHDebugPS/UI/UIResources.resx +++ b/src/SSHDebugPS/UI/UIResources.resx @@ -239,6 +239,22 @@ Optional Docker Host name + + Podman _CLI host: + + + Podman _host (Optional): + Hostname for Podman daemon configuration + + + Specify a URL for connecting to a different Podman host. + + + Location from which to run the Podman CLI. To manage remote connections, in the menu go to Tools -> Options and find Cross Platform -> Connection Manager. + + + Optional Podman Host name + Container List diff --git a/src/SSHDebugPS/UI/ViewModels/ContainerPickerViewModel.cs b/src/SSHDebugPS/UI/ViewModels/ContainerPickerViewModel.cs index 0f19c2f20..ea58f08de 100644 --- a/src/SSHDebugPS/UI/ViewModels/ContainerPickerViewModel.cs +++ b/src/SSHDebugPS/UI/ViewModels/ContainerPickerViewModel.cs @@ -11,6 +11,7 @@ using System.Windows.Threading; using liblinux.Persistence; using Microsoft.SSHDebugPS.Docker; +using Microsoft.SSHDebugPS.Podman; using Microsoft.SSHDebugPS.SSH; using Microsoft.SSHDebugPS.Utilities; using System.Globalization; @@ -151,6 +152,8 @@ private static IContainerDiscoveryStrategy CreateDiscoveryStrategy(ContainerRunt { case ContainerRuntimeType.Docker: return new DockerDiscoveryStrategy(); + case ContainerRuntimeType.Podman: + return new PodmanDiscoveryStrategy(); default: Debug.Fail($"Unsupported container runtime type: {runtimeType}"); return null; @@ -183,7 +186,7 @@ private void RefreshContainersListInternal() return; } - IEnumerable containers; + IEnumerable containers; if (SelectedConnection is LocalConnectionViewModel) { diff --git a/src/SSHDebugPS/UI/ViewModels/ContainerViewModel.cs b/src/SSHDebugPS/UI/ViewModels/ContainerViewModel.cs index b36798a10..1a0f4df51 100644 --- a/src/SSHDebugPS/UI/ViewModels/ContainerViewModel.cs +++ b/src/SSHDebugPS/UI/ViewModels/ContainerViewModel.cs @@ -131,9 +131,9 @@ public bool IsSelected } public class DockerContainerViewModel - : ContainerViewModel + : ContainerViewModel { - public DockerContainerViewModel(DockerContainerInstance instance) + public DockerContainerViewModel(ContainerInstance instance) : base(instance) { } diff --git a/src/SSHDebugPS/Utilities/TelemetryHelper.cs b/src/SSHDebugPS/Utilities/TelemetryHelper.cs index 85a13f327..8f019b1c2 100644 --- a/src/SSHDebugPS/Utilities/TelemetryHelper.cs +++ b/src/SSHDebugPS/Utilities/TelemetryHelper.cs @@ -6,6 +6,7 @@ namespace Microsoft.SSHDebugPS.Utilities internal static class TelemetryHelper { public const string Event_DockerPSParseFailure = @"VS/Diagnostics/Debugger/SSHDebugPS/DockerPSParseFailure"; + public const string Event_PodmanPSParseFailure = @"VS/Diagnostics/Debugger/SSHDebugPS/PodmanPSParseFailure"; public const string Event_ProcFSError = @"VS/Diagnostics/Debugger/SSHDebugPS/ProcFSError"; public static readonly string Property_ExceptionName = "vs.diagnostics.debugger.ExceptionName"; } From 6b2a477fb958b2925fcd4485556a2e97cad57dfe Mon Sep 17 00:00:00 2001 From: Andrew Wang Date: Fri, 19 Jun 2026 17:31:10 -0700 Subject: [PATCH 05/25] Merge pull request #1590 from microsoft/dev/waan/fixDebuggerTestingPipeline Fix DebuggerTesting Release Pipeline --- eng/pipelines/templates/DebuggerTesting-release.template.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/pipelines/templates/DebuggerTesting-release.template.yml b/eng/pipelines/templates/DebuggerTesting-release.template.yml index 7922ed9d2..426ca8469 100644 --- a/eng/pipelines/templates/DebuggerTesting-release.template.yml +++ b/eng/pipelines/templates/DebuggerTesting-release.template.yml @@ -10,7 +10,7 @@ steps: parameters: Command: 'restore' solution: '$(Build.SourcesDirectory)\src\MIDebugEngine.sln' - FeedsToUse: 'config' + selectOrConfig: 'config' NugetConfigPath: '$(Build.SourcesDirectory)\eng\pipelines\NuGet.release.config' - template: ../tasks/MSBuild.yml From 768ae317250aa624e5797c402a5b152bd58fe264 Mon Sep 17 00:00:00 2001 From: "microsoft-github-policy-service[bot]" <77245923+microsoft-github-policy-service[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2026 08:42:46 -0700 Subject: [PATCH 06/25] Auto-generated baselines by 1ES Pipeline Templates (#1591) Updated for https://dev.azure.com/devdiv/0bdbc590-a062-4c3f-b0f6-9383f67865ee/_build?definitionId=14629 by using baselines generated in https://dev.azure.com/devdiv/0bdbc590-a062-4c3f-b0f6-9383f67865ee/_build/results?buildId=14436488 --- .../1espt/PipelineAutobaseliningConfig.yml | 39 +++++++++++++++++++ .config/guardian/.gdnbaselines | 34 ++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 .config/1espt/PipelineAutobaseliningConfig.yml create mode 100644 .config/guardian/.gdnbaselines diff --git a/.config/1espt/PipelineAutobaseliningConfig.yml b/.config/1espt/PipelineAutobaseliningConfig.yml new file mode 100644 index 000000000..f2bffa5a6 --- /dev/null +++ b/.config/1espt/PipelineAutobaseliningConfig.yml @@ -0,0 +1,39 @@ +## DO NOT MODIFY THIS FILE MANUALLY. This is part of auto-baselining from 1ES Pipeline Templates. Go to [https://aka.ms/1espt-autobaselining] for more details. + +pipelines: + 13876: + retail: + source: + credscan: + lastModifiedDate: 2025-09-04 + eslint: + lastModifiedDate: 2025-09-04 + psscriptanalyzer: + lastModifiedDate: 2025-09-04 + armory: + lastModifiedDate: 2025-09-04 + accessibilityinsights: + lastModifiedDate: 2025-09-04 + binary: + credscan: + lastModifiedDate: 2025-09-04 + binskim: + lastModifiedDate: 2025-09-04 + spotbugs: + lastModifiedDate: 2025-09-04 + 14629: + retail: + source: + eslint: + lastModifiedDate: 2026-06-20 + psscriptanalyzer: + lastModifiedDate: 2026-06-20 + armory: + lastModifiedDate: 2026-06-20 + accessibilityinsights: + lastModifiedDate: 2026-02-02 + binary: + binskim: + lastModifiedDate: 2026-06-20 + spotbugs: + lastModifiedDate: 2026-06-20 diff --git a/.config/guardian/.gdnbaselines b/.config/guardian/.gdnbaselines new file mode 100644 index 000000000..9d85828aa --- /dev/null +++ b/.config/guardian/.gdnbaselines @@ -0,0 +1,34 @@ +{ + "properties": { + "helpUri": "https://eng.ms/docs/microsoft-security/security/azure-security/cloudai-security-fundamentals-engineering/security-integration/guardian-wiki/microsoft-guardian/general/baselines" + }, + "version": "1.0.0", + "baselines": { + "default": { + "name": "default", + "createdDate": "2026-06-20 00:42:20Z", + "lastUpdatedDate": "2026-06-20 00:43:35Z" + } + }, + "results": { + "987d5bfd08c6484fca74a418b398f2b2b831f95a0a8022af4251cf41c7b272a8": { + "signature": "987d5bfd08c6484fca74a418b398f2b2b831f95a0a8022af4251cf41c7b272a8", + "alternativeSignatures": [ + "2dfa8bd672245951be7a34cabdc210238f3477d4334fa2a05fa14b06f46c72c0", + "d477464fed3f31fe9eb78a0e76b8a0b42dd755ebbdbd58729442fd058e0d7870", + "fdbb90fe596ed499cfa3055081eba38ce96d1bd08f7e257afe8a87e1a21a2c96" + ], + "target": "test/DebuggerTesting/Compilation/XCodeRunCompiler.cs", + "line": 44, + "uriBaseId": "file:///D:/a/_work/1/s/", + "memberOf": [ + "default" + ], + "tool": "roslynanalyzers", + "ruleId": "CA1310", + "createdDate": "2026-06-20 00:42:20Z", + "expirationDate": "2026-12-07 00:45:46Z", + "justification": "This error is baselined with an expiration date of 180 days from 2026-06-20 00:45:46Z" + } + } +} \ No newline at end of file From dbcbb165df29bdf8366b495c866dc611700f3e10 Mon Sep 17 00:00:00 2001 From: Gregg Miskelly Date: Mon, 22 Jun 2026 15:33:13 -0700 Subject: [PATCH 07/25] Enable nullable referenrce types in DebugEngineHost (#1592) This PR contains the work to enable nullable reference types for the first project in MIEngine -- the DebugEngineHost implementations. **Changes:** - Turn on `enable` for the DebugEngineHost projects and update APIs/fields to nullable annotations (`?`, `is null`, `is not null`, null-forgiving where needed). - Add shared compatibility code for NRT on older frameworks (`NullableHelpers`, `NullableAttributes`) and flow annotations (e.g., `[MaybeNullWhen(false)]`). - Set repo-wide C# language version to 12.0 and simplify shared source inclusion via `*.cs` globs + ``. --- build/all_projects.settings.targets | 2 + src/DebugEngineHost.Common/HostLogChannel.cs | 10 +- .../DebugEngineHost.Stub.csproj | 7 + .../DebugEngineHost.ref.cs | 26 +-- .../Shared/NullableHelpers.cs | 151 ++++++++++++++++++ .../DebugEngineHost.VSCode.csproj | 16 +- .../HostConfigurationSection.cs | 2 +- .../HostConfigurationStore.cs | 11 +- src/DebugEngineHost.VSCode/HostLoader.cs | 2 +- src/DebugEngineHost.VSCode/HostLogger.cs | 16 +- src/DebugEngineHost.VSCode/HostMarshal.cs | 9 +- .../HostNatvisProject.cs | 4 +- .../HostOutputWindow.cs | 5 +- .../HostRunInTerminal.cs | 7 +- src/DebugEngineHost.VSCode/HostTelemetry.cs | 33 ++-- .../VSCode/AssemblyResolver.cs | 12 +- .../VSCode/EngineConfiguration.cs | 30 ++-- .../VSCode/ExceptionBreakpointFilter.cs | 7 +- .../VSCode/ExceptionSettings.cs | 3 +- .../VSCode/HandleCollection.cs | 10 +- src/DebugEngineHost/DebugEngineHost.csproj | 16 +- .../FeedbackDiagnosticFileProvider.cs | 4 +- src/DebugEngineHost/Host.cs | 3 +- .../HostConfigurationSection.cs | 2 +- src/DebugEngineHost/HostConfigurationStore.cs | 64 ++++---- src/DebugEngineHost/HostLoader.cs | 33 ++-- src/DebugEngineHost/HostLogger.cs | 34 ++-- src/DebugEngineHost/HostMarshal.cs | 3 +- src/DebugEngineHost/HostNatvisProject.cs | 78 ++++----- src/DebugEngineHost/HostOutputWindow.cs | 21 ++- src/DebugEngineHost/HostRunInTerminal.cs | 5 +- src/DebugEngineHost/HostTelemetry.cs | 6 +- src/DebugEngineHost/HostWaitDialog.cs | 8 +- src/DebugEngineHost/HostWaitLoop.cs | 10 +- src/DebugEngineHost/RegistryMonitor.cs | 13 +- src/DebugEngineHost/VSFeedbackLogger.cs | 14 +- .../VSImpl/VSEventCallbackWrapper.cs | 9 +- src/DebugEngineHost/VSImpl/VsWaitDialog.cs | 8 +- src/DebugEngineHost/VSImpl/VsWaitLoop.cs | 12 +- src/Shared/NullableAttributes.cs | 150 +++++++++++++++++ 40 files changed, 596 insertions(+), 260 deletions(-) create mode 100644 src/DebugEngineHost.Stub/Shared/NullableHelpers.cs create mode 100644 src/Shared/NullableAttributes.cs diff --git a/build/all_projects.settings.targets b/build/all_projects.settings.targets index 872e74d5d..2adfaf275 100755 --- a/build/all_projects.settings.targets +++ b/build/all_projects.settings.targets @@ -21,6 +21,8 @@ $(MIEngineRoot)\tools $(MIEngineRoot)obj\$(Configuration)\$(MSBuildProjectName)\ $(ToolsHome)\NuGet\NuGet.exe + + 12.0 diff --git a/src/DebugEngineHost.Common/HostLogChannel.cs b/src/DebugEngineHost.Common/HostLogChannel.cs index 14bdc1b57..699cdb3ad 100644 --- a/src/DebugEngineHost.Common/HostLogChannel.cs +++ b/src/DebugEngineHost.Common/HostLogChannel.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. /* @@ -51,18 +51,18 @@ public interface ILogChannel public class HostLogChannel : ILogChannel { private readonly Action _log; - private StreamWriter _logFile; + private StreamWriter? _logFile; private LogLevel _minLevelToBeLogged; private readonly object _lock = new object(); - private HostLogChannel() { } + private HostLogChannel() { _log = null!; } - public HostLogChannel(Action logAction, string file, LogLevel logLevel) + public HostLogChannel(Action logAction, string? file, LogLevel logLevel) { _log = logAction; - if (!string.IsNullOrEmpty(file)) + if (!IsNullOrEmpty(file)) { _logFile = File.CreateText(file); } diff --git a/src/DebugEngineHost.Stub/DebugEngineHost.Stub.csproj b/src/DebugEngineHost.Stub/DebugEngineHost.Stub.csproj index d84411a11..197d85e9d 100755 --- a/src/DebugEngineHost.Stub/DebugEngineHost.Stub.csproj +++ b/src/DebugEngineHost.Stub/DebugEngineHost.Stub.csproj @@ -3,6 +3,7 @@ 1.0.0 + enable @@ -23,6 +24,12 @@ + + + Shared\%(Filename).cs + + + diff --git a/src/DebugEngineHost.Stub/DebugEngineHost.ref.cs b/src/DebugEngineHost.Stub/DebugEngineHost.ref.cs index 3d4462044..7b7fae658 100644 --- a/src/DebugEngineHost.Stub/DebugEngineHost.ref.cs +++ b/src/DebugEngineHost.Stub/DebugEngineHost.ref.cs @@ -1,10 +1,10 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using Microsoft.VisualStudio.Debugger.Interop; using System; using System.Collections.Generic; -using System.Diagnostics; +using ConditionalAttribute = global::System.Diagnostics.ConditionalAttribute; using System.Threading; @@ -88,7 +88,7 @@ public void Dispose() { } /// Name of the value to obtain /// [Optional] null if the value doesn't exist, otherwise the value /// - public object GetValue(string valueName) + public object? GetValue(string valueName) { throw new NotImplementedException(); } @@ -146,7 +146,7 @@ public string RegistryRoot /// /// The metric to read. /// [Optional] value of the metric. Null if the metric is not defined. - public object GetEngineMetric(string metric) + public object? GetEngineMetric(string metric) { throw new NotImplementedException(); } @@ -178,7 +178,7 @@ public T GetDebuggerConfigurationSetting(string settingName, T defaultValue) /// /// launch options type name /// - public object GetCustomLauncher(string launcherTypeName) + public object? GetCustomLauncher(string launcherTypeName) { throw new NotImplementedException(); } @@ -277,8 +277,8 @@ public static void EnableNatvisDiagnostics(Action callback, LogLevel lev /// /// Sets the log file to write to. /// - /// The file to write engine logs to. - public static void SetEngineLogFile(string logFile) + /// The file to write engine logs to, or null if none + public static void SetEngineLogFile(string? logFile) { throw new NotImplementedException(); } @@ -287,7 +287,7 @@ public static void SetEngineLogFile(string logFile) /// Gets the engine log channel created by 'EnableHostLogging' /// /// A logger object if logging is enabled, or null if it is not - public static ILogChannel GetEngineLogChannel() + public static ILogChannel? GetEngineLogChannel() { throw new NotImplementedException(); } @@ -296,7 +296,7 @@ public static ILogChannel GetEngineLogChannel() /// Gets the Natvis log channel if its been created. /// /// A logger object if logging is enabled, or null if it is not - public static ILogChannel GetNatvisLogChannel() + public static ILogChannel? GetNatvisLogChannel() { throw new NotImplementedException(); } @@ -339,7 +339,7 @@ public static class HostLoader /// CLSID to CoCreate /// [Optional] loaded object. Null if the type is not registered, or points to a type that doesn't exist [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "Co")] - public static object VsCoCreateManagedObject(HostConfigurationStore configStore, Guid clsid) + public static object? VsCoCreateManagedObject(HostConfigurationStore configStore, Guid clsid) { throw new NotImplementedException(); } @@ -472,7 +472,7 @@ public static void FindNatvis(NatvisLoader loader) /// /// Enable's tracking the VS 'Natvis Diagnostic Messages (C++ only)' setting. /// - public static IDisposable WatchNatvisOptionSetting(HostConfigurationStore configStore, ILogChannel natvisLogger) + public static IDisposable? WatchNatvisOptionSetting(HostConfigurationStore configStore, ILogChannel natvisLogger) { throw new NotImplementedException(); } @@ -480,7 +480,7 @@ public static IDisposable WatchNatvisOptionSetting(HostConfigurationStore config /// /// Return the solution's root directory, null if no solution /// - public static string FindSolutionRoot() + public static string? FindSolutionRoot() { throw new NotImplementedException(); } @@ -663,7 +663,7 @@ public static void SendEvent(string eventName, params KeyValuePair /// Exception object to report. /// Name of the engine reporting the exception. Ex:Microsoft.MIEngine - public static void ReportCurrentException(Exception currentException, string engineName) + public static void ReportCurrentException(Exception currentException, string? engineName) { throw new NotImplementedException(); } diff --git a/src/DebugEngineHost.Stub/Shared/NullableHelpers.cs b/src/DebugEngineHost.Stub/Shared/NullableHelpers.cs new file mode 100644 index 000000000..9dc195616 --- /dev/null +++ b/src/DebugEngineHost.Stub/Shared/NullableHelpers.cs @@ -0,0 +1,151 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +global using static global::Microsoft.DebugEngineHost.NullableHelpers; + +namespace Microsoft.DebugEngineHost +{ + using System.Diagnostics.CodeAnalysis; + using ConditionalAttribute = System.Diagnostics.ConditionalAttribute; + using SysDebug = System.Diagnostics.Debug; + +#pragma warning disable 8763 // A method marked [DoesNotReturn] should not return. + + /// + /// Helper class to support nullable reference work when compiling against .NET Standard / .NET Framework + /// + public static class NullableHelpers + { + /// + /// Wrapper around string.IsNullOrEmpty to add the `[NotNullWhen(false)]` annotation + /// + /// string to test + /// True if the string is null or empty + static public bool IsNullOrEmpty([NotNullWhen(false)] string? s) + { + return string.IsNullOrEmpty(s); + } + + /// + /// Wrapper around string.IsNullOrWhiteSpace to add the `[NotNullWhen(false)]` annotation + /// + /// string to test + /// True if the string is null, empty, or only whitespace + static public bool IsNullOrWhiteSpace([NotNullWhen(false)] string? s) + { + return string.IsNullOrWhiteSpace(s); + } + + /// + /// This is a shim on top of the class which adds attributes used + /// in nullability analysis. This is important because without the DoesNotReturnIf/DoesNotReturn attributes, + /// on Debug.Assert/Debug.Fail the C# compiler will see code like: + /// + /// Debug.Assert(myArg != null, "Invalid argument") + /// + /// And decide that because the code was attempting to handle 'myArg' being null, that it must be possible + /// for it to be null. + /// + [System.Diagnostics.DebuggerNonUserCode()] + public static class Debug + { + /// + /// Checks for a condition; if the condition is false, displays a message box that shows the call stack. + /// + /// The conditional expression to evaluate. If the condition is true, a failure message is not sent and the message box is not displayed. + [Conditional("DEBUG")] + public static void Assert([DoesNotReturnIf(false)] bool condition) + { + SysDebug.Assert(condition); + } + + /// + /// Checks for a condition; if the condition is false, outputs a specified message and displays a message box that shows the call stack. + /// + /// The conditional expression to evaluate. If the condition is true, the specified message is not sent and the message box is not displayed. + /// The message to send to the collection. + [Conditional("DEBUG")] + public static void Assert([DoesNotReturnIf(false)] bool condition, string message) + { + SysDebug.Assert(condition, message); + } + + /// + /// Checks for a condition; if the condition is false, outputs two specified messages and displays a message box that shows the call stack. + /// + /// The conditional expression to evaluate. If the condition is true, the specified messages are not sent and the message box is not displayed. + /// The message to send to the collection. + /// The detailed message to send to the collection. + [Conditional("DEBUG")] + public static void Assert([DoesNotReturnIf(false)] bool condition, string message, string detailMessage) + { + SysDebug.Assert(condition, message, detailMessage); + } + + /// + /// Emits the specified error message. + /// + /// A message to emit. + [Conditional("DEBUG")] + [DoesNotReturn] + public static void Fail(string message) + { + SysDebug.Fail(message); + } + + /// + /// Emits an error message and a detailed error message. + /// + /// A message to emit. + /// A detailed message to emit. + [Conditional("DEBUG")] + [DoesNotReturn] + public static void Fail(string message, string detailMessage) + { + SysDebug.Fail(message, detailMessage); + } + + /// + /// Writes a message followed by a line terminator to the debugger. + /// + /// A message to write. + [Conditional("DEBUG")] + public static void WriteLine(string message) + { + SysDebug.WriteLine(message); + } + + /// + /// Writes the value of the object's method to the debugger. + /// + /// An object whose value is sent to the debugger. + [Conditional("DEBUG")] + public static void WriteLine(object value) + { + SysDebug.WriteLine(value); + } + + /// + /// Writes a category name and message to the debugger. + /// + /// A message to write. + /// A category name used to organize the output. + [Conditional("DEBUG")] + public static void WriteLine(string message, string category) + { + SysDebug.WriteLine(message, category); + } + + /// + /// Writes a category name and the value of the object's method to the debugger. + /// + /// An object whose value is sent to the debugger. + /// A category name used to organize the output. + [Conditional("DEBUG")] + public static void WriteLine(object value, string category) + { + SysDebug.WriteLine(value, category); + } + } + } +} \ No newline at end of file diff --git a/src/DebugEngineHost.VSCode/DebugEngineHost.VSCode.csproj b/src/DebugEngineHost.VSCode/DebugEngineHost.VSCode.csproj index 428c8b4ca..58c85fc60 100644 --- a/src/DebugEngineHost.VSCode/DebugEngineHost.VSCode.csproj +++ b/src/DebugEngineHost.VSCode/DebugEngineHost.VSCode.csproj @@ -3,6 +3,7 @@ 1.0.0 + enable @@ -20,11 +21,16 @@ netstandard2.0 - - - - - + + + Shared\%(Filename).cs + + + Shared\%(Filename).cs + + + Shared\%(Filename).cs + diff --git a/src/DebugEngineHost.VSCode/HostConfigurationSection.cs b/src/DebugEngineHost.VSCode/HostConfigurationSection.cs index 632257540..5fe40a546 100644 --- a/src/DebugEngineHost.VSCode/HostConfigurationSection.cs +++ b/src/DebugEngineHost.VSCode/HostConfigurationSection.cs @@ -21,7 +21,7 @@ public void Dispose() GC.SuppressFinalize(this); } - public object GetValue(string valueName) + public object? GetValue(string valueName) { ExceptionSettings.TriggerState state; if (_defaultTriggers.TryGetValue(valueName, out state)) diff --git a/src/DebugEngineHost.VSCode/HostConfigurationStore.cs b/src/DebugEngineHost.VSCode/HostConfigurationStore.cs index 9736c3735..2f391b70d 100644 --- a/src/DebugEngineHost.VSCode/HostConfigurationStore.cs +++ b/src/DebugEngineHost.VSCode/HostConfigurationStore.cs @@ -15,11 +15,12 @@ public sealed class HostConfigurationStore public HostConfigurationStore(string adapterId) { - _config = EngineConfiguration.TryGet(adapterId); - if (_config == null) + EngineConfiguration? config = EngineConfiguration.TryGet(adapterId); + if (config is null) { throw new ArgumentOutOfRangeException(nameof(adapterId)); } + _config = config; } public void SetEngineGuid(Guid value) @@ -35,12 +36,12 @@ public string RegistryRoot } } - public object GetCustomLauncher(string launcherTypeName) + public object? GetCustomLauncher(string launcherTypeName) { throw new NotImplementedException(); } - public object GetEngineMetric(string metric) + public object? GetEngineMetric(string metric) { if (string.CompareOrdinal("GlobalVisualizersDirectory", metric) == 0) { @@ -55,7 +56,7 @@ public object GetEngineMetric(string metric) public void GetExceptionCategorySettings(Guid categoryId, out HostConfigurationSection categoryConfigSection, out string categoryName) { var category = _config.ExceptionSettings.Categories.FirstOrDefault((x) => x.Id == categoryId); - if (category == null) + if (category is null) { throw new InvalidDataException(string.Format(CultureInfo.CurrentCulture, HostResources.Error_ExceptionCategoryMissing, categoryId)); } diff --git a/src/DebugEngineHost.VSCode/HostLoader.cs b/src/DebugEngineHost.VSCode/HostLoader.cs index 59d9d9f27..b3727e92b 100644 --- a/src/DebugEngineHost.VSCode/HostLoader.cs +++ b/src/DebugEngineHost.VSCode/HostLoader.cs @@ -7,7 +7,7 @@ namespace Microsoft.DebugEngineHost { public static class HostLoader { - public static object VsCoCreateManagedObject(HostConfigurationStore configStore, Guid clsid) + public static object? VsCoCreateManagedObject(HostConfigurationStore configStore, Guid clsid) { throw new NotImplementedException(); } diff --git a/src/DebugEngineHost.VSCode/HostLogger.cs b/src/DebugEngineHost.VSCode/HostLogger.cs index 1bd6805ab..9d784538e 100644 --- a/src/DebugEngineHost.VSCode/HostLogger.cs +++ b/src/DebugEngineHost.VSCode/HostLogger.cs @@ -7,14 +7,14 @@ namespace Microsoft.DebugEngineHost { public static class HostLogger { - private static ILogChannel s_natvisLogChannel; - private static ILogChannel s_engineLogChannel; + private static ILogChannel? s_natvisLogChannel; + private static ILogChannel? s_engineLogChannel; - private static string s_engineLogFile; + private static string? s_engineLogFile; public static void EnableNatvisDiagnostics(Action callback, LogLevel level = LogLevel.Verbose) { - if (s_natvisLogChannel == null) + if (s_natvisLogChannel is null) { // TODO: Support writing natvis logs to a file. s_natvisLogChannel = new HostLogChannel(callback, null, level); @@ -23,23 +23,23 @@ public static void EnableNatvisDiagnostics(Action callback, LogLevel lev public static void EnableHostLogging(Action callback, LogLevel level = LogLevel.Verbose) { - if (s_engineLogChannel == null) + if (s_engineLogChannel is null) { s_engineLogChannel = new HostLogChannel(callback, s_engineLogFile, level); } } - public static void SetEngineLogFile(string logFile) + public static void SetEngineLogFile(string? logFile) { s_engineLogFile = logFile; } - public static ILogChannel GetEngineLogChannel() + public static ILogChannel? GetEngineLogChannel() { return s_engineLogChannel; } - public static ILogChannel GetNatvisLogChannel() + public static ILogChannel? GetNatvisLogChannel() { return s_natvisLogChannel; } diff --git a/src/DebugEngineHost.VSCode/HostMarshal.cs b/src/DebugEngineHost.VSCode/HostMarshal.cs index 41f780164..f135307d0 100644 --- a/src/DebugEngineHost.VSCode/HostMarshal.cs +++ b/src/DebugEngineHost.VSCode/HostMarshal.cs @@ -80,8 +80,7 @@ public static IDebugDocumentPosition2 GetDocumentPositionForIntPtr(IntPtr docume { lock (s_documentPositions) { - IDebugDocumentPosition2 documentPosition; - if (!s_documentPositions.TryGet((int)documentPositionId, out documentPosition)) + if (!s_documentPositions.TryGet((int)documentPositionId, out IDebugDocumentPosition2? documentPosition)) { throw new ArgumentOutOfRangeException(nameof(documentPositionId)); } @@ -94,8 +93,7 @@ public static IDebugFunctionPosition2 GetDebugFunctionPositionForIntPtr(IntPtr f { lock (s_functionPositions) { - IDebugFunctionPosition2 functionPosition; - if (!s_functionPositions.TryGet((int)functionPositionId, out functionPosition)) + if (!s_functionPositions.TryGet((int)functionPositionId, out IDebugFunctionPosition2? functionPosition)) { throw new ArgumentOutOfRangeException(nameof(functionPositionId)); } @@ -134,8 +132,7 @@ public static IDebugCodeContext2 GetDebugCodeContextForIntPtr(IntPtr contextId) { lock (s_codeContexts) { - IDebugCodeContext2 codeContext; - if (!s_codeContexts.TryGet(contextId.ToInt32(), out codeContext)) + if (!s_codeContexts.TryGet(contextId.ToInt32(), out IDebugCodeContext2? codeContext)) { throw new ArgumentOutOfRangeException(nameof(contextId)); } diff --git a/src/DebugEngineHost.VSCode/HostNatvisProject.cs b/src/DebugEngineHost.VSCode/HostNatvisProject.cs index e24c4f812..a9c7855e2 100644 --- a/src/DebugEngineHost.VSCode/HostNatvisProject.cs +++ b/src/DebugEngineHost.VSCode/HostNatvisProject.cs @@ -14,13 +14,13 @@ public static void FindNatvis(NatvisLoader loader) // In-solution natvis is not supported for VS Code now, so do nothing. } - public static IDisposable WatchNatvisOptionSetting(HostConfigurationStore configStore, ILogChannel natvisLogger) + public static IDisposable? WatchNatvisOptionSetting(HostConfigurationStore configStore, ILogChannel natvisLogger) { // VS Code does not have a registry setting for Natvis Diagnostics return null; } - public static string FindSolutionRoot() + public static string? FindSolutionRoot() { // This was added in MIEngine to support breakpoint sourcefile mapping. // TODO: Return the project root if we want a similar implementation. diff --git a/src/DebugEngineHost.VSCode/HostOutputWindow.cs b/src/DebugEngineHost.VSCode/HostOutputWindow.cs index 63b249ab6..eba011b40 100644 --- a/src/DebugEngineHost.VSCode/HostOutputWindow.cs +++ b/src/DebugEngineHost.VSCode/HostOutputWindow.cs @@ -2,13 +2,12 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; -using System.Diagnostics; namespace Microsoft.DebugEngineHost { public static class HostOutputWindow { - private static Action s_launchErrorCallback; + private static Action? s_launchErrorCallback; public static void InitializeLaunchErrorCallback(Action launchErrorCallback) { @@ -18,7 +17,7 @@ public static void InitializeLaunchErrorCallback(Action launchErrorCallb public static void WriteLaunchError(string outputMessage) { - if (s_launchErrorCallback != null) + if (s_launchErrorCallback is not null) { s_launchErrorCallback(outputMessage); } diff --git a/src/DebugEngineHost.VSCode/HostRunInTerminal.cs b/src/DebugEngineHost.VSCode/HostRunInTerminal.cs index b8c94e835..60dbcf109 100644 --- a/src/DebugEngineHost.VSCode/HostRunInTerminal.cs +++ b/src/DebugEngineHost.VSCode/HostRunInTerminal.cs @@ -3,14 +3,13 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; namespace Microsoft.DebugEngineHost { public static class HostRunInTerminal { - private static Action, Dictionary, Action, Action> s_runInTerminalCallback; + private static Action, Dictionary, Action, Action>? s_runInTerminalCallback; /// /// Checks to see if RunInTerminal is available @@ -18,7 +17,7 @@ public static class HostRunInTerminal /// public static bool IsRunInTerminalAvailable() { - return s_runInTerminalCallback != null; + return s_runInTerminalCallback is not null; } /// @@ -26,7 +25,7 @@ public static bool IsRunInTerminalAvailable() /// public static void RunInTerminal(string title, string cwd, bool useExternalConsole, IReadOnlyList commandArgs, IReadOnlyDictionary environmentVars, Action success, Action failure) { - if (s_runInTerminalCallback != null) + if (s_runInTerminalCallback is not null) { Dictionary env = new Dictionary(); foreach (var item in environmentVars) diff --git a/src/DebugEngineHost.VSCode/HostTelemetry.cs b/src/DebugEngineHost.VSCode/HostTelemetry.cs index 0eed205ce..0801a507e 100644 --- a/src/DebugEngineHost.VSCode/HostTelemetry.cs +++ b/src/DebugEngineHost.VSCode/HostTelemetry.cs @@ -3,7 +3,7 @@ using System; using System.Collections.Generic; -using System.Diagnostics; +using ConditionalAttribute = global::System.Diagnostics.ConditionalAttribute; using System.Linq; using System.Reflection; @@ -29,11 +29,11 @@ public static class HostTelemetry private const string TelemetryHostVersion = @"VS.Diagnostics.Debugger.HostVersion"; private const string TelemetryAdapterId = @"VS.Diagnostics.Debugger.AdapterId"; - private static Action[]> s_telemetryCallback; - private static string s_engineName; - private static string s_engineVersion; - private static string s_hostVersion; - private static string s_adapterId; + private static Action[]>? s_telemetryCallback; + private static string? s_engineName; + private static string? s_engineVersion; + private static string? s_hostVersion; + private static string? s_adapterId; public static void InitializeTelemetry(Action[]> telemetryCallback, TypeInfo engineType, string adapterId) { @@ -56,7 +56,7 @@ public static void InitializeTelemetry(Action[] eventProperties) { #if LAB - if (s_telemetryCallback != null) + if (s_telemetryCallback is not null) { s_telemetryCallback(eventName, eventProperties); } @@ -70,34 +70,35 @@ public static void SendEvent(string eventName, params KeyValuePairException object to report. /// [Optional] Name of the engine reporting the exception. Ex:Microsoft.MIEngine. /// In OpenDebugAD7, this is optional. - public static void ReportCurrentException(Exception currentException, string engineName) + public static void ReportCurrentException(Exception currentException, string? engineName) { try { // Report the inner-most exception - while (currentException.InnerException != null) + while (currentException.InnerException is not null) { currentException = currentException.InnerException; } - if (s_hostVersion == null) + if (s_hostVersion is null) { // InitializeTelemetry not called yet return; } - if (engineName == null) + if (engineName is null) { + Debug.Assert(s_engineName is not null, "Should be impossible -- if we got past the previous check, InitializeTelemetry was already called"); engineName = s_engineName; } SendEvent(TelemetryNonFatalWatsonEventName, new KeyValuePair(TelemetryNonFatalErrorImplementationName, engineName), - new KeyValuePair(TelemetryNonFatalErrorExceptionTypeName, currentException.GetType().FullName), - new KeyValuePair(TelemetryNonFatalErrorExceptionStackName, currentException.StackTrace), + new KeyValuePair(TelemetryNonFatalErrorExceptionTypeName, currentException.GetType().FullName ?? string.Empty), + new KeyValuePair(TelemetryNonFatalErrorExceptionStackName, currentException.StackTrace ?? string.Empty), new KeyValuePair(TelemetryNonFatalErrorExceptionHResult, currentException.HResult), - new KeyValuePair(TelemetryEngineVersion, s_engineVersion), - new KeyValuePair(TelemetryAdapterId, s_adapterId), + new KeyValuePair(TelemetryEngineVersion, s_engineVersion ?? string.Empty), + new KeyValuePair(TelemetryAdapterId, s_adapterId ?? string.Empty), new KeyValuePair(TelemetryHostVersion, s_hostVersion) ); } @@ -110,7 +111,7 @@ public static void ReportCurrentException(Exception currentException, string eng private static string GetVersionAttributeValue(TypeInfo engineType) { var attribute = engineType.Assembly.GetCustomAttribute(typeof(System.Reflection.AssemblyFileVersionAttribute)) as AssemblyFileVersionAttribute; - if (attribute == null) + if (attribute is null) return string.Empty; return attribute.Version; diff --git a/src/DebugEngineHost.VSCode/VSCode/AssemblyResolver.cs b/src/DebugEngineHost.VSCode/VSCode/AssemblyResolver.cs index e8b46a10d..943379461 100644 --- a/src/DebugEngineHost.VSCode/VSCode/AssemblyResolver.cs +++ b/src/DebugEngineHost.VSCode/VSCode/AssemblyResolver.cs @@ -19,18 +19,18 @@ public static void Initialize() AssemblyLoadContext.Default.Resolving += OnAssemblyResolve; } - private static Assembly OnAssemblyResolve(AssemblyLoadContext loadContext, AssemblyName assemblyName) + private static Assembly? OnAssemblyResolve(AssemblyLoadContext loadContext, AssemblyName assemblyName) { - Assembly asm = InnerResolveHandler(assemblyName); + Assembly? asm = InnerResolveHandler(assemblyName); return asm; } - private static Assembly InnerResolveHandler(AssemblyName assemblyName) + private static Assembly? InnerResolveHandler(AssemblyName assemblyName) { string assemblyFileName = string.Concat(assemblyName.Name, ".dll"); - if (assemblyName.CultureInfo != null && !assemblyName.CultureInfo.Equals(CultureInfo.InvariantCulture)) + if (assemblyName.CultureInfo is not null && !assemblyName.CultureInfo.Equals(CultureInfo.InvariantCulture)) { //Prepend the culture directory (e.g. ja\Microsoft.VisualStudio.Test.resources.dll) assemblyFileName = Path.Combine(assemblyName.CultureInfo.Name, assemblyFileName); @@ -45,9 +45,9 @@ private static Assembly InnerResolveHandler(AssemblyName assemblyName) } } - Assembly asm = AssemblyLoadContext.Default.LoadFromAssemblyPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, assemblyFileName)); + Assembly? asm = AssemblyLoadContext.Default.LoadFromAssemblyPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, assemblyFileName)); - if (asm == null) + if (asm is null) { lock (s_unresolvedNames) { diff --git a/src/DebugEngineHost.VSCode/VSCode/EngineConfiguration.cs b/src/DebugEngineHost.VSCode/VSCode/EngineConfiguration.cs index 265d4246c..678a9d20d 100644 --- a/src/DebugEngineHost.VSCode/VSCode/EngineConfiguration.cs +++ b/src/DebugEngineHost.VSCode/VSCode/EngineConfiguration.cs @@ -21,14 +21,14 @@ namespace Microsoft.DebugEngineHost.VSCode { public sealed class EngineConfiguration { - private static string s_adapterDirectory; + private static string? s_adapterDirectory; private static readonly Dictionary s_dict = new Dictionary(); - public string AdapterId { get; private set; } + public string AdapterId { get; private set; } = null!; private bool _isReadOnly; - private string _assemblyName; - private string _engineClass; + private string _assemblyName = null!; + private string _engineClass = null!; private readonly ExceptionSettings _exceptionSettings = new ExceptionSettings(); private bool _conditionalBP; private bool _functionBP; @@ -89,14 +89,14 @@ public bool DataBP /// Path to the directory public static string GetAdapterDirectory() { - if (s_adapterDirectory == null) + if (s_adapterDirectory is null) { // Configuration goes in the directory of this assembly string thisModulePath = typeof(EngineConfiguration).GetTypeInfo().Assembly.ManifestModule.FullyQualifiedName; Interlocked.CompareExchange(ref s_adapterDirectory, Path.GetDirectoryName(thisModulePath), null); } - return s_adapterDirectory; + return s_adapterDirectory!; } /// @@ -107,7 +107,7 @@ public static string GetAdapterDirectory() /// The directory to use. public static void SetAdapterDirectory(string adapterDirectory) { - if (adapterDirectory == null) + if (adapterDirectory is null) { throw new ArgumentNullException(nameof(adapterDirectory)); } @@ -118,11 +118,11 @@ public static void SetAdapterDirectory(string adapterDirectory) } } - public static EngineConfiguration TryGet(string adapterId) + public static EngineConfiguration? TryGet(string adapterId) { lock (s_dict) { - EngineConfiguration result; + EngineConfiguration? result; if (s_dict.TryGetValue(adapterId, out result)) { return result; @@ -131,6 +131,10 @@ public static EngineConfiguration TryGet(string adapterId) string engineConfigPath = Path.Combine(GetAdapterDirectory(), adapterId + ".ad7Engine.json"); string engineConfigText = File.ReadAllText(engineConfigPath); result = JsonConvert.DeserializeObject(engineConfigText); + if (result is null) + { + return null; + } result.AdapterId = adapterId; result.ExceptionSettings.MakeReadOnly(); result._isReadOnly = true; @@ -149,15 +153,15 @@ public object LoadEngine() AssemblyName assemblyName = new System.Reflection.AssemblyName(this.EngineAssemblyName); Assembly engineAssembly = Assembly.Load(assemblyName); - Type engineClass = engineAssembly.GetType(this.EngineClassName); - if (engineClass == null) + Type? engineClass = engineAssembly.GetType(this.EngineClassName); + if (engineClass is null) { throw new InvalidDataException(string.Format(CultureInfo.CurrentCulture, HostResources.Error_ClassNotFound, this.EngineClassName, this.EngineAssemblyName)); } - object instance = Activator.CreateInstance(engineClass); + object? instance = Activator.CreateInstance(engineClass); - if (instance == null) + if (instance is null) { throw new InvalidDataException(string.Format(CultureInfo.CurrentCulture, HostResources.Error_ConstructorNotFound, this.EngineClassName, this.EngineAssemblyName)); } diff --git a/src/DebugEngineHost.VSCode/VSCode/ExceptionBreakpointFilter.cs b/src/DebugEngineHost.VSCode/VSCode/ExceptionBreakpointFilter.cs index 6e2064e84..618c28b7d 100644 --- a/src/DebugEngineHost.VSCode/VSCode/ExceptionBreakpointFilter.cs +++ b/src/DebugEngineHost.VSCode/VSCode/ExceptionBreakpointFilter.cs @@ -4,7 +4,6 @@ using Microsoft.VisualStudio.Debugger.Interop; using Newtonsoft.Json; using System; -using System.Diagnostics; namespace Microsoft.DebugEngineHost.VSCode { @@ -14,13 +13,13 @@ namespace Microsoft.DebugEngineHost.VSCode /// sealed public class ExceptionBreakpointFilter { - private string _filter; + private string _filter = null!; /// /// The label for the button that will appear in the UI /// [JsonRequired] - public string label { get; set; } + public string label { get; set; } = null!; /// /// The identifier for this filter. @@ -43,7 +42,7 @@ public string filter public bool supportsCondition { get; set; } [JsonRequired] - public string conditionDescription { get; set; } + public string conditionDescription { get; set; } = null!; [JsonRequired] public Guid categoryId { get; set; } diff --git a/src/DebugEngineHost.VSCode/VSCode/ExceptionSettings.cs b/src/DebugEngineHost.VSCode/VSCode/ExceptionSettings.cs index 2328784ff..980a18831 100644 --- a/src/DebugEngineHost.VSCode/VSCode/ExceptionSettings.cs +++ b/src/DebugEngineHost.VSCode/VSCode/ExceptionSettings.cs @@ -6,7 +6,6 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Diagnostics; using System.Globalization; using System.Linq; @@ -26,7 +25,7 @@ public enum TriggerState sealed public class CategoryConfiguration { [JsonRequired] - public string Name; + public string Name = null!; [JsonRequired] public Guid Id; diff --git a/src/DebugEngineHost.VSCode/VSCode/HandleCollection.cs b/src/DebugEngineHost.VSCode/VSCode/HandleCollection.cs index 1d23ea8a7..902dc0b4e 100644 --- a/src/DebugEngineHost.VSCode/VSCode/HandleCollection.cs +++ b/src/DebugEngineHost.VSCode/VSCode/HandleCollection.cs @@ -5,6 +5,7 @@ using System; using System.Linq; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; namespace Microsoft.DebugEngineHost.VSCode { @@ -34,7 +35,7 @@ public int Create(T value) return handle; } - public bool TryGet(int handle, out T value) + public bool TryGet(int handle, [MaybeNullWhen(false)] out T value) { if (_handleMap.TryGetValue(handle, out value)) { @@ -43,11 +44,11 @@ public bool TryGet(int handle, out T value) return false; } - public bool TryGetFirst(out T value) + public bool TryGetFirst([MaybeNullWhen(false)] out T value) { if (IsEmpty) { - value = default(T); + value = default; return false; } @@ -58,8 +59,7 @@ public T this[int handle] { get { - T value; - if (!TryGet(handle, out value)) + if (!TryGet(handle, out T? value)) { throw new ArgumentOutOfRangeException(nameof(handle)); } diff --git a/src/DebugEngineHost/DebugEngineHost.csproj b/src/DebugEngineHost/DebugEngineHost.csproj index a0f375ed7..66706dea0 100755 --- a/src/DebugEngineHost/DebugEngineHost.csproj +++ b/src/DebugEngineHost/DebugEngineHost.csproj @@ -3,6 +3,7 @@ 1.0.0 + enable @@ -26,11 +27,16 @@ $(VSSDKRoot)VisualStudioIntegration\Common\Assemblies\v4.0\Microsoft.VisualStudio.Debugger.Engine.dll - - - - - + + + Shared\%(Filename).cs + + + Shared\%(Filename).cs + + + Shared\%(Filename).cs + diff --git a/src/DebugEngineHost/FeedbackDiagnosticFileProvider.cs b/src/DebugEngineHost/FeedbackDiagnosticFileProvider.cs index db4a5f58b..07b9bb79e 100644 --- a/src/DebugEngineHost/FeedbackDiagnosticFileProvider.cs +++ b/src/DebugEngineHost/FeedbackDiagnosticFileProvider.cs @@ -1,13 +1,13 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; using System.Collections.Generic; using System.ComponentModel.Composition; -using System.Diagnostics; using System.IO; using System.Threading.Tasks; using Microsoft.Internal.VisualStudio.Shell.Embeddable.Feedback; +using Process = global::System.Diagnostics.Process; namespace Microsoft.DebugEngineHost { diff --git a/src/DebugEngineHost/Host.cs b/src/DebugEngineHost/Host.cs index 62e634ff8..40e1d8bf5 100644 --- a/src/DebugEngineHost/Host.cs +++ b/src/DebugEngineHost/Host.cs @@ -1,9 +1,8 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Text; using System.Threading; diff --git a/src/DebugEngineHost/HostConfigurationSection.cs b/src/DebugEngineHost/HostConfigurationSection.cs index 7f3f3f402..5a037f5ee 100644 --- a/src/DebugEngineHost/HostConfigurationSection.cs +++ b/src/DebugEngineHost/HostConfigurationSection.cs @@ -30,7 +30,7 @@ public void Dispose() /// /// Name of the value to obtain /// [Optional] null if the value doesn't exist, otherwise the value - public object GetValue(string valueName) + public object? GetValue(string valueName) { return _key.GetValue(valueName); } diff --git a/src/DebugEngineHost/HostConfigurationStore.cs b/src/DebugEngineHost/HostConfigurationStore.cs index 1f491a3d9..787d8359b 100644 --- a/src/DebugEngineHost/HostConfigurationStore.cs +++ b/src/DebugEngineHost/HostConfigurationStore.cs @@ -1,10 +1,9 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using Microsoft.Win32; using System; using System.Collections.Generic; -using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; @@ -21,7 +20,7 @@ public sealed class HostConfigurationStore private const string LaunchersSectionName = "MILaunchers"; private const string NatvisDiagnosticsSectionName = "NatvisDiagnostics"; - private string _engineId; + private string? _engineId; private string _registryRoot; // HKLM RegistryKey @@ -29,15 +28,16 @@ public sealed class HostConfigurationStore public HostConfigurationStore(string registryRoot) { - if (string.IsNullOrEmpty(registryRoot)) + if (IsNullOrEmpty(registryRoot)) throw new ArgumentNullException(nameof(registryRoot)); _registryRoot = registryRoot; - _configKey = Registry.LocalMachine.OpenSubKey(registryRoot); - if (_configKey == null) + RegistryKey? configKey = Registry.LocalMachine.OpenSubKey(registryRoot); + if (configKey is null) { throw new HostConfigurationException(registryRoot); } + _configKey = configKey; } /// @@ -46,7 +46,7 @@ public HostConfigurationStore(string registryRoot) /// The new engine GUID to set public void SetEngineGuid(Guid value) { - if (_engineId != null) + if (_engineId is not null) { throw new InvalidOperationException(); } @@ -63,9 +63,9 @@ public string RegistryRoot } } - public object GetEngineMetric(string metric) + public object? GetEngineMetric(string metric) { - if (_engineId == null) + if (_engineId is null) { throw new InvalidOperationException(); } @@ -76,8 +76,8 @@ public object GetEngineMetric(string metric) public void GetExceptionCategorySettings(Guid categoryId, out HostConfigurationSection categoryConfigSection, out string categoryName) { string subKeyName = @"AD7Metrics\Exception\" + categoryId.ToString("B", CultureInfo.InvariantCulture); - RegistryKey categoryKey = _configKey.OpenSubKey(subKeyName); - if (categoryKey == null) + RegistryKey? categoryKey = _configKey.OpenSubKey(subKeyName); + if (categoryKey is null) { throw new HostConfigurationException("$RegRoot$\\" + subKeyName); } @@ -91,7 +91,7 @@ public T GetDebuggerConfigurationSetting(string settingName, T defaultValue) return GetDebuggerConfigurationSetting(DebuggerSectionName, settingName, defaultValue); } - public object GetCustomLauncher(string launcherTypeName) + public object? GetCustomLauncher(string launcherTypeName) { string guidstr = GetDebuggerConfigurationSetting(LaunchersSectionName, launcherTypeName, Guid.Empty.ToString()); Guid clsidLauncher = new Guid(guidstr); @@ -104,8 +104,8 @@ public object GetCustomLauncher(string launcherTypeName) private T GetDebuggerConfigurationSetting(string sectionName, string settingName, T defaultValue) { - object valueObj = GetOptionalValue(sectionName, settingName); - if (valueObj == null) + object? valueObj = GetOptionalValue(sectionName, settingName); + if (valueObj is null) { return defaultValue; } @@ -124,11 +124,11 @@ private T GetDebuggerConfigurationSetting(string sectionName, string settingN return result; } - private object GetOptionalValue(string section, string valueName) + private object? GetOptionalValue(string section, string valueName) { - using (RegistryKey key = _configKey.OpenSubKey(section)) + using (RegistryKey? key = _configKey.OpenSubKey(section)) { - if (key == null) + if (key is null) { return null; } @@ -141,12 +141,17 @@ private object GetOptionalValue(string section, string valueName) /// This method grabs the Debugger Subkey in HKCU /// /// The subkey of Debugger if it exists. Returns null otherwise. - public HostConfigurationSection GetCurrentUserDebuggerSection() + public HostConfigurationSection? GetCurrentUserDebuggerSection() { - using (RegistryKey hkcuRoot = Registry.CurrentUser.OpenSubKey(_registryRoot)) + using (RegistryKey? hkcuRoot = Registry.CurrentUser.OpenSubKey(_registryRoot)) { - RegistryKey debuggerSection = hkcuRoot.OpenSubKey(DebuggerSectionName); - if (debuggerSection != null) + if (hkcuRoot is null) + { + return null; + } + + RegistryKey? debuggerSection = hkcuRoot.OpenSubKey(DebuggerSectionName); + if (debuggerSection is not null) { return new HostConfigurationSection(debuggerSection); } @@ -158,16 +163,21 @@ public HostConfigurationSection GetCurrentUserDebuggerSection() /// Grabs the Debugger/NatvisDiagnostic subkey in HKCU /// /// The NatvisDiagnostic subkey if it exists. Returns null otherwise. - public HostConfigurationSection GetNatvisDiagnosticSection() + public HostConfigurationSection? GetNatvisDiagnosticSection() { - using (RegistryKey hkcuRoot = Registry.CurrentUser.OpenSubKey(_registryRoot)) + using (RegistryKey? hkcuRoot = Registry.CurrentUser.OpenSubKey(_registryRoot)) { - using (RegistryKey debuggerSection = hkcuRoot.OpenSubKey(DebuggerSectionName)) + if (hkcuRoot is null) + { + return null; + } + + using (RegistryKey? debuggerSection = hkcuRoot.OpenSubKey(DebuggerSectionName)) { - if (debuggerSection != null) + if (debuggerSection is not null) { - RegistryKey natvisDiagnosticKey = debuggerSection.OpenSubKey(NatvisDiagnosticsSectionName); - if (natvisDiagnosticKey != null) + RegistryKey? natvisDiagnosticKey = debuggerSection.OpenSubKey(NatvisDiagnosticsSectionName); + if (natvisDiagnosticKey is not null) { return new HostConfigurationSection(natvisDiagnosticKey); } diff --git a/src/DebugEngineHost/HostLoader.cs b/src/DebugEngineHost/HostLoader.cs index 84d666298..f19f6bc97 100644 --- a/src/DebugEngineHost/HostLoader.cs +++ b/src/DebugEngineHost/HostLoader.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using Microsoft.Win32; @@ -23,21 +23,24 @@ public static class HostLoader /// Registry root to lookup the type /// CLSID to CoCreate /// [Optional] loaded object. Null if the type is not registered, or points to a type that doesn't exist - public static object VsCoCreateManagedObject(HostConfigurationStore configStore, Guid clsid) + public static object? VsCoCreateManagedObject(HostConfigurationStore configStore, Guid clsid) { - string assemblyNameString, className, codeBase; + string? assemblyNameString, className, codeBase; if (!GetManagedTypeInfoForCLSID(configStore, clsid, out assemblyNameString, out className, out codeBase)) { return null; } - if (codeBase != null && !File.Exists(codeBase)) + Debug.Assert(assemblyNameString is not null, "assemblyNameString should be set when GetManagedTypeInfoForCLSID returns true"); + Debug.Assert(className is not null, "className should be set when GetManagedTypeInfoForCLSID returns true"); + + if (codeBase is not null && !File.Exists(codeBase)) { return null; } AssemblyName assemblyName = new AssemblyName(assemblyNameString); - if (codeBase != null) + if (codeBase is not null) { assemblyName.CodeBase = "file:///" + codeBase; } @@ -46,34 +49,34 @@ public static object VsCoCreateManagedObject(HostConfigurationStore configStore, return assemblyObject.CreateInstance(className); } - private static bool GetManagedTypeInfoForCLSID(HostConfigurationStore configStore, Guid clsid, out string assembly, out string className, out string codeBase) + private static bool GetManagedTypeInfoForCLSID(HostConfigurationStore configStore, Guid clsid, out string? assembly, out string? className, out string? codeBase) { assembly = null; className = null; codeBase = null; string keyPath = configStore.RegistryRoot + @"\CLSID\" + clsid.ToString("B"); - using (RegistryKey key = Registry.LocalMachine.OpenSubKey(keyPath)) + using (RegistryKey? key = Registry.LocalMachine.OpenSubKey(keyPath)) { - if (key == null) + if (key is null) return false; - object oAssembly = key.GetValue("Assembly"); - object oClassName = key.GetValue("Class"); - object oCodeBase = key.GetValue("CodeBase"); + object? oAssembly = key.GetValue("Assembly"); + object? oClassName = key.GetValue("Class"); + object? oCodeBase = key.GetValue("CodeBase"); - if (oAssembly == null || !(oAssembly is string)) + if (oAssembly is not string) return false; - if (oClassName == null || !(oClassName is string)) + if (oClassName is not string) return false; // CodeBase is not required, but it is an error if it isn't a string - if (oCodeBase != null && !(oCodeBase is string)) + if (oCodeBase is not null and not string) return false; assembly = (string)oAssembly; className = (string)oClassName; - codeBase = (string)oCodeBase; + codeBase = oCodeBase as string; return true; } diff --git a/src/DebugEngineHost/HostLogger.cs b/src/DebugEngineHost/HostLogger.cs index fbeca7eb4..ab29a977f 100644 --- a/src/DebugEngineHost/HostLogger.cs +++ b/src/DebugEngineHost/HostLogger.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; @@ -11,22 +11,22 @@ namespace Microsoft.DebugEngineHost { public static class HostLogger { - private static ILogChannel s_natvisLogChannel; - private static ILogChannel s_engineLogChannel; + private static ILogChannel? s_natvisLogChannel; + private static ILogChannel? s_engineLogChannel; - private static string s_engineLogFile; + private static string? s_engineLogFile; - private static FeedbackLogBuffer s_circularBuffer; - private static VSFeedbackLogger s_feedbackLogger; + private static FeedbackLogBuffer? s_circularBuffer; + private static VSFeedbackLogger? s_feedbackLogger; public static void EnableHostLogging(Action callback, LogLevel level = LogLevel.Verbose) { - if (s_engineLogChannel == null) + if (s_engineLogChannel is null) { s_engineLogChannel = new HostLogChannel(callback, s_engineLogFile, level); } - if (s_feedbackLogger == null) + if (s_feedbackLogger is null) { s_feedbackLogger = new VSFeedbackLogger(EnsureFeedbackBuffer()); } @@ -34,7 +34,7 @@ public static void EnableHostLogging(Action callback, LogLevel level = L public static void EnableNatvisDiagnostics(Action callback, LogLevel level = LogLevel.Verbose) { - if (s_natvisLogChannel== null) + if (s_natvisLogChannel is null) { s_natvisLogChannel = new HostLogChannel(callback, null, level); } @@ -45,17 +45,17 @@ public static void DisableNatvisDiagnostics() s_natvisLogChannel = null; } - public static void SetEngineLogFile(string logFile) + public static void SetEngineLogFile(string? logFile) { s_engineLogFile = logFile; } - public static ILogChannel GetEngineLogChannel() + public static ILogChannel? GetEngineLogChannel() { return s_engineLogChannel; } - public static ILogChannel GetNatvisLogChannel() + public static ILogChannel? GetNatvisLogChannel() { return s_natvisLogChannel; } @@ -65,7 +65,7 @@ public static ILogChannel GetNatvisLogChannel() /// public static bool IsFeedbackLogEnabled { - get { return s_circularBuffer != null; } + get { return s_circularBuffer is not null; } } /// @@ -73,7 +73,7 @@ public static bool IsFeedbackLogEnabled /// public static void WriteFeedbackLog(string message) { - if (string.IsNullOrEmpty(message)) + if (IsNullOrEmpty(message)) { return; } @@ -87,7 +87,7 @@ public static void WriteFeedbackLog(string message) private static FeedbackLogBuffer EnsureFeedbackBuffer() { - if (s_circularBuffer == null) + if (s_circularBuffer is null) { Interlocked.CompareExchange(ref s_circularBuffer, new FeedbackLogBuffer(), null); } @@ -102,8 +102,8 @@ internal static bool HasFeedbackEntries { get { - FeedbackLogBuffer buffer = s_circularBuffer; - return buffer != null && buffer.HasEntries; + FeedbackLogBuffer? buffer = s_circularBuffer; + return buffer is not null && buffer.HasEntries; } } diff --git a/src/DebugEngineHost/HostMarshal.cs b/src/DebugEngineHost/HostMarshal.cs index d3d6f1844..9c249c2af 100644 --- a/src/DebugEngineHost/HostMarshal.cs +++ b/src/DebugEngineHost/HostMarshal.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using Microsoft.VisualStudio.Debugger.Interop; @@ -8,7 +8,6 @@ using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; -using System.Diagnostics; namespace Microsoft.DebugEngineHost { diff --git a/src/DebugEngineHost/HostNatvisProject.cs b/src/DebugEngineHost/HostNatvisProject.cs index 11c3510eb..e7fb0f0f8 100644 --- a/src/DebugEngineHost/HostNatvisProject.cs +++ b/src/DebugEngineHost/HostNatvisProject.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; @@ -24,17 +24,19 @@ namespace Microsoft.DebugEngineHost { internal class RegisterMonitorWrapper : IDisposable { - public RegistryMonitor CurrentMonitor { get; set; } + private RegistryMonitor? _currentMonitor; + + public RegistryMonitor CurrentMonitor => _currentMonitor ?? throw new InvalidOperationException(); internal RegisterMonitorWrapper(RegistryMonitor currentMonitor) { - CurrentMonitor = currentMonitor; + _currentMonitor = currentMonitor; } public void Dispose() { - CurrentMonitor.Dispose(); - CurrentMonitor = null; + _currentMonitor?.Dispose(); + _currentMonitor = null; } } @@ -66,12 +68,12 @@ public static void FindNatvis(NatvisLoader loader) paths.ForEach((s) => loader(s)); } - public static IDisposable WatchNatvisOptionSetting(HostConfigurationStore configStore, ILogChannel natvisLogger) + public static IDisposable? WatchNatvisOptionSetting(HostConfigurationStore configStore, ILogChannel natvisLogger) { - RegisterMonitorWrapper rmw = null; + RegisterMonitorWrapper? rmw = null; - HostConfigurationSection natvisDiagnosticSection = configStore.GetNatvisDiagnosticSection(); - if (natvisDiagnosticSection != null) + HostConfigurationSection? natvisDiagnosticSection = configStore.GetNatvisDiagnosticSection(); + if (natvisDiagnosticSection is not null) { // DiagnosticSection exists, set current log level and watch for changes. SetNatvisLogLevel(natvisDiagnosticSection); @@ -81,9 +83,9 @@ public static IDisposable WatchNatvisOptionSetting(HostConfigurationStore config else { // NatvisDiagnostic section has not been created, we need to watch for the creation. - HostConfigurationSection debuggerSection = configStore.GetCurrentUserDebuggerSection(); + HostConfigurationSection? debuggerSection = configStore.GetCurrentUserDebuggerSection(); - if (debuggerSection != null) + if (debuggerSection is not null) { // We only care about the debugger subkey's keys since we are waiting for the NatvisDiagnostics // section to be created. @@ -93,9 +95,9 @@ public static IDisposable WatchNatvisOptionSetting(HostConfigurationStore config rm.RegChanged += (sender, e) => { - HostConfigurationSection checkForSection = configStore.GetNatvisDiagnosticSection(); + HostConfigurationSection? checkForSection = configStore.GetNatvisDiagnosticSection(); - if (checkForSection != null) + if (checkForSection is not null) { // NatvisDiagnostic section found. Update the logger SetNatvisLogLevel(checkForSection); @@ -134,8 +136,8 @@ private static RegistryMonitor CreateAndStartNatvisDiagnosticMonitor(HostConfigu private static void SetNatvisLogLevel(HostConfigurationSection natvisDiagnosticSection) { - string level = natvisDiagnosticSection.GetValue("Level") as string; - if (level != null) + string? level = natvisDiagnosticSection.GetValue("Level") as string; + if (level is not null) { level = level.ToLower(CultureInfo.InvariantCulture); } @@ -169,13 +171,13 @@ private static void SetNatvisLogLevel(HostConfigurationSection natvisDiagnosticS string formattedMessage = string.Format(CultureInfo.InvariantCulture, "Natvis: {0}", message); HostOutputWindow.WriteLaunchError(formattedMessage); }, logLevel); - HostLogger.GetNatvisLogChannel().SetLogLevel(logLevel); + HostLogger.GetNatvisLogChannel()!.SetLogLevel(logLevel); } } - public static string FindSolutionRoot() + public static string? FindSolutionRoot() { - string path = null; + string? path = null; try { ThreadHelper.JoinableTaskFactory.Run(async () => @@ -217,14 +219,14 @@ internal enum VSENUMPROJFLAGS /// Gets the WorkspaceService from Microsoft.VisualStudio.Workspace /// /// This package won't be automatically loaded by MEF, so we need to manually acquire exported MEF Parts. - private static IVsFolderWorkspaceService GetWorkspaceService() + private static IVsFolderWorkspaceService? GetWorkspaceService() { - IComponentModel componentModel = ServiceProvider.GlobalProvider.GetService(typeof(SComponentModel).GUID) as IComponentModel; - if (componentModel != null) + IComponentModel? componentModel = ServiceProvider.GlobalProvider.GetService(typeof(SComponentModel).GUID) as IComponentModel; + if (componentModel is not null) { var workspaceServices = componentModel.DefaultExportProvider.GetExports(); - if (workspaceServices != null && workspaceServices.Any()) + if (workspaceServices is not null && workspaceServices.Any()) { return workspaceServices.First().Value; } @@ -237,11 +239,11 @@ private async static Task> GetOpenFolderSourceLocationsAsync var workspaceService = GetWorkspaceService(); IEnumerable sourcesArray = new List(); - if (workspaceService != null) + if (workspaceService is not null) { - IWorkspace currentWorkspace = workspaceService.CurrentWorkspace; - IIndexWorkspaceService indexWorkspaceService = currentWorkspace?.GetService(throwIfNotFound: false); - if (indexWorkspaceService != null) + IWorkspace? currentWorkspace = workspaceService.CurrentWorkspace; + IIndexWorkspaceService? indexWorkspaceService = currentWorkspace?.GetService(throwIfNotFound: false); + if (indexWorkspaceService is not null) { if (indexWorkspaceService.State != IndexWorkspaceState.Completed) { @@ -280,8 +282,8 @@ public void Report(string value) public async static System.Threading.Tasks.Task FindNatvisInSolutionImplAsync(List paths) { - var solution = (IVsSolution)Package.GetGlobalService(typeof(SVsSolution)); - if (solution == null) + var solution = Package.GetGlobalService(typeof(SVsSolution)) as IVsSolution; + if (solution is null) { return; // failed to find a solution } @@ -321,8 +323,8 @@ public async static System.Threading.Tasks.Task FindNatvisInSolutionImplAsync(Li public static void FindNatvisInVSIXImpl(List paths) { - var extManager = (IVsExtensionManagerPrivate)Package.GetGlobalService(typeof(SVsExtensionManager)); - if (extManager == null) + var extManager = Package.GetGlobalService(typeof(SVsExtensionManager)) as IVsExtensionManagerPrivate; + if (extManager is null) { return; // failed to find the extension manager } @@ -330,13 +332,13 @@ public static void FindNatvisInVSIXImpl(List paths) BuildEnvironmentPath("NativeCrossPlatformVisualizer", extManager, paths); } - public static string FindSolutionRootImpl() + public static string? FindSolutionRootImpl() { - string root = null; - string slnFile; - string slnUserFile; - var solution = (IVsSolution)Package.GetGlobalService(typeof(SVsSolution)); - if (solution == null) + string? root = null; + string? slnFile; + string? slnUserFile; + var solution = (IVsSolution?)Package.GetGlobalService(typeof(SVsSolution)); + if (solution is null) { return null; // failed to find a solution } @@ -363,8 +365,8 @@ private static void BuildEnvironmentPath(string name, IVsExtensionManagerPrivate private static void LoadNatvisFromProject(IVsHierarchy hier, List paths, bool solutionLevel) { - IVsProject4 proj = hier as IVsProject4; - if (proj == null) + IVsProject4? proj = hier as IVsProject4; + if (proj is null) { return; } diff --git a/src/DebugEngineHost/HostOutputWindow.cs b/src/DebugEngineHost/HostOutputWindow.cs index 9dec6445a..b2c54b671 100644 --- a/src/DebugEngineHost/HostOutputWindow.cs +++ b/src/DebugEngineHost/HostOutputWindow.cs @@ -1,9 +1,8 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; using System.Collections.Generic; -using System.Diagnostics; using System.Threading; using Microsoft.VisualStudio; using Microsoft.VisualStudio.Shell; @@ -13,9 +12,9 @@ namespace Microsoft.DebugEngineHost { internal static class VsOutputWindowWrapper { - private static Lazy outputWindowLazy = new Lazy(() => + private static Lazy outputWindowLazy = new Lazy(() => { - IVsOutputWindow outputWindow = null; + IVsOutputWindow? outputWindow = null; try { ThreadHelper.ThrowIfNotOnUIThread(); @@ -28,9 +27,9 @@ internal static class VsOutputWindowWrapper return outputWindow; }, LazyThreadSafetyMode.PublicationOnly); - private static Lazy shellLazy = new Lazy(() => + private static Lazy shellLazy = new Lazy(() => { - IVsUIShell shell = null; + IVsUIShell? shell = null; try { ThreadHelper.ThrowIfNotOnUIThread(); @@ -75,8 +74,8 @@ public static void Write(string message, string pane = DefaultOutputPane) try { // Get the Output window - IVsOutputWindow outputWindow = outputWindowLazy.Value; - if (outputWindow == null) + IVsOutputWindow? outputWindow = outputWindowLazy.Value; + if (outputWindow is null) { return; } @@ -108,10 +107,10 @@ public static void Write(string message, string pane = DefaultOutputPane) outputPane.Activate(); // Show the output window - IVsUIShell shell = shellLazy.Value; - if (shell != null) + IVsUIShell? shell = shellLazy.Value; + if (shell is not null) { - object inputVariant = null; + object? inputVariant = null; shell.PostExecCommand(VSConstants.GUID_VSStandardCommandSet97, (uint)VSConstants.VSStd97CmdID.OutputWindow, 0, ref inputVariant); } } diff --git a/src/DebugEngineHost/HostRunInTerminal.cs b/src/DebugEngineHost/HostRunInTerminal.cs index db666042f..05079fdd1 100644 --- a/src/DebugEngineHost/HostRunInTerminal.cs +++ b/src/DebugEngineHost/HostRunInTerminal.cs @@ -1,4 +1,7 @@ -using System; +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; using System.Collections.Generic; using System.Linq; using System.Text; diff --git a/src/DebugEngineHost/HostTelemetry.cs b/src/DebugEngineHost/HostTelemetry.cs index eea4a0004..39729b7aa 100644 --- a/src/DebugEngineHost/HostTelemetry.cs +++ b/src/DebugEngineHost/HostTelemetry.cs @@ -1,14 +1,14 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Shell.Interop; using System.Globalization; using Microsoft.Internal.VisualStudio.Shell; +using Conditional = global::System.Diagnostics.ConditionalAttribute; namespace Microsoft.DebugEngineHost { @@ -56,7 +56,7 @@ public static void SendEvent(string eventName, params KeyValuePair /// Exception object to report. /// Name of the engine reporting the exception. Ex:Microsoft.MIEngine - public static void ReportCurrentException(Exception currentException, string engineName) + public static void ReportCurrentException(Exception currentException, string? engineName) { Debug.Fail(string.Format(CultureInfo.InvariantCulture, "{0} was raised and would normally be reported to telemetry.\n\nStack trace: {1}", currentException.GetType(), currentException.StackTrace)); diff --git a/src/DebugEngineHost/HostWaitDialog.cs b/src/DebugEngineHost/HostWaitDialog.cs index 5de1eafe0..96216b196 100644 --- a/src/DebugEngineHost/HostWaitDialog.cs +++ b/src/DebugEngineHost/HostWaitDialog.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; @@ -17,7 +17,7 @@ namespace Microsoft.DebugEngineHost /// public sealed class HostWaitDialog : IDisposable { - private VSImpl.VSWaitDialog _theDialog; + private VSImpl.VSWaitDialog? _theDialog; public HostWaitDialog(string format, string caption) { try @@ -31,7 +31,7 @@ public HostWaitDialog(string format, string caption) } public void ShowWaitDialog(string item) { - if (_theDialog != null) + if (_theDialog is not null) { _theDialog.ShowWaitDialog(item); } @@ -39,7 +39,7 @@ public void ShowWaitDialog(string item) public void EndWaitDialog() { - if (_theDialog != null) + if (_theDialog is not null) { _theDialog.EndWaitDialog(); } diff --git a/src/DebugEngineHost/HostWaitLoop.cs b/src/DebugEngineHost/HostWaitLoop.cs index f1edbd281..e6153eef7 100644 --- a/src/DebugEngineHost/HostWaitLoop.cs +++ b/src/DebugEngineHost/HostWaitLoop.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; @@ -14,7 +14,7 @@ namespace Microsoft.DebugEngineHost public sealed class HostWaitLoop { private readonly object _progressLock = new object(); - private VSImpl.VsWaitLoop _vsWaitLoop; + private VSImpl.VsWaitLoop? _vsWaitLoop; public HostWaitLoop(string message) { @@ -34,7 +34,7 @@ public HostWaitLoop(string message) /// Text to set. public void SetText(string text) { - _vsWaitLoop.SetText(text); + _vsWaitLoop?.SetText(text); } /// @@ -46,7 +46,7 @@ public void SetText(string text) /// Thrown by the JIT if Visual Studio is not installed public void Wait(WaitHandle launchCompleteHandle, CancellationTokenSource cancellationSource) { - if (_vsWaitLoop != null) + if (_vsWaitLoop is not null) { _vsWaitLoop.Wait(launchCompleteHandle, cancellationSource); @@ -65,7 +65,7 @@ public void SetProgress(int totalSteps, int currentStep, string progressText) { lock (_progressLock) { - if (_vsWaitLoop != null) + if (_vsWaitLoop is not null) { _vsWaitLoop.SetProgress(totalSteps, currentStep, progressText); } diff --git a/src/DebugEngineHost/RegistryMonitor.cs b/src/DebugEngineHost/RegistryMonitor.cs index 0281bc371..d3e53410f 100644 --- a/src/DebugEngineHost/RegistryMonitor.cs +++ b/src/DebugEngineHost/RegistryMonitor.cs @@ -1,5 +1,5 @@ -// // Copyright (c) Microsoft. All rights reserved. -// // Licensed under the MIT license. See LICENSE file in the project root for full license information. +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. using Microsoft.Win32; using Microsoft.Win32.SafeHandles; @@ -9,7 +9,6 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.InteropServices; -using System.Runtime.Remoting.Messaging; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -51,7 +50,7 @@ private static extern int RegNotifyChangeKeyValue(SafeRegistryHandle hKey, bool private readonly bool _watchSubtree; // Set when monitoring is stopped - private AutoResetEvent _stoppedEvent; + private AutoResetEvent? _stoppedEvent; // Members to handle multiple stop calls. private bool _isStopped = false; @@ -60,7 +59,7 @@ private static extern int RegNotifyChangeKeyValue(SafeRegistryHandle hKey, bool /// /// Occurs when the specified registry key has changed. /// - public event EventHandler RegChanged; + public event EventHandler? RegChanged; private readonly ILogChannel _nativsLogger; @@ -126,7 +125,7 @@ private void Monitor() _nativsLogger?.WriteLine(LogLevel.Error, Resource.Error_WatchRegistry, errorCode); break; } - RegChanged?.Invoke(this, null); + RegChanged?.Invoke(this, EventArgs.Empty); } } } @@ -134,7 +133,7 @@ private void Monitor() } finally { - _stoppedEvent.Dispose(); + _stoppedEvent?.Dispose(); _stoppedEvent = null; _section.Dispose(); diff --git a/src/DebugEngineHost/VSFeedbackLogger.cs b/src/DebugEngineHost/VSFeedbackLogger.cs index 30dd240cb..bb7eb4852 100644 --- a/src/DebugEngineHost/VSFeedbackLogger.cs +++ b/src/DebugEngineHost/VSFeedbackLogger.cs @@ -1,11 +1,11 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using System.Diagnostics; using System.IO; using System.Linq; using System.Text; using Newtonsoft.Json.Linq; +using Process = global::System.Diagnostics.Process; namespace Microsoft.DebugEngineHost { @@ -21,10 +21,10 @@ internal class VSFeedbackLogger private readonly System.DateTime _vsStartTime; private bool _enabled; - private readonly FileSystemWatcher _vsFeedbackFileWatcher; + private readonly FileSystemWatcher? _vsFeedbackFileWatcher; private readonly FeedbackLogBuffer _circularBuffer; - private StreamWriter _logWriter; + private StreamWriter? _logWriter; private readonly object _syncObj = new object(); internal VSFeedbackLogger(FeedbackLogBuffer circularBuffer) @@ -103,7 +103,7 @@ private void OnFeedbackSemaphoreDeleted(object sender, FileSystemEventArgs e) _enabled = false; _circularBuffer.FlushNewEntries(); - if (_logWriter != null) + if (_logWriter is not null) { _logWriter.Dispose(); _logWriter = null; @@ -128,8 +128,8 @@ private bool IsLoggingEnabledForThisVSInstance(string semaphoreFilePath) string content = File.ReadAllText(semaphoreFilePath); JObject root = JObject.Parse(content); - JContainer pidCollection = root["processIds"] as JContainer; - if (pidCollection != null) + JContainer? pidCollection = root["processIds"] as JContainer; + if (pidCollection is not null) { return pidCollection.Values().Contains(_vsPid); } diff --git a/src/DebugEngineHost/VSImpl/VSEventCallbackWrapper.cs b/src/DebugEngineHost/VSImpl/VSEventCallbackWrapper.cs index cd1ded32b..98f7a7535 100644 --- a/src/DebugEngineHost/VSImpl/VSEventCallbackWrapper.cs +++ b/src/DebugEngineHost/VSImpl/VSEventCallbackWrapper.cs @@ -1,11 +1,10 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using Microsoft.VisualStudio.Debugger.Interop; using Microsoft.VisualStudio.OLE.Interop; using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Runtime.InteropServices; using System.Text; @@ -31,7 +30,7 @@ internal class VSEventCallbackWrapper : IDebugEventCallback2 private readonly object _cacheLock = new object(); private int _cachedEventCallbackThread; - private IDebugEventCallback2 _cacheEventCallback; + private IDebugEventCallback2? _cacheEventCallback; internal VSEventCallbackWrapper(IDebugEventCallback2 ad7Callback) { @@ -90,11 +89,11 @@ private IDebugEventCallback2 GetAD7EventCallback() // We send esentially all events from the same thread, so lets optimize the common case int currentThreadId = Thread.CurrentThread.ManagedThreadId; - if (_cacheEventCallback != null && _cachedEventCallbackThread == currentThreadId) + if (_cacheEventCallback is not null && _cachedEventCallbackThread == currentThreadId) { lock (_cacheLock) { - if (_cacheEventCallback != null && _cachedEventCallbackThread == currentThreadId) + if (_cacheEventCallback is not null && _cachedEventCallbackThread == currentThreadId) { return _cacheEventCallback; } diff --git a/src/DebugEngineHost/VSImpl/VsWaitDialog.cs b/src/DebugEngineHost/VSImpl/VsWaitDialog.cs index 1dc56fcd2..402450ddc 100644 --- a/src/DebugEngineHost/VSImpl/VsWaitDialog.cs +++ b/src/DebugEngineHost/VSImpl/VsWaitDialog.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; @@ -11,7 +11,7 @@ namespace Microsoft.DebugEngineHost.VSImpl { internal class VSWaitDialog { - private readonly IVsThreadedWaitDialog2 _waitDialog; + private readonly IVsThreadedWaitDialog2? _waitDialog; private const int m_delayShowDialogTimeInSeconds = 2; private bool _started; private string _format; @@ -36,7 +36,7 @@ public VSWaitDialog(string format, string caption) } public void ShowWaitDialog(string item) { - if (_waitDialog == null) + if (_waitDialog is null) { return; } @@ -69,7 +69,7 @@ public void ShowWaitDialog(string item) public void EndWaitDialog() { - if (_waitDialog == null) + if (_waitDialog is null) { return; } diff --git a/src/DebugEngineHost/VSImpl/VsWaitLoop.cs b/src/DebugEngineHost/VSImpl/VsWaitLoop.cs index 8cba857d5..afd691899 100644 --- a/src/DebugEngineHost/VSImpl/VsWaitLoop.cs +++ b/src/DebugEngineHost/VSImpl/VsWaitLoop.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; @@ -11,7 +11,6 @@ using System.Threading; using Microsoft.Win32.SafeHandles; using Microsoft.VisualStudio; -using System.Diagnostics; using System.Runtime.InteropServices; namespace Microsoft.DebugEngineHost.VSImpl @@ -24,7 +23,7 @@ namespace Microsoft.DebugEngineHost.VSImpl // ************************************************************ internal class VsWaitLoop { - private readonly IVsCommonMessagePump _messagePump; + private readonly IVsCommonMessagePump? _messagePump; private VsWaitLoop(string text) { @@ -52,10 +51,10 @@ private VsWaitLoop(string text) _messagePump = messagePump; } - static public VsWaitLoop TryCreate(string text) + static public VsWaitLoop? TryCreate(string text) { VsWaitLoop waitLoop = new VsWaitLoop(text); - if (waitLoop._messagePump == null) + if (waitLoop._messagePump is null) return null; return waitLoop; @@ -71,6 +70,7 @@ static public VsWaitLoop TryCreate(string text) [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2001:AvoidCallingProblematicMethods", MessageId = "System.Runtime.InteropServices.SafeHandle.DangerousGetHandle")] public void Wait(WaitHandle launchCompleteHandle, CancellationTokenSource cancellationSource) { + Debug.Assert(_messagePump is not null, "Wait should only be called on instances created via TryCreate that returned non-null"); int hr; SafeWaitHandle safeWaitHandle = launchCompleteHandle.SafeWaitHandle; @@ -118,11 +118,13 @@ public void Wait(WaitHandle launchCompleteHandle, CancellationTokenSource cancel public void SetProgress(int totalSteps, int currentStep, string progressText) { + Debug.Assert(_messagePump is not null, "SetProgress should only be called on a valid VsWaitLoop instance"); _messagePump.SetProgressInfo(totalSteps, currentStep, progressText); } public void SetText(string text) { + Debug.Assert(_messagePump is not null, "SetText should only be called on a valid VsWaitLoop instance"); _messagePump.SetWaitText(text); } } diff --git a/src/Shared/NullableAttributes.cs b/src/Shared/NullableAttributes.cs new file mode 100644 index 000000000..63d4ca7f2 --- /dev/null +++ b/src/Shared/NullableAttributes.cs @@ -0,0 +1,150 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// Source: + +// This file is a tweaked version of https://github.com/dotnet/runtime/blob/c95aa3f48aa591dca870ee00c31c3d8bdc740b5a/src/libraries/System.Private.CoreLib/src/System/Diagnostics/CodeAnalysis/NullableAttributes.cs + +#pragma warning disable CA1019 // Define accessors for attribute arguments + +// Currently, we always need these definitions, but it can be removed if we ever compile against .NET 5 or newer +#if !NETCOREAPP + +namespace System.Diagnostics.CodeAnalysis +{ + /// Specifies that null is allowed as an input even if the corresponding type disallows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] + internal sealed class AllowNullAttribute : Attribute { } + + /// Specifies that null is disallowed as an input even if the corresponding type allows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] + internal sealed class DisallowNullAttribute : Attribute { } + + /// Specifies that an output may be null even if the corresponding type disallows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] + internal sealed class MaybeNullAttribute : Attribute { } + + /// Specifies that an output will not be null even if the corresponding type allows it. Specifies that an input argument was not null when the call returns. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] + internal sealed class NotNullAttribute : Attribute { } + + /// Specifies that when a method returns , the parameter may be null even if the corresponding type disallows it. + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + internal sealed class MaybeNullWhenAttribute : Attribute + { + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter may be null. + /// + public MaybeNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + + /// Gets the return value condition. + public bool ReturnValue { get; } + } + + /// Specifies that when a method returns , the parameter will not be null even if the corresponding type allows it. + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + internal sealed class NotNullWhenAttribute : Attribute + { + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + + /// Gets the return value condition. + public bool ReturnValue { get; } + } + + /// Specifies that the output will be non-null if the named parameter is non-null. + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true, Inherited = false)] + internal sealed class NotNullIfNotNullAttribute : Attribute + { + /// Initializes the attribute with the associated parameter name. + /// + /// The associated parameter name. The output will be non-null if the argument to the parameter specified is non-null. + /// + public NotNullIfNotNullAttribute(string parameterName) => ParameterName = parameterName; + + /// Gets the associated parameter name. + public string ParameterName { get; } + } + + /// Applied to a method that will never return under any circumstance. + [AttributeUsage(AttributeTargets.Method, Inherited = false)] + internal sealed class DoesNotReturnAttribute : Attribute { } + + /// Specifies that the method will not return if the associated Boolean parameter is passed the specified value. + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + internal sealed class DoesNotReturnIfAttribute : Attribute + { + /// Initializes the attribute with the specified parameter value. + /// + /// The condition parameter value. Code after the method will be considered unreachable by diagnostics if the argument to + /// the associated parameter matches this value. + /// + public DoesNotReturnIfAttribute(bool parameterValue) => ParameterValue = parameterValue; + + /// Gets the condition parameter value. + public bool ParameterValue { get; } + } + + /// Specifies that the method or property will ensure that the listed field and property members have not-null values. + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] + internal sealed class MemberNotNullAttribute : Attribute + { + /// Initializes the attribute with a field or property member. + /// + /// The field or property member that is promised to be not-null. + /// + public MemberNotNullAttribute(string member) => Members = new[] { member }; + + /// Initializes the attribute with the list of field and property members. + /// + /// The list of field and property members that are promised to be not-null. + /// + public MemberNotNullAttribute(params string[] members) => Members = members; + + /// Gets field or property member names. + public string[] Members { get; } + } + + /// Specifies that the method or property will ensure that the listed field and property members have not-null values when returning with the specified return value condition. + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] + internal sealed class MemberNotNullWhenAttribute : Attribute + { + /// Initializes the attribute with the specified return value condition and a field or property member. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + /// + /// The field or property member that is promised to be not-null. + /// + + public MemberNotNullWhenAttribute(bool returnValue, string member) + { + ReturnValue = returnValue; + Members = new[] { member }; + } + + /// Initializes the attribute with the specified return value condition and list of field and property members. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + /// + /// The list of field and property members that are promised to be not-null. + /// + public MemberNotNullWhenAttribute(bool returnValue, params string[] members) + { + ReturnValue = returnValue; + Members = members; + } + + /// Gets the return value condition. + public bool ReturnValue { get; } + + /// Gets field or property member names. + public string[] Members { get; } + } +} + +#endif \ No newline at end of file From bf363d3d0a21c630015a9539a1bfc78699ccd5f0 Mon Sep 17 00:00:00 2001 From: "CSIGS@microsoft.com" Date: Mon, 29 Jun 2026 09:22:07 -0700 Subject: [PATCH 08/25] LEGO: Pull request from lego/hb_d72c5677-3f00-4225-b18e-0a1e8a8f5f0e_20260627094707947 to main (#1597) Juno: check in to lego/hb_d72c5677-3f00-4225-b18e-0a1e8a8f5f0e_20260627094707947. --- loc/lcl/JPN/Microsoft.SSHDebugPS.dll.lcl | 83 +++++++++++++++++++++++- 1 file changed, 82 insertions(+), 1 deletion(-) diff --git a/loc/lcl/JPN/Microsoft.SSHDebugPS.dll.lcl b/loc/lcl/JPN/Microsoft.SSHDebugPS.dll.lcl index 5d9f51eae..01a808ff5 100644 --- a/loc/lcl/JPN/Microsoft.SSHDebugPS.dll.lcl +++ b/loc/lcl/JPN/Microsoft.SSHDebugPS.dll.lcl @@ -154,6 +154,15 @@ + + + + + + + + + @@ -211,6 +220,15 @@ + + + + + + + + + @@ -319,6 +337,24 @@ + + + + + + + + + + + + + + + + + + ]]> @@ -781,6 +817,51 @@ + + + + + + + + + + + + Options and find Cross Platform -> Connection Manager.]]> + + [オプション]5D; に移動し、[クロス プラットフォーム]5D; -> [接続マネージャー]5D; を探します。]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -868,7 +949,7 @@ - + From 96c4fd542c4fc8525ed90cdc3d7d0e64881e3311 Mon Sep 17 00:00:00 2001 From: "CSIGS@microsoft.com" Date: Mon, 29 Jun 2026 09:22:55 -0700 Subject: [PATCH 09/25] LEGO: Pull request from lego/hb_d72c5677-3f00-4225-b18e-0a1e8a8f5f0e_20260628094630378 to main (#1598) Juno: check in to lego/hb_d72c5677-3f00-4225-b18e-0a1e8a8f5f0e_20260628094630378. --- loc/lcl/CHS/Microsoft.SSHDebugPS.dll.lcl | 83 +++++++++++++++++++++++- loc/lcl/CHT/Microsoft.SSHDebugPS.dll.lcl | 83 +++++++++++++++++++++++- loc/lcl/CSY/Microsoft.SSHDebugPS.dll.lcl | 83 +++++++++++++++++++++++- loc/lcl/ESN/Microsoft.SSHDebugPS.dll.lcl | 83 +++++++++++++++++++++++- loc/lcl/FRA/Microsoft.SSHDebugPS.dll.lcl | 83 +++++++++++++++++++++++- loc/lcl/ITA/Microsoft.SSHDebugPS.dll.lcl | 83 +++++++++++++++++++++++- loc/lcl/KOR/Microsoft.SSHDebugPS.dll.lcl | 83 +++++++++++++++++++++++- loc/lcl/PLK/Microsoft.SSHDebugPS.dll.lcl | 83 +++++++++++++++++++++++- loc/lcl/PTB/Microsoft.SSHDebugPS.dll.lcl | 83 +++++++++++++++++++++++- loc/lcl/RUS/Microsoft.SSHDebugPS.dll.lcl | 83 +++++++++++++++++++++++- loc/lcl/TRK/Microsoft.SSHDebugPS.dll.lcl | 83 +++++++++++++++++++++++- 11 files changed, 902 insertions(+), 11 deletions(-) diff --git a/loc/lcl/CHS/Microsoft.SSHDebugPS.dll.lcl b/loc/lcl/CHS/Microsoft.SSHDebugPS.dll.lcl index f7bcef49c..0e731b967 100644 --- a/loc/lcl/CHS/Microsoft.SSHDebugPS.dll.lcl +++ b/loc/lcl/CHS/Microsoft.SSHDebugPS.dll.lcl @@ -154,6 +154,15 @@ + + + + + + + + + @@ -211,6 +220,15 @@ + + + + + + + + + @@ -319,6 +337,24 @@ + + + + + + + + + + + + + + + + + + ]]> @@ -781,6 +817,51 @@ + + + + + + + + + + + + Options and find Cross Platform -> Connection Manager.]]> + + “选项”,并查找“跨平台”->“连接管理器”。]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -868,7 +949,7 @@ - + diff --git a/loc/lcl/CHT/Microsoft.SSHDebugPS.dll.lcl b/loc/lcl/CHT/Microsoft.SSHDebugPS.dll.lcl index 94075ea4f..cd4a53326 100644 --- a/loc/lcl/CHT/Microsoft.SSHDebugPS.dll.lcl +++ b/loc/lcl/CHT/Microsoft.SSHDebugPS.dll.lcl @@ -154,6 +154,15 @@ + + + + + + + + + @@ -211,6 +220,15 @@ + + + + + + + + + @@ -319,6 +337,24 @@ + + + + + + + + + + + + + + + + + + ]]> @@ -781,6 +817,51 @@ + + + + + + + + + + + + Options and find Cross Platform -> Connection Manager.]]> + + [選項]5D;,然後找到 [跨平台]5D; -> [連線管理員]5D;。]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -868,7 +949,7 @@ - + diff --git a/loc/lcl/CSY/Microsoft.SSHDebugPS.dll.lcl b/loc/lcl/CSY/Microsoft.SSHDebugPS.dll.lcl index 8f5db7fab..f0124f73b 100644 --- a/loc/lcl/CSY/Microsoft.SSHDebugPS.dll.lcl +++ b/loc/lcl/CSY/Microsoft.SSHDebugPS.dll.lcl @@ -154,6 +154,15 @@ + + + + + + + + + @@ -211,6 +220,15 @@ + + + + + + + + + @@ -319,6 +337,24 @@ + + + + + + + + + + + + + + + + + + ]]> @@ -781,6 +817,51 @@ + + + + + + + + + + + + Options and find Cross Platform -> Connection Manager.]]> + + Možnosti a vyhledejte možnost Pro různé platformy > Správce připojení.]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -868,7 +949,7 @@ - + diff --git a/loc/lcl/ESN/Microsoft.SSHDebugPS.dll.lcl b/loc/lcl/ESN/Microsoft.SSHDebugPS.dll.lcl index 3f0c41e93..40a4d86d7 100644 --- a/loc/lcl/ESN/Microsoft.SSHDebugPS.dll.lcl +++ b/loc/lcl/ESN/Microsoft.SSHDebugPS.dll.lcl @@ -154,6 +154,15 @@ + + + + + + + + + @@ -211,6 +220,15 @@ + + + + + + + + + @@ -319,6 +337,24 @@ + + + + + + + + + + + + + + + + + + ]]> @@ -781,6 +817,51 @@ + + + + + + + + + + + + Options and find Cross Platform -> Connection Manager.]]> + + Opciones y busque Multiplataforma -> Administrador de conexiones.]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -868,7 +949,7 @@ - + diff --git a/loc/lcl/FRA/Microsoft.SSHDebugPS.dll.lcl b/loc/lcl/FRA/Microsoft.SSHDebugPS.dll.lcl index 32dc1aadd..eb7049a08 100644 --- a/loc/lcl/FRA/Microsoft.SSHDebugPS.dll.lcl +++ b/loc/lcl/FRA/Microsoft.SSHDebugPS.dll.lcl @@ -154,6 +154,15 @@ + + + + + + + + + @@ -211,6 +220,15 @@ + + + + + + + + + @@ -319,6 +337,24 @@ + + + + + + + + + + + + + + + + + + ]]> @@ -781,6 +817,51 @@ + + + + + + + + + + + + Options and find Cross Platform -> Connection Manager.]]> + + Options, puis recherchez Multiplateforme -> Gestionnaire des connexions.]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -868,7 +949,7 @@ - + diff --git a/loc/lcl/ITA/Microsoft.SSHDebugPS.dll.lcl b/loc/lcl/ITA/Microsoft.SSHDebugPS.dll.lcl index 126f24abe..dae7bb32d 100644 --- a/loc/lcl/ITA/Microsoft.SSHDebugPS.dll.lcl +++ b/loc/lcl/ITA/Microsoft.SSHDebugPS.dll.lcl @@ -154,6 +154,15 @@ + + + + + + + + + @@ -211,6 +220,15 @@ + + + + + + + + + @@ -319,6 +337,24 @@ + + + + + + + + + + + + + + + + + + ]]> @@ -781,6 +817,51 @@ + + + + + + + + + + + + Options and find Cross Platform -> Connection Manager.]]> + + Opzioni e trovare Multipiattaforma -> Gestione connessioni.]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -868,7 +949,7 @@ - + diff --git a/loc/lcl/KOR/Microsoft.SSHDebugPS.dll.lcl b/loc/lcl/KOR/Microsoft.SSHDebugPS.dll.lcl index 4dcd06cc2..e53e5e42c 100644 --- a/loc/lcl/KOR/Microsoft.SSHDebugPS.dll.lcl +++ b/loc/lcl/KOR/Microsoft.SSHDebugPS.dll.lcl @@ -154,6 +154,15 @@ + + + + + + + + + @@ -211,6 +220,15 @@ + + + + + + + + + @@ -319,6 +337,24 @@ + + + + + + + + + + + + + + + + + + ]]> @@ -781,6 +817,51 @@ + + + + + + + + + + + + Options and find Cross Platform -> Connection Manager.]]> + + [옵션]5D;으로 이동하여 [플랫폼 간]5D; > [연결 관리자]5D;를 찾습니다.]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -868,7 +949,7 @@ - + diff --git a/loc/lcl/PLK/Microsoft.SSHDebugPS.dll.lcl b/loc/lcl/PLK/Microsoft.SSHDebugPS.dll.lcl index f5c9c8b0e..7a382298a 100644 --- a/loc/lcl/PLK/Microsoft.SSHDebugPS.dll.lcl +++ b/loc/lcl/PLK/Microsoft.SSHDebugPS.dll.lcl @@ -154,6 +154,15 @@ + + + + + + + + + @@ -211,6 +220,15 @@ + + + + + + + + + @@ -319,6 +337,24 @@ + + + + + + + + + + + + + + + + + + ]]> @@ -781,6 +817,51 @@ + + + + + + + + + + + + Options and find Cross Platform -> Connection Manager.]]> + + Opcje > i znajdź pozycje Wiele platform -> Menedżer połączeń.]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -868,7 +949,7 @@ - + diff --git a/loc/lcl/PTB/Microsoft.SSHDebugPS.dll.lcl b/loc/lcl/PTB/Microsoft.SSHDebugPS.dll.lcl index 915c119a8..18d11eb8e 100644 --- a/loc/lcl/PTB/Microsoft.SSHDebugPS.dll.lcl +++ b/loc/lcl/PTB/Microsoft.SSHDebugPS.dll.lcl @@ -154,6 +154,15 @@ + + + + + + + + + @@ -211,6 +220,15 @@ + + + + + + + + + @@ -319,6 +337,24 @@ + + + + + + + + + + + + + + + + + + ]]> @@ -781,6 +817,51 @@ + + + + + + + + + + + + Options and find Cross Platform -> Connection Manager.]]> + + Opções e localize Multiplataforma -> Gerenciador de Conexões.]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -868,7 +949,7 @@ - + diff --git a/loc/lcl/RUS/Microsoft.SSHDebugPS.dll.lcl b/loc/lcl/RUS/Microsoft.SSHDebugPS.dll.lcl index 535ce8576..d5ba360f9 100644 --- a/loc/lcl/RUS/Microsoft.SSHDebugPS.dll.lcl +++ b/loc/lcl/RUS/Microsoft.SSHDebugPS.dll.lcl @@ -154,6 +154,15 @@ + + + + + + + + + @@ -211,6 +220,15 @@ + + + + + + + + + @@ -319,6 +337,24 @@ + + + + + + + + + + + + + + + + + + ]]> @@ -781,6 +817,51 @@ + + + + + + + + + + + + Options and find Cross Platform -> Connection Manager.]]> + + "Параметры" и найдите раздел "Кроссплатформенные" > "Диспетчер подключений".]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -868,7 +949,7 @@ - + diff --git a/loc/lcl/TRK/Microsoft.SSHDebugPS.dll.lcl b/loc/lcl/TRK/Microsoft.SSHDebugPS.dll.lcl index ed3606cc1..6619b27b9 100644 --- a/loc/lcl/TRK/Microsoft.SSHDebugPS.dll.lcl +++ b/loc/lcl/TRK/Microsoft.SSHDebugPS.dll.lcl @@ -154,6 +154,15 @@ + + + + + + + + + @@ -211,6 +220,15 @@ + + + + + + + + + @@ -319,6 +337,24 @@ + + + + + + + + + + + + + + + + + + ]]> @@ -781,6 +817,51 @@ + + + + + + + + + + + + Options and find Cross Platform -> Connection Manager.]]> + + Seçenekler'e gidin ve Çoklu Platform -> Bağlantı Yöneticisi'ni bulun.]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -868,7 +949,7 @@ - + From 1043c023ed1474ba243c456ad38d064feaa583b2 Mon Sep 17 00:00:00 2001 From: "CSIGS@microsoft.com" Date: Mon, 29 Jun 2026 09:23:34 -0700 Subject: [PATCH 10/25] LEGO: Pull request from lego/hb_d72c5677-3f00-4225-b18e-0a1e8a8f5f0e_20260629094703468 to main (#1599) Juno: check in to lego/hb_d72c5677-3f00-4225-b18e-0a1e8a8f5f0e_20260629094703468. --- loc/lcl/DEU/Microsoft.SSHDebugPS.dll.lcl | 83 +++++++++++++++++++++++- 1 file changed, 82 insertions(+), 1 deletion(-) diff --git a/loc/lcl/DEU/Microsoft.SSHDebugPS.dll.lcl b/loc/lcl/DEU/Microsoft.SSHDebugPS.dll.lcl index e04c77111..62891d658 100644 --- a/loc/lcl/DEU/Microsoft.SSHDebugPS.dll.lcl +++ b/loc/lcl/DEU/Microsoft.SSHDebugPS.dll.lcl @@ -154,6 +154,15 @@ + + + + + + + + + @@ -211,6 +220,15 @@ + + + + + + + + + @@ -319,6 +337,24 @@ + + + + + + + + + + + + + + + + + + ]]> @@ -781,6 +817,51 @@ + + + + + + + + + + + + Options and find Cross Platform -> Connection Manager.]]> + + „Optionen“ > „Plattformübergreifend“ > „Verbindungs-Manager“.]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -868,7 +949,7 @@ - + From dd91e131c6a4e0227946d4df87a91f52d152f8a3 Mon Sep 17 00:00:00 2001 From: Gregg Miskelly Date: Mon, 29 Jun 2026 09:32:58 -0700 Subject: [PATCH 11/25] Prepare for nullable reference types in MICore (#1595) This PR contains work to prepare for enabling nullable reference types in MICore. Changes: - Correct two incorrect annotations in DebugEngineHost - Refactor MICommandFactory to use constructor injection (readonly _debugger field) - Move MICommandFactory.GetInstance() from DebuggedProcess to Debugger constructor - Fix bug: firstException should only be set when null (was checking != null) - Add null safety: _transport?.Close(), ThreadCreatedEvent?.Invoke, ThreadExitedEvent?.Invoke - Add null throw in SendToTransport for null transport - Use GetTargetProcessExitedReason() instead of raw _closeMessage - Null-safe access to _initialErrors/_initializationLog in OnDebuggerProcessExit - Pass EventArgs.Empty instead of null for DebuggerExitEvent - Use pattern matching for IsModuleLoad check - Make _commandLock readonly, initialize _lastCommandText --- src/DebugEngineHost.Common/HostLogChannel.cs | 6 +-- .../DebugEngineHost.ref.cs | 4 +- src/DebugEngineHost.VSCode/HostLogger.cs | 2 +- .../HostRunInTerminal.cs | 8 ++-- src/DebugEngineHost/HostLogger.cs | 2 +- src/DebugEngineHost/HostRunInTerminal.cs | 2 +- .../CommandFactories/MICommandFactory.cs | 12 ++++-- src/MICore/CommandFactories/gdb.cs | 5 +++ src/MICore/CommandFactories/lldb.cs | 5 +++ src/MICore/Debugger.cs | 39 +++++++++++-------- .../Engine.Impl/DebuggedProcess.cs | 1 - 11 files changed, 53 insertions(+), 33 deletions(-) diff --git a/src/DebugEngineHost.Common/HostLogChannel.cs b/src/DebugEngineHost.Common/HostLogChannel.cs index 699cdb3ad..5f4434193 100644 --- a/src/DebugEngineHost.Common/HostLogChannel.cs +++ b/src/DebugEngineHost.Common/HostLogChannel.cs @@ -50,15 +50,15 @@ public interface ILogChannel public class HostLogChannel : ILogChannel { - private readonly Action _log; + private readonly Action? _log; private StreamWriter? _logFile; private LogLevel _minLevelToBeLogged; private readonly object _lock = new object(); - private HostLogChannel() { _log = null!; } + private HostLogChannel() { } - public HostLogChannel(Action logAction, string? file, LogLevel logLevel) + public HostLogChannel(Action? logAction, string? file, LogLevel logLevel) { _log = logAction; diff --git a/src/DebugEngineHost.Stub/DebugEngineHost.ref.cs b/src/DebugEngineHost.Stub/DebugEngineHost.ref.cs index 7b7fae658..c34e817e5 100644 --- a/src/DebugEngineHost.Stub/DebugEngineHost.ref.cs +++ b/src/DebugEngineHost.Stub/DebugEngineHost.ref.cs @@ -259,7 +259,7 @@ public static class HostLogger /// /// The callback to use to send the engine log. /// The level of the log to filter the channel on. - public static void EnableHostLogging(Action callback, LogLevel level = LogLevel.Verbose) + public static void EnableHostLogging(Action? callback, LogLevel level = LogLevel.Verbose) { throw new NotImplementedException(); } @@ -527,7 +527,7 @@ public static bool IsRunInTerminalAvailable() /// true if the message is sent, false if not. [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "cwd")] [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures")] - public static void RunInTerminal(string title, string workingDirectory, bool useExternalConsole, IReadOnlyList commandArgs, IReadOnlyDictionary environmentVariables, Action success, Action failure) + public static void RunInTerminal(string title, string workingDirectory, bool useExternalConsole, IReadOnlyList commandArgs, IReadOnlyDictionary environmentVariables, Action success, Action failure) { throw new NotImplementedException(); } diff --git a/src/DebugEngineHost.VSCode/HostLogger.cs b/src/DebugEngineHost.VSCode/HostLogger.cs index 9d784538e..75190588c 100644 --- a/src/DebugEngineHost.VSCode/HostLogger.cs +++ b/src/DebugEngineHost.VSCode/HostLogger.cs @@ -21,7 +21,7 @@ public static void EnableNatvisDiagnostics(Action callback, LogLevel lev } } - public static void EnableHostLogging(Action callback, LogLevel level = LogLevel.Verbose) + public static void EnableHostLogging(Action? callback, LogLevel level = LogLevel.Verbose) { if (s_engineLogChannel is null) { diff --git a/src/DebugEngineHost.VSCode/HostRunInTerminal.cs b/src/DebugEngineHost.VSCode/HostRunInTerminal.cs index 60dbcf109..62faf45de 100644 --- a/src/DebugEngineHost.VSCode/HostRunInTerminal.cs +++ b/src/DebugEngineHost.VSCode/HostRunInTerminal.cs @@ -9,7 +9,7 @@ namespace Microsoft.DebugEngineHost { public static class HostRunInTerminal { - private static Action, Dictionary, Action, Action>? s_runInTerminalCallback; + private static Action, Dictionary, Action, Action>? s_runInTerminalCallback; /// /// Checks to see if RunInTerminal is available @@ -23,11 +23,11 @@ public static bool IsRunInTerminalAvailable() /// /// Passes the call to the UI to attempt to RunInTerminal if possible. /// - public static void RunInTerminal(string title, string cwd, bool useExternalConsole, IReadOnlyList commandArgs, IReadOnlyDictionary environmentVars, Action success, Action failure) + public static void RunInTerminal(string title, string cwd, bool useExternalConsole, IReadOnlyList commandArgs, IReadOnlyDictionary environmentVars, Action success, Action failure) { if (s_runInTerminalCallback is not null) { - Dictionary env = new Dictionary(); + Dictionary env = new Dictionary(); foreach (var item in environmentVars) { env.Add(item.Key, item.Value); @@ -41,7 +41,7 @@ public static void RunInTerminal(string title, string cwd, bool useExternalConso /// Registers callback to call when RunInTerminal is called /// /// Callback for RunInTerminal - public static void RegisterRunInTerminalCallback(Action, Dictionary, Action, Action> runInTerminalCallback) + public static void RegisterRunInTerminalCallback(Action, Dictionary, Action, Action> runInTerminalCallback) { Debug.Assert(runInTerminalCallback != null, "Callback should not be null."); s_runInTerminalCallback = runInTerminalCallback; diff --git a/src/DebugEngineHost/HostLogger.cs b/src/DebugEngineHost/HostLogger.cs index ab29a977f..271fcf718 100644 --- a/src/DebugEngineHost/HostLogger.cs +++ b/src/DebugEngineHost/HostLogger.cs @@ -19,7 +19,7 @@ public static class HostLogger private static FeedbackLogBuffer? s_circularBuffer; private static VSFeedbackLogger? s_feedbackLogger; - public static void EnableHostLogging(Action callback, LogLevel level = LogLevel.Verbose) + public static void EnableHostLogging(Action? callback, LogLevel level = LogLevel.Verbose) { if (s_engineLogChannel is null) { diff --git a/src/DebugEngineHost/HostRunInTerminal.cs b/src/DebugEngineHost/HostRunInTerminal.cs index 05079fdd1..243857d8d 100644 --- a/src/DebugEngineHost/HostRunInTerminal.cs +++ b/src/DebugEngineHost/HostRunInTerminal.cs @@ -22,7 +22,7 @@ public static bool IsRunInTerminalAvailable() [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "cwd")] [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures")] - public static void RunInTerminal(string title, string workingDirectory, bool useExternalConsole, IReadOnlyList commandArgs, IReadOnlyDictionary environmentVariables, Action success, Action failure) + public static void RunInTerminal(string title, string workingDirectory, bool useExternalConsole, IReadOnlyList commandArgs, IReadOnlyDictionary environmentVariables, Action success, Action failure) { throw new NotImplementedException(); } diff --git a/src/MICore/CommandFactories/MICommandFactory.cs b/src/MICore/CommandFactories/MICommandFactory.cs index 07de24092..a5d0f01ce 100644 --- a/src/MICore/CommandFactories/MICommandFactory.cs +++ b/src/MICore/CommandFactories/MICommandFactory.cs @@ -48,7 +48,7 @@ public enum AsyncBreakSignal public abstract class MICommandFactory { - protected Debugger _debugger; + protected readonly Debugger _debugger; public MIMode Mode { get; private set; } @@ -56,6 +56,11 @@ public abstract class MICommandFactory internal int MajorVersion { get; set; } + protected MICommandFactory(Debugger debugger) + { + _debugger = debugger; + } + public static MICommandFactory GetInstance(MIMode mode, Debugger debugger) { MICommandFactory commandFactory; @@ -63,15 +68,14 @@ public static MICommandFactory GetInstance(MIMode mode, Debugger debugger) switch (mode) { case MIMode.Gdb: - commandFactory = new GdbMICommandFactory(); + commandFactory = new GdbMICommandFactory(debugger); break; case MIMode.Lldb: - commandFactory = new LlldbMICommandFactory(); + commandFactory = new LlldbMICommandFactory(debugger); break; default: throw new ArgumentException(null, nameof(mode)); } - commandFactory._debugger = debugger; commandFactory.Mode = mode; commandFactory.Radix = 10; return commandFactory; diff --git a/src/MICore/CommandFactories/gdb.cs b/src/MICore/CommandFactories/gdb.cs index 83100ab3e..60954d46a 100644 --- a/src/MICore/CommandFactories/gdb.cs +++ b/src/MICore/CommandFactories/gdb.cs @@ -19,6 +19,11 @@ internal class GdbMICommandFactory : MICommandFactory private int _currentThreadId = 0; private uint _currentFrameLevel = 0; + public GdbMICommandFactory(Debugger debugger) + : base(debugger) + { + } + public override string Name { get { return "GDB"; } diff --git a/src/MICore/CommandFactories/lldb.cs b/src/MICore/CommandFactories/lldb.cs index b430f3257..f423c2fc0 100644 --- a/src/MICore/CommandFactories/lldb.cs +++ b/src/MICore/CommandFactories/lldb.cs @@ -16,6 +16,11 @@ namespace MICore { internal class LlldbMICommandFactory : MICommandFactory { + public LlldbMICommandFactory(Debugger debugger) + : base(debugger) + { + } + public override string Name { get { return "LLDB"; } diff --git a/src/MICore/Debugger.cs b/src/MICore/Debugger.cs index 710d6d70c..b411cd086 100755 --- a/src/MICore/Debugger.cs +++ b/src/MICore/Debugger.cs @@ -72,7 +72,7 @@ public bool IsClosed public uint MaxInstructionSize { get; private set; } public bool Is64BitArch { get; private set; } public CommandLock CommandLock { get { return _commandLock; } } - public MICommandFactory MICommandFactory { get; protected set; } + public MICommandFactory MICommandFactory { get; } public Logger Logger { private set; get; } [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes")] @@ -124,12 +124,12 @@ public StoppingEventArgs(Results results, BreakRequest asyncRequest = BreakReque } private ITransport _transport; - private CommandLock _commandLock = new CommandLock(); + private readonly CommandLock _commandLock = new CommandLock(); /// /// The last command we sent over the transport. This includes both the command name and arguments. /// - private string _lastCommandText; + private string _lastCommandText = string.Empty; private uint _lastCommandId; /// @@ -169,6 +169,7 @@ public Debugger(LaunchOptions launchOptions, Logger logger) _debuggeePids = new Dictionary(); Logger = logger; _miResults = new MIResults(logger); + MICommandFactory = MICommandFactory.GetInstance(launchOptions.DebuggerMIMode, this); } protected void SetDebuggerPid(int debuggerPid) @@ -392,7 +393,7 @@ private async Task DoInternalBreakActions(bool fIsAsyncBreak) } catch (Exception e) when (ExceptionHelper.BeforeCatch(e, Logger, reportOnlyCorrupting: true)) { - if (firstException != null) + if (firstException is null) { firstException = e; } @@ -404,7 +405,7 @@ private async Task DoInternalBreakActions(bool fIsAsyncBreak) { if (this.IsClosed) { - source.TrySetException(new DebuggerDisposedException(_closeMessage)); + source.TrySetException(new DebuggerDisposedException(GetTargetProcessExitedReason())); } else { @@ -524,7 +525,7 @@ private void Close(string closeMessage) Debug.Assert(_closeMessage == null, "Why was Close called more than once? Should be impossible."); _closeMessage = closeMessage; - _transport.Close(); + _transport?.Close(); lock (_waitingOperations) { foreach (var value in _waitingOperations.Values) @@ -905,7 +906,7 @@ private Task CmdAsyncInternal(string command, ResultClass expectedResul { if (this.IsClosed) { - throw new DebuggerDisposedException(_closeMessage); + throw new DebuggerDisposedException(GetTargetProcessExitedReason()); } id = ++_lastCommandId; @@ -992,13 +993,13 @@ public void OnDebuggerProcessExit(/*OPTIONAL*/ string exitCode) if (isMinGWOrCygwin && IsUnsupportedWindowsGdbVersion(_gdbVersion)) { exception = new MIDebuggerInitializeFailedUnsupportedGdbException( - this.MICommandFactory.Name, _initialErrors.ToList().AsReadOnly(), _initializationLog.ToList().AsReadOnly(), _gdbVersion); + this.MICommandFactory.Name, (_initialErrors?.ToList() ?? new List()).AsReadOnly(), (_initializationLog?.ToList() ?? new List()).AsReadOnly(), _gdbVersion); SendUnsupportedWindowsGdbEvent(_gdbVersion); } else { exception = new MIDebuggerInitializeFailedException( - this.MICommandFactory.Name, _initialErrors.ToList().AsReadOnly(), _initializationLog.ToList().AsReadOnly()); + this.MICommandFactory.Name, (_initialErrors?.ToList() ?? new List()).AsReadOnly(), (_initializationLog?.ToList() ?? new List()).AsReadOnly()); } _initialErrors = null; @@ -1029,7 +1030,7 @@ public void OnDebuggerProcessExit(/*OPTIONAL*/ string exitCode) { if (DebuggerExitEvent != null) { - DebuggerExitEvent(this, null); + DebuggerExitEvent(this, EventArgs.Empty); } } } @@ -1419,8 +1420,7 @@ this.LaunchOptions is LocalLaunchOptions && private void OnNotificationOutput(string cmd) { - Results results = null; - if ((results = MICommandFactory.IsModuleLoad(cmd)) != null) + if (MICommandFactory.IsModuleLoad(cmd) is Results results) { if (LibraryLoadEvent != null) { @@ -1460,12 +1460,12 @@ private void OnNotificationOutput(string cmd) else if (cmd.StartsWith("thread-created,", StringComparison.Ordinal)) { results = _miResults.ParseResultList(cmd.Substring("thread-created,".Length)); - ThreadCreatedEvent(this, new ResultEventArgs(results, 0)); + ThreadCreatedEvent?.Invoke(this, new ResultEventArgs(results, 0)); } else if (cmd.StartsWith("thread-exited,", StringComparison.Ordinal)) { results = _miResults.ParseResultList(cmd.Substring("thread-exited,".Length)); - ThreadExitedEvent(this, new ResultEventArgs(results, 0)); + ThreadExitedEvent?.Invoke(this, new ResultEventArgs(results, 0)); } else if (cmd.StartsWith("telemetry,", StringComparison.Ordinal)) { @@ -1619,14 +1619,21 @@ private async void PostCommand(string cmd) private void SendToTransport(string cmd) { - _transport.Send(cmd); + ITransport transport = _transport; + if (transport is null) + { + Debug.Fail("Invalid: `SendToTransport` called before `Init`"); + throw new InvalidOperationException(); + } + + transport.Send(cmd); // https://github.com/Microsoft/MIEngine/issues/616 : // If it is local gdb (MinGW/Cygwin) on Windows, we need to send an extra line after commands // so that if it errors, the error will come through. if (this.SendNewLineAfterCmd) { - _transport.Send(String.Empty); + transport.Send(String.Empty); } } diff --git a/src/MIDebugEngine/Engine.Impl/DebuggedProcess.cs b/src/MIDebugEngine/Engine.Impl/DebuggedProcess.cs index 804709bc2..f11ff57d6 100755 --- a/src/MIDebugEngine/Engine.Impl/DebuggedProcess.cs +++ b/src/MIDebugEngine/Engine.Impl/DebuggedProcess.cs @@ -64,7 +64,6 @@ public DebuggedProcess(bool bLaunched, LaunchOptions launchOptions, ISampleEngin _libraryLoaded = new List(); _loadOrder = 0; _deleteEntryPointBreakpoint = false; - MICommandFactory = MICommandFactory.GetInstance(launchOptions.DebuggerMIMode, this); _waitDialog = (MICommandFactory.SupportsStopOnDynamicLibLoad() && launchOptions.WaitDynamicLibLoad) ? new HostWaitDialog(ResourceStrings.LoadingSymbolMessage, ResourceStrings.LoadingSymbolCaption) : null; Natvis = new Natvis.Natvis(this, launchOptions.ShowDisplayString, configStore); From 0921c7085bca235b2193c4a51e077237a9efe1af Mon Sep 17 00:00:00 2001 From: Gregg Miskelly Date: Tue, 30 Jun 2026 13:12:32 -0700 Subject: [PATCH 12/25] Enable nullable reference types in MICore (#1603) - Add enable to MICore.csproj - Add GlobalUsings.cs with NullableHelpers static import - Add NullableAttributes.cs shared file - Add nullable annotations (?) to all appropriate type declarations - Add [NotNullWhen], [DoesNotReturn] attributes - Add Debug.Assert statements for non-null invariants - Replace string.IsNullOrEmpty/IsNullOrWhiteSpace with NullableHelpers versions - Add using System.Diagnostics where needed for Debug class - Remove redundant using aliases replaced by GlobalUsings This also cleans up the code in `PipeTransport.ExecuteSyncCommand` as that code had the classic Process.Start deadlock --- docs/CodingStandards-CSharp-for-AI.md | 14 + src/MICore/Checksum.cs | 4 +- .../CommandFactories/MICommandFactory.cs | 15 +- src/MICore/CommandFactories/gdb.cs | 11 +- src/MICore/CommandFactories/lldb.cs | 9 +- src/MICore/CommandLock.cs | 21 +- src/MICore/Debugger.cs | 101 +++-- src/MICore/DebuggerDisposedException.cs | 6 +- src/MICore/ExceptionHelper.cs | 5 +- src/MICore/GlobalUsings.cs | 5 + src/MICore/IncludeExcludeList.cs | 10 +- src/MICore/JsonLaunchOptions.cs | 193 ++++----- src/MICore/LaunchCommand.cs | 19 +- src/MICore/LaunchOptions.cs | 384 ++++++++++-------- src/MICore/Logger.cs | 13 +- src/MICore/MICore.csproj | 4 + src/MICore/MIException.cs | 6 +- src/MICore/MIResults.cs | 234 +++++------ src/MICore/PlatformUtilities.cs | 8 +- src/MICore/ProcessMonitor.cs | 10 +- src/MICore/RunInTerminalLauncher.cs | 9 +- .../Transports/ClientServerTransport.cs | 3 +- src/MICore/Transports/ITransport.cs | 4 +- src/MICore/Transports/LocalTransport.cs | 9 +- src/MICore/Transports/MockTransport.cs | 25 +- src/MICore/Transports/PipeTransport.cs | 71 ++-- .../Transports/RunInTerminalTransport.cs | 50 +-- src/MICore/Transports/ServerTransport.cs | 11 +- src/MICore/Transports/StreamTransport.cs | 45 +- src/MICore/Transports/TcpTransport.cs | 8 +- .../Transports/UnixShellPortTransport.cs | 31 +- src/MICore/UnixUtilities.cs | 9 +- src/MICore/Utilities.cs | 4 +- 33 files changed, 721 insertions(+), 630 deletions(-) create mode 100644 src/MICore/GlobalUsings.cs diff --git a/docs/CodingStandards-CSharp-for-AI.md b/docs/CodingStandards-CSharp-for-AI.md index 9791fd020..66611555f 100644 --- a/docs/CodingStandards-CSharp-for-AI.md +++ b/docs/CodingStandards-CSharp-for-AI.md @@ -40,3 +40,17 @@ These aren't in `.editorconfig` but show up everywhere — follow the existing c - **Host calls go through `DebugEngineHost`.** MIDebugEngine never references `Microsoft.VisualStudio.*` directly; use `HostLogger`, `HostMarshal`, `HostOutputWindow`, etc. - **AD7 surface lives on `AD7*` partial classes.** Keep VS-SDK COM concerns out of the core `Debugged*` classes. - **Worker thread discipline.** AD7 callbacks must not block. Use `Task.Run` for work and post results back via the engine's `WorkerThread` / `EngineCallback`. Mirror existing call sites; do not invent new threading patterns. + +## Nullable reference types and `Debug` + +Projects with nullable reference types enabled **must never** use `System.Diagnostics.Debug` directly. Do **not** add `using System.Diagnostics;` or `using Debug = System.Diagnostics.Debug;` in any file in a nullable-enabled project. + +Instead, use `Microsoft.DebugEngineHost.NullableHelpers.Debug`, which is a wrapper that adds `[DoesNotReturn]` / `[DoesNotReturnIf(false)]` attributes so the C# nullable flow analyser understands that `Debug.Assert`/`Debug.Fail` stop execution. This wrapper is brought into scope via the file `GlobalUsings.cs` in each nullable-enabled project: + +```csharp +global using static global::Microsoft.DebugEngineHost.NullableHelpers; +``` + +With that global using in place every `Debug.Assert(...)` and `Debug.Fail(...)` call in the project automatically resolves to `NullableHelpers.Debug` — no per-file `using` is needed. **Never shadow this with a per-file alias or a `using System.Diagnostics;` import.** + +The same global using also brings `IsNullOrEmpty` and `IsNullOrWhiteSpace` into scope as drop-in replacements for `string.IsNullOrEmpty`/`string.IsNullOrWhiteSpace` with the proper `[NotNullWhen(false)]` annotation. diff --git a/src/MICore/Checksum.cs b/src/MICore/Checksum.cs index 0421be385..c98a02328 100644 --- a/src/MICore/Checksum.cs +++ b/src/MICore/Checksum.cs @@ -22,8 +22,8 @@ public enum MIHashAlgorithmName public class Checksum { - private string _checksumString = null; - private byte[] _bytes = null; + private string? _checksumString = null; + private byte[] _bytes; public readonly MIHashAlgorithmName MIHashAlgorithmName; diff --git a/src/MICore/CommandFactories/MICommandFactory.cs b/src/MICore/CommandFactories/MICommandFactory.cs index a5d0f01ce..54923e820 100644 --- a/src/MICore/CommandFactories/MICommandFactory.cs +++ b/src/MICore/CommandFactories/MICommandFactory.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Threading.Tasks; using System.IO; using System.Text; @@ -81,7 +80,7 @@ public static MICommandFactory GetInstance(MIMode mode, Debugger debugger) return commandFactory; } - public static string SpanNextAddr(string line, out ulong addr) + public static string? SpanNextAddr(string line, out ulong addr) { addr = 0; char[] endOfNum = { ' ', '\t', '\"' }; @@ -483,7 +482,7 @@ internal bool PreparePath(string path, bool useUnixFormat, out string pathMI) return requiresQuotes; } - public virtual async Task BreakInsert(string filename, bool useUnixFormat, uint line, string condition, bool enabled, IEnumerable checksums = null, ResultClass resultClass = ResultClass.done) + public virtual async Task BreakInsert(string filename, bool useUnixFormat, uint line, string condition, bool enabled, IEnumerable? checksums = null, ResultClass resultClass = ResultClass.done) { StringBuilder cmd = await BuildBreakInsert(condition, enabled); @@ -533,7 +532,7 @@ public virtual Task BreakWatch(string address, uint size, ResultClass r public virtual bool SupportsDataBreakpoints { get { return false; } } - public virtual async Task BreakInfo(string bkptno) + public virtual async Task BreakInfo(string bkptno) { Results bindResult = await _debugger.CmdAsync("-break-info " + bkptno, ResultClass.None); if (bindResult.ResultClass != ResultClass.done) @@ -563,7 +562,7 @@ public virtual async Task BreakDelete(string bkptno, ResultClass resultClass = R public virtual async Task BreakCondition(string bkptno, string expr) { - if (string.IsNullOrWhiteSpace(expr)) + if (IsNullOrWhiteSpace(expr)) { expr = string.Empty; } @@ -632,7 +631,7 @@ public virtual void DecodeExceptionReceivedProperties(Results miExceptionResult, #region Miscellaneous - public virtual Task AutoComplete(string command, int threadId, uint frameLevel) + public virtual Task AutoComplete(string command, int threadId, uint frameLevel) { throw new NotImplementedException(); } @@ -708,9 +707,9 @@ public virtual AsyncBreakSignal GetAsyncBreakSignal(Results results) return MICore.AsyncBreakSignal.None; } - public Results IsModuleLoad(string cmd) + public Results? IsModuleLoad(string cmd) { - Results results = null; + Results? results = null; if (cmd.StartsWith("library-loaded,", StringComparison.Ordinal)) { MIResults res = new MIResults(_debugger.Logger); diff --git a/src/MICore/CommandFactories/gdb.cs b/src/MICore/CommandFactories/gdb.cs index 60954d46a..a268df1c2 100644 --- a/src/MICore/CommandFactories/gdb.cs +++ b/src/MICore/CommandFactories/gdb.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Threading.Tasks; using System.IO; using System.Text; @@ -57,7 +56,7 @@ protected override async Task ThreadFrameCmdAsync(string command, strin { // first aquire an exclusive lock. This is used as we don't want to fight with other commands that also require the current // thread to be set to a particular value - ExclusiveLockToken lockToken = await _debugger.CommandLock.AquireExclusive(); + ExclusiveLockToken? lockToken = await _debugger.CommandLock.AquireExclusive(); try { @@ -103,7 +102,7 @@ protected override async Task ThreadCmdAsync(string command, string arg { // first aquire an exclusive lock. This is used as we don't want to fight with other commands that also require the current // thread to be set to a particular value - ExclusiveLockToken lockToken = await _debugger.CommandLock.AquireExclusive(); + ExclusiveLockToken? lockToken = await _debugger.CommandLock.AquireExclusive(); try { @@ -193,7 +192,7 @@ public override async Task> StartAddressesForLine(string file, uint { while (true) { - string resultLine = stringReader.ReadLine(); + string? resultLine = stringReader.ReadLine(); if (resultLine == null) break; @@ -281,7 +280,7 @@ public override TargetArchitecture ParseTargetArchitectureResult(string result) { while (true) { - string resultLine = stringReader.ReadLine(); + string? resultLine = stringReader.ReadLine(); if (resultLine == null) break; @@ -327,7 +326,7 @@ public override async Task Catch(string name, bool onlyOnce = false, ResultClass await _debugger.ConsoleCmdAsync(command + name, allowWhileRunning: false); } - public override async Task AutoComplete(string command, int threadId, uint frameLevel) + public override async Task AutoComplete(string command, int threadId, uint frameLevel) { string cmd = "-complete"; string args = $"\"{command}\""; diff --git a/src/MICore/CommandFactories/lldb.cs b/src/MICore/CommandFactories/lldb.cs index f423c2fc0..ffcb609b3 100644 --- a/src/MICore/CommandFactories/lldb.cs +++ b/src/MICore/CommandFactories/lldb.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Threading.Tasks; using System.IO; using System.Text; @@ -116,13 +115,13 @@ protected override async Task ThreadCmdAsync(string command, string arg public override Task> StartAddressesForLine(string file, uint line) { - return Task.FromResult>(null); + return Task.FromResult(new List()); } public override Task EnableTargetAsyncOption() { // lldb-mi doesn't support target-async mode, and doesn't seem to need to - return Task.FromResult((object)null); + return Task.CompletedTask; } public override string GetTargetArchitectureCommand() @@ -136,7 +135,7 @@ public override TargetArchitecture ParseTargetArchitectureResult(string result) { while (true) { - string resultLine = stringReader.ReadLine(); + string? resultLine = stringReader.ReadLine(); if (resultLine == null) break; @@ -216,7 +215,7 @@ private async Task RequiresOnKeywordForBreakInsert() { // Query for the version. string version = await Version(); - if (!string.IsNullOrWhiteSpace(version) && version.Trim().Equals(OldLLDBMIVersionString, StringComparison.Ordinal)) + if (!IsNullOrWhiteSpace(version) && version.Trim().Equals(OldLLDBMIVersionString, StringComparison.Ordinal)) { _requiresOnKeywordForBreakInsert = true; } diff --git a/src/MICore/CommandLock.cs b/src/MICore/CommandLock.cs index 3538ceff9..64b817fb4 100644 --- a/src/MICore/CommandLock.cs +++ b/src/MICore/CommandLock.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Text; using System.Threading; @@ -18,7 +17,7 @@ sealed public class ExclusiveLockToken : IDisposable // NOTE: I tried to make this a value object, but value objects don't work quite as expected in async methods and calling 'Close' // wasn't updating the backing value object which was stored in the state machine class { - private CommandLock _commandLock; + private CommandLock? _commandLock; private int _value; internal ExclusiveLockToken(CommandLock commandLock, int value) @@ -39,7 +38,7 @@ public static bool IsNullOrClosed(ExclusiveLockToken token) return (token == null || token._value == 0); } - public override bool Equals(object obj) + public override bool Equals(object? obj) { throw new NotImplementedException(); // this method should never be called } @@ -66,6 +65,7 @@ public void ConvertToSharedLock() _value = 0; _commandLock = null; + Debug.Assert(commandLock is not null, "Should be impossible. A non-zero _value implies _commandLock is set."); commandLock.ConvertExclusiveLockToShared(value); } @@ -78,6 +78,7 @@ public void Close() _value = 0; _commandLock = null; + Debug.Assert(commandLock is not null, "Should be impossible. A non-zero _value implies _commandLock is set."); commandLock.ReleaseExclusive(value); } } @@ -99,9 +100,9 @@ sealed public class CommandLock private int _prevExclusiveToken; private int _pendingSharedLockRequests; - private TaskCompletionSource _waitingSharedLockSource; + private TaskCompletionSource? _waitingSharedLockSource; private readonly Queue> _waitingExclusiveLockRequests = new Queue>(); - private string _closeMessage; + private string _closeMessage = string.Empty; public CommandLock() { @@ -184,7 +185,7 @@ public Task AquireShared() // Internal method called from the ExclusiveLockToken class as part of closing an exclusive lock internal void ReleaseExclusive(int tokenValue) { - Action actionAfterReleaseLock = null; + Action? actionAfterReleaseLock = null; lock (this.LockObject) { @@ -212,7 +213,7 @@ internal void ReleaseExclusive(int tokenValue) // Internal method called from the ExclusiveLockToken class to convert an exclusive lock into a shared lock internal void ConvertExclusiveLockToShared(int tokenValue) { - Action actionAfterReleaseLock = null; + Action? actionAfterReleaseLock = null; lock (this.LockObject) { @@ -240,7 +241,7 @@ internal void ConvertExclusiveLockToShared(int tokenValue) public void ReleaseShared() { - Action actionAfterReleaseLock = null; + Action? actionAfterReleaseLock = null; lock (this.LockObject) { @@ -269,7 +270,7 @@ public void ReleaseShared() } // NOTE: This method MUST be called with this.LockObject held - private Action GetAfterReleaseLockAction() + private Action? GetAfterReleaseLockAction() { Debug.Assert(_lockStatus == StatusFree, "Why is GetAfterReleaseLockAction called when the lock is not free?"); @@ -298,7 +299,7 @@ private ExclusiveLockToken GetNextExclusiveLockToken() } // NOTE: This method MUST be called with this.LockObject held - private Action MaybeSignalPendingSharedLockRequests() + private Action? MaybeSignalPendingSharedLockRequests() { Debug.Assert(_lockStatus >= 0, "Why is MaybeGetSharedLockAction called when the lock is not free/reading?"); diff --git a/src/MICore/Debugger.cs b/src/MICore/Debugger.cs index b411cd086..e77fd8e76 100755 --- a/src/MICore/Debugger.cs +++ b/src/MICore/Debugger.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Threading; using System.Text; -using System.Diagnostics; using System.Threading.Tasks; using System.Globalization; using System.Linq; @@ -29,22 +28,22 @@ public class Debugger : ITransportCallback private const string Event_UnsupportedWindowsGdb = "VS/Diagnostics/Debugger/MIEngine/UnsupportedWindowsGdb"; private const string Property_GdbVersion = "VS.Diagnostics.Debugger.MIEngine.GdbVersion"; - public event EventHandler BreakModeEvent; - public event EventHandler RunModeEvent; - public event EventHandler ProcessExitEvent; - public event EventHandler DebuggerExitEvent; - public event EventHandler DebuggerAbortedEvent; - public event EventHandler OutputStringEvent; - public event EventHandler EvaluationEvent; - public event EventHandler ErrorEvent; - public event EventHandler ModuleLoadEvent; // occurs when stopped after a libraryLoadEvent - public event EventHandler LibraryLoadEvent; // a shared library was loaded - public event EventHandler BreakChangeEvent; // a breakpoint was changed - public event EventHandler BreakCreatedEvent; // a breakpoint was created - public event EventHandler ThreadCreatedEvent; - public event EventHandler ThreadExitedEvent; - public event EventHandler ThreadGroupExitedEvent; - public event EventHandler TelemetryEvent; + public event EventHandler? BreakModeEvent; + public event EventHandler? RunModeEvent; + public event EventHandler? ProcessExitEvent; + public event EventHandler? DebuggerExitEvent; + public event EventHandler? DebuggerAbortedEvent; + public event EventHandler? OutputStringEvent; + public event EventHandler? EvaluationEvent; + public event EventHandler? ErrorEvent; + public event EventHandler? ModuleLoadEvent; // occurs when stopped after a libraryLoadEvent + public event EventHandler? LibraryLoadEvent; // a shared library was loaded + public event EventHandler? BreakChangeEvent; // a breakpoint was changed + public event EventHandler? BreakCreatedEvent; // a breakpoint was created + public event EventHandler? ThreadCreatedEvent; + public event EventHandler? ThreadExitedEvent; + public event EventHandler? ThreadGroupExitedEvent; + public event EventHandler? TelemetryEvent; private int _exiting; public ProcessState ProcessState { get; private set; } private MIResults _miResults; @@ -80,10 +79,10 @@ public bool IsClosed public LaunchOptions LaunchOptions { get { return this._launchOptions; } } private Queue> _internalBreakActions = new Queue>(); - private TaskCompletionSource _internalBreakActionCompletionSource; - private TaskCompletionSource _consoleDebuggerInitializeCompletionSource = new TaskCompletionSource(); - private LinkedList _initializationLog = new LinkedList(); - private LinkedList _initialErrors = new LinkedList(); + private TaskCompletionSource? _internalBreakActionCompletionSource; + private TaskCompletionSource? _consoleDebuggerInitializeCompletionSource = new TaskCompletionSource(); + private LinkedList? _initializationLog = new LinkedList(); + private LinkedList? _initialErrors = new LinkedList(); private int _localDebuggerPid = -1; protected bool _connected; @@ -123,7 +122,7 @@ public StoppingEventArgs(Results results, BreakRequest asyncRequest = BreakReque { } } - private ITransport _transport; + private ITransport? _transport; private readonly CommandLock _commandLock = new CommandLock(); /// @@ -136,12 +135,12 @@ public StoppingEventArgs(Results results, BreakRequest asyncRequest = BreakReque /// Message used in any DebuggerDisposedExceptions once the debugger is closed. Setting /// this to non-null indicates that the debugger is now closed. It is only set once. /// - private string _closeMessage; + private string? _closeMessage; /// - /// [Optional] If a console command is being executed, list where we append the output + /// If a console command is being executed, list where we append the output /// - private StringBuilder _consoleCommandOutput; + private StringBuilder? _consoleCommandOutput; private bool _pendingInternalBreak; internal bool IsRequestingInternalAsyncBreak @@ -153,7 +152,7 @@ internal bool IsRequestingInternalAsyncBreak } private bool _waitingToStop; - private Timer _breakTimer = null; + private Timer? _breakTimer = null; private int _retryCount; private const int BREAK_DELTA = 3000; // millisec before trying to break again private const int BREAK_RETRY_MAX = 3; // maximum times to retry @@ -198,7 +197,7 @@ private bool IsUnixDebuggerRunning() return false; } - private void RetryBreak(object o) + private void RetryBreak(object? o) { lock (_internalBreakActions) { @@ -231,7 +230,7 @@ public Task AddInternalBreakAction(Func func) { if (_internalBreakActionCompletionSource == null) { - _internalBreakActionCompletionSource = new TaskCompletionSource(); + _internalBreakActionCompletionSource = new TaskCompletionSource(); } _internalBreakActions.Enqueue(func); @@ -369,9 +368,9 @@ protected void OnStateChanged(string mode, Results results) /// Returns true if the process is continued and we should not enter break state, returns false if the process is stopped and we should enter break state. private async Task DoInternalBreakActions(bool fIsAsyncBreak) { - TaskCompletionSource source = null; - Func item = null; - Exception firstException = null; + TaskCompletionSource? source = null; + Func? item = null; + Exception? firstException = null; while (true) { lock (_internalBreakActions) @@ -431,7 +430,7 @@ private async Task DoInternalBreakActions(bool fIsAsyncBreak) return processContinued; } - public void Init(ITransport transport, LaunchOptions options, HostWaitLoop waitLoop = null) + public void Init(ITransport transport, LaunchOptions options, HostWaitLoop? waitLoop = null) { _lastCommandId = 1000; _transport = transport; @@ -587,8 +586,8 @@ public Task CmdBreak(BreakRequest request) protected bool IsLocalLaunchUsingServer() { return (_launchOptions is LocalLaunchOptions localLaunchOptions && - (!String.IsNullOrWhiteSpace(localLaunchOptions.MIDebuggerServerAddress) || - !String.IsNullOrWhiteSpace(localLaunchOptions.DebugServer))); + (!IsNullOrWhiteSpace(localLaunchOptions.MIDebuggerServerAddress) || + !IsNullOrWhiteSpace(localLaunchOptions.DebugServer))); } internal bool IsLocalGdbTarget() @@ -941,7 +940,7 @@ void ITransportCallback.OnStdOutLine(string line) if (_initializationLog != null) { _initializationLog.AddLast(line); - if (string.IsNullOrEmpty(_gdbVersion)) + if (IsNullOrEmpty(_gdbVersion)) { TryInitializeGdbVersion(line); } @@ -973,7 +972,7 @@ void ITransportCallback.OnStdErrorLine(string line) } } - public void OnDebuggerProcessExit(/*OPTIONAL*/ string exitCode) + public void OnDebuggerProcessExit(string? exitCode) { // GDB has exited. Cleanup. Only let one thread perform the cleanup if (Interlocked.CompareExchange(ref _exiting, 1, 0) == 0) @@ -1012,7 +1011,7 @@ public void OnDebuggerProcessExit(/*OPTIONAL*/ string exitCode) string message; - if (string.IsNullOrEmpty(exitCode)) + if (IsNullOrEmpty(exitCode)) message = string.Format(CultureInfo.CurrentCulture, MICoreResources.Error_MIDebuggerExited_UnknownCode, this.MICommandFactory.Name); else message = string.Format(CultureInfo.CurrentCulture, MICoreResources.Error_MIDebuggerExited_WithCode, this.MICommandFactory.Name, exitCode); @@ -1050,7 +1049,7 @@ void TryInitializeGdbVersion(string line) } int majorVersion = 0; - if (!string.IsNullOrWhiteSpace(_gdbVersion)) + if (!IsNullOrWhiteSpace(_gdbVersion)) { int.TryParse(_gdbVersion.Split('.').FirstOrDefault(), out majorVersion); } @@ -1111,7 +1110,7 @@ protected virtual void ScheduleResultProcessing(Action func) // a Token is a sequence of decimal digits followed by something else // returns null if not a token, or not followed by something else - private string ParseToken(ref string cmd) + private string? ParseToken(ref string cmd) { if (char.IsDigit(cmd, 0)) { @@ -1157,7 +1156,7 @@ internal void OnComplete(Results results, MICommandFactory commandFactory) { if (_expectedResultClass != ResultClass.None && _expectedResultClass != results.ResultClass) { - string miError = null; + string miError; if (results.ResultClass == ResultClass.error) { // Fixes: https://github.com/microsoft/vscode-cpptools/issues/2492 @@ -1241,7 +1240,7 @@ public void ProcessStdOutLine(string line) } else { - string token = ParseToken(ref line); + string? token = ParseToken(ref line); char c = line[0]; string noprefix = line.Substring(1).Trim(); @@ -1251,7 +1250,7 @@ public void ProcessStdOutLine(string line) if (c == '^') { uint id = uint.Parse(token, CultureInfo.InvariantCulture); - WaitingOperationDescriptor waitingOperation = null; + WaitingOperationDescriptor? waitingOperation = null; lock (_waitingOperations) { if (_waitingOperations.TryGetValue(id, out waitingOperation)) @@ -1273,7 +1272,7 @@ public void ProcessStdOutLine(string line) uint id = uint.Parse(token, CultureInfo.InvariantCulture); lock (_waitingOperations) { - WaitingOperationDescriptor waitingOperation; + WaitingOperationDescriptor? waitingOperation; if (_waitingOperations.TryGetValue(id, out waitingOperation) && line == waitingOperation.Command) { @@ -1317,7 +1316,7 @@ private void OnUnknown(string cmd) Debug.WriteLine("DBG:Unknown command: {0}", cmd); } - private void OnResult(string cmd, string token) + private void OnResult(string cmd, string? token) { uint id = token != null ? uint.Parse(token, CultureInfo.InvariantCulture) : 0; Results results = _miResults.ParseCommandOutput(cmd); @@ -1494,7 +1493,7 @@ private void OnNotificationOutput(string cmd) public string GetLastSentCommandName() { string lastCommandText = _lastCommandText; - if (string.IsNullOrEmpty(lastCommandText)) + if (IsNullOrEmpty(lastCommandText)) { // We haven't sent any commands yet return string.Empty; @@ -1571,7 +1570,7 @@ private void HandleThreadGroupExited(Results results) string threadGroupId = results.TryFindString("id"); bool isThreadGroupEmpty = false; - if (!String.IsNullOrEmpty(threadGroupId)) + if (!IsNullOrEmpty(threadGroupId)) { lock (_debuggeePids) { @@ -1619,7 +1618,7 @@ private async void PostCommand(string cmd) private void SendToTransport(string cmd) { - ITransport transport = _transport; + ITransport? transport = _transport; if (transport is null) { Debug.Fail("Invalid: `SendToTransport` called before `Init`"); @@ -1640,7 +1639,7 @@ private void SendToTransport(string cmd) public static ulong ParseAddr(string addr, bool throwOnError = false) { ulong res = 0; - if (string.IsNullOrEmpty(addr)) + if (IsNullOrEmpty(addr)) { if (throwOnError) { @@ -1676,7 +1675,7 @@ public static ulong ParseAddr(string addr, bool throwOnError = false) public static uint ParseUint(string str, bool throwOnError = false) { uint value = 0; - if (string.IsNullOrEmpty(str)) + if (IsNullOrEmpty(str)) { if (throwOnError) { @@ -1719,9 +1718,9 @@ public void VerifyNotDebuggingCoreDump() public class DebuggerAbortedEventArgs { public readonly string Message; - public readonly string /*OPTIONAL*/ ExitCode; + public readonly string? ExitCode; - public DebuggerAbortedEventArgs(string message, string exitCode) + public DebuggerAbortedEventArgs(string message, string? exitCode) { Debug.Assert(message != null, "Invalid argument"); this.Message = message; diff --git a/src/MICore/DebuggerDisposedException.cs b/src/MICore/DebuggerDisposedException.cs index 19d5c3547..01b0aa71e 100644 --- a/src/MICore/DebuggerDisposedException.cs +++ b/src/MICore/DebuggerDisposedException.cs @@ -13,12 +13,12 @@ public class DebuggerDisposedException : ObjectDisposedException /// /// Command where the abort happened. /// - public string AbortedCommand { get; private set; } + public string? AbortedCommand { get; private set; } /// /// Constructor for the DebuggerDisposedException which takes message, innerException and aborted command. /// - public DebuggerDisposedException(string message, Exception innerException, string abortedCommand = null) : base(message, innerException) + public DebuggerDisposedException(string message, Exception? innerException, string? abortedCommand = null) : base(message, innerException) { AbortedCommand = abortedCommand; } @@ -26,7 +26,7 @@ public DebuggerDisposedException(string message, Exception innerException, strin /// /// Constructor for the DebuggerDisposedException which takes message and aborted command. /// - public DebuggerDisposedException(string message, string abortedCommand = null) : this(message, null, abortedCommand) + public DebuggerDisposedException(string message, string? abortedCommand = null) : this(message, null, abortedCommand) { } } diff --git a/src/MICore/ExceptionHelper.cs b/src/MICore/ExceptionHelper.cs index 7295142b8..65d24187c 100644 --- a/src/MICore/ExceptionHelper.cs +++ b/src/MICore/ExceptionHelper.cs @@ -4,7 +4,6 @@ using Microsoft.DebugEngineHost; using System; using System.Collections.Generic; -using System.Diagnostics; using System.Globalization; using System.Linq; using System.Text; @@ -21,7 +20,7 @@ public static class ExceptionHelper /// For logging messages /// If true, only corrupting exceptions are reported /// true - public static bool BeforeCatch(Exception currentException, Logger logger, bool reportOnlyCorrupting) + public static bool BeforeCatch(Exception currentException, Logger? logger, bool reportOnlyCorrupting) { if (reportOnlyCorrupting && !IsCorruptingException(currentException)) { @@ -33,7 +32,7 @@ public static bool BeforeCatch(Exception currentException, Logger logger, bool r HostTelemetry.ReportCurrentException(currentException, "Microsoft.MIDebugEngine"); logger?.WriteLine(LogLevel.Error, "EXCEPTION: ", currentException.GetType()); - logger?.WriteTextBlock(LogLevel.Error, "EXCEPTION: ", currentException.StackTrace); + logger?.WriteTextBlock(LogLevel.Error, "EXCEPTION: ", currentException.StackTrace ?? string.Empty); } catch { diff --git a/src/MICore/GlobalUsings.cs b/src/MICore/GlobalUsings.cs new file mode 100644 index 000000000..dc4ca9822 --- /dev/null +++ b/src/MICore/GlobalUsings.cs @@ -0,0 +1,5 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +global using static global::Microsoft.DebugEngineHost.NullableHelpers; +global using Process = global::System.Diagnostics.Process; diff --git a/src/MICore/IncludeExcludeList.cs b/src/MICore/IncludeExcludeList.cs index 78ade62d7..ceeec2c7f 100644 --- a/src/MICore/IncludeExcludeList.cs +++ b/src/MICore/IncludeExcludeList.cs @@ -12,8 +12,8 @@ namespace MICore.SymbolLocator public class IncludeExcludeList { static readonly char[] WildCardCharacters = new char[] { '*', '?' }; - Lazy> _wildcardEntries; - Lazy> _qualifiedEntries; + Lazy> _wildcardEntries = new Lazy>(() => new List()); + Lazy> _qualifiedEntries = new Lazy>(() => new HashSet(StringComparer.Ordinal)); public bool IsEmpty { @@ -31,7 +31,7 @@ public IncludeExcludeList() public void Add(string entry) { - if (string.IsNullOrEmpty(entry)) + if (IsNullOrEmpty(entry)) return; if (entry.IndexOfAny(WildCardCharacters) >= 0) @@ -92,12 +92,12 @@ public bool Contains(string moduleName) public void Clear() { - if (_wildcardEntries == null || _wildcardEntries.IsValueCreated) + if (_wildcardEntries.IsValueCreated) { _wildcardEntries = new Lazy>(() => new List()); } - if (_qualifiedEntries == null || _qualifiedEntries.IsValueCreated) + if (_qualifiedEntries.IsValueCreated) { _qualifiedEntries = new Lazy>(() => new HashSet(StringComparer.Ordinal)); } diff --git a/src/MICore/JsonLaunchOptions.cs b/src/MICore/JsonLaunchOptions.cs index f34021960..06b64d771 100644 --- a/src/MICore/JsonLaunchOptions.cs +++ b/src/MICore/JsonLaunchOptions.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Globalization; using System.Runtime.Serialization; using Newtonsoft.Json; @@ -18,32 +17,32 @@ public abstract partial class BaseOptions /// Semicolon separated list of directories to use to search for .so files. Example: "c:\dir1;c:\dir2". /// [JsonProperty("additionalSOLibSearchPath", DefaultValueHandling = DefaultValueHandling.Ignore)] - public string AdditionalSOLibSearchPath { get; set; } + public string? AdditionalSOLibSearchPath { get; set; } /// /// Full path to program executable. /// [JsonProperty("program")] - public string Program { get; set; } + public string? Program { get; set; } /// /// The type of the engine. Must be "cppdbg". /// [JsonProperty("type", DefaultValueHandling = DefaultValueHandling.Ignore)] - public string Type { get; set; } + public string? Type { get; set; } /// /// The architecture of the debuggee. This will automatically be detected unless this parameter is set. Allowed values are x86, arm, arm64, mips, x64, amd64, x86_64. /// [JsonProperty("targetArchitecture", DefaultValueHandling = DefaultValueHandling.Ignore)] - public string TargetArchitecture { get; set; } + public string? TargetArchitecture { get; set; } /// /// .natvis files to be used when debugging this process. This option is not compatible with GDB pretty printing. Please also see "showDisplayString" if using this setting. /// [JsonProperty("visualizerFile", DefaultValueHandling = DefaultValueHandling.Ignore)] [JsonConverter(typeof(VisualizerFileConverter))] - public List VisualizerFile { get; set; } + public List? VisualizerFile { get; set; } /// /// When a visualizerFile is specified, showDisplayString will enable the display string. Turning this option on can cause slower performance during debugging. @@ -55,25 +54,25 @@ public abstract partial class BaseOptions /// Indicates the console debugger that the MIDebugEngine will connect to. Allowed values are "gdb" "lldb". /// [JsonProperty(nameof(MIMode), DefaultValueHandling = DefaultValueHandling.Ignore)] - public string MIMode { get; set; } + public string? MIMode { get; set; } /// /// The path to the mi debugger (such as gdb). When unspecified, it will search path first for the debugger. /// [JsonProperty("miDebuggerPath", DefaultValueHandling = DefaultValueHandling.Ignore)] - public string MiDebuggerPath { get; set; } + public string? MiDebuggerPath { get; set; } /// /// Arguments for the mi debugger. /// [JsonProperty("miDebuggerArgs", DefaultValueHandling = DefaultValueHandling.Ignore)] - public string MiDebuggerArgs { get; set; } + public string? MiDebuggerArgs { get; set; } /// /// Network address of the MI Debugger Server to connect to (example: localhost:1234). /// [JsonProperty("miDebuggerServerAddress", DefaultValueHandling = DefaultValueHandling.Ignore)] - public string MiDebuggerServerAddress { get; set; } + public string? MiDebuggerServerAddress { get; set; } /// /// If true, use gdb extended-remote mode to connect to gdbserver. @@ -85,37 +84,37 @@ public abstract partial class BaseOptions /// Optional source file mappings passed to the debug engine. Example: '{ "/original/source/path":"/current/source/path" }' /// [JsonProperty("sourceFileMap", DefaultValueHandling = DefaultValueHandling.Ignore)] - public Dictionary SourceFileMap { get; protected set; } + public Dictionary? SourceFileMap { get; protected set; } /// /// When present, this tells the debugger to connect to a remote computer using another executable as a pipe that will relay standard input/output between VS Code and the MI-enabled debugger backend executable (such as gdb). /// [JsonProperty("pipeTransport", DefaultValueHandling = DefaultValueHandling.Ignore)] - public PipeTransport PipeTransport { get; set; } + public PipeTransport? PipeTransport { get; set; } /// /// Supports explcit control of symbol loading. The processing of Exceptions lists and symserver entries. /// [JsonProperty("symbolLoadInfo", DefaultValueHandling = DefaultValueHandling.Ignore)] - public SymbolLoadInfo SymbolLoadInfo { get; set; } + public SymbolLoadInfo? SymbolLoadInfo { get; set; } /// /// One or more GDB/LLDB commands to execute in order to setup the underlying debugger. Example: "setupCommands": [ { "text": "-enable-pretty-printing", "description": "Enable GDB pretty printing", "ignoreFailures": true }]. /// [JsonProperty("setupCommands", DefaultValueHandling = DefaultValueHandling.Ignore)] - public List SetupCommands { get; protected set; } + public List? SetupCommands { get; protected set; } /// /// One or more commands to execute in order to setup underlying debugger after debugger has been attached. i.e. flashing and resetting the board /// [JsonProperty("postRemoteConnectCommands", DefaultValueHandling = DefaultValueHandling.Ignore)] - public List PostRemoteConnectCommands { get; protected set; } + public List? PostRemoteConnectCommands { get; protected set; } /// /// Explicitly control whether hardware breakpoints are used. If an optional limit is provided, additionally restrict the number of hardware breakpoints for remote targets. Example: "hardwareBreakpoints": { "require": true, "limit": 5 }. /// [JsonProperty("hardwareBreakpoints", DefaultValueHandling = DefaultValueHandling.Ignore)] - public HardwareBreakpointInfo HardwareBreakpointInfo { get; set; } + public HardwareBreakpointInfo? HardwareBreakpointInfo { get; set; } /// /// Controls how breakpoints set externally (usually via raw GDB commands) are handled when hit. "throw" acts as if an exception was thrown by the application and "stop" only pauses the debug session. @@ -127,21 +126,23 @@ public abstract partial class BaseOptions /// Controls GDB's debuginfod behavior. /// [JsonProperty("debuginfod", DefaultValueHandling = DefaultValueHandling.Ignore)] - public DebuginfodSettings Debuginfod { get; set; } + public DebuginfodSettings? Debuginfod { get; set; } } internal class VisualizerFileConverter : JsonConverter { - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) { - List visualizerFile = new List(); + List? visualizerFile = null; if (reader.TokenType == JsonToken.StartArray) { visualizerFile = serializer.Deserialize>(reader); } else if (reader.TokenType == JsonToken.String) { - visualizerFile.Add(reader.Value.ToString()); + object? value = reader.Value; + Debug.Assert(value is not null, "Should be impossible -- `TokenType == JsonToken.String` means `Value` cannot be null"); + visualizerFile = new List { value.ToString() }; } else { @@ -155,7 +156,7 @@ public override bool CanConvert(Type objectType) throw new NotImplementedException(); } - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) { throw new NotImplementedException(); } @@ -180,20 +181,20 @@ public AttachOptions() public AttachOptions( string program, int processId, - string type = null, - string targetArchitecture = null, - List visualizerFile = null, + string? type = null, + string? targetArchitecture = null, + List? visualizerFile = null, bool? showDisplayString = null, - string additionalSOLibSearchPath = null, - string MIMode = null, - string miDebuggerPath = null, - string miDebuggerArgs = null, - string miDebuggerServerAddress = null, + string? additionalSOLibSearchPath = null, + string? MIMode = null, + string? miDebuggerPath = null, + string? miDebuggerArgs = null, + string? miDebuggerServerAddress = null, bool? useExtendedRemote = null, - HardwareBreakpointInfo hardwareBreakpointInfo = null, - Dictionary sourceFileMap = null, - PipeTransport pipeTransport = null, - SymbolLoadInfo symbolLoadInfo = null) + HardwareBreakpointInfo? hardwareBreakpointInfo = null, + Dictionary? sourceFileMap = null, + PipeTransport? pipeTransport = null, + SymbolLoadInfo? symbolLoadInfo = null) { this.Program = program; this.Type = type; @@ -221,10 +222,10 @@ public partial class Environment #region Public Properties for Serialization [JsonProperty("name", DefaultValueHandling = DefaultValueHandling.Ignore)] - public string Name { get; set; } + public string? Name { get; set; } [JsonProperty("value", DefaultValueHandling = DefaultValueHandling.Ignore)] - public string Value { get; set; } + public string? Value { get; set; } #endregion @@ -234,7 +235,7 @@ public Environment() { } - public Environment(string name = null, string value = null) + public Environment(string? name = null, string? value = null) { this.Name = name; this.Value = value; @@ -259,7 +260,7 @@ public partial class SymbolLoadInfo /// Otherwise only load symbols for libs that match. /// [JsonProperty("exceptionList", DefaultValueHandling = DefaultValueHandling.Ignore)] - public string ExceptionList { get; set; } + public string? ExceptionList { get; set; } #endregion @@ -269,7 +270,7 @@ public SymbolLoadInfo() { } - public SymbolLoadInfo(bool? loadAll = null, string exceptionList = null) + public SymbolLoadInfo(bool? loadAll = null, string? exceptionList = null) { this.LoadAll = loadAll; this.ExceptionList = exceptionList; @@ -344,19 +345,19 @@ public partial class LaunchOptions : BaseOptions /// Command line arguments passed to the program. /// [JsonProperty("args", DefaultValueHandling = DefaultValueHandling.Ignore)] - public List Args { get; private set; } + public List? Args { get; private set; } /// /// The working directory of the target /// [JsonProperty("cwd", DefaultValueHandling = DefaultValueHandling.Ignore)] - public string Cwd { get; set; } + public string? Cwd { get; set; } /// /// If provided, this replaces the default commands used to launch a target with some other commands. For example, this can be "-target-attach" in order to attach to a target process. An empty command list replaces the launch commands with nothing, which can be useful if the debugger is being provided launch options as command line options. Example: "customLaunchSetupCommands": [ { "text": "target-run", "description": "run target", "ignoreFailures": false }]. /// [JsonProperty("customLaunchSetupCommands", DefaultValueHandling = DefaultValueHandling.Ignore)] - public List CustomLaunchSetupCommands { get; private set; } + public List? CustomLaunchSetupCommands { get; private set; } /// /// The command to execute after the debugger is fully setup in order to cause the target process to run. Allowed values are "exec-run", "exec-continue", "None". The default value is "exec-run". @@ -369,7 +370,7 @@ public partial class LaunchOptions : BaseOptions /// Environment variables to add to the environment for the program. Example: [ { "name": "squid", "value": "clam" } ]. /// [JsonProperty("environment", DefaultValueHandling = DefaultValueHandling.Ignore)] - public List Environment { get; private set; } + public List? Environment { get; private set; } /// /// Optional parameter. If true, the debugger should stop at the entrypoint of the target. If processId is passed, has no effect. @@ -381,19 +382,19 @@ public partial class LaunchOptions : BaseOptions /// Optional full path to debug server to launch. Defaults to null. /// [JsonProperty("debugServerPath", DefaultValueHandling = DefaultValueHandling.Ignore)] - public string DebugServerPath { get; set; } + public string? DebugServerPath { get; set; } /// /// Optional debug server args. Defaults to null. /// [JsonProperty("debugServerArgs", DefaultValueHandling = DefaultValueHandling.Ignore)] - public string DebugServerArgs { get; set; } + public string? DebugServerArgs { get; set; } /// /// Optional server-started pattern to look for in the debug server output. Defaults to null. /// [JsonProperty("serverStarted", DefaultValueHandling = DefaultValueHandling.Ignore)] - public string ServerStarted { get; set; } + public string? ServerStarted { get; set; } /// /// Optional time, in milliseconds, for the debugger to wait for the debugServer to start up. Default is 10000. @@ -417,7 +418,7 @@ public partial class LaunchOptions : BaseOptions /// Optional full path to a core dump file for the specified program. Defaults to null. /// [JsonProperty("coreDumpPath", DefaultValueHandling = DefaultValueHandling.Ignore)] - public string CoreDumpPath { get; set; } + public string? CoreDumpPath { get; set; } /// /// If true, a console is launched for the debuggee. If false, no console is launched. Note this option is ignored in some cases for technical reasons. @@ -453,35 +454,35 @@ public LaunchOptions() public LaunchOptions( string program, - List args = null, - string type = null, - string targetArchitecture = null, - string cwd = null, - List setupCommands = null, - List postRemoteConnectCommands = null, - List customLaunchSetupCommands = null, + List? args = null, + string? type = null, + string? targetArchitecture = null, + string? cwd = null, + List? setupCommands = null, + List? postRemoteConnectCommands = null, + List? customLaunchSetupCommands = null, LaunchCompleteCommand? launchCompleteCommand = null, - List visualizerFile = null, + List? visualizerFile = null, bool? showDisplayString = null, - List environment = null, - string additionalSOLibSearchPath = null, - string MIMode = null, - string miDebuggerPath = null, - string miDebuggerArgs = null, - string miDebuggerServerAddress = null, + List? environment = null, + string? additionalSOLibSearchPath = null, + string? MIMode = null, + string? miDebuggerPath = null, + string? miDebuggerArgs = null, + string? miDebuggerServerAddress = null, bool? useExtendedRemote = null, bool? stopAtEntry = null, - string debugServerPath = null, - string debugServerArgs = null, - string serverStarted = null, + string? debugServerPath = null, + string? debugServerArgs = null, + string? serverStarted = null, bool? filterStdout = null, bool? filterStderr = null, int? serverLaunchTimeout = null, - string coreDumpPath = null, + string? coreDumpPath = null, bool? externalConsole = null, - HardwareBreakpointInfo hardwareBreakpointInfo = null, - Dictionary sourceFileMap = null, - PipeTransport pipeTransport = null, + HardwareBreakpointInfo? hardwareBreakpointInfo = null, + Dictionary? sourceFileMap = null, + PipeTransport? pipeTransport = null, bool? stopAtConnect = null) { this.Program = program; @@ -525,11 +526,11 @@ public LaunchOptions( /// private class LaunchCompleteCommandConverter : JsonConverter { - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) { if (objectType == typeof(LaunchCompleteCommand?) && reader.TokenType == JsonToken.String) { - String value = reader.Value.ToString(); + string value = reader.Value!.ToString(); if (value.Equals("exec-continue", StringComparison.Ordinal)) { return MICore.LaunchCompleteCommand.ExecContinue; @@ -546,8 +547,7 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist throw new InvalidLaunchOptionsException(String.Format(CultureInfo.CurrentCulture, MICoreResources.Error_InvalidLaunchCompleteCommandValue, reader.Value)); } - Debug.Fail(String.Format(CultureInfo.CurrentCulture, "Unexpected objectType '{0}' passed for launchCompleteCommand serialization.", objectType.ToString())); - return null; + throw new InvalidLaunchOptionsException(String.Format(CultureInfo.CurrentCulture, "Unexpected objectType '{0}' passed for launchCompleteCommand serialization.", objectType.ToString())); } public override bool CanConvert(Type objectType) @@ -555,7 +555,7 @@ public override bool CanConvert(Type objectType) return objectType == typeof(LaunchCompleteCommand?); } - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) { throw new NotImplementedException(); } @@ -571,19 +571,19 @@ public partial class PipeTransport : PipeTransportOptions /// When present, this tells the debugger override the PipeTransport's fields if the client's current platform is Windows and the field is defined in this configuration. /// [JsonProperty("windows", DefaultValueHandling = DefaultValueHandling.Ignore)] - public PipeTransportOptions Windows { get; private set; } + public PipeTransportOptions? Windows { get; private set; } /// /// When present, this tells the debugger override the PipeTransport's fields if the client's current platform is OSX and the field is defined in this configuration. /// [JsonProperty("osx", DefaultValueHandling = DefaultValueHandling.Ignore)] - public PipeTransportOptions OSX { get; private set; } + public PipeTransportOptions? OSX { get; private set; } /// /// When present, this tells the debugger override the PipeTransport's fields if the client's current platform is Linux and the field is defined in this configuration. /// [JsonProperty("linux", DefaultValueHandling = DefaultValueHandling.Ignore)] - public PipeTransportOptions Linux { get; private set; } + public PipeTransportOptions? Linux { get; private set; } #endregion @@ -594,7 +594,7 @@ public PipeTransport() } - public PipeTransport(PipeTransportOptions windows = null, PipeTransportOptions osx = null, PipeTransportOptions linux = null) + public PipeTransport(PipeTransportOptions? windows = null, PipeTransportOptions? osx = null, PipeTransportOptions? linux = null) { this.Windows = windows; this.OSX = osx; @@ -613,37 +613,37 @@ public partial class PipeTransportOptions /// The fully qualified path to the working directory for the pipe program. /// [JsonProperty("pipeCwd", DefaultValueHandling = DefaultValueHandling.Ignore)] - public string PipeCwd { get; set; } + public string? PipeCwd { get; set; } /// /// The fully qualified pipe command to execute. /// [JsonProperty("pipeProgram", DefaultValueHandling = DefaultValueHandling.Ignore)] - public string PipeProgram { get; set; } + public string? PipeProgram { get; set; } /// /// Command line arguments passed to the pipe program to configure the connection. /// [JsonProperty("pipeArgs", DefaultValueHandling = DefaultValueHandling.Ignore)] - public List PipeArgs { get; private set; } + public List? PipeArgs { get; private set; } /// /// Command line arguments passed to the pipe program to execute a remote command. /// [JsonProperty("pipeCmd", DefaultValueHandling = DefaultValueHandling.Ignore)] - public List PipeCmd { get; private set; } + public List? PipeCmd { get; private set; } /// /// The full path to the debugger on the target machine, for example /usr/bin/gdb. /// [JsonProperty("debuggerPath", DefaultValueHandling = DefaultValueHandling.Ignore)] - public string DebuggerPath { get; set; } + public string? DebuggerPath { get; set; } /// /// Environment variables passed to the pipe program. /// [JsonProperty("pipeEnv", DefaultValueHandling = DefaultValueHandling.Ignore)] - public Dictionary PipeEnv { get; private set; } + public Dictionary? PipeEnv { get; private set; } /// /// Should arguments that contain characters that need to be quoted (example: spaces) be quoted? Defaults to 'true'. If set to false, the debugger command will no longer be automatically quoted. @@ -658,10 +658,10 @@ public partial class PipeTransportOptions public PipeTransportOptions() { this.PipeArgs = new List(); - this.PipeEnv = new Dictionary(); + this.PipeEnv = new Dictionary(); } - public PipeTransportOptions(string pipeCwd = null, string pipeProgram = null, List pipeArgs = null, string debuggerPath = null, Dictionary pipeEnv = null, bool? quoteArgs = null) + public PipeTransportOptions(string? pipeCwd = null, string? pipeProgram = null, List? pipeArgs = null, string? debuggerPath = null, Dictionary? pipeEnv = null, bool? quoteArgs = null) { this.PipeCwd = pipeCwd; this.PipeProgram = pipeProgram; @@ -682,13 +682,13 @@ public partial class SetupCommand /// The debugger command to execute. /// [JsonProperty("text", DefaultValueHandling = DefaultValueHandling.Ignore)] - public string Text { get; set; } + public string? Text { get; set; } /// /// Optional description for the command. /// [JsonProperty("description", DefaultValueHandling = DefaultValueHandling.Ignore)] - public string Description { get; set; } + public string? Description { get; set; } /// /// If true, failures from the command should be ignored. Default value is false. @@ -704,7 +704,7 @@ public SetupCommand() { } - public SetupCommand(string text = null, string description = null, bool? ignoreFailures = null) + public SetupCommand(string? text = null, string? description = null, bool? ignoreFailures = null) { this.Text = text; this.Description = description; @@ -722,7 +722,7 @@ public partial class SourceFileMapOptions /// The editor's path. /// [JsonProperty("editorPath", DefaultValueHandling = DefaultValueHandling.Ignore)] - public string EditorPath { get; set; } + public string? EditorPath { get; set; } /// /// Use this source mapping for breakpoint binding? Default is true. @@ -738,7 +738,7 @@ public SourceFileMapOptions() { } - public SourceFileMapOptions(string editorPath = null, bool? useForBreakpoints = null) + public SourceFileMapOptions(string? editorPath = null, bool? useForBreakpoints = null) { this.EditorPath = editorPath; this.UseForBreakpoints = useForBreakpoints; @@ -751,16 +751,16 @@ public static class LaunchOptionHelpers { public static BaseOptions GetLaunchOrAttachOptions(JObject parsedJObject) { - BaseOptions baseOptions = null; - string requestType = parsedJObject["request"]?.Value(); - if (String.IsNullOrWhiteSpace(requestType)) + BaseOptions? baseOptions = null; + string? requestType = parsedJObject["request"]?.Value(); + if (IsNullOrWhiteSpace(requestType)) { // If request isn't specified, see if we can determine what it is - if (!String.IsNullOrWhiteSpace(parsedJObject["processId"]?.Value())) + if (!IsNullOrWhiteSpace(parsedJObject["processId"]?.Value())) { requestType = "attach"; } - else if (!String.IsNullOrWhiteSpace(parsedJObject["program"]?.Value())) + else if (!IsNullOrWhiteSpace(parsedJObject["program"]?.Value())) { requestType = "launch"; } @@ -784,6 +784,11 @@ public static BaseOptions GetLaunchOrAttachOptions(JObject parsedJObject) throw new InvalidLaunchOptionsException(String.Format(CultureInfo.CurrentCulture, MICoreResources.Error_BadRequiredAttribute, "request")); } + if (baseOptions is null) + { + throw new InvalidLaunchOptionsException(String.Format(CultureInfo.CurrentCulture, MICoreResources.Error_BadRequiredAttribute, "request")); + } + return baseOptions; } } diff --git a/src/MICore/LaunchCommand.cs b/src/MICore/LaunchCommand.cs index 64941087a..af2db9189 100644 --- a/src/MICore/LaunchCommand.cs +++ b/src/MICore/LaunchCommand.cs @@ -21,11 +21,11 @@ public class LaunchCommand public readonly string Description; public readonly bool IgnoreFailures; public readonly bool IsMICommand; - public /*OPTIONAL*/ Action FailureHandler { get; private set; } - public /*OPTIONAL*/ Func SuccessHandler { get; private set; } - public /*Optional*/ Func SuccessResultsHandler { get; private set; } + public Action? FailureHandler { get; private set; } + public Func? SuccessHandler { get; private set; } + public Func? SuccessResultsHandler { get; private set; } - public LaunchCommand(string commandText, string description = null, bool ignoreFailures = false, Action failureHandler = null, Func successHandler = null, Func successResultsHandler = null) + public LaunchCommand(string commandText, string? description = null, bool ignoreFailures = false, Action? failureHandler = null, Func? successHandler = null, Func? successResultsHandler = null) { if (commandText == null) throw new ArgumentNullException(nameof(commandText)); @@ -34,9 +34,7 @@ public LaunchCommand(string commandText, string description = null, bool ignoreF throw new ArgumentOutOfRangeException(nameof(commandText)); this.IsMICommand = commandText[0] == '-'; this.CommandText = commandText; - this.Description = description; - if (string.IsNullOrWhiteSpace(description)) - this.Description = this.CommandText; + this.Description = IsNullOrWhiteSpace(description) ? this.CommandText : description; this.IgnoreFailures = ignoreFailures; this.FailureHandler = failureHandler; @@ -46,7 +44,10 @@ public LaunchCommand(string commandText, string description = null, bool ignoreF public static ReadOnlyCollection CreateCollection(List source) { - IList commands = source?.Select(x => new LaunchCommand(x.Text, x.Description, x.IgnoreFailures.GetValueOrDefault(false))).ToList(); + IList? commands = source + ?.Where(x => !string.IsNullOrWhiteSpace(x.Text)) + ?.Select(x => new LaunchCommand(x.Text!, x.Description, x.IgnoreFailures.GetValueOrDefault(false))) + ?.ToList(); if(commands == null) { commands = new List(0); @@ -57,7 +58,7 @@ public static ReadOnlyCollection CreateCollection(List CreateCollection(Xml.LaunchOptions.Command[] source) { - LaunchCommand[] commandArray = source?.Select(x => new LaunchCommand(x.Value, x.Description, x.IgnoreFailures)).ToArray(); + LaunchCommand[]? commandArray = source?.Select(x => new LaunchCommand(x.Value, x.Description, x.IgnoreFailures)).ToArray(); if (commandArray == null) { commandArray = new LaunchCommand[0]; diff --git a/src/MICore/LaunchOptions.cs b/src/MICore/LaunchOptions.cs index 2a8594fe7..5f39eca51 100644 --- a/src/MICore/LaunchOptions.cs +++ b/src/MICore/LaunchOptions.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; @@ -21,6 +20,7 @@ using Newtonsoft.Json.Linq; using System.Text; using MICore.Json.LaunchOptions; +using System.Diagnostics.CodeAnalysis; namespace MICore { @@ -74,18 +74,18 @@ public sealed class PipeLaunchOptions : LaunchOptions /// Command to be invoked on the pipe program /// Current working directory of pipe program. If empty directory of the pipePath is set as the cwd. /// Environment variables set before invoking the pipe program - public PipeLaunchOptions(string pipePath, string pipeArguments, string pipeCommandArguments, string pipeCwd, MICore.Xml.LaunchOptions.EnvironmentEntry[] pipeEnvironment) + public PipeLaunchOptions(string pipePath, string? pipeArguments, string? pipeCommandArguments, string? pipeCwd, MICore.Xml.LaunchOptions.EnvironmentEntry[]? pipeEnvironment) : this(pipePath, pipeArguments, pipeCommandArguments, pipeCwd, (pipeEnvironment != null) ? pipeEnvironment.Select(e => new EnvironmentEntry(e)).ToArray() : new EnvironmentEntry[] { }) { } - public PipeLaunchOptions(string pipePath, string pipeArguments, string pipeCommandArguments, string pipeCwd, IList pipeEnvironment) + public PipeLaunchOptions(string pipePath, string? pipeArguments, string? pipeCommandArguments, string? pipeCwd, IList? pipeEnvironment) { - if (string.IsNullOrEmpty(pipePath)) + if (IsNullOrEmpty(pipePath)) throw new ArgumentNullException(nameof(pipePath)); this.PipePath = pipePath; this.PipeArguments = pipeArguments; - this.PipeCommandArguments = pipeCommandArguments; + this.PipeCommandArguments = pipeCommandArguments ?? string.Empty; this.PipeCwd = pipeCwd; this.PipeEnvironment = new ReadOnlyCollection(pipeEnvironment ?? new List(0)); @@ -94,25 +94,29 @@ public PipeLaunchOptions(string pipePath, string pipeArguments, string pipeComma private static string gdbPathDefault = @"/usr/bin/gdb"; static internal PipeLaunchOptions CreateFromJson(JObject parsedOptions) { - Debug.Assert(parsedOptions["pipeTransport"] != null && parsedOptions["pipeTransport"].HasValues, "PipeTransport should exist and have values."); + JToken? pipeTransportToken = parsedOptions["pipeTransport"]; + if (pipeTransportToken is null || !pipeTransportToken.HasValues) + { + throw new InvalidLaunchOptionsException(MICoreResources.Error_UnknownLaunchOptions); + } - Json.LaunchOptions.PipeTransport pipeTransport = parsedOptions["pipeTransport"].ToObject(); + Json.LaunchOptions.PipeTransport pipeTransport = pipeTransportToken.ToObject() ?? throw new InvalidLaunchOptionsException(MICoreResources.Error_UnknownLaunchOptions); // PipeProgram must be specified - if (String.IsNullOrWhiteSpace(pipeTransport.PipeProgram)) + string? pipeProgram = pipeTransport.PipeProgram; + if (IsNullOrWhiteSpace(pipeProgram)) { throw new InvalidLaunchOptionsException(String.Format(CultureInfo.CurrentCulture, MICoreResources.Error_EmptyPipePath)); } - string pipeCwd = pipeTransport.PipeCwd; - string pipeProgram = pipeTransport.PipeProgram; - List pipeArgs = pipeTransport.PipeArgs; - List pipeCmd = pipeTransport.PipeCmd; - string debuggerPath = pipeTransport.DebuggerPath; + string? pipeCwd = pipeTransport.PipeCwd; + List? pipeArgs = pipeTransport.PipeArgs; + List? pipeCmd = pipeTransport.PipeCmd; + string? debuggerPath = pipeTransport.DebuggerPath; bool quoteArgs = pipeTransport.QuoteArgs.GetValueOrDefault(true); - Dictionary pipeEnv = pipeTransport.PipeEnv; + Dictionary? pipeEnv = pipeTransport.PipeEnv; - Json.LaunchOptions.PipeTransportOptions platformSpecificTransportOptions = null; + Json.LaunchOptions.PipeTransportOptions? platformSpecificTransportOptions = null; if (PlatformUtilities.IsOSX() && pipeTransport.OSX != null) { platformSpecificTransportOptions = pipeTransport.OSX; @@ -161,10 +165,10 @@ static internal PipeLaunchOptions CreateFromJson(JObject parsedOptions) return pipeOptions; } - private static string EnsurePipeArguments(List pipeArgs, string debuggerPath, string debuggerPathDefault, bool quoteArgs) + private static string EnsurePipeArguments(List? pipeArgs, string? debuggerPath, string debuggerPathDefault, bool quoteArgs) { // Debugger path. Assume /usr/bin/gdb unless specified - string dbgPath = String.IsNullOrWhiteSpace(debuggerPath) ? debuggerPathDefault : debuggerPath; + string dbgPath = IsNullOrWhiteSpace(debuggerPath) ? debuggerPathDefault : debuggerPath; // debugger command: /usr/bin/gdb --interpreter=mi string dbgCmdArguments = String.Format(CultureInfo.InvariantCulture, "{0} {1}", dbgPath, "--interpreter=mi"); @@ -193,11 +197,11 @@ internal static string ReplaceDebuggerCommandToken(string cmdArgs, string comman } } - private static IList GetEnvironmentEntries(IDictionary env) + private static IList GetEnvironmentEntries(IDictionary? env) { List entries = new List(); - if (env != null && env.Keys.Any()) + if (env is not null) { foreach (var key in env.Keys) { @@ -225,7 +229,7 @@ static internal PipeLaunchOptions CreateFromXml(Xml.LaunchOptions.PipeLaunchOpti /// [Optional] Arguments to pass to the pipe executable. /// /// - public string PipeArguments { get; private set; } + public string? PipeArguments { get; private set; } /// /// [Optional] Arguments to pass to the PipePath program that include a format specifier ('{0}') for a custom command. @@ -235,7 +239,7 @@ static internal PipeLaunchOptions CreateFromXml(Xml.LaunchOptions.PipeLaunchOpti /// /// [Optional] Current working directory when the pipe program is invoked. /// - public string PipeCwd { get; private set; } + public string? PipeCwd { get; private set; } /// /// [Optional] Enviroment variables for the pipe program. @@ -247,7 +251,7 @@ public sealed class TcpLaunchOptions : LaunchOptions { public TcpLaunchOptions(string hostname, int port, bool secure) { - if (string.IsNullOrEmpty(hostname)) + if (IsNullOrEmpty(hostname)) { throw new ArgumentException(null, nameof(hostname)); } @@ -282,9 +286,9 @@ static internal TcpLaunchOptions CreateFromXml(Xml.LaunchOptions.TcpLaunchOption /// X509Chain object for the chain of certificate authorities associated with the remote certificate. /// One or more errors associated with the remote certificate. /// true if the specified certificate is accepted - public delegate bool MIServerCertificateValidationCallback(object sender, object/*X509Certificate*/ certificate, object/*X509Chain*/ chain, SslPolicyErrors sslPolicyErrors); + public delegate bool MIServerCertificateValidationCallback(object sender, object/*X509Certificate*/? certificate, object/*X509Chain*/? chain, SslPolicyErrors sslPolicyErrors); - public MIServerCertificateValidationCallback ServerCertificateValidationCallback { get; set; } + public MIServerCertificateValidationCallback? ServerCertificateValidationCallback { get; set; } } public sealed class EnvironmentEntry @@ -297,11 +301,11 @@ public EnvironmentEntry(Xml.LaunchOptions.EnvironmentEntry xmlEntry) public EnvironmentEntry(Json.LaunchOptions.Environment jsonEntry) { - this.Name = jsonEntry.Name; + this.Name = LaunchOptions.RequireAttribute(jsonEntry.Name, "environment.name"); this.Value = jsonEntry.Value; } - public EnvironmentEntry(string name, string value) + public EnvironmentEntry(string name, string? value) { this.Name = name; this.Value = value; @@ -313,9 +317,9 @@ public EnvironmentEntry(string name, string value) public string Name { get; private set; } /// - /// [Required] Value of the environment variable + /// Value of the environment variable, or null to delete it /// - public string Value { get; private set; } + public string? Value { get; private set; } } public sealed class SourceMapEntry @@ -331,7 +335,7 @@ public SourceMapEntry(Xml.LaunchOptions.SourceMapEntry xmlEntry) this.UseForBreakpoints = xmlEntry.UseForBreakpoints; } - private string _editorPath; + private string _editorPath = string.Empty; public string EditorPath { get @@ -340,7 +344,7 @@ public string EditorPath } set { - if (string.IsNullOrEmpty(value)) + if (IsNullOrEmpty(value)) { throw new ArgumentNullException("EditorPath"); } @@ -349,7 +353,7 @@ public string EditorPath } - private string _compileTimePath; + private string _compileTimePath = string.Empty; public string CompileTimePath { get @@ -363,9 +367,9 @@ public string CompileTimePath } public bool UseForBreakpoints { get; set; } - public static ReadOnlyCollection CreateCollection(Xml.LaunchOptions.SourceMapEntry[] source) + public static ReadOnlyCollection CreateCollection(Xml.LaunchOptions.SourceMapEntry[]? source) { - SourceMapEntry[] pathArray = source?.Select(x => new SourceMapEntry(x)).ToArray(); + SourceMapEntry[]? pathArray = source?.Select(x => new SourceMapEntry(x)).ToArray(); if (pathArray == null) { @@ -375,14 +379,19 @@ public static ReadOnlyCollection CreateCollection(Xml.LaunchOpti return new ReadOnlyCollection(pathArray); } - public static ReadOnlyCollection CreateCollection(Dictionary source) + public static ReadOnlyCollection CreateCollection(Dictionary? source) { - var sourceMaps = new List(source.Keys.Count); + int count = source?.Keys.Count ?? 0; + if (count == 0) + { + return new ReadOnlyCollection(Array.Empty()); + } - foreach (var item in source) + var sourceMaps = new List(); + foreach (var item in source!) { string compileTimePath = item.Key; - string editorPath = null; + string? editorPath = null; bool useForBreakpoints = true; if (item.Value is string value) @@ -394,7 +403,7 @@ public static ReadOnlyCollection CreateCollection(Dictionary(); + jObject.ToObject() ?? throw new InvalidLaunchOptionsException(String.Format(CultureInfo.CurrentCulture, MICoreResources.Error_SourceFileMapFormat, compileTimePath)); editorPath = sourceMapItem.EditorPath; useForBreakpoints = sourceMapItem.UseForBreakpoints.GetValueOrDefault(true); @@ -409,7 +418,7 @@ public static ReadOnlyCollection CreateCollection(Dictionary /// [Required] Arguments for the MI Debugger. /// - public string MIDebuggerArgs { get; private set; } + public string? MIDebuggerArgs { get; private set; } /// /// [Optional] Server address that MI Debugger server is listening to /// - public string MIDebuggerServerAddress { get; private set; } + public string? MIDebuggerServerAddress { get; private set; } /// /// [Optional] If true, use gdb extended-remote mode to connect to gdbserver. @@ -701,17 +714,17 @@ private static string EnsureDebuggerPath(string miDebuggerPath, string debuggerB /// /// [Optional] MI Debugger Server exe, if non-null then the MIEngine will start the debug server before starting the debugger /// - public string DebugServer { get; private set; } + public string? DebugServer { get; private set; } /// /// [Optional] Args for MI Debugger Server exe /// - public string DebugServerArgs { get; private set; } + public string? DebugServerArgs { get; private set; } /// /// [Optional] Server started pattern (in Regex format) /// - public string ServerStarted { get; private set; } + public string? ServerStarted { get; private set; } /// /// [Optional] Log strings written to stderr and examine for server started pattern @@ -781,15 +794,15 @@ public sealed class UnixShellPortLaunchOptions : LaunchOptions public string StartRemoteDebuggerCommand { get; private set; } public Microsoft.VisualStudio.Debugger.Interop.UnixPortSupplier.IDebugUnixShellPort UnixPort { get; private set; } - public UnixShellPortLaunchOptions(string startRemoteDebuggerCommand, + public UnixShellPortLaunchOptions(string? startRemoteDebuggerCommand, Microsoft.VisualStudio.Debugger.Interop.UnixPortSupplier.IDebugUnixShellPort unixPort, MIMode miMode, - BaseLaunchOptions baseLaunchOptions) + BaseLaunchOptions? baseLaunchOptions) { this.UnixPort = unixPort; this.DebuggerMIMode = miMode; - if (string.IsNullOrEmpty(startRemoteDebuggerCommand)) + if (IsNullOrEmpty(startRemoteDebuggerCommand)) { switch (miMode) { @@ -814,7 +827,7 @@ public UnixShellPortLaunchOptions(string startRemoteDebuggerCommand, } string prefix = GetDebuginfodEnvironmentPrefix(); - if (!string.IsNullOrEmpty(prefix)) + if (!IsNullOrEmpty(prefix)) { this.StartRemoteDebuggerCommand = prefix + this.StartRemoteDebuggerCommand; } @@ -827,14 +840,14 @@ public UnixShellPortLaunchOptions(string startRemoteDebuggerCommand, public abstract class LaunchOptions { private const string XmlNamespace = "http://schemas.microsoft.com/vstudio/MDDDebuggerOptions/2014"; - private static Lazy s_serializationAssembly = new Lazy(LoadSerializationAssembly, LazyThreadSafetyMode.ExecutionAndPublication); + private static Lazy s_serializationAssembly = new Lazy(LoadSerializationAssembly, LazyThreadSafetyMode.ExecutionAndPublication); private bool _initializationComplete; private MIMode _miMode; /// /// [Optional] Launcher used to start the application on the device /// - public IPlatformAppLauncher DeviceAppLauncher { get; private set; } + public IPlatformAppLauncher? DeviceAppLauncher { get; private set; } public MIMode DebuggerMIMode { @@ -848,11 +861,11 @@ public MIMode DebuggerMIMode public bool NoDebug { get; private set; } = false; - private Xml.LaunchOptions.BaseLaunchOptions _baseOptions; + private Xml.LaunchOptions.BaseLaunchOptions? _baseOptions; /// /// Hold on to options in serializable form to support child process debugging /// - public Xml.LaunchOptions.BaseLaunchOptions BaseOptions + public Xml.LaunchOptions.BaseLaunchOptions? BaseOptions { get { return _baseOptions; } protected set @@ -865,18 +878,18 @@ protected set } } - private string _exePath; + private string? _exePath; /// - /// [Required] Path to the executable file. This could be a path on the remote machine (for Pipe transport) + /// Path to the executable file. This could be a path on the remote machine (for Pipe transport) /// or the local machine (Local transport). /// - public virtual string ExePath + public virtual string? ExePath { get { return _exePath; } set { - if (string.IsNullOrWhiteSpace(value)) + if (IsNullOrWhiteSpace(value)) throw new ArgumentOutOfRangeException("ExePath"); VerifyCanModifyProperty(nameof(ExePath)); @@ -884,13 +897,13 @@ public virtual string ExePath } } - private string _exeArguments; + private string? _exeArguments; /// - /// [Optional] Additional arguments to specify when launching the process + /// Additional arguments to specify when launching the process /// public string ExeArguments { - get { return _exeArguments; } + get => _exeArguments ?? string.Empty; set { VerifyCanModifyProperty(nameof(ExeArguments)); @@ -913,11 +926,11 @@ protected set } } - private string _coreDumpPath; + private string? _coreDumpPath; /// /// [Optional] Path to a core dump file for the specified executable. /// - public string CoreDumpPath + public string? CoreDumpPath { get { @@ -931,16 +944,18 @@ protected set _coreDumpPath = value; } } + + [MemberNotNullWhen(true, nameof(CoreDumpPath))] public bool IsCoreDump { - get { return !String.IsNullOrEmpty(this.CoreDumpPath); } + get { return !IsNullOrEmpty(this.CoreDumpPath); } } - private string _workingDirectory; + private string? _workingDirectory; /// /// [Optional] Working directory to use for the MI Debugger when launching the process /// - public string WorkingDirectory + public string? WorkingDirectory { get { return _workingDirectory; } set @@ -950,11 +965,11 @@ public string WorkingDirectory } } - private string _absolutePrefixSoLibSearchPath; + private string? _absolutePrefixSoLibSearchPath; /// /// [Optional] Absolute prefix for directories to search for shared library symbols /// - public string AbsolutePrefixSOLibSearchPath + public string? AbsolutePrefixSOLibSearchPath { get { return _absolutePrefixSoLibSearchPath; } set @@ -964,11 +979,11 @@ public string AbsolutePrefixSOLibSearchPath } } - private string _additionalSOLibSearchPath; + private string? _additionalSOLibSearchPath; /// /// [Optional] Additional directories to search for shared library symbols /// - public string AdditionalSOLibSearchPath + public string? AdditionalSOLibSearchPath { get { return _additionalSOLibSearchPath; } set @@ -1071,14 +1086,18 @@ public bool UseUnixSymbolPaths } } - private ReadOnlyCollection _setupCommands; + private ReadOnlyCollection? _setupCommands; /// /// [Required] Additional commands used to setup debugging. May be an empty collection /// public ReadOnlyCollection SetupCommands { - get { return _setupCommands; } + get + { + _setupCommands ??= new ReadOnlyCollection(Array.Empty()); + return _setupCommands; + } set { if (value == null) @@ -1089,14 +1108,18 @@ public ReadOnlyCollection SetupCommands } } - private ReadOnlyCollection _postRemoteConnectCommands; + private ReadOnlyCollection? _postRemoteConnectCommands; /// /// [Required] Additional commands used to setup debugging once the remote connection has been made. May be an empty collection /// public ReadOnlyCollection PostRemoteConnectCommands { - get { return _postRemoteConnectCommands; } + get + { + _postRemoteConnectCommands ??= new ReadOnlyCollection(Array.Empty()); + return _postRemoteConnectCommands; + } set { if (value == null) @@ -1108,14 +1131,14 @@ public ReadOnlyCollection PostRemoteConnectCommands } - private ReadOnlyCollection _customLaunchSetupCommands; + private ReadOnlyCollection? _customLaunchSetupCommands; /// /// [Optional] If provided, this replaces the default commands used to launch a target with some other commands. For example, /// this can be '-target-attach' in order to attach to a target process.An empty command list replaces the launch commands with nothing, /// which can be useful if the debugger is being provided launch options as command line options. /// - public ReadOnlyCollection CustomLaunchSetupCommands + public ReadOnlyCollection? CustomLaunchSetupCommands { get { return _customLaunchSetupCommands; } set @@ -1149,9 +1172,9 @@ protected set } } - private ReadOnlyCollection _sourceMap; + private ReadOnlyCollection? _sourceMap; - public ReadOnlyCollection SourceMap + public ReadOnlyCollection? SourceMap { get { return _sourceMap; } set @@ -1335,48 +1358,55 @@ public string GetOptionsString() } } - public static LaunchOptions GetInstance(HostConfigurationStore configStore, string exePath, string args, string dir, string options, bool noDebug, IDeviceAppLauncherEventCallback eventCallback, TargetEngine targetEngine, Logger logger) + public static LaunchOptions GetInstance(HostConfigurationStore? configStore, string exePath, string? args, string? dir, string? options, bool noDebug, IDeviceAppLauncherEventCallback? eventCallback, TargetEngine targetEngine, Logger? logger) { - if (string.IsNullOrWhiteSpace(exePath)) + if (IsNullOrWhiteSpace(exePath)) throw new ArgumentNullException(nameof(exePath)); options = options?.Trim(); - if (string.IsNullOrEmpty(options)) + if (IsNullOrEmpty(options)) throw new InvalidLaunchOptionsException(MICoreResources.Error_StringIsNullOrEmpty); logger?.WriteTextBlock(LogLevel.Verbose, "LaunchOptions", options); - LaunchOptions launchOptions = null; + LaunchOptions? launchOptions = null; Guid clsidLauncher = Guid.Empty; - object launcher = null; - object launcherXmlOptions = null; + object? launcher = null; + object? launcherXmlOptions = null; if (options[0] == '{') { try { - JObject parsedOptions = JsonConvert.DeserializeObject(options, new JsonSerializerSettings { DateParseHandling = DateParseHandling.None }); + JObject? parsedOptions = JsonConvert.DeserializeObject(options, new JsonSerializerSettings { DateParseHandling = DateParseHandling.None }); if (parsedOptions is null) { throw new InvalidLaunchOptionsException(MICoreResources.Error_UnknownLaunchOptions); } // if the customLauncher element is present then try using the custom launcher implementation from the config store - if (parsedOptions["customLauncher"] != null && !string.IsNullOrWhiteSpace(parsedOptions["customLauncher"].Value())) + JToken? customLauncherToken = parsedOptions["customLauncher"]; + if (customLauncherToken is not null && customLauncherToken.Value() is string customLauncherName && !IsNullOrWhiteSpace(customLauncherName)) { - string customLauncherName = parsedOptions["customLauncher"].Value(); - var jsonLauncher = configStore?.GetCustomLauncher(customLauncherName); + if (configStore is null || eventCallback is null) + { + throw new InvalidLaunchOptionsException(MICoreResources.Error_UnknownLaunchOptions); + } + + var jsonLauncher = configStore.GetCustomLauncher(customLauncherName); if (jsonLauncher == null) { throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, MICoreResources.Error_UnknownCustomLauncher, customLauncherName)); } - if (jsonLauncher as IPlatformAppLauncher == null) + + if (jsonLauncher is not IPlatformAppLauncher platformAppLauncher) { throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, MICoreResources.Error_LauncherNotFound, customLauncherName)); } - launchOptions = ExecuteLauncher(configStore, (IPlatformAppLauncher)jsonLauncher, exePath, args, dir, parsedOptions, eventCallback, targetEngine, logger); + + launchOptions = ExecuteLauncher(configStore, platformAppLauncher, exePath, args, dir, parsedOptions, eventCallback, targetEngine, logger); } - else if (parsedOptions["pipeTransport"] != null && parsedOptions["pipeTransport"].HasValues) + else if (parsedOptions["pipeTransport"] is JToken pipeTransportToken && pipeTransportToken.HasValues) { launchOptions = PipeLaunchOptions.CreateFromJson(parsedOptions); } @@ -1487,23 +1517,38 @@ public static LaunchOptions GetInstance(HostConfigurationStore configStore, stri if (clsidLauncher != Guid.Empty) { + if (launcherXmlOptions is null || configStore is null || eventCallback is null) + { + throw new InvalidLaunchOptionsException(MICoreResources.Error_UnknownLaunchOptions); + } + launchOptions = ExecuteLauncher(configStore, clsidLauncher, exePath, args, dir, launcherXmlOptions, eventCallback, targetEngine, logger); } else if (launcher != null) { + if (launcherXmlOptions is null || configStore is null || eventCallback is null) + { + throw new InvalidLaunchOptionsException(MICoreResources.Error_UnknownLaunchOptions); + } + launchOptions = ExecuteLauncher(configStore, (IPlatformAppLauncher)launcher, exePath, args, dir, launcherXmlOptions, eventCallback, targetEngine, logger); } + if (launchOptions is null) + { + throw new InvalidLaunchOptionsException(MICoreResources.Error_UnknownLaunchOptions); + } + if (targetEngine == TargetEngine.Native) { if (launchOptions.ExePath == null) launchOptions.ExePath = exePath; } - if (string.IsNullOrEmpty(launchOptions.ExeArguments)) - launchOptions.ExeArguments = args; + if (IsNullOrEmpty(launchOptions.ExeArguments)) + launchOptions.ExeArguments = args ?? string.Empty; - if (string.IsNullOrEmpty(launchOptions.WorkingDirectory)) + if (IsNullOrEmpty(launchOptions.WorkingDirectory)) launchOptions.WorkingDirectory = dir; launchOptions.NoDebug = noDebug; @@ -1524,22 +1569,22 @@ public static LaunchOptions CreateForAttachRequest(Microsoft.VisualStudio.Debugg Logger logger) { var suppOptions = GetOptionsFromFile(logger); - string connection; + string? connection; ((IDebugPort2)unixPort).GetPortName(out connection); - AttachOptionsForConnection attachOptions = null; + AttachOptionsForConnection? attachOptions = null; if (suppOptions != null && suppOptions.AttachOptions != null) { - attachOptions = suppOptions.AttachOptions.FirstOrDefault((o) => o.ConnectionName == connection || o.ConnectionName == "*" || string.IsNullOrWhiteSpace(o.ConnectionName)); + attachOptions = suppOptions.AttachOptions.FirstOrDefault((o) => o.ConnectionName == connection || o.ConnectionName == "*" || IsNullOrWhiteSpace(o.ConnectionName)); } - bool isServerMode = attachOptions?.ServerOptions != null; LaunchOptions options; - if (isServerMode && unixPort is Microsoft.VisualStudio.Debugger.Interop.UnixPortSupplier.IDebugGdbServerAttach) + ServerOptions? serverOptions = attachOptions?.ServerOptions; + if (serverOptions is not null && unixPort is Microsoft.VisualStudio.Debugger.Interop.UnixPortSupplier.IDebugGdbServerAttach gdbServerAttachPort) { - string addr = ((Microsoft.VisualStudio.Debugger.Interop.UnixPortSupplier.IDebugGdbServerAttach)unixPort).GdbServerAttachProcess(processId, attachOptions.ServerOptions.PreAttachCommand); - options = new LocalLaunchOptions(attachOptions.ServerOptions.MIDebuggerPath, addr, attachOptions.ServerOptions.MIDebuggerArgs); + string addr = gdbServerAttachPort.GdbServerAttachProcess(processId, serverOptions.PreAttachCommand); + options = new LocalLaunchOptions(serverOptions.MIDebuggerPath, addr, serverOptions.MIDebuggerArgs); options._miMode = miMode; - options.ExePath = attachOptions.ServerOptions.ExePath; + options.ExePath = serverOptions.ExePath; } else { @@ -1561,10 +1606,10 @@ public static LaunchOptions CreateForAttachRequest(Microsoft.VisualStudio.Debugg return options; } - internal static SupplementalLaunchOptions GetOptionsFromFile(Logger logger) + internal static SupplementalLaunchOptions? GetOptionsFromFile(Logger? logger) { // load supplemental options from the solution root - string slnRoot = null; + string? slnRoot = null; // During glass testing, the Shell assembly is not available try @@ -1574,18 +1619,18 @@ internal static SupplementalLaunchOptions GetOptionsFromFile(Logger logger) catch (FileNotFoundException) { } - if (!string.IsNullOrEmpty(slnRoot)) + if (!IsNullOrEmpty(slnRoot)) { string optFile = Path.Combine(slnRoot, "Microsoft.MIEngine.Options.xml"); if (File.Exists(optFile)) { - string suppOptions = null; + string? suppOptions = null; using (var reader = File.OpenText(optFile)) { suppOptions = reader.ReadToEnd(); } - if (!string.IsNullOrEmpty(suppOptions)) + if (!IsNullOrEmpty(suppOptions)) { try { @@ -1604,7 +1649,7 @@ internal static SupplementalLaunchOptions GetOptionsFromFile(Logger logger) return null; } - internal void LoadSupplementalOptions(Logger logger) + internal void LoadSupplementalOptions(Logger? logger) { if (SourceMap == null) { @@ -1615,7 +1660,7 @@ internal void LoadSupplementalOptions(Logger logger) Merge(options); } - void MergeMap(Xml.LaunchOptions.SourceMapEntry[] inMap) + void MergeMap(Xml.LaunchOptions.SourceMapEntry[]? inMap) { // merge the source mapping lists List map = new List(); @@ -1654,9 +1699,9 @@ private void Merge(AttachOptionsForConnection suppOptions) PostRemoteConnectCommands = new ReadOnlyCollection(postRemoteConnectCmds); MergeMap(suppOptions.SourceMap); - if (!string.IsNullOrWhiteSpace(suppOptions.AdditionalSOLibSearchPath)) + if (!IsNullOrWhiteSpace(suppOptions.AdditionalSOLibSearchPath)) { - if (string.IsNullOrWhiteSpace(AdditionalSOLibSearchPath)) + if (IsNullOrWhiteSpace(AdditionalSOLibSearchPath)) { AdditionalSOLibSearchPath = suppOptions.AdditionalSOLibSearchPath; } @@ -1665,7 +1710,7 @@ private void Merge(AttachOptionsForConnection suppOptions) AdditionalSOLibSearchPath += ';' + suppOptions.AdditionalSOLibSearchPath; } } - if (string.IsNullOrWhiteSpace(WorkingDirectory)) + if (IsNullOrWhiteSpace(WorkingDirectory)) { WorkingDirectory = suppOptions.WorkingDirectory; } @@ -1713,8 +1758,8 @@ public static XmlReader OpenXml(string content) namespaceManager.AddNamespace(string.Empty, XmlNamespace); XmlParserContext context = new XmlParserContext(settings.NameTable, namespaceManager, string.Empty, XmlSpace.None); - StringReader stringReader = null; - XmlReader reader = null; + StringReader? stringReader = null; + XmlReader? reader = null; bool success = false; try @@ -1756,7 +1801,7 @@ public static object Deserialize(XmlSerializer serializer, XmlReader reader) { try { - return serializer.Deserialize(reader); + return serializer.Deserialize(reader) ?? throw new InvalidLaunchOptionsException(MICoreResources.Error_UnknownLaunchOptions); } catch (InvalidOperationException outerException) { @@ -1812,11 +1857,11 @@ private IEnumerable GetSOLibSearchPathCandidates() } } - if (!string.IsNullOrEmpty(_additionalSOLibSearchPath)) + if (!IsNullOrEmpty(_additionalSOLibSearchPath)) { foreach (string directory in _additionalSOLibSearchPath.Split(';')) { - if (string.IsNullOrWhiteSpace(directory)) + if (IsNullOrWhiteSpace(directory)) continue; // To make sure that all directory names are in a canonical form, if there are any trailing slashes, remove them @@ -1830,7 +1875,7 @@ private IEnumerable GetSOLibSearchPathCandidates() } } - internal static List GetEnvironmentEntries(Xml.LaunchOptions.EnvironmentEntry[] entries) + internal static List GetEnvironmentEntries(Xml.LaunchOptions.EnvironmentEntry[]? entries) { List envList = new List(); if (entries != null) @@ -1843,7 +1888,7 @@ internal static List GetEnvironmentEntries(Xml.LaunchOptions.E return envList; } - internal static List GetEnvironmentEntries(List entries) + internal static List GetEnvironmentEntries(List? entries) { List envList = new List(); if (entries != null) @@ -1860,7 +1905,7 @@ protected void InitializeCommonOptions(Json.LaunchOptions.BaseOptions options) { this.ExePath = options.Program; - if (this.TargetArchitecture == TargetArchitecture.Unknown && !String.IsNullOrWhiteSpace(options.TargetArchitecture)) + if (this.TargetArchitecture == TargetArchitecture.Unknown && !IsNullOrWhiteSpace(options.TargetArchitecture)) { this.TargetArchitecture = ConvertTargetArchitectureAttribute(options.TargetArchitecture); } @@ -1871,7 +1916,7 @@ protected void InitializeCommonOptions(Json.LaunchOptions.BaseOptions options) } this.ShowDisplayString = options.ShowDisplayString.GetValueOrDefault(false); - this.AdditionalSOLibSearchPath = String.IsNullOrEmpty(this.AdditionalSOLibSearchPath) ? + this.AdditionalSOLibSearchPath = IsNullOrEmpty(this.AdditionalSOLibSearchPath) ? options.AdditionalSOLibSearchPath : String.Concat(this.AdditionalSOLibSearchPath, ";", options.AdditionalSOLibSearchPath); @@ -1884,7 +1929,7 @@ protected void InitializeCommonOptions(Json.LaunchOptions.BaseOptions options) { SymbolInfoLoadAll = options.SymbolLoadInfo.LoadAll.GetValueOrDefault(true); - if (!string.IsNullOrWhiteSpace(options.SymbolLoadInfo.ExceptionList)) + if (!IsNullOrWhiteSpace(options.SymbolLoadInfo.ExceptionList)) { if (DebuggerMIMode == MIMode.Lldb) { @@ -1900,8 +1945,8 @@ protected void InitializeCommonOptions(Json.LaunchOptions.BaseOptions options) } } - this.SetupCommands = LaunchCommand.CreateCollection(options.SetupCommands); - this.PostRemoteConnectCommands = LaunchCommand.CreateCollection(options.PostRemoteConnectCommands); + this.SetupCommands = LaunchCommand.CreateCollection(options.SetupCommands ?? new List()); + this.PostRemoteConnectCommands = LaunchCommand.CreateCollection(options.PostRemoteConnectCommands ?? new List()); this.RequireHardwareBreakpoints = options.HardwareBreakpointInfo?.Require ?? false; this.HardwareBreakpointLimit = options.HardwareBreakpointInfo?.Limit ?? 0; @@ -1922,7 +1967,7 @@ protected void InitializeCommonOptions(Xml.LaunchOptions.BaseLaunchOptions sourc if (this.ExePath == null) { string exePath = source.ExePath; - if (!string.IsNullOrWhiteSpace(exePath)) + if (!IsNullOrWhiteSpace(exePath)) { this.ExePath = exePath; } @@ -1935,13 +1980,13 @@ protected void InitializeCommonOptions(Xml.LaunchOptions.BaseLaunchOptions sourc this.DebuggerMIMode = ConvertMIModeAttribute(source.MIMode); - if (string.IsNullOrEmpty(this.ExeArguments)) + if (IsNullOrEmpty(this.ExeArguments)) this.ExeArguments = source.ExeArguments; - if (string.IsNullOrEmpty(this.WorkingDirectory)) + if (IsNullOrEmpty(this.WorkingDirectory)) this.WorkingDirectory = source.WorkingDirectory; - if (!string.IsNullOrEmpty(source.VisualizerFile)) + if (!IsNullOrEmpty(source.VisualizerFile)) this.VisualizerFiles.Add(source.VisualizerFile); this.ShowDisplayString = source.ShowDisplayString; @@ -1965,14 +2010,14 @@ protected void InitializeCommonOptions(Xml.LaunchOptions.BaseLaunchOptions sourc this.LaunchCompleteCommand = (LaunchCompleteCommand)source.LaunchCompleteCommand; string additionalSOLibSearchPath = source.AdditionalSOLibSearchPath; - if (!string.IsNullOrEmpty(additionalSOLibSearchPath)) + if (!IsNullOrEmpty(additionalSOLibSearchPath)) { - if (string.IsNullOrEmpty(this.AdditionalSOLibSearchPath)) + if (IsNullOrEmpty(this.AdditionalSOLibSearchPath)) this.AdditionalSOLibSearchPath = additionalSOLibSearchPath; else this.AdditionalSOLibSearchPath = string.Concat(this.AdditionalSOLibSearchPath, ";", additionalSOLibSearchPath); } - if (string.IsNullOrEmpty(this.AbsolutePrefixSOLibSearchPath)) + if (IsNullOrEmpty(this.AbsolutePrefixSOLibSearchPath)) this.AbsolutePrefixSOLibSearchPath = source.AbsolutePrefixSOLibSearchPath; if (source.DebugChildProcessesSpecified) @@ -1988,14 +2033,14 @@ protected void InitializeCommonOptions(Xml.LaunchOptions.BaseLaunchOptions sourc this.CoreDumpPath = source.CoreDumpPath; // Ensure that CoreDumpPath and ProcessId are not specified at the same time - if (!String.IsNullOrEmpty(source.CoreDumpPath) && source.ProcessIdSpecified) + if (!IsNullOrEmpty(source.CoreDumpPath) && source.ProcessIdSpecified) throw new InvalidLaunchOptionsException(String.Format(CultureInfo.InvariantCulture, MICoreResources.Error_CannotSpecifyBoth, nameof(source.CoreDumpPath), nameof(source.ProcessId))); if (source.SymbolLoadInfo != null) { SymbolInfoLoadAll = source.SymbolLoadInfo.LoadAllSpecified ? source.SymbolLoadInfo.LoadAll : true; - if (DebuggerMIMode == MIMode.Lldb && !string.IsNullOrWhiteSpace(source.SymbolLoadInfo.ExceptionList)) + if (DebuggerMIMode == MIMode.Lldb && !IsNullOrWhiteSpace(source.SymbolLoadInfo.ExceptionList)) { throw new InvalidLaunchOptionsException(String.Format(CultureInfo.InvariantCulture, MICoreResources.Error_OptionNotSupported, nameof(source.SymbolLoadInfo.ExceptionList), nameof(MIMode.Lldb))); } @@ -2012,7 +2057,7 @@ protected void InitializeCommonOptions(Xml.LaunchOptions.BaseLaunchOptions sourc this.Environment = new ReadOnlyCollection(GetEnvironmentEntries(source.Environment)); } - private static List TryAddWindowsDebuggeeConsoleRedirection(List arguments) + private static List? TryAddWindowsDebuggeeConsoleRedirection(List? arguments) { if (PlatformUtilities.IsWindows()) // Only do this on Windows { @@ -2025,7 +2070,7 @@ private static List TryAddWindowsDebuggeeConsoleRedirection(List foreach (string rawArgument in arguments) { // Skip on null or blank arguments. - if (string.IsNullOrWhiteSpace(rawArgument)) + if (IsNullOrWhiteSpace(rawArgument)) { continue; } @@ -2049,7 +2094,7 @@ private static List TryAddWindowsDebuggeeConsoleRedirection(List // If one (or more) are not redirected, then add redirection if (!stdInRedirected || !stdOutRedirected || !stdErrRedirected) { - int argLength = arguments.Count; + int argLength = arguments?.Count ?? 0; List argList = new List(argLength + 3); if (arguments != null) { @@ -2082,12 +2127,12 @@ public void InitializeLaunchOptions(Json.LaunchOptions.LaunchOptions launch) { this.DebuggerMIMode = ConvertMIModeString(RequireAttribute(launch.MIMode, nameof(launch.MIMode))); - List args = launch.Args; + List? args = launch.Args; if (Host.GetHostUIIdentifier() == HostUIIdentifier.VSCode && HostRunInTerminal.IsRunInTerminalAvailable() && !launch.ExternalConsole.GetValueOrDefault(false) && - string.IsNullOrEmpty(launch.CoreDumpPath) && + IsNullOrEmpty(launch.CoreDumpPath) && !launch.AvoidWindowsConsoleRedirection.GetValueOrDefault(false) && !(this is PipeLaunchOptions)) // Make sure we are not doing a PipeLaunch { @@ -2099,7 +2144,7 @@ public void InitializeLaunchOptions(Json.LaunchOptions.LaunchOptions launch) this.CoreDumpPath = launch.CoreDumpPath; - if (launch.CustomLaunchSetupCommands.Any()) + if (launch.CustomLaunchSetupCommands != null && launch.CustomLaunchSetupCommands.Any()) { this.CustomLaunchSetupCommands = LaunchCommand.CreateCollection(launch.CustomLaunchSetupCommands); } @@ -2119,9 +2164,9 @@ public void InitializeAttachOptions(Json.LaunchOptions.AttachOptions attach) this.ProcessId = attach.ProcessId; } - public static string RequireAttribute(string attributeValue, string attributeName) + public static string RequireAttribute(string? attributeValue, string attributeName) { - if (string.IsNullOrWhiteSpace(attributeValue)) + if (IsNullOrWhiteSpace(attributeValue)) throw new InvalidLaunchOptionsException(string.Format(CultureInfo.CurrentCulture, MICoreResources.Error_MissingAttribute, attributeName)); return attributeValue; @@ -2137,17 +2182,18 @@ public static int RequirePortAttribute(int attributeValue, string attributeName) return attributeValue; } - private static LaunchOptions ExecuteLauncher(HostConfigurationStore configStore, Guid clsidLauncher, string exePath, string args, string dir, object launcherXmlOptions, IDeviceAppLauncherEventCallback eventCallback, TargetEngine targetEngine, Logger logger) + private static LaunchOptions ExecuteLauncher(HostConfigurationStore configStore, Guid clsidLauncher, string exePath, string? args, string? dir, object launcherXmlOptions, IDeviceAppLauncherEventCallback eventCallback, TargetEngine targetEngine, Logger? logger) { - var deviceAppLauncher = (IPlatformAppLauncher)HostLoader.VsCoCreateManagedObject(configStore, clsidLauncher); + var deviceAppLauncher = HostLoader.VsCoCreateManagedObject(configStore, clsidLauncher) as IPlatformAppLauncher; if (deviceAppLauncher == null) { throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, MICoreResources.Error_LauncherNotFound, clsidLauncher.ToString("B"))); } + return ExecuteLauncher(configStore, deviceAppLauncher, exePath, args, dir, launcherXmlOptions, eventCallback, targetEngine, logger); } - private static LaunchOptions ExecuteLauncher(HostConfigurationStore configStore, IPlatformAppLauncher deviceAppLauncher, string exePath, string args, string dir, object launcherOptions, IDeviceAppLauncherEventCallback eventCallback, TargetEngine targetEngine, Logger logger) + private static LaunchOptions ExecuteLauncher(HostConfigurationStore configStore, IPlatformAppLauncher deviceAppLauncher, string exePath, string? args, string? dir, object launcherOptions, IDeviceAppLauncherEventCallback eventCallback, TargetEngine targetEngine, Logger? logger) { bool success = false; @@ -2181,7 +2227,7 @@ private static LaunchOptions ExecuteLauncher(HostConfigurationStore configStore, private static XmlSerializer GetXmlSerializer(Type type) { - Assembly serializationAssembly = s_serializationAssembly.Value; + Assembly? serializationAssembly = s_serializationAssembly.Value; if (serializationAssembly == null) { return new XmlSerializer(type); @@ -2189,8 +2235,8 @@ private static XmlSerializer GetXmlSerializer(Type type) else { // NOTE: You can look at MIEngine\src\MICore\obj\Debug\sgen\.cs to see the source code for this assembly. - Type serializerType = serializationAssembly.GetType("Microsoft.Xml.Serialization.GeneratedAssembly." + type.Name + "Serializer"); - ConstructorInfo constructor = serializerType?.GetConstructor(new Type[0]); + Type? serializerType = serializationAssembly.GetType("Microsoft.Xml.Serialization.GeneratedAssembly." + type.Name + "Serializer"); + ConstructorInfo? constructor = serializerType?.GetConstructor(new Type[0]); if (constructor == null) { throw new Exception(string.Format(CultureInfo.CurrentCulture, MICoreResources.Error_UnableToLoadSerializer, type.Name)); @@ -2201,17 +2247,17 @@ private static XmlSerializer GetXmlSerializer(Type type) } } - private static Assembly LoadSerializationAssembly() + private static Assembly? LoadSerializationAssembly() { // This code looks to see if we have sgen-created XmlSerializers assembly next to this dll, which will be true // when the MIEngine is running in Visual Studio. If so, it loads it, so that we can get the performance advantages // of a static XmlSerializers assembly. Otherwise we return null, and we will use a dynamic deserializer. string thisModulePath = typeof(LaunchOptions).GetTypeInfo().Assembly.ManifestModule.FullyQualifiedName; - string thisModuleDir = Path.GetDirectoryName(thisModulePath); + string thisModuleDir = Path.GetDirectoryName(thisModulePath) ?? string.Empty; string thisModuleName = Path.GetFileNameWithoutExtension(thisModulePath); string serializerAssemblyPath = Path.Combine(thisModuleDir, thisModuleName + ".XmlSerializers.dll"); - string thisModuleVersion = typeof(LaunchOptions).GetTypeInfo().Assembly.GetName().Version.ToString(); + string thisModuleVersion = typeof(LaunchOptions).GetTypeInfo().Assembly.GetName().Version?.ToString() ?? string.Empty; if (!File.Exists(serializerAssemblyPath)) return null; @@ -2314,9 +2360,9 @@ public static MIMode ConvertMIModeAttribute(Xml.LaunchOptions.MIMode source) return (MIMode)source; } - protected static string ParseArguments(IEnumerable arguments, bool quoteArguments = true) + protected static string ParseArguments(IEnumerable? arguments, bool quoteArguments = true) { - if (arguments.Any()) + if (arguments != null && arguments.Any()) { StringBuilder stringBuilder = new StringBuilder(); foreach (string arg in arguments) @@ -2339,7 +2385,7 @@ protected static string QuoteArgument(string arg) // Quote if: // 1. string is null or empty and convert to a quoted empty string. // 2. Its not quoted and it has an argument seperator. - if (string.IsNullOrEmpty(arg) || (arg[0] != '"' && arg.IndexOfAny(s_ARGUMENT_SEPARATORS) >= 0)) + if (IsNullOrEmpty(arg) || (arg[0] != '"' && arg.IndexOfAny(s_ARGUMENT_SEPARATORS) >= 0)) { return '"' + arg + '"'; } @@ -2372,7 +2418,7 @@ public interface IPlatformAppLauncher : IDisposable /// [Optional] Working directory of the executable provided in the VsDebugTargetInfo by the project system. Some launchers may ignore this. /// [Required] Deserialized XML options structure or, when using json options, a JObject /// Indicates the type of debugging being done. - void SetLaunchOptions(string exePath, string args, string dir, object launcherOptions, TargetEngine targetEngine); + void SetLaunchOptions(string exePath, string? args, string? dir, object launcherOptions, TargetEngine targetEngine); /// /// Does whatever steps are necessary to setup for debugging. On Android this will include launching diff --git a/src/MICore/Logger.cs b/src/MICore/Logger.cs index 361d34722..0fb9571ed 100644 --- a/src/MICore/Logger.cs +++ b/src/MICore/Logger.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Linq; using System.Runtime.CompilerServices; @@ -26,11 +25,11 @@ public class Logger /// /// Optional logger to get engine diagnostics logs /// - private ILogChannel EngineLogger => HostLogger.GetEngineLogChannel(); + private ILogChannel? EngineLogger => HostLogger.GetEngineLogChannel(); /// /// Optional logger to get natvis diagnostics logs /// - public ILogChannel NatvisLogger => HostLogger.GetNatvisLogChannel(); + public ILogChannel? NatvisLogger => HostLogger.GetNatvisLogChannel(); private static int s_count; private readonly int _id; @@ -38,8 +37,8 @@ public class Logger public class LogInfo { - public string logFile; - public Action logToOutput; + public string? logFile; + public Action? logToOutput; public bool enabled; }; @@ -149,7 +148,7 @@ public void WriteTextBlock(LogLevel level, string prefix, string textBlock) if (HostLogger.IsFeedbackLogEnabled) { - HostLogger.WriteFeedbackLog((!string.IsNullOrEmpty(prefix) ? prefix : string.Empty) + textBlock); + HostLogger.WriteFeedbackLog((!IsNullOrEmpty(prefix) ? prefix : string.Empty) + textBlock); } } @@ -202,7 +201,7 @@ private void WriteTextBlockImpl(LogLevel level, string prefix, string textBlock) if (line == null) break; - if (!string.IsNullOrEmpty(prefix)) + if (!IsNullOrEmpty(prefix)) WriteLineImpl(level, prefix + line); else WriteLineImpl(level, line); diff --git a/src/MICore/MICore.csproj b/src/MICore/MICore.csproj index abdc47283..2e860fcbf 100755 --- a/src/MICore/MICore.csproj +++ b/src/MICore/MICore.csproj @@ -2,6 +2,7 @@ 14.0 + enable {54C33AFA-438D-4932-A2F0-D0F2BB2FADC9} Library Properties @@ -23,6 +24,9 @@ + + Shared\%(Filename).cs + True True diff --git a/src/MICore/MIException.cs b/src/MICore/MIException.cs index 7350d7f4a..a289d9aef 100644 --- a/src/MICore/MIException.cs +++ b/src/MICore/MIException.cs @@ -84,7 +84,7 @@ public override string Message get { string message = string.Format(CultureInfo.CurrentCulture, MICoreResources.Error_UnexpectedMIOutput, _debuggerName, _command); - if (!string.IsNullOrWhiteSpace(_miError)) + if (!IsNullOrWhiteSpace(_miError)) { message = string.Concat(message, " ", _miError); } @@ -105,7 +105,7 @@ public class MIDebuggerInitializeFailedException : Exception public readonly IReadOnlyList OutputLines; private readonly string _debuggerName; private readonly IReadOnlyList _errorLines; - private string _message; + private string? _message; public MIDebuggerInitializeFailedException(string debuggerName, IReadOnlyList errorLines, IReadOnlyList outputLines) { @@ -120,7 +120,7 @@ public override string Message { if (_message == null) { - if (_errorLines.Any(x => !string.IsNullOrWhiteSpace(x))) + if (_errorLines.Any(x => !IsNullOrWhiteSpace(x))) { _message = string.Format(CultureInfo.InvariantCulture, MICoreResources.Error_DebuggerInitializeFailed_StdErr, _debuggerName, string.Join("\r\n", _errorLines)); } diff --git a/src/MICore/MIResults.cs b/src/MICore/MIResults.cs index 0b8e4b9a2..539d2b0f2 100644 --- a/src/MICore/MIResults.cs +++ b/src/MICore/MIResults.cs @@ -6,10 +6,14 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -using System.Diagnostics; using System.Collections; using System.Globalization; +using System.Diagnostics.CodeAnalysis; using Microsoft.DebugEngineHost; +using DebuggerDisplayAttribute = global::System.Diagnostics.DebuggerDisplayAttribute; +using DebuggerTypeProxyAttribute = global::System.Diagnostics.DebuggerTypeProxyAttribute; +using DebuggerBrowsableAttribute = global::System.Diagnostics.DebuggerBrowsableAttribute; +using DebuggerBrowsableState = global::System.Diagnostics.DebuggerBrowsableState; namespace MICore { @@ -39,7 +43,7 @@ public virtual ResultValue Find(string name) throw new MIResultFormatException(name, this); } - public virtual bool TryFind(string name, out ResultValue result) + public virtual bool TryFind(string name, [NotNullWhen(true)] out ResultValue? result) { if (Contains(name)) { @@ -49,7 +53,7 @@ public virtual bool TryFind(string name, out ResultValue result) { result = null; } - return result != null; + return result is not null; } public virtual bool Contains(string name) @@ -100,7 +104,7 @@ public uint FindUint(string name) /// The value of the property or null if it cannot be found public uint? TryFindUint(string name) { - ConstValue c; + ConstValue? c; if (!TryFind(name, out c)) { return null; @@ -147,7 +151,7 @@ public ulong FindAddr(string name) /// The value of the address or null if it can't be found public ulong? TryFindAddr(string name) { - ConstValue c; + ConstValue? c; if (!TryFind(name, out c)) { return null; @@ -175,7 +179,7 @@ public string FindString(string name) public string TryFindString(string name) { - ConstValue c; + ConstValue? c; if (!TryFind(name, out c)) { return string.Empty; @@ -186,14 +190,14 @@ public string TryFindString(string name) public T Find(string name) where T : ResultValue { var c = Find(name); - if (c is T) + if (c is T t) { - return c as T; + return t; } throw new MIResultFormatException(name, this); } - public bool TryFind(string name, out T result) where T : ResultValue + public bool TryFind(string name, [NotNullWhen(true)] out T? result) where T : ResultValue { if (Contains(name)) { @@ -203,12 +207,12 @@ public bool TryFind(string name, out T result) where T : ResultValue { result = null; } - return result != null; + return result is not null; } - public T TryFind(string name) where T : ResultValue + public T? TryFind(string name) where T : ResultValue { - T result; + T? result; if (!TryFind(name, out result)) { return null; @@ -279,7 +283,7 @@ public NamedResultValue[] Content { get { - List values = null; + List? values = null; if (_value is ValueListValue) { @@ -307,7 +311,7 @@ public NamedResultValue[] Content }); } - return values?.ToArray(); + return values?.ToArray() ?? Array.Empty(); } } } @@ -410,7 +414,7 @@ public T[] FindAll(string name) where T : class /// /// The list of names that must be added to the TupleValue. /// The list of names that will be added to the TupleValue if they exist in this TupleValue. - public TupleValue Subset(IEnumerable requiredNames, IEnumerable optionalNames = null) + public TupleValue Subset(IEnumerable requiredNames, IEnumerable? optionalNames = null) { List values = new List(); @@ -422,11 +426,11 @@ public TupleValue Subset(IEnumerable requiredNames, IEnumerable } // Iterate the optional list and add the values of the name exists. - if (null != optionalNames) + if (optionalNames is not null) { foreach (string name in optionalNames) { - ResultValue value; + ResultValue? value; if (this.TryFind(name, out value)) { values.Add(new NamedResultValue(name, value)); @@ -566,7 +570,7 @@ public ResultsTypeProxy(Results results) public readonly ResultClass ResultClass; - public Results(ResultClass resultsClass, List list = null) + public Results(ResultClass resultsClass, List? list = null) : base(list ?? new List()) { ResultClass = resultsClass; @@ -678,7 +682,6 @@ public bool StartsWith(string theString, string pattern) } } - private string _resultString; private Logger Logger { get; set; } public MIResults(Logger logger) @@ -692,8 +695,8 @@ public MIResults(Logger logger) /// public Results ParseCommandOutput(string output) { - _resultString = output.Trim(); - int comma = _resultString.IndexOf(','); + string resultString = output.Trim(); + int comma = resultString.IndexOf(','); Results results; ResultClass resultClass = ResultClass.None; if (comma < 0) @@ -704,22 +707,22 @@ public Results ParseCommandOutput(string output) else { resultClass = ParseResultClass(output.Substring(0, comma)); - Span wholeString = new Span(_resultString); - results = ParseResultList(wholeString.AdvanceTo(comma + 1), resultClass); + Span wholeString = new Span(resultString); + results = ParseResultList(resultString, wholeString.AdvanceTo(comma + 1), resultClass); } return results; } public Results ParseResultList(string listStr, ResultClass resultClass = ResultClass.None) { - _resultString = listStr.Trim(); - return ParseResultList(new Span(_resultString), resultClass); + string resultString = listStr.Trim(); + return ParseResultList(resultString, new Span(resultString), resultClass); } - private Results ParseResultList(Span listStr, ResultClass resultClass = ResultClass.None) + private Results ParseResultList(string resultString, Span listStr, ResultClass resultClass = ResultClass.None) { Span rest; - var list = ParseResultList((Span s, ref int i) => + var list = ParseResultList(resultString, (Span s, ref int i) => { return true; }, (Span s, ref int i) => @@ -735,8 +738,8 @@ private Results ParseResultList(Span listStr, ResultClass resultClass = ResultCl } else { - ParseError("trailing chars", rest); - throw new MIResultFormatException(CreateErrorMessageFromSpan(rest), results); + ParseError(resultString, "trailing chars", rest); + throw new MIResultFormatException(CreateErrorMessageFromSpan(resultString, rest), results); } } @@ -746,7 +749,7 @@ public string ParseCString(string input) { throw new ArgumentNullException(nameof(input)); } - else if (string.IsNullOrEmpty(input)) + else if (IsNullOrEmpty(input)) { return string.Empty; } @@ -756,25 +759,8 @@ public string ParseCString(string input) { return input; } - _resultString = cstr; Span rest; - var s = ParseCString(new Span(cstr), out rest); - return s == null ? string.Empty : s.AsString; - } - - private string ParseCString(Span input) - { - if (input.IsEmpty) - { - return string.Empty; - } - - if (_resultString[input.Start] != '\"') // not a Cstring, just return the string - { - return input.Extract(_resultString); - } - Span rest; - var s = ParseCString(input, out rest); + var s = ParseCString(cstr, new Span(cstr), out rest); return s == null ? string.Empty : s.AsString; } @@ -782,30 +768,26 @@ private string ParseCString(Span input) /// value ==>const | tuple | list /// /// - private ResultValue ParseValue(Span resultStr, out Span rest) + private ResultValue? ParseValue(string resultString, Span resultStr, out Span rest) { - ResultValue value = null; rest = Span.Empty; if (resultStr.IsEmpty) { return null; } - switch (_resultString[resultStr.Start]) + switch (resultString[resultStr.Start]) { case '\"': - value = ParseCString(resultStr, out rest); - break; + return ParseCString(resultString, resultStr, out rest); case '{': - value = ParseTuple(resultStr, out rest); - break; + return ParseTuple(resultString, resultStr, out rest); case '[': - value = ParseList(resultStr, out rest); - break; + return ParseList(resultString, resultStr, out rest); default: - ParseError("unexpected char", resultStr); + ParseError(resultString, "unexpected char", resultStr); break; } - return value; + return null; } /// @@ -816,30 +798,26 @@ private ResultValue ParseValue(Span resultStr, out Span rest) /// value -- const | tuple | tuplelist | list /// /// - private ResultValue ParseResultValue(Span resultStr, out Span rest) + private ResultValue? ParseResultValue(string resultString, Span resultStr, out Span rest) { - ResultValue value = null; rest = Span.Empty; if (resultStr.IsEmpty) { return null; } - switch (_resultString[resultStr.Start]) + switch (resultString[resultStr.Start]) { case '\"': - value = ParseCString(resultStr, out rest); - break; + return ParseCString(resultString, resultStr, out rest); case '{': - value = ParseResultTuple(resultStr, out rest); - break; + return ParseResultTuple(resultString, resultStr, out rest); case '[': - value = ParseList(resultStr, out rest); - break; + return ParseList(resultString, resultStr, out rest); default: - ParseError("unexpected char", resultStr); + ParseError(resultString, "unexpected char", resultStr); break; } - return value; + return null; } /// @@ -853,19 +831,20 @@ private static bool IsValueChar(char c) /// /// result ==> variable "=" value /// + /// the original result string /// trimmed input string /// trimmed remainder after result - private NamedResultValue ParseResult(Span resultStr, out Span rest) + private NamedResultValue? ParseResult(string resultString, Span resultStr, out Span rest) { rest = Span.Empty; - int equals = resultStr.IndexOf(_resultString, '='); + int equals = resultStr.IndexOf(resultString, '='); if (equals < 1) { - ParseError("variable not found", resultStr); + ParseError(resultString, "variable not found", resultStr); return null; } - string name = resultStr.Prefix(equals).Extract(_resultString); - ResultValue value = ParseResultValue(resultStr.Advance(equals + 1), out rest); + string name = resultStr.Prefix(equals).Extract(resultString); + ResultValue? value = ParseResultValue(resultString, resultStr.Advance(equals + 1), out rest); if (value == null) { return null; @@ -890,13 +869,13 @@ private static ResultClass ParseResultClass(string resultClass) } } - private ConstValue ParseCString(Span input, out Span rest) + private ConstValue? ParseCString(string resultString, Span input, out Span rest) { rest = input; StringBuilder output = new StringBuilder(); - if (input.IsEmpty || _resultString[input.Start] != '\"') + if (input.IsEmpty || resultString[input.Start] != '\"') { - ParseError("Cstring expected", input); + ParseError(resultString, "Cstring expected", input); return null; } int i = input.Start + 1; @@ -904,12 +883,12 @@ private ConstValue ParseCString(Span input, out Span rest) for (; i < input.Extent; i++) { - char c = _resultString[i]; + char c = resultString[i]; if (c == '\"') { // closing quote, so we are (probably) done i++; - if ((i < input.Extent) && (_resultString[i] == c)) + if ((i < input.Extent) && (resultString[i] == c)) { // double quotes mean we emit a single quote, and carry on ; @@ -923,7 +902,7 @@ private ConstValue ParseCString(Span input, out Span rest) else if (c == '\\') { // escaped character - c = _resultString[++i]; + c = resultString[++i]; switch (c) { case 'n': c = '\n'; break; @@ -933,11 +912,11 @@ private ConstValue ParseCString(Span input, out Span rest) if (c >= '0' && c <= '3') { i = i - 1; - if (SpanOctalChars(_resultString, ref i, output)) + if (SpanOctalChars(resultString, ref i, output)) { continue; // handled the output of the octal-encoded chars } - c = _resultString[i]; // just emit the '\\' + c = resultString[i]; // just emit the '\\' } break; } @@ -946,7 +925,7 @@ private ConstValue ParseCString(Span input, out Span rest) } if (!endFound) { - ParseError("CString not terminated", input); + ParseError(resultString, "CString not terminated", input); return null; } rest = input.AdvanceTo(i); @@ -1003,14 +982,14 @@ bool SpanOctalChars(string str, ref int i, StringBuilder output) private delegate bool EdgeCondition(Span s, ref int i); - private List ParseResultList(EdgeCondition begin, EdgeCondition end, Span input, out Span rest) + private List? ParseResultList(string resultString, EdgeCondition begin, EdgeCondition end, Span input, out Span rest) { rest = Span.Empty; List list = new List(); int i = input.Start; if (!begin(input, ref i)) { - ParseError("Unexpected opening character", input); + ParseError(resultString, "Unexpected opening character", input); return null; } if (end(input, ref i)) // tuple is empty @@ -1019,20 +998,20 @@ private List ParseResultList(EdgeCondition begin, EdgeConditio return list; } input = input.AdvanceTo(i); - var item = ParseResult(input, out rest); + var item = ParseResult(resultString, input, out rest); if (item == null) { - ParseError("Result expected", input); + ParseError(resultString, "Result expected", input); return null; } list.Add(item); input = rest; - while (!input.IsEmpty && _resultString[input.Start] == ',') + while (!input.IsEmpty && resultString[input.Start] == ',') { - item = ParseResult(input.Advance(1), out rest); + item = ParseResult(resultString, input.Advance(1), out rest); if (item == null) { - ParseError("Result expected", input); + ParseError(resultString, "Result expected", input); return null; } list.Add(item); @@ -1042,7 +1021,7 @@ private List ParseResultList(EdgeCondition begin, EdgeConditio i = input.Start; if (!end(input, ref i)) // tuple is not closed { - ParseError("Unexpected list termination", input); + ParseError(resultString, "Unexpected list termination", input); rest = Span.Empty; return null; } @@ -1050,11 +1029,11 @@ private List ParseResultList(EdgeCondition begin, EdgeConditio return list; } - private List ParseResultList(char begin, char end, Span input, out Span rest) + private List? ParseResultList(string resultString, char begin, char end, Span input, out Span rest) { - return ParseResultList((Span s, ref int i) => + return ParseResultList(resultString, (Span s, ref int i) => { - if (_resultString[i] == begin) + if (resultString[i] == begin) { i++; return true; @@ -1062,7 +1041,7 @@ private List ParseResultList(char begin, char end, Span input, return false; }, (Span s, ref int i) => { - if (i < s.Extent && _resultString[i] == end) + if (i < s.Extent && resultString[i] == end) { i++; return true; @@ -1075,21 +1054,25 @@ private List ParseResultList(char begin, char end, Span input, /// tuple ==> "{}" | "{" result ( "," result )* "}" /// /// if one tuple found a TupleValue, otherwise a ValueListValue of TupleValues - private ResultValue ParseResultTuple(Span input, out Span rest) + private ResultValue? ParseResultTuple(string resultString, Span input, out Span rest) { - var list = ParseResultList('{', '}', input, out rest); + var list = ParseResultList(resultString, '{', '}', input, out rest); if (list == null) { return null; } var tlist = new List(); TupleValue v; - while (rest.StartsWith(_resultString, ",{")) + while (rest.StartsWith(resultString, ",{")) { // a tuple list v = new TupleValue(list); tlist.Add(v); - list = ParseResultList('{', '}', rest.Advance(1), out rest); + list = ParseResultList(resultString, '{', '}', rest.Advance(1), out rest); + if (list == null) + { + return null; + } } v = new TupleValue(list); if (tlist.Count != 0) @@ -1103,9 +1086,9 @@ private ResultValue ParseResultTuple(Span input, out Span rest) /// /// tuple ==> "{}" | "{" result ( "," result )* "}" /// - private TupleValue ParseTuple(Span input, out Span rest) + private TupleValue? ParseTuple(string resultString, Span input, out Span rest) { - var list = ParseResultList('{', '}', input, out rest); + var list = ParseResultList(resultString, '{', '}', input, out rest); if (list == null) { return null; @@ -1116,65 +1099,65 @@ private TupleValue ParseTuple(Span input, out Span rest) /// /// list ==> "[]" | "[" value ( "," value )* "]" | "[" result ( "," result )* "]" /// - private ResultValue ParseList(Span input, out Span rest) + private ResultValue? ParseList(string resultString, Span input, out Span rest) { rest = Span.Empty; - if (_resultString[input.Start] != '[') + if (resultString[input.Start] != '[') { - ParseError("List expected", input); + ParseError(resultString, "List expected", input); return null; } - if (_resultString[input.Start + 1] == ']') // list is empty + if (resultString[input.Start + 1] == ']') // list is empty { rest = input.Advance(2); // eat through the closing brace return new ValueListValue(new List()); } - if (IsValueChar(_resultString[input.Start + 1])) + if (IsValueChar(resultString[input.Start + 1])) { - return ParseValueList(input, out rest); + return ParseValueList(resultString, input, out rest); } else { - return ParseResultList(input, out rest); + return ParseResultList(resultString, input, out rest); } } /// /// list ==> "[" value ( "," value )* "]" /// - private ValueListValue ParseValueList(Span input, out Span rest) + private ValueListValue? ParseValueList(string resultString, Span input, out Span rest) { rest = Span.Empty; List list = new List(); - if (_resultString[input.Start] != '[') + if (resultString[input.Start] != '[') { - ParseError("List expected", input); + ParseError(resultString, "List expected", input); return null; } input = input.Advance(1); - var item = ParseValue(input, out rest); + var item = ParseValue(resultString, input, out rest); if (item == null) { - ParseError("Value expected", input); + ParseError(resultString, "Value expected", input); return null; } list.Add(item); input = rest; - while (!input.IsEmpty && _resultString[input.Start] == ',') + while (!input.IsEmpty && resultString[input.Start] == ',') { - item = ParseValue(input.Advance(1), out rest); + item = ParseValue(resultString, input.Advance(1), out rest); if (item == null) { - ParseError("Value expected", input); + ParseError(resultString, "Value expected", input); return null; } list.Add(item); input = rest; } - if (input.IsEmpty || _resultString[input.Start] != ']') // list is not closed + if (input.IsEmpty || resultString[input.Start] != ']') // list is not closed { - ParseError("List not terminated", input); + ParseError(resultString, "List not terminated", input); rest = Span.Empty; return null; } @@ -1185,9 +1168,9 @@ private ValueListValue ParseValueList(Span input, out Span rest) /// /// list ==> "[" result ( "," result )* "]" /// - private ResultListValue ParseResultList(Span input, out Span rest) + private ResultListValue? ParseResultList(string resultString, Span input, out Span rest) { - var list = ParseResultList('[', ']', input, out rest); + var list = ParseResultList(resultString, '[', ']', input, out rest); if (list == null) { return null; @@ -1195,26 +1178,25 @@ private ResultListValue ParseResultList(Span input, out Span rest) return new ResultListValue(list); } - private void ParseError(string message, Span input) + private void ParseError(string resultString, string message, Span input) { - string result = CreateErrorMessageFromSpan(input); + string result = CreateErrorMessageFromSpan(resultString, input); Debug.Fail(message + ": " + result); Logger?.WriteLine(LogLevel.Error, String.Format(CultureInfo.CurrentCulture, "MI parsing error: {0}: \"{1}\"", message, result)); - } // The amount of characters to send to the UI upon an error. private static int PARSE_ERROR_MSG_LIMIT = 1000; - private string CreateErrorMessageFromSpan(Span input) + private string CreateErrorMessageFromSpan(string resultString, Span input) { if (input.Length > PARSE_ERROR_MSG_LIMIT) { input = new Span(input.Start, PARSE_ERROR_MSG_LIMIT); } - return input.Extract(_resultString); + return input.Extract(resultString); } } } diff --git a/src/MICore/PlatformUtilities.cs b/src/MICore/PlatformUtilities.cs index f833ff689..d5356aa7d 100755 --- a/src/MICore/PlatformUtilities.cs +++ b/src/MICore/PlatformUtilities.cs @@ -3,8 +3,8 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Runtime.InteropServices; +using ProcessStartInfo = global::System.Diagnostics.ProcessStartInfo; namespace MICore { @@ -73,13 +73,13 @@ public static bool IsLinux() } // Abstract API call to add an environment variable to a new process - public static void SetEnvironmentVariable(this ProcessStartInfo processStartInfo, string key, string value) + public static void SetEnvironmentVariable(this ProcessStartInfo processStartInfo, string key, string? value) { processStartInfo.Environment[key] = value; } // Abstract API call to add an environment variable to a new process - public static string GetEnvironmentVariable(this ProcessStartInfo processStartInfo, string key) + public static string? GetEnvironmentVariable(this ProcessStartInfo processStartInfo, string key) { if (processStartInfo.Environment.ContainsKey(key)) return processStartInfo.Environment[key]; @@ -99,7 +99,7 @@ public static string WindowsPathToUnixPath(string windowsPath) public static string PathToHostOSPath(string path) { - if (string.IsNullOrWhiteSpace(path)) + if (IsNullOrWhiteSpace(path)) { return path; } diff --git a/src/MICore/ProcessMonitor.cs b/src/MICore/ProcessMonitor.cs index 3e7acd0c2..35d43cddf 100644 --- a/src/MICore/ProcessMonitor.cs +++ b/src/MICore/ProcessMonitor.cs @@ -10,7 +10,7 @@ public class ProcessMonitor : IDisposable { private readonly TimeSpan _EXIT_POLL_DELTA = TimeSpan.FromMilliseconds(200); private int _processId; - private Timer _exitMonitorTimer; + private Timer? _exitMonitorTimer; public ProcessMonitor(int processId) { @@ -27,19 +27,19 @@ public void Start() _exitMonitorTimer = new Timer(MonitorForExit, null, TimeSpan.FromMilliseconds(0), _EXIT_POLL_DELTA); } - public event EventHandler ProcessExited; + public event EventHandler? ProcessExited; private bool HasExited() { return !UnixUtilities.IsProcessRunning(_processId); } - private void MonitorForExit(object o) + private void MonitorForExit(object? o) { if (HasExited()) { - _exitMonitorTimer.Dispose(); - ProcessExited?.Invoke(this, null); + _exitMonitorTimer?.Dispose(); + ProcessExited?.Invoke(this, EventArgs.Empty); } } diff --git a/src/MICore/RunInTerminalLauncher.cs b/src/MICore/RunInTerminalLauncher.cs index 546b10c0d..848b913f6 100644 --- a/src/MICore/RunInTerminalLauncher.cs +++ b/src/MICore/RunInTerminalLauncher.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Diagnostics; using System.Linq; using Microsoft.DebugEngineHost; @@ -13,9 +12,9 @@ namespace MICore { internal class RunInTerminalLauncher { - private string _title; + private readonly string _title; - private Dictionary _environment; + private readonly Dictionary _environment; /// /// @@ -25,7 +24,7 @@ internal class RunInTerminalLauncher public RunInTerminalLauncher(string title, ReadOnlyCollection envEntries) { _title = title; - _environment = new Dictionary(); + _environment = new Dictionary(); if (envEntries != null && envEntries.Any()) { @@ -41,7 +40,7 @@ public void Launch(List cmdArgs, bool useExternalConsole, Action l { if (HostRunInTerminal.IsRunInTerminalAvailable()) { - HostRunInTerminal.RunInTerminal(_title, string.Empty, useExternalConsole, cmdArgs, new ReadOnlyDictionary(_environment), launchCompleteAction, launchFailureAction); + HostRunInTerminal.RunInTerminal(_title, string.Empty, useExternalConsole, cmdArgs, new ReadOnlyDictionary(_environment), launchCompleteAction, launchFailureAction); } } } diff --git a/src/MICore/Transports/ClientServerTransport.cs b/src/MICore/Transports/ClientServerTransport.cs index 88cee7c06..87fe8a4c9 100644 --- a/src/MICore/Transports/ClientServerTransport.cs +++ b/src/MICore/Transports/ClientServerTransport.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Text; using System.Threading; -using System.Diagnostics; using System.IO; using System.Collections; using System.Text.RegularExpressions; @@ -26,7 +25,7 @@ public ClientServerTransport(ITransport clientTransport, ISignalingTransport ser _serverTransport = serverTransport; } - public void Init(ITransportCallback transportCallback, LaunchOptions options, Logger logger, HostWaitLoop waitLoop = null) + public void Init(ITransportCallback transportCallback, LaunchOptions options, Logger logger, HostWaitLoop? waitLoop = null) { _launchTimeout = ((LocalLaunchOptions)options).ServerLaunchTimeout; _serverTransport.Init(transportCallback, options, logger, waitLoop); diff --git a/src/MICore/Transports/ITransport.cs b/src/MICore/Transports/ITransport.cs index ca377a865..77ae05dea 100644 --- a/src/MICore/Transports/ITransport.cs +++ b/src/MICore/Transports/ITransport.cs @@ -13,7 +13,7 @@ namespace MICore public interface ITransport { - void Init(ITransportCallback transportCallback, LaunchOptions options, Logger logger, HostWaitLoop waitLoop = null); + void Init(ITransportCallback transportCallback, LaunchOptions options, Logger logger, HostWaitLoop? waitLoop = null); void Send(string cmd); void Close(); bool IsClosed { get; } @@ -71,7 +71,7 @@ public interface ITransportCallback /// Fired when either the target process exits or when the stdout stream is closed. /// /// [Optional] exit code from the target process. null if unknown. - void OnDebuggerProcessExit(string exitCode); + void OnDebuggerProcessExit(string? exitCode); /// /// Appends a line of text to the initialization log which is dumped to the output diff --git a/src/MICore/Transports/LocalTransport.cs b/src/MICore/Transports/LocalTransport.cs index 85c3e56bd..5efa8a680 100755 --- a/src/MICore/Transports/LocalTransport.cs +++ b/src/MICore/Transports/LocalTransport.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Text; using System.Threading; -using System.Diagnostics; using System.IO; using System.Collections; using System.Runtime.InteropServices; @@ -21,7 +20,7 @@ public LocalTransport() public override void InitStreams(LaunchOptions options, out StreamReader reader, out StreamWriter writer) { LocalLaunchOptions localOptions = (LocalLaunchOptions)options; - string miDebuggerDir = System.IO.Path.GetDirectoryName(localOptions.MIDebuggerPath); + string miDebuggerDir = System.IO.Path.GetDirectoryName(localOptions.MIDebuggerPath) ?? string.Empty; Process proc = new Process(); proc.StartInfo.FileName = localOptions.MIDebuggerPath; @@ -35,8 +34,8 @@ public override void InitStreams(LaunchOptions options, out StreamReader reader, if (PlatformUtilities.IsWindows() && options.DebuggerMIMode == MIMode.Gdb) { - string path = proc.StartInfo.GetEnvironmentVariable("PATH"); - path = (string.IsNullOrEmpty(path) ? miDebuggerDir : path + ";" + miDebuggerDir); + string? path = proc.StartInfo.GetEnvironmentVariable("PATH"); + path = (IsNullOrEmpty(path) ? miDebuggerDir : path + ";" + miDebuggerDir); proc.StartInfo.SetEnvironmentVariable("PATH", path); } @@ -47,7 +46,7 @@ public override void InitStreams(LaunchOptions options, out StreamReader reader, // Allow to execute custom commands before launching debugger. // For ex., instructing GDB not to break for certain signals - if (options.DebuggerMIMode == MIMode.Gdb && !string.IsNullOrWhiteSpace(options.WorkingDirectory)) + if (options.DebuggerMIMode == MIMode.Gdb && !IsNullOrWhiteSpace(options.WorkingDirectory)) { var gdbInitFile = Path.Combine(options.WorkingDirectory, ".gdbinit"); if (File.Exists(gdbInitFile)) diff --git a/src/MICore/Transports/MockTransport.cs b/src/MICore/Transports/MockTransport.cs index d041fa357..691214edf 100644 --- a/src/MICore/Transports/MockTransport.cs +++ b/src/MICore/Transports/MockTransport.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Text; using System.Threading; -using System.Diagnostics; using System.IO; using Microsoft.DebugEngineHost; @@ -20,12 +19,12 @@ namespace MICore public class MockTransport : ITransport { - private ITransportCallback _callback; - private Thread _thread; - private string _nextCommand; + private ITransportCallback? _callback; + private Thread? _thread; + private string? _nextCommand; private bool _bQuit; - private TextReader _reader; - private AutoResetEvent _commandEvent; + private TextReader? _reader; + private AutoResetEvent? _commandEvent; private string _filename; private int _lineNumber; @@ -34,7 +33,7 @@ public MockTransport(string logfilename) _filename = logfilename; } - public void Init(ITransportCallback transportCallback, LaunchOptions options, Logger logger, HostWaitLoop waitLoop = null) + public void Init(ITransportCallback transportCallback, LaunchOptions options, Logger logger, HostWaitLoop? waitLoop = null) { _bQuit = false; _callback = transportCallback; @@ -47,6 +46,8 @@ public void Init(ITransportCallback transportCallback, LaunchOptions options, Lo public void Send(string cmd) { + if (_commandEvent is null) + throw new InvalidOperationException("MockTransport has not been initialized"); Debug.Assert(_nextCommand == null); _nextCommand = cmd; _commandEvent.Set(); @@ -55,7 +56,7 @@ public void Send(string cmd) public void Close() { _bQuit = true; - if (_thread != Thread.CurrentThread) + if (_thread != null && _thread != Thread.CurrentThread) { _thread.Join(); } @@ -70,6 +71,10 @@ public int DebuggerPid private void TransportLoop() { + Debug.Assert(_reader is not null, "Should be impossible -- TransportLoop cannot run until Init is called"); + Debug.Assert(_commandEvent is not null, "Should be impossible -- TransportLoop cannot run until Init is called"); + Debug.Assert(_callback is not null, "Should be impossible -- TransportLoop cannot run until Init is called"); + _lineNumber = 0; // discard first line @@ -78,14 +83,14 @@ private void TransportLoop() while (!_bQuit) { - string line = _reader.ReadLine(); + string? line = _reader.ReadLine(); if (line == null) { break; } line = line.TrimEnd(); _lineNumber++; - Debug.WriteLine("#{0}:{1}", _lineNumber, line); + Debug.WriteLine($"#{_lineNumber}:{line}"); if (line[0] == '-') { diff --git a/src/MICore/Transports/PipeTransport.cs b/src/MICore/Transports/PipeTransport.cs index c456ac590..a78458578 100644 --- a/src/MICore/Transports/PipeTransport.cs +++ b/src/MICore/Transports/PipeTransport.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Text; using System.Threading; -using System.Diagnostics; using System.IO; using System.Collections; using System.Threading.Tasks; @@ -23,15 +22,15 @@ public class PipeTransport : StreamTransport private static readonly object _lock = new object(); - private Process _process; - private StreamReader _stdErrReader; + private Process? _process; + private StreamReader? _stdErrReader; private int _remainingReaders; private ManualResetEvent _allReadersDone = new ManualResetEvent(false); private bool _killOnClose; private bool _filterStderr; private int _debuggerPid = -1; - private string _pipePath; - private string _cmdArgs; + private string? _pipePath; + private string? _cmdArgs; public PipeTransport(bool killOnClose = false, bool filterStderr = false, bool filterStdout = false) : base(filterStdout) { @@ -108,8 +107,8 @@ public override void InitStreams(LaunchOptions options, out StreamReader reader, { PipeLaunchOptions pipeOptions = (PipeLaunchOptions)options; - string workingDirectory = pipeOptions.PipeCwd; - if (!string.IsNullOrWhiteSpace(workingDirectory)) + string? workingDirectory = pipeOptions.PipeCwd; + if (!IsNullOrWhiteSpace(workingDirectory)) { if (!LocalLaunchOptions.CheckDirectoryPath(workingDirectory)) { @@ -119,14 +118,14 @@ public override void InitStreams(LaunchOptions options, out StreamReader reader, else { workingDirectory = Path.GetDirectoryName(pipeOptions.PipePath); - if (!LocalLaunchOptions.CheckDirectoryPath(workingDirectory)) + if (workingDirectory is null || !LocalLaunchOptions.CheckDirectoryPath(workingDirectory)) { // If provided PipeCwd is not an absolute path, the working directory will be set to null. workingDirectory = null; } } - if (string.IsNullOrWhiteSpace(pipeOptions.PipePath)) + if (IsNullOrWhiteSpace(pipeOptions.PipePath)) { throw new ArgumentException(MICoreResources.Error_EmptyPipePath); } @@ -229,7 +228,7 @@ protected override void OnReadStreamAborted() try { - if (_process.WaitForExit(1000)) + if (_process is not null && _process.WaitForExit(1000)) { // If the pipe process has already exited, or is just about to exit, we want to send the abort event from OnProcessExit // instead of from here since that will have access to stderr @@ -251,7 +250,7 @@ private async void AsyncReadFromStream(StreamReader stream, Action lineH { while (true) { - string line = await stream.ReadLineAsync(); + string? line = await stream.ReadLineAsync(); if (line == null) break; @@ -266,11 +265,13 @@ private async void AsyncReadFromStream(StreamReader stream, Action lineH private async void AsyncReadFromStdError() { + Debug.Assert(_stdErrReader is not null, "Should be impossible. AsyncReadFromStdError started before _stdErrReader was assigned."); + StreamReader stdErrReader = _stdErrReader; try { while (true) { - string line = await _stdErrReader.ReadLineAsync(); + string? line = await stdErrReader.ReadLineAsync(); if (line == null) break; @@ -279,7 +280,7 @@ private async void AsyncReadFromStdError() line = FilterLine(line); } - if (!string.IsNullOrWhiteSpace(line)) + if (!IsNullOrWhiteSpace(line)) { this.Callback.OnStdErrorLine(line); } @@ -304,7 +305,7 @@ private void DecrementReaders() } } - private void OnProcessExit(object sender, EventArgs e) + private void OnProcessExit(object? sender, EventArgs e) { // Wait until 'Init' gets a chance to set m_Reader/m_Writer before sending up the debugger exit event if (_reader == null || _writer == null) @@ -325,7 +326,8 @@ private void OnProcessExit(object sender, EventArgs e) // We are sometimes seeing m_process throw InvalidOperationExceptions by the time we get here. // Attempt to get the real exit code, if we can't, still log the message with unknown exit code. - string exitCode = null; + Debug.Assert(_process is not null, "Should be impossible - OnProcessExit is an event handler registered on the process"); + string? exitCode = null; try { exitCode = string.Format(CultureInfo.InvariantCulture, "{0} (0x{0:X})", _process.ExitCode); @@ -349,9 +351,11 @@ private void OnProcessExit(object sender, EventArgs e) private int WrappedExecuteSyncCommand(string commandDescription, string commandText, int timeout) { + Debug.Assert(_cmdArgs is not null && _pipePath is not null, "Should be impossible -- cannot send commands until Init"); + int exitCode = -1; - string output = null; - string error = null; + string output; + string error; string pipeArgs = PipeLaunchOptions.ReplaceDebuggerCommandToken(_cmdArgs, commandText, true); string fullCommand = string.Format(CultureInfo.InvariantCulture, "{0} {1}", _pipePath, pipeArgs); @@ -375,14 +379,14 @@ private int WrappedExecuteSyncCommand(string commandDescription, string commandT public override int ExecuteSyncCommand(string commandDescription, string commandText, int timeout, out string output, out string error) { - output = null; - error = null; + Debug.Assert(_cmdArgs is not null && _pipePath is not null, "Should be impossible -- cannot send commands until Init"); + int exitCode = -1; - Process proc = new Process(); + using Process proc = new Process(); proc.StartInfo.FileName = _pipePath; proc.StartInfo.Arguments = PipeLaunchOptions.ReplaceDebuggerCommandToken(_cmdArgs, commandText, true); - Logger.WriteLine(LogLevel.Verbose, "Running process {0} {1}", proc.StartInfo.FileName, proc.StartInfo.Arguments); + Logger?.WriteLine(LogLevel.Verbose, "Running process {0} {1}", proc.StartInfo.FileName, proc.StartInfo.Arguments); proc.StartInfo.WorkingDirectory = System.IO.Path.GetDirectoryName(_pipePath); proc.EnableRaisingEvents = false; proc.StartInfo.RedirectStandardInput = false; @@ -391,13 +395,28 @@ public override int ExecuteSyncCommand(string commandDescription, string command proc.StartInfo.UseShellExecute = false; proc.StartInfo.CreateNoWindow = true; proc.Start(); - proc.WaitForExit(timeout); - exitCode = proc.ExitCode; + Task stdOutTask = proc.StandardOutput.ReadToEndAsync(); + Task stdErrTask = proc.StandardError.ReadToEndAsync(); + if (proc.WaitForExit(timeout)) + { + exitCode = proc.ExitCode; + output = stdOutTask.Result; + error = stdErrTask.Result; - output = proc.StandardOutput.ReadToEnd(); - error = proc.StandardError.ReadToEnd(); + return exitCode; + } + else + { + try + { + proc.Kill(); + } + catch + { + } - return exitCode; + throw new TimeoutException(); + } } public override bool CanExecuteCommand() diff --git a/src/MICore/Transports/RunInTerminalTransport.cs b/src/MICore/Transports/RunInTerminalTransport.cs index a9fbb8fc5..ada0b141e 100644 --- a/src/MICore/Transports/RunInTerminalTransport.cs +++ b/src/MICore/Transports/RunInTerminalTransport.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; using System.Collections.Generic; -using System.Diagnostics; using System.Globalization; using System.IO; using System.IO.Pipes; @@ -18,16 +17,16 @@ namespace MICore public class RunInTerminalTransport : StreamTransport { private int _debuggerPid; - private StreamReader _pidReader; + private StreamReader? _pidReader; - private ProcessMonitor _shellProcessMonitor; + private ProcessMonitor? _shellProcessMonitor; private CancellationTokenSource _streamReadPidCancellationTokenSource = new CancellationTokenSource(); - private Task _waitForConnection = null; + private Task? _waitForConnection = null; - private StreamWriter _commandStream = null; - private StreamReader _outputStream = null; + private StreamWriter? _commandStream; + private StreamReader? _outputStream; - private StreamReader _errorStream = null; + private StreamReader? _errorStream = null; public override int DebuggerPid { @@ -37,9 +36,9 @@ public override int DebuggerPid } } - public override async void Init(ITransportCallback transportCallback, LaunchOptions options, Logger logger, HostWaitLoop waitLoop = null) + public override async void Init(ITransportCallback transportCallback, LaunchOptions options, Logger logger, HostWaitLoop? waitLoop = null) { - LocalLaunchOptions localOptions = options as LocalLaunchOptions; + LocalLaunchOptions localOptions = (LocalLaunchOptions)options; Encoding encNoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); @@ -66,7 +65,7 @@ public override async void Init(ITransportCallback transportCallback, LaunchOpti _pidReader = new StreamReader(pidPipe, encNoBom, false, UnixUtilities.StreamBufferSize); string thisModulePath = typeof(RunInTerminalTransport).GetTypeInfo().Assembly.ManifestModule.FullyQualifiedName; - string launchCommand = Path.Combine(Path.GetDirectoryName(thisModulePath), "WindowsDebugLauncher.exe"); + string launchCommand = Path.Combine(Path.GetDirectoryName(thisModulePath) ?? string.Empty, "WindowsDebugLauncher.exe"); if (!File.Exists(launchCommand)) { @@ -112,13 +111,13 @@ public override async void Init(ITransportCallback transportCallback, LaunchOpti string debuggeeDir; if (Path.IsPathRooted(options.ExePath) && File.Exists(options.ExePath)) { - debuggeeDir = Path.GetDirectoryName(options.ExePath); + debuggeeDir = Path.GetDirectoryName(options.ExePath) ?? string.Empty; } else { // If we don't know where the app is, default to HOME, and if we somehow can't get that, go with the root directory. - debuggeeDir = Environment.GetEnvironmentVariable("HOME"); - if (string.IsNullOrEmpty(debuggeeDir)) + debuggeeDir = Environment.GetEnvironmentVariable("HOME") ?? string.Empty; + if (IsNullOrEmpty(debuggeeDir)) debuggeeDir = "/"; } @@ -132,7 +131,7 @@ public override async void Init(ITransportCallback transportCallback, LaunchOpti debuggerCmd, localOptions.GetMiDebuggerArgs()); - logger?.WriteTextBlock(LogLevel.Verbose, "DbgCmd:", launchDebuggerCommand); + logger.WriteTextBlock(LogLevel.Verbose, "DbgCmd:", launchDebuggerCommand); using (FileStream dbgCmdStream = new FileStream(dbgCmdScript, FileMode.CreateNew)) using (StreamWriter dbgCmdWriter = new StreamWriter(dbgCmdStream, encNoBom) { AutoFlush = true }) @@ -176,7 +175,7 @@ public override async void Init(ITransportCallback transportCallback, LaunchOpti throw new InvalidOperationException(error); }, logger); - logger?.WriteLine(LogLevel.Verbose, "Wait for connection completion."); + logger.WriteLine(LogLevel.Verbose, "Wait for connection completion."); if (_waitForConnection != null) { @@ -198,7 +197,7 @@ public override async void Init(ITransportCallback transportCallback, LaunchOpti private static string GetOSXLaunchScript() { string thisModulePath = typeof(RunInTerminalTransport).GetTypeInfo().Assembly.ManifestModule.FullyQualifiedName; - string launchScript = Path.Combine(Path.GetDirectoryName(thisModulePath), "osxlaunchhelper.scpt"); + string launchScript = Path.Combine(Path.GetDirectoryName(thisModulePath) ?? string.Empty, "osxlaunchhelper.scpt"); if (!File.Exists(launchScript)) { string message = string.Format(CultureInfo.CurrentCulture, MICoreResources.Error_InternalFileMissing, launchScript); @@ -214,7 +213,7 @@ private void LogDebuggerErrors() { while (!_streamReadPidCancellationTokenSource.IsCancellationRequested) { - string line = this.GetLineFromStream(_errorStream, _streamReadPidCancellationTokenSource.Token); + string? line = this.GetLineFromStream(_errorStream, _streamReadPidCancellationTokenSource.Token); if (line == null) break; Logger?.WriteTextBlock(LogLevel.Error, "dbgerr:", line); @@ -224,12 +223,13 @@ private void LogDebuggerErrors() public override void InitStreams(LaunchOptions options, out StreamReader reader, out StreamWriter writer) { + Debug.Assert(_commandStream is not null && _outputStream is not null, "Should be impossible -- `Init` should be called before `InitStreams`"); // Mono seems to stop responding when the debugger sends a large response unless we specify a larger buffer here writer = _commandStream; reader = _outputStream; } - private Action debuggerPidCallback; + private Action? debuggerPidCallback; public void RegisterDebuggerPidCallback(Action pidCallback) { debuggerPidCallback = pidCallback; @@ -240,10 +240,12 @@ private void LaunchSuccess(int? pid) if (_pidReader != null) { int shellPid; - Task readShellPidTask = _pidReader.ReadLineAsync(); + Task readShellPidTask = _pidReader.ReadLineAsync(); if (readShellPidTask.Wait(TimeSpan.FromSeconds(10))) { - shellPid = int.Parse(readShellPidTask.Result, CultureInfo.InvariantCulture); + string? shellPidLine = readShellPidTask.Result; + Debug.Assert(shellPidLine is not null, "Should be impossible. Shell pid line was null."); + shellPid = int.Parse(shellPidLine, CultureInfo.InvariantCulture); // Used for testing Logger?.WriteLine(LogLevel.Verbose, string.Concat("ShellPid=", shellPid)); } @@ -268,11 +270,13 @@ private void LaunchSuccess(int? pid) shellProcess.Exited += ShellExited; } - Task readDebuggerPidTask = _pidReader.ReadLineAsync(); + Task readDebuggerPidTask = _pidReader.ReadLineAsync(); try { readDebuggerPidTask.Wait(_streamReadPidCancellationTokenSource.Token); - _debuggerPid = int.Parse(readDebuggerPidTask.Result, CultureInfo.InvariantCulture); + string? debuggerPidLine = readDebuggerPidTask.Result; + Debug.Assert(debuggerPidLine is not null, "Should be impossible. Debugger pid line was null."); + _debuggerPid = int.Parse(debuggerPidLine, CultureInfo.InvariantCulture); } catch (OperationCanceledException) { @@ -289,7 +293,7 @@ private void LaunchSuccess(int? pid) } } - private void ShellExited(object sender, EventArgs e) + private void ShellExited(object? sender, EventArgs e) { if (sender is ProcessMonitor) { diff --git a/src/MICore/Transports/ServerTransport.cs b/src/MICore/Transports/ServerTransport.cs index f68923970..c973f5643 100644 --- a/src/MICore/Transports/ServerTransport.cs +++ b/src/MICore/Transports/ServerTransport.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Text; using System.Threading; -using System.Diagnostics; using System.IO; using System.Collections; using System.Text.RegularExpressions; @@ -15,8 +14,8 @@ namespace MICore { public class ServerTransport : PipeTransport, ISignalingTransport { - private string _startPattern; - public string _messagePrefix; + private string? _startPattern; + public string? _messagePrefix; private bool _started; public ManualResetEvent StartedEvent { get; } @@ -30,7 +29,7 @@ public ServerTransport(bool killOnClose, bool filterStderr = false, bool filterS public override void InitStreams(LaunchOptions options, out StreamReader reader, out StreamWriter writer) { LocalLaunchOptions localOptions = (LocalLaunchOptions)options; - string miDebuggerDir = System.IO.Path.GetDirectoryName(localOptions.MIDebuggerPath); + string miDebuggerDir = System.IO.Path.GetDirectoryName(localOptions.MIDebuggerPath) ?? string.Empty; Process proc = new Process(); proc.StartInfo.FileName = localOptions.DebugServer; @@ -42,9 +41,9 @@ public override void InitStreams(LaunchOptions options, out StreamReader reader, InitProcess(proc, out reader, out writer); } - protected override string FilterLine(string line) + protected override string? FilterLine(string line) { - if (!_started && (String.IsNullOrWhiteSpace(_startPattern) || Regex.IsMatch(line, _startPattern, RegexOptions.None, new TimeSpan(0, 0, 0, 0, 10) /* 10 ms */))) + if (!_started && (IsNullOrWhiteSpace(_startPattern) || Regex.IsMatch(line, _startPattern, RegexOptions.None, new TimeSpan(0, 0, 0, 0, 10) /* 10 ms */))) { _started = true; StartedEvent.Set(); diff --git a/src/MICore/Transports/StreamTransport.cs b/src/MICore/Transports/StreamTransport.cs index 9de2f3c32..d63aa8979 100644 --- a/src/MICore/Transports/StreamTransport.cs +++ b/src/MICore/Transports/StreamTransport.cs @@ -4,7 +4,6 @@ using Microsoft.DebugEngineHost; using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Linq; using System.Text; @@ -15,16 +14,16 @@ namespace MICore { public abstract class StreamTransport : ITransport { - private ITransportCallback _callback; - private Thread _thread; + private ITransportCallback? _callback; + private Thread? _thread; private bool _bQuit; private CancellationTokenSource _streamReadCancellationTokenSource = new CancellationTokenSource(); - protected StreamReader _reader; - protected StreamWriter _writer; + protected StreamReader? _reader; + protected StreamWriter? _writer; private bool _filterStdout; private Object _locker = new object(); - protected Logger Logger + protected Logger? Logger { get; private set; } @@ -40,7 +39,7 @@ protected StreamTransport(bool filterStdout) public abstract void InitStreams(LaunchOptions options, out StreamReader reader, out StreamWriter writer); protected virtual string GetThreadName() { return "MI.StreamTransport"; } - public virtual void Init(ITransportCallback transportCallback, LaunchOptions options, Logger logger, HostWaitLoop waitLoop = null) + public virtual void Init(ITransportCallback transportCallback, LaunchOptions options, Logger logger, HostWaitLoop? waitLoop = null) { Logger = logger; _callback = transportCallback; @@ -55,7 +54,7 @@ private void StartThread(string name) _thread.Start(); } - protected virtual string FilterLine(string line) + protected virtual string? FilterLine(string line) { return line; } @@ -66,7 +65,7 @@ private void TransportLoop() { while (!_bQuit) { - string line = GetLine(); + string? line = GetLine(); if (line == null) break; @@ -80,9 +79,9 @@ private void TransportLoop() { line = FilterLine(line); } - if (!String.IsNullOrWhiteSpace(line) && !line.StartsWith("-", StringComparison.Ordinal)) + if (!IsNullOrWhiteSpace(line) && !line.StartsWith("-", StringComparison.Ordinal)) { - _callback.OnStdOutLine(line); + Callback.OnStdOutLine(line); } } catch (ObjectDisposedException) @@ -106,11 +105,14 @@ private void TransportLoop() // If we are shutting down without notice from the debugger (e.g., the terminal // where the debugger was hosted was closed), at this point it's possible that // there is a thread blocked doing a read() syscall. - ForceDisposeStreamReader(_reader); + if (_reader != null) + { + ForceDisposeStreamReader(_reader); + } try { - _writer.Dispose(); + _writer?.Dispose(); _writer = null; } catch @@ -133,7 +135,7 @@ protected virtual void OnReadStreamAborted() { try { - _callback.OnDebuggerProcessExit(null); + Callback.OnDebuggerProcessExit(null); } catch { @@ -142,7 +144,7 @@ protected virtual void OnReadStreamAborted() } protected void Echo(string cmd) { - if (!String.IsNullOrWhiteSpace(cmd)) + if (!IsNullOrWhiteSpace(cmd)) { Logger?.WriteLine(LogLevel.Verbose, "<-" + cmd); Logger?.Flush(); @@ -155,16 +157,17 @@ protected void Echo(string cmd) } } - private string GetLine() + private string? GetLine() { + Debug.Assert(_reader is not null, "Should be impossible - GetLine is only called from the transport loop started after Init"); return GetLineFromStream(_reader, _streamReadCancellationTokenSource.Token); } - protected string GetLineFromStream(StreamReader reader, CancellationToken token) + protected string? GetLineFromStream(StreamReader reader, CancellationToken token) { try { - Task task = reader.ReadLineAsync(); + Task task = reader.ReadLineAsync(); task.Wait(token); return task.Result; } @@ -217,7 +220,11 @@ public bool IsClosed protected ITransportCallback Callback { - get { return _callback; } + get + { + Debug.Assert(_callback is not null, "Should be impossible - Callback is only used after Init completes"); + return _callback; + } } /// diff --git a/src/MICore/Transports/TcpTransport.cs b/src/MICore/Transports/TcpTransport.cs index 72bb93145..398ea9fe5 100755 --- a/src/MICore/Transports/TcpTransport.cs +++ b/src/MICore/Transports/TcpTransport.cs @@ -15,7 +15,7 @@ namespace MICore { public class TcpTransport : StreamTransport { - private TcpClient _client; + private TcpClient? _client; public TcpTransport() { @@ -40,7 +40,7 @@ public override void InitStreams(LaunchOptions options, out StreamReader reader, if (tcpOptions.ServerCertificateValidationCallback == null) { //if no callback specified, accept any certificate - callback = delegate (object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) + callback = delegate (object sender, X509Certificate? certificate, X509Chain? chain, SslPolicyErrors sslPolicyErrors) { return sslPolicyErrors == SslPolicyErrors.None; }; @@ -48,7 +48,7 @@ public override void InitStreams(LaunchOptions options, out StreamReader reader, else { //else use the callback specified - callback = (object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) => tcpOptions.ServerCertificateValidationCallback(sender, certificate, chain, sslPolicyErrors); + callback = (object sender, X509Certificate? certificate, X509Chain? chain, SslPolicyErrors sslPolicyErrors) => tcpOptions.ServerCertificateValidationCallback(sender, certificate, chain, sslPolicyErrors); } var certStore = new X509Store(StoreName.My, StoreLocation.CurrentUser); @@ -83,7 +83,7 @@ public override int DebuggerPid public override void Close() { base.Close(); - ((IDisposable)_client).Dispose(); + ((IDisposable?)_client)?.Dispose(); } public override int ExecuteSyncCommand(string commandDescription, string commandText, int timeout, out string output, out string error) diff --git a/src/MICore/Transports/UnixShellPortTransport.cs b/src/MICore/Transports/UnixShellPortTransport.cs index 95cd72933..22de4e6ed 100644 --- a/src/MICore/Transports/UnixShellPortTransport.cs +++ b/src/MICore/Transports/UnixShellPortTransport.cs @@ -17,20 +17,20 @@ namespace MICore public class UnixShellPortTransport : ITransport, IDebugUnixShellCommandCallback { private readonly object _closeLock = new object(); - private ITransportCallback _callback; - private Logger _logger; - private string _startRemoteDebuggerCommand; - private IDebugUnixShellAsyncCommand _asyncCommand; + private ITransportCallback? _callback; + private Logger? _logger; + private string? _startRemoteDebuggerCommand; + private IDebugUnixShellAsyncCommand? _asyncCommand; private bool _bQuit; private bool _debuggerLaunched = false; - private UnixShellPortLaunchOptions _launchOptions; + private UnixShellPortLaunchOptions? _launchOptions; private const string ErrorPrefix = "Error:"; private class KillCommandCallback: IDebugUnixShellCommandCallback { - private readonly Logger _logger; - public KillCommandCallback(Logger logger) + private readonly Logger? _logger; + public KillCommandCallback(Logger? logger) { this._logger = logger; } @@ -49,7 +49,7 @@ public UnixShellPortTransport() { } - public void Init(ITransportCallback transportCallback, LaunchOptions options, Logger logger, HostWaitLoop waitLoop = null) + public void Init(ITransportCallback transportCallback, LaunchOptions options, Logger logger, HostWaitLoop? waitLoop = null) { _launchOptions = (UnixShellPortLaunchOptions)options; _callback = transportCallback; @@ -68,12 +68,13 @@ public void Close() return; _bQuit = true; - _asyncCommand.Abort(); + _asyncCommand?.Abort(); } } public void Send(string cmd) { + Debug.Assert(_asyncCommand is not null, "Should be impossible - Send is only called by the engine after Init"); _logger?.WriteLine(LogLevel.Verbose, "<-" + cmd); _logger?.Flush(); _asyncCommand.WriteLine(cmd); @@ -94,12 +95,14 @@ bool ITransport.IsClosed void IDebugUnixShellCommandCallback.OnOutputLine(string line) { + Debug.Assert(_callback is not null, "Should be impossible - OnOutputLine is a callback from the command started in Init which sets _callback"); + if (!_debuggerLaunched) { _debuggerLaunched = true; } - if (!string.IsNullOrEmpty(line)) + if (!IsNullOrEmpty(line)) { _callback.OnStdOutLine(line); } @@ -110,6 +113,8 @@ void IDebugUnixShellCommandCallback.OnOutputLine(string line) void IDebugUnixShellCommandCallback.OnExit(string exitCode) { + Debug.Assert(_callback is not null, "Should be impossible - OnExit is a callback from the command started in Init which sets _callback"); + if (!_bQuit) { _callback.AppendToInitializationLog(string.Format(CultureInfo.InvariantCulture, "{0} exited with code {1}.", _startRemoteDebuggerCommand, exitCode ?? "???")); @@ -128,8 +133,9 @@ void IDebugUnixShellCommandCallback.OnExit(string exitCode) public int ExecuteSyncCommand(string commandDescription, string commandText, int timeout, out string output, out string error) { + Debug.Assert(_launchOptions is not null, "Should be impossible - ExecuteSyncCommand is only called during active debugging after Init"); int errorCode = -1; - error = null; // In SSH transport, stderr is printed on stdout. + error = string.Empty; // In SSH transport, stderr is printed on stdout. _launchOptions.UnixPort.ExecuteSyncCommand(commandDescription, commandText, out output, timeout, out errorCode); return errorCode; } @@ -141,6 +147,9 @@ public bool CanExecuteCommand() public bool Interrupt(int pid) { + Debug.Assert(_launchOptions is not null, "Should be impossible - Interrupt is only called during active debugging after Init"); + Debug.Assert(_callback is not null, "Should be impossible - Interrupt is only called during active debugging after Init"); + string killCmd = string.Format(CultureInfo.InvariantCulture, "/bin/sh -c \"kill -5 {0}\"", pid); try diff --git a/src/MICore/UnixUtilities.cs b/src/MICore/UnixUtilities.cs index 36332aeef..ae251487e 100644 --- a/src/MICore/UnixUtilities.cs +++ b/src/MICore/UnixUtilities.cs @@ -4,7 +4,6 @@ using Microsoft.DebugEngineHost; using System; using System.Collections.Generic; -using System.Diagnostics; using System.Globalization; using System.IO; using System.Text; @@ -104,7 +103,7 @@ internal static string GetDebuggerCommand(LocalLaunchOptions localOptions) } } - internal static string MakeFifo(string identifier = null, Logger logger = null) + internal static string MakeFifo(string? identifier = null, Logger? logger = null) { string path = Path.Combine(Path.GetTempPath(), Utilities.GetMIEngineTemporaryFilename(identifier)); @@ -200,9 +199,9 @@ public static bool IsBinarySigned(string filePath, Logger logger) return p.ExitCode == 0; } - internal static void OutputNonEmptyString(string str, string prefix, Logger logger) + internal static void OutputNonEmptyString(string? str, string prefix, Logger logger) { - if (!String.IsNullOrWhiteSpace(str) && logger != null) + if (!IsNullOrWhiteSpace(str) && logger != null) { logger.WriteLine(LogLevel.Verbose, prefix + str); } @@ -223,7 +222,7 @@ internal static void KillProcessTree(Process p) ps.StartInfo.RedirectStandardOutput = true; ps.StartInfo.UseShellExecute = false; ps.Start(); - string line; + string? line; List> processAndParent = new List>(); char[] whitespace = new char[] { ' ', '\t' }; while ((line = ps.StandardOutput.ReadLine()) != null) diff --git a/src/MICore/Utilities.cs b/src/MICore/Utilities.cs index 6a418a518..270742437 100644 --- a/src/MICore/Utilities.cs +++ b/src/MICore/Utilities.cs @@ -11,10 +11,10 @@ internal static class Utilities { private const string TempNamePrefix = "Microsoft-MIEngine-"; private const string Separator = "-"; - internal static string GetMIEngineTemporaryFilename(string identifier = null) + internal static string GetMIEngineTemporaryFilename(string? identifier = null) { // add the identifier + separator if the identifier exists - string optionalIdentifier = string.IsNullOrEmpty(identifier) ? string.Empty : identifier + Separator; + string optionalIdentifier = IsNullOrEmpty(identifier) ? string.Empty : identifier + Separator; string filename = String.Concat(TempNamePrefix, optionalIdentifier, Path.GetRandomFileName()); return filename; From a5d90d82d70a67e5b32bf49609239d8ba3b11eb8 Mon Sep 17 00:00:00 2001 From: SachinM123 <114114188+SachinM123@users.noreply.github.com> Date: Thu, 2 Jul 2026 09:49:56 -0700 Subject: [PATCH 13/25] Fix race condition monitor thread in RegistryMonitor (#1604) This PR fixes a race condition in Microsoft.DebugEngineHost.RegisteryMonitor where if `Stop` was called before the monitor thread started, it would never shutdown. This fixes https://github.com/microsoft/MIEngine/issues/1593 --- src/DebugEngineHost/RegistryMonitor.cs | 31 +++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/src/DebugEngineHost/RegistryMonitor.cs b/src/DebugEngineHost/RegistryMonitor.cs index d3e53410f..2fdc26e34 100644 --- a/src/DebugEngineHost/RegistryMonitor.cs +++ b/src/DebugEngineHost/RegistryMonitor.cs @@ -72,6 +72,15 @@ public RegistryMonitor(HostConfigurationSection section, bool watchSubtree, ILog public void Start() { + lock (_stopLock) + { + if (_stoppedEvent != null) + { + throw new InvalidOperationException("RegistryMonitor already started."); + } + _isStopped = false; + _stoppedEvent = new AutoResetEvent(false); + } Thread registryMonitor = new Thread(Monitor); registryMonitor.IsBackground = true; registryMonitor.Name = "Microsoft.DebugEngineHost.RegistryMonitor"; @@ -97,7 +106,15 @@ private void Monitor() bool stopped = false; try { - _stoppedEvent = new AutoResetEvent(false); + // Ensure stop isn't requested before we create/wait on events. + lock (_stopLock) + { + if (_isStopped) + { + return; + } + } + using (AutoResetEvent registryChangedEvent = new AutoResetEvent(false)) { IntPtr handle = registryChangedEvent.SafeWaitHandle.DangerousGetHandle(); @@ -111,7 +128,9 @@ private void Monitor() { while (!stopped) { - int waitResult = WaitHandle.WaitAny(new WaitHandle[] { _stoppedEvent, registryChangedEvent }); + AutoResetEvent? stoppedEvent = _stoppedEvent; + Debug.Assert(stoppedEvent is not null, "Should be impossible - this code can only run after `Start` is called."); + int waitResult = WaitHandle.WaitAny(new WaitHandle[] { stoppedEvent, registryChangedEvent }); if (waitResult == 0) { @@ -133,9 +152,11 @@ private void Monitor() } finally { - _stoppedEvent?.Dispose(); - _stoppedEvent = null; - + lock (_stopLock) + { + _stoppedEvent?.Dispose(); + _stoppedEvent = null; + } _section.Dispose(); } } From bb0255229398101c7bcf6cc97edca6c87261dae5 Mon Sep 17 00:00:00 2001 From: tieo <65707274+tieo@users.noreply.github.com> Date: Mon, 6 Jul 2026 20:44:21 +0200 Subject: [PATCH 14/25] Don't expose a memory reference for non-pointer evaluation results (#1601) Gate the memoryReference on pointer and array types in OpenDebugAD7 A non-pointer scalar shown in a data tip or the Variables view carried a memoryReference, so VS Code rendered the "view binary data" icon and navigated to an address equal to the value (hovering a uint32_t of 1 opened a memory view at 0x1). The earlier approach gated AD7Property.GetMemoryContext in the engine, but that method is shared with Visual Studio, where the Memory and Disassembly windows resolve a typed address expression through it. Restricting it there breaks entering a scalar or address expression into those windows. Move the restriction to the DAP layer: in AD7Utils.GetMemoryReferenceFromIDebugProperty, emit a memoryReference only when the property type is a pointer or an array. GetMemoryContext is left unchanged, so the Visual Studio memory and disassembly navigation keep working, while VS Code no longer offers a memory view for scalars. VS Code has no free-form address entry, so gating the reference removes no entry point there. --- src/OpenDebugAD7/AD7Utils.cs | 44 ++++++++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/src/OpenDebugAD7/AD7Utils.cs b/src/OpenDebugAD7/AD7Utils.cs index 6933256d1..32ef2a376 100644 --- a/src/OpenDebugAD7/AD7Utils.cs +++ b/src/OpenDebugAD7/AD7Utils.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; @@ -20,7 +20,23 @@ public static bool IsAnnotatedFrame(ref FRAMEINFO frameInfo) public static string GetMemoryReferenceFromIDebugProperty(IDebugProperty2 property) { - if (property != null && property.GetMemoryContext(out IDebugMemoryContext2 memoryContext) == HRConstants.S_OK) + if (property == null) + { + return null; + } + + // Only a pointer or an array holds an address a memoryReference can point at. + // For any other type the value is not an address, so a memoryReference would + // make the client show a memory view that navigates to the value itself. The + // engine's GetMemoryContext still resolves an address for any type, which the + // Visual Studio Memory and Disassembly windows rely on; the restriction here + // applies only to the reference reported over DAP. + if (!IsPointerOrArray(property)) + { + return null; + } + + if (property.GetMemoryContext(out IDebugMemoryContext2 memoryContext) == HRConstants.S_OK) { CONTEXT_INFO[] contextInfo = new CONTEXT_INFO[1]; if (memoryContext.GetInfo(enum_CONTEXT_INFO_FIELDS.CIF_ADDRESS, contextInfo) == HRConstants.S_OK) @@ -34,5 +50,29 @@ public static string GetMemoryReferenceFromIDebugProperty(IDebugProperty2 proper return null; } + + private static bool IsPointerOrArray(IDebugProperty2 property) + { + DEBUG_PROPERTY_INFO[] propertyInfo = new DEBUG_PROPERTY_INFO[1]; + if (property.GetPropertyInfo(enum_DEBUGPROP_INFO_FLAGS.DEBUGPROP_INFO_TYPE, Constants.EvaluationRadix, Constants.EvaluationTimeout, null, 0, propertyInfo) != HRConstants.S_OK) + { + return false; + } + + if (!propertyInfo[0].dwFields.HasFlag(enum_DEBUGPROP_INFO_FLAGS.DEBUGPROP_INFO_TYPE)) + { + return false; + } + + string typeName = propertyInfo[0].bstrType; + if (string.IsNullOrEmpty(typeName)) + { + return false; + } + + typeName = typeName.TrimEnd(); + return typeName.EndsWith("*", StringComparison.Ordinal) // pointer, e.g. "int *" + || typeName.EndsWith("]", StringComparison.Ordinal); // array, e.g. "int [10]" + } } } From 2d1de85e3d1ff89b5c7e998378ec17b8deee8e61 Mon Sep 17 00:00:00 2001 From: Andrew Wang Date: Fri, 10 Jul 2026 10:05:45 -0700 Subject: [PATCH 15/25] Merge pull request #1608 from microsoft/dev/wardengnaw/updateMinor8 Increment MinorVersion from 8 to 10 --- build/version.settings.targets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/version.settings.targets b/build/version.settings.targets index 53ed9072f..cad25b7d9 100644 --- a/build/version.settings.targets +++ b/build/version.settings.targets @@ -3,7 +3,7 @@ 18 - 8 + 10 2025 From ca8f423faab357bb2aaf31cfb5d3fb676ff95cc4 Mon Sep 17 00:00:00 2001 From: tzcnt <104330640+tzcnt@users.noreply.github.com> Date: Fri, 10 Jul 2026 21:30:05 -0700 Subject: [PATCH 16/25] handle synthetic frames produced by GDB FrameFilter without a level (#1605) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Synthetic frames produced by GDB's FrameFilter, e.g. async stack backtraces for C++20 coroutines produced by a [GDB script](https://github.com/tzcnt/tmc-examples/blob/backtrace/coro_backtrace_gdb.py) do not have a `Level` and cannot be assigned a `Level` with the current version of GDB. Attempting to set a breakpoint inside of an async stack then causes an error when walking the synthetic backtrace: ``` ERROR: Error while trying to enter break state. Debugging will now stop. Unrecognized format of field "level" in result: {addr=0x000055555555fef0,func=[async] main::$_0::operator()(unsigned long) const [clone .destroy],file=,line=80,arch=i386:x86-64} ``` This PR updates MIEngine’s callstack and expression-evaluation paths to tolerate GDB FrameFilter “synthetic” frames that omit the `level` field, avoiding stack-walk failures and guarding frame-relative MI commands (locals/args/registers/eval) when the frame is not addressable by level. **Changes:** - Make `ThreadContext.Level` nullable and update stack walking/parsing to allow frames without `level`. - Add defensive guards across locals/args/registers/expression evaluation paths when `Level == null`. - Introduce a user-facing resource string for unsupported evaluation scenarios in non-level-bearing frames. --- src/MIDebugEngine/AD7.Impl/AD7StackFrame.cs | 17 +++++++++-- src/MIDebugEngine/AD7.Impl/AD7Thread.cs | 15 ++++++++-- .../Engine.Impl/DebuggedProcess.cs | 27 +++++++++++++++--- .../Engine.Impl/DebuggedThread.cs | 7 ++++- src/MIDebugEngine/Engine.Impl/Structures.cs | 11 ++++++-- src/MIDebugEngine/Engine.Impl/Variables.cs | 28 +++++++++++++++++-- .../Natvis.Impl/VisualizationCache.cs | 7 ++++- src/MIDebugEngine/ResourceStrings.Designer.cs | 11 +++++++- src/MIDebugEngine/ResourceStrings.resx | 3 ++ 9 files changed, 109 insertions(+), 17 deletions(-) diff --git a/src/MIDebugEngine/AD7.Impl/AD7StackFrame.cs b/src/MIDebugEngine/AD7.Impl/AD7StackFrame.cs index 4711f1eab..5e375fab8 100644 --- a/src/MIDebugEngine/AD7.Impl/AD7StackFrame.cs +++ b/src/MIDebugEngine/AD7.Impl/AD7StackFrame.cs @@ -345,6 +345,13 @@ private void CreateParameterProperties(enum_DEBUGPROP_INFO_FLAGS dwFields, out u private void CreateRegisterContent(enum_DEBUGPROP_INFO_FLAGS dwFields, out uint elementsReturned, out IEnumDebugPropertyInfo2 enumObject) { + if (ThreadContext.Level == null) + { + elementsReturned = 0; + enumObject = new AD7PropertyInfoEnum(Array.Empty()); + return; + } + IReadOnlyCollection registerGroups = Engine.DebuggedProcess.GetRegisterGroups(); elementsReturned = (uint)registerGroups.Count; @@ -352,7 +359,7 @@ private void CreateRegisterContent(enum_DEBUGPROP_INFO_FLAGS dwFields, out uint Tuple[] values = null; Engine.DebuggedProcess.WorkerThread.RunOperation(async () => { - values = await Engine.DebuggedProcess.GetRegisters(Thread.GetDebuggedThread().Id, ThreadContext.Level); + values = await Engine.DebuggedProcess.GetRegisters(Thread.GetDebuggedThread().Id, ThreadContext.Level.Value); }); int i = 0; foreach (var grp in registerGroups) @@ -366,10 +373,16 @@ private void CreateRegisterContent(enum_DEBUGPROP_INFO_FLAGS dwFields, out uint public string EvaluateExpression(string expr) { + if (ThreadContext.Level == null) + { + return null; + } + uint level = ThreadContext.Level.Value; + string val = null; Engine.DebuggedProcess.WorkerThread.RunOperation(async () => { - val = await Engine.DebuggedProcess.MICommandFactory.DataEvaluateExpression(expr, Thread.Id, ThreadContext.Level); + val = await Engine.DebuggedProcess.MICommandFactory.DataEvaluateExpression(expr, Thread.Id, level); }); return val; } diff --git a/src/MIDebugEngine/AD7.Impl/AD7Thread.cs b/src/MIDebugEngine/AD7.Impl/AD7Thread.cs index b2b0a8d35..1c11d34f8 100644 --- a/src/MIDebugEngine/AD7.Impl/AD7Thread.cs +++ b/src/MIDebugEngine/AD7.Impl/AD7Thread.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Text; using Microsoft.VisualStudio.Debugger.Interop; using System.Diagnostics; @@ -134,14 +135,22 @@ int IDebugThread2.EnumFrameInfo(enum_FRAMEINFO_FLAGS dwFieldSpec, uint nRadix, o } else { - uint low = stackFrames[0].Level; - uint high = stackFrames[stackFrames.Count - 1].Level; + // -stack-list-arguments takes a low/high *frame index* range. When a GDB Python + // frame filter is active, those numbers index the decorated stack, not the GDB frame + // level. Thus, synthetic frames are indexed even though they carry no level, and we + // must request the whole [0, count-1] decorated range. + // Synthetic frames in the range have no arguments and are skipped in + // GetParameterInfoOnly, and results are matched back to frames by level below, + // so ordering within the range is moot. + uint low = 0; + uint high = (uint)(stackFrames.Count - 1); FilterUnknownFrames(stackFrames); numStackFrames = stackFrames.Count; frameInfoArray = new FRAMEINFO[numStackFrames]; List parameters = null; - if ((dwFieldSpec & enum_FRAMEINFO_FLAGS.FIF_FUNCNAME_ARGS) != 0 && !_engine.DebuggedProcess.MICommandFactory.SupportsFrameFormatting) + if ((dwFieldSpec & enum_FRAMEINFO_FLAGS.FIF_FUNCNAME_ARGS) != 0 && !_engine.DebuggedProcess.MICommandFactory.SupportsFrameFormatting + && stackFrames.Any(f => f.Level != null)) { _engine.DebuggedProcess.WorkerThread.RunOperation(async () => parameters = await _engine.DebuggedProcess.GetParameterInfoOnly(this, (dwFieldSpec & enum_FRAMEINFO_FLAGS.FIF_FUNCNAME_ARGS_VALUES) != 0, (dwFieldSpec & enum_FRAMEINFO_FLAGS.FIF_FUNCNAME_ARGS_TYPES) != 0, low, high)); diff --git a/src/MIDebugEngine/Engine.Impl/DebuggedProcess.cs b/src/MIDebugEngine/Engine.Impl/DebuggedProcess.cs index f11ff57d6..27edfad98 100755 --- a/src/MIDebugEngine/Engine.Impl/DebuggedProcess.cs +++ b/src/MIDebugEngine/Engine.Impl/DebuggedProcess.cs @@ -1989,7 +1989,13 @@ internal async Task> GetLocalsAndParameters(AD7Thread { List variables = new List(); - ValueListValue localsAndParameters = await MICommandFactory.StackListVariables(PrintValue.NoValues, thread.Id, ctx.Level); + if (ctx.Level == null) + { + return variables; + } + uint level = ctx.Level.Value; + + ValueListValue localsAndParameters = await MICommandFactory.StackListVariables(PrintValue.NoValues, thread.Id, level); foreach (var localOrParamResult in localsAndParameters.Content) { @@ -2000,7 +2006,7 @@ internal async Task> GetLocalsAndParameters(AD7Thread variables.Add(vi); } - if (ReturnValue != null && ctx.Level == 0 && ReturnValue.Client.Id == thread.Id) + if (ReturnValue != null && level == 0 && ReturnValue.Client.Id == thread.Id) variables.Add(ReturnValue); return variables; @@ -2012,7 +2018,12 @@ public async Task> GetParameterInfoOnly(AD7Threa { List parameters = new List(); - ValueListValue localAndParameters = await MICommandFactory.StackListVariables(PrintValue.SimpleValues, thread.Id, ctx.Level); + if (ctx.Level == null) + { + return parameters; + } + + ValueListValue localAndParameters = await MICommandFactory.StackListVariables(PrintValue.SimpleValues, thread.Id, ctx.Level.Value); foreach (var results in localAndParameters.Content.Where(r => r.TryFindString("arg") == "1")) { @@ -2050,7 +2061,15 @@ public async Task> GetParameterInfoOnly(AD7Thread thread, boo foreach (var f in frames) { - int level = f.FindInt("level"); + // Synthetic frames injected by a GDB Python frame filter have no "level" field. + // GDB doesn't support querying them by level either, so their argument lists + // can't be retrieved. Just skip them instead of failing the whole stack walk. + uint? levelOpt = f.TryFindUint("level"); + if (levelOpt == null) + { + continue; + } + int level = (int)levelOpt.Value; ListValue argList = null; f.TryFind("args", out argList); List args = new List(); diff --git a/src/MIDebugEngine/Engine.Impl/DebuggedThread.cs b/src/MIDebugEngine/Engine.Impl/DebuggedThread.cs index 614ef61a9..04e54b9d4 100644 --- a/src/MIDebugEngine/Engine.Impl/DebuggedThread.cs +++ b/src/MIDebugEngine/Engine.Impl/DebuggedThread.cs @@ -325,7 +325,12 @@ private ThreadContext CreateContext(TupleValue frame) MITextPosition textPosition = !ignoreSource ? MITextPosition.TryParse(this._debugger, frame) : null; string func = frame.TryFindString("func"); - uint level = frame.FindUint("level"); + // Synthetic frames injected by a GDB Python frame filter (e.g. an async-stack + // decorator) do not carry a "level" field, since they have no underlying debugger + // frame. Leave the level null in that case; frame-relative operations (locals, + // args, registers, evaluation) early-out on a null level rather than failing the + // whole stack walk. Real frames keep their true level. + uint? level = frame.TryFindUint("level"); string from = frame.TryFindString("from"); return new ThreadContext(pc, textPosition, func, level, from); diff --git a/src/MIDebugEngine/Engine.Impl/Structures.cs b/src/MIDebugEngine/Engine.Impl/Structures.cs index 2cfebba42..c1b3e6886 100644 --- a/src/MIDebugEngine/Engine.Impl/Structures.cs +++ b/src/MIDebugEngine/Engine.Impl/Structures.cs @@ -12,7 +12,7 @@ namespace Microsoft.MIDebugEngine { internal class ThreadContext { - public ThreadContext(ulong? addr, MITextPosition textPosition, string function, uint level, string from) + public ThreadContext(ulong? addr, MITextPosition textPosition, string function, uint? level, string from) { pc = addr; sp = 0; @@ -32,7 +32,14 @@ public ThreadContext(ulong? addr, MITextPosition textPosition, string function, public string From { get; private set; } - public uint Level { get; private set; } + /// + /// [Optional] The GDB/LLDB frame level. This is null for synthetic frames + /// injected by a Python frame filter (e.g. an async-stack decorator): those + /// frames have no underlying debugger frame, so they carry no level and cannot + /// be targeted by frame-relative MI commands (locals, args, registers, eval). + /// Callers that pass the level to the debugger must early-out when it is null. + /// + public uint? Level { get; private set; } /// /// Finds the module for this context diff --git a/src/MIDebugEngine/Engine.Impl/Variables.cs b/src/MIDebugEngine/Engine.Impl/Variables.cs index 031dd40c8..b6dd867bb 100644 --- a/src/MIDebugEngine/Engine.Impl/Variables.cs +++ b/src/MIDebugEngine/Engine.Impl/Variables.cs @@ -585,10 +585,16 @@ public string EvalDependentExpression(string expr) { this.VerifyNotDisposed(); + if (_ctx.Level == null) + { + throw GetEvalUnsupportedInFrameException("-data-evaluate-expression"); + } + uint frameLevel = _ctx.Level.Value; + string val = null; Task eval = Task.Run(async () => { - val = await _engine.DebuggedProcess.MICommandFactory.DataEvaluateExpression(expr, Client.GetDebuggedThread().Id, _ctx.Level); + val = await _engine.DebuggedProcess.MICommandFactory.DataEvaluateExpression(expr, Client.GetDebuggedThread().Id, frameLevel); }); eval.Wait(); return val; @@ -633,7 +639,16 @@ internal async Task Eval(uint radix, enum_EVALFLAGS dwFlags = 0, DAPEvalFlags dw } int threadId = Client.GetDebuggedThread().Id; - uint frameLevel = _ctx.Level; + + // Synthetic frames injected by a Python frame filter have no level and provide + // no evaluation context. This is normally unreachable (annotated frames expose + // no expression context), but guard defensively rather than crash on a null level. + if (_ctx.Level == null) + { + SetAsError(ResourceStrings.ExpressionEvalUnsupportedInFrame); + return; + } + uint frameLevel = _ctx.Level.Value; string expression = _strippedName; @@ -910,6 +925,8 @@ private void SetAsError(string msg) Error = true; } + private Exception GetEvalUnsupportedInFrameException(string command) => new UnexpectedMIResultException(_debuggedProcess.MICommandFactory.Name, command, ResourceStrings.ExpressionEvalUnsupportedInFrame); + private bool IsArrayType() { if (DisplayHint == "array") @@ -957,10 +974,15 @@ public void Assign(string expression) { this.VerifyNotDisposed(); + if (_ctx.Level == null) + { + throw GetEvalUnsupportedInFrameException("-var-assign"); + } + _engine.DebuggedProcess.WorkerThread.RunOperation(async () => { int threadId = Client.GetDebuggedThread().Id; - uint frameLevel = _ctx.Level; + uint frameLevel = _ctx.Level.Value; _engine.DebuggedProcess.FlushBreakStateData(); Value = await _engine.DebuggedProcess.MICommandFactory.VarAssign(_internalName, expression, threadId, frameLevel); diff --git a/src/MIDebugEngine/Natvis.Impl/VisualizationCache.cs b/src/MIDebugEngine/Natvis.Impl/VisualizationCache.cs index de81f6594..060e91f23 100644 --- a/src/MIDebugEngine/Natvis.Impl/VisualizationCache.cs +++ b/src/MIDebugEngine/Natvis.Impl/VisualizationCache.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -21,7 +22,11 @@ public VisualizerKey(IVariableInformation variable) { _name = variable.FullName(); _threadId = variable.Client.Id; - _level = (int)variable.ThreadContext.Level; + // Variables only ever come from real (level-bearing) frames; synthetic frames + // from a Python frame filter expose no locals to visualize. Fall back to 0 to + // keep the key total in the null case rather than throwing. + Debug.Assert(variable.ThreadContext.Level.HasValue, "How are we getting a variable from a synthetic thread context?"); + _level = (int)(variable.ThreadContext.Level ?? 0); } public VisualizerKey(string name, int threadId, int level) diff --git a/src/MIDebugEngine/ResourceStrings.Designer.cs b/src/MIDebugEngine/ResourceStrings.Designer.cs index fd9cda12e..9c24bddc4 100755 --- a/src/MIDebugEngine/ResourceStrings.Designer.cs +++ b/src/MIDebugEngine/ResourceStrings.Designer.cs @@ -210,7 +210,16 @@ internal static string ExceptionSettingsError { return ResourceManager.GetString("ExceptionSettingsError", resourceCulture); } } - + + /// + /// Looks up a localized string similar to Expression evaluation is unavailable in the current frame. + /// + internal static string ExpressionEvalUnsupportedInFrame { + get { + return ResourceManager.GetString("ExpressionEvalUnsupportedInFrame", resourceCulture); + } + } + /// /// Looks up a localized string similar to Error: {0}. /// diff --git a/src/MIDebugEngine/ResourceStrings.resx b/src/MIDebugEngine/ResourceStrings.resx index e6b9e7cc2..273e41f22 100755 --- a/src/MIDebugEngine/ResourceStrings.resx +++ b/src/MIDebugEngine/ResourceStrings.resx @@ -132,6 +132,9 @@ Error while updating exception settings. {0} + + Expression evaluation is unavailable in the current frame + File not found: {0} From 1743a60e1ce093779809ded323eb1df172426a4a Mon Sep 17 00:00:00 2001 From: Gregg Miskelly Date: Mon, 13 Jul 2026 12:41:52 -0700 Subject: [PATCH 17/25] Auto-deploy debug adapter when building CppTests (#1610) ## Why is this change being made This PR fixes the way we deploy the debug adapter for CppTests so that you don't need to rerun CI-Build.ps1/.sh between test iterations. This way you can make a code change, then run a test from the command line, from Test Explorer in Visual Studio, or the Testing panel in VS Code. ## Summary of changes - Removed steps from CI-Build.ps1/.sh to publish OpenDebugAD7 - Added targets to CppTests.csproj to deploy the debug adapter - Added project references so that building CppTests.csproj will ensure all the other projects are built - Removed solution build dependencies from the .sln files since they are now in the .csproj files - Added code to copy config.xml in test/CppTests project directory - Updated documentation from the changes Unrelated, but I also added src/MIDebugEngine-Unix.sln as the default solution in VS Code ## Testing * [X] **.NET CLI**: ran `git clean`, copied in the correct config.xml, and verified I could run a test using `dotnet test` * [X] **Visual Studio**: ran `git clean`, copied in the correct config.xml, and verified I could run a test * [X] **Visual Studio**: made a change to DebuggedProcess.cs, then rerun a test and verified my new code was running * [X] **Visual Studio Code**: cloned the repo into WSL, copied in the correct config.xml, opened the workspace and verified I could run tests from the Testing pannel. --- .github/workflows/Build-And-Test.yml | 4 +- .gitignore | 1 + .vscode/settings.json | 3 + ...er.PIAs.Portable.Packages.settings.targets | 8 +- docs/Building-outside-of-VS-for-AI.md | 12 +-- docs/RunningCppTests-outside-of-VS-for-AI.md | 6 +- eng/Scripts/CI-Build.ps1 | 30 +----- eng/Scripts/CI-Build.sh | 5 - src/MIDebugEngine-Unix.sln | 3 - src/MIDebugEngine.sln | 3 - src/MakePIAPortable/MakePIAPortable.csproj | 5 + test/CppTests/CppTests.csproj | 94 ++++++++++++++++++- 12 files changed, 117 insertions(+), 57 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.github/workflows/Build-And-Test.yml b/.github/workflows/Build-And-Test.yml index db5f95201..6f176ff78 100644 --- a/.github/workflows/Build-And-Test.yml +++ b/.github/workflows/Build-And-Test.yml @@ -39,7 +39,7 @@ jobs: - name: Build MIDebugEngine run: | - ${{ github.workspace }}/eng/Scripts/CI-Build.cmd -c $env:Configuration -t vs + ${{ github.workspace }}/eng/Scripts/CI-Build.cmd -c $env:Configuration env: Configuration: ${{ matrix.configuration }} @@ -71,7 +71,7 @@ jobs: - name: Build MIDebugEngine run: | - ${{ github.workspace }}/eng/Scripts/CI-Build.cmd -t vscode + ${{ github.workspace }}/eng/Scripts/CI-Build.cmd - name: Copy Test Configuration run: | diff --git a/.gitignore b/.gitignore index 6034e0b4e..044173c4c 100755 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ project.lock.json src/*/*.nuget.props src/*/*.nuget.targets .DS_Store +test/CppTests/config.xml diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..5d5cf08d2 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "dotnet.defaultSolution": "src/MIDebugEngine-Unix.sln" +} \ No newline at end of file diff --git a/build/Debugger.PIAs.Portable.Packages.settings.targets b/build/Debugger.PIAs.Portable.Packages.settings.targets index 3c154ca54..70a2a4758 100644 --- a/build/Debugger.PIAs.Portable.Packages.settings.targets +++ b/build/Debugger.PIAs.Portable.Packages.settings.targets @@ -1,4 +1,6 @@ - + @@ -15,4 +17,8 @@ + + + + \ No newline at end of file diff --git a/docs/Building-outside-of-VS-for-AI.md b/docs/Building-outside-of-VS-for-AI.md index da19eccbc..8d2d8edfe 100644 --- a/docs/Building-outside-of-VS-for-AI.md +++ b/docs/Building-outside-of-VS-for-AI.md @@ -21,16 +21,10 @@ From a VS Developer Command Prompt or after putting `MSBuild.exe` on `PATH`: ```powershell # Debug, VS extension flavor (default) -eng\Scripts\CI-Build.ps1 -Configuration Debug -TargetPlatform vs - -# Debug, VS Code adapter flavor (also publishes OpenDebugAD7 + native deps to -# bin\DebugAdapterProtocolTests\Debug\extension\debugAdapters) -eng\Scripts\CI-Build.ps1 -Configuration Debug -TargetPlatform vscode +eng\Scripts\CI-Build.ps1 -Configuration Debug ``` -The script restores NuGet, builds `MIDebugEngine.sln` with `msbuild`, and (for `-TargetPlatform vscode`) `dotnet publish`es OpenDebugAD7 and stages the adapter under `bin\DebugAdapterProtocolTests\\extension\debugAdapters`. - -If `msbuild.exe` isn't on `PATH`, locate it with vswhere: +The script restores NuGet, builds `MIDebugEngine.sln` with `msbuild`. If `msbuild.exe` isn't on `PATH`, locate it with vswhere: ```powershell $msbuild = & "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" -latest -prerelease -requires Microsoft.Component.MSBuild -find "MSBuild\**\Bin\MSBuild.exe" @@ -43,7 +37,7 @@ $env:Path = (Split-Path $msbuild) + ';' + $env:Path eng/Scripts/CI-Build.sh ``` -This runs `dotnet build src/MIDebugEngine-Unix.sln` and then `PublishOpenDebugAD7.sh -c Debug -o bin/DebugAdapterProtocolTests/Debug/extension/debugAdapters`. +This runs `dotnet build src/MIDebugEngine-Unix.sln` ## Outputs diff --git a/docs/RunningCppTests-outside-of-VS-for-AI.md b/docs/RunningCppTests-outside-of-VS-for-AI.md index 8109de4ea..9f4da6529 100644 --- a/docs/RunningCppTests-outside-of-VS-for-AI.md +++ b/docs/RunningCppTests-outside-of-VS-for-AI.md @@ -9,11 +9,11 @@ 1. A VSCode-flavor build (see [Building-outside-of-VS-for-AI.md](Building-outside-of-VS-for-AI.md)): ```powershell - eng\Scripts\CI-Build.ps1 -Configuration Debug -TargetPlatform vscode # Windows + eng\Scripts\CI-Build.ps1 # Windows ``` ```bash - eng/Scripts/CI-Build.sh # Linux / macOS + eng/Scripts/CI-Build.sh # Linux / macOS ``` 2. A native toolchain for the test debuggees: @@ -35,7 +35,7 @@ - **macOS:** `lldb-mi` is downloaded by `tools/DownloadLldbMI.sh`; CI does this automatically inside `eng/Scripts/CI-Test.sh`. -3. A `config.xml` next to `CppTests.dll`. Pick the right template from `bin/DebugAdapterProtocolTests//CppTests/TestConfigurations/` and copy it as `config.xml`: +3. A `config.xml` next to `CppTests.csproj`. Pick the right template from `test\CppTests\TestConfigurations` and copy it as `config.xml`: | Platform / debugger | Template | | --- | --- | diff --git a/eng/Scripts/CI-Build.ps1 b/eng/Scripts/CI-Build.ps1 index 9c47c0293..f01aff583 100644 --- a/eng/Scripts/CI-Build.ps1 +++ b/eng/Scripts/CI-Build.ps1 @@ -1,15 +1,7 @@ param( [ValidateSet("Debug", "Release")] [Alias("c")] -[string]$Configuration="Debug", - -[ValidateSet("win-x86", "win-arm64")] -[Alias("r")] -[string]$RID="win-x86", - -[ValidateSet("vs", "vscode")] -[Alias("t")] -[string]$TargetPlatform="vs" +[string]$Configuration="Debug" ) $ErrorActionPreference="Stop" @@ -31,24 +23,4 @@ msbuild $RootPath\src\MIDebugEngine.sln /p:Configuration=$Configuration if ($lastexitcode -ne 0) { throw "Failed to build MIDebugEngine.sln" -} - - -if ($TargetPlatform -eq "vscode") -{ - $dotnetPath = (Get-Command dotnet.exe -ErrorAction Ignore).Path; - - if (!$dotnetPath) { - throw "Missing .NET SDK. Please install the SDK at https://dotnet.microsoft.com/download" - } - - dotnet publish $RootPath\src\OpenDebugAD7\OpenDebugAD7.csproj -c $Configuration -r $RID --self-contained -o $RootPath\bin\DebugAdapterProtocolTests\$Configuration\extension\debugAdapters - if ($lastexitcode -ne 0) - { - throw "Failed to publish OpenDebugAD7" - } - - Copy-Item $RootPath\bin\$Configuration\Microsoft.MIDebugEngine.dll $RootPath\bin\DebugAdapterProtocolTests\$Configuration\extension\debugAdapters/. - Copy-Item $RootPath\bin\$Configuration\Microsoft.MICore.dll $RootPath\bin\DebugAdapterProtocolTests\$Configuration\extension\debugAdapters\. - Copy-Item $RootPath\bin\$Configuration\vscode\WindowsDebugLauncher.exe $RootPath\bin\DebugAdapterProtocolTests\$Configuration\extension\debugAdapters\. } \ No newline at end of file diff --git a/eng/Scripts/CI-Build.sh b/eng/Scripts/CI-Build.sh index 6fb33d77e..2d69ea339 100755 --- a/eng/Scripts/CI-Build.sh +++ b/eng/Scripts/CI-Build.sh @@ -13,9 +13,4 @@ if ! dotnet build "$RootDir"/src/MIDebugEngine-Unix.sln; then exit 1 fi -if ! "$RootDir"/PublishOpenDebugAD7.sh -c Debug -o "$RootDir"/bin/DebugAdapterProtocolTests/Debug/extension/debugAdapters; then - echo "ERROR: Failed to build MIDebugEngine-Unix.sln" - exit 1 -fi - exit 0 \ No newline at end of file diff --git a/src/MIDebugEngine-Unix.sln b/src/MIDebugEngine-Unix.sln index 21a53b902..8dfd0c2a1 100644 --- a/src/MIDebugEngine-Unix.sln +++ b/src/MIDebugEngine-Unix.sln @@ -36,9 +36,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DebugEngineHost.VSCode", "D EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MakePIAPortable", "MakePIAPortable\MakePIAPortable.csproj", "{114039A0-87B5-425B-90C9-6AFC1960A247}" - ProjectSection(ProjectDependencies) = postProject - {CC5BDD33-7EB1-4FB9-BC67-806773018989} = {CC5BDD33-7EB1-4FB9-BC67-806773018989} - EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DebugAdapterProtocolTests", "DebugAdapterProtocolTests", "{9D97EF1A-BCD5-4932-BBCC-98194CF8A841}" EndProject diff --git a/src/MIDebugEngine.sln b/src/MIDebugEngine.sln index 88e672933..7e1c4e476 100755 --- a/src/MIDebugEngine.sln +++ b/src/MIDebugEngine.sln @@ -64,9 +64,6 @@ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WindowsDebugLauncher", "WindowsDebugLauncher\WindowsDebugLauncher.csproj", "{AE7F97CA-DFD2-41BC-B581-98C91C83065C}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MakePIAPortable", "MakePIAPortable\MakePIAPortable.csproj", "{114039A0-87B5-425B-90C9-6AFC1960A247}" - ProjectSection(ProjectDependencies) = postProject - {CC5BDD33-7EB1-4FB9-BC67-806773018989} = {CC5BDD33-7EB1-4FB9-BC67-806773018989} - EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MIDebugPackage", "MIDebugPackage\MIDebugPackage.csproj", "{A8D8E02F-4258-4F2E-88B6-A50FEBD5AD8C}" ProjectSection(ProjectDependencies) = postProject diff --git a/src/MakePIAPortable/MakePIAPortable.csproj b/src/MakePIAPortable/MakePIAPortable.csproj index 4c51828d1..da719912c 100644 --- a/src/MakePIAPortable/MakePIAPortable.csproj +++ b/src/MakePIAPortable/MakePIAPortable.csproj @@ -40,6 +40,11 @@ + + + + + diff --git a/test/CppTests/CppTests.csproj b/test/CppTests/CppTests.csproj index fa41fba4e..2d1e06551 100644 --- a/test/CppTests/CppTests.csproj +++ b/test/CppTests/CppTests.csproj @@ -1,4 +1,5 @@ - + + @@ -8,7 +9,16 @@ $(OutputPath)\CppTests - + + + + $(MIEngineRoot)bin\DebugAdapterProtocolTests\$(Configuration)\extension\debugAdapters\ + $(MIDefaultOutputPath)vscode\ + + <_ShouldDeployDebugAdapter>false + <_ShouldDeployDebugAdapter Condition="$(Configuration.Contains('Debug'))">true + + + @@ -42,8 +54,47 @@ + + + + + + + + + <_DebugAdapterSourceFiles Include="$(OpenDebugAD7OutputPath)*" /> + + + <_DebugAdapterDestFiles Include="@(_DebugAdapterSourceFiles->'$(DebugAdaptersDeployPath)%(Filename)%(Extension)')" /> + + + + + + + + + @@ -62,4 +113,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + From 8ee6a043a8296e198ff56c10d0e2296faa159d68 Mon Sep 17 00:00:00 2001 From: Gregg Miskelly Date: Tue, 14 Jul 2026 10:41:06 -0700 Subject: [PATCH 18/25] Support environment variables in config.xml (#1611) This PR adds support for an `Environment` dictionary in config.xml to make it easy to configure cygwin tests on Windows. --- .../OpenDebug/CrossPlatCpp/DebuggerRunner.cs | 3 ++- .../TestConfigurations/config_msys_gdb.xml | 8 +++++-- .../Attribution/DebuggerSettings.cs | 6 ++++- .../Attribution/TestSettings.cs | 5 ++-- .../Attribution/TestSettingsHelper.cs | 23 +++++++++++++++++-- test/DebuggerTesting/IDebuggerSettings.cs | 2 ++ 6 files changed, 39 insertions(+), 8 deletions(-) diff --git a/test/CppTests/OpenDebug/CrossPlatCpp/DebuggerRunner.cs b/test/CppTests/OpenDebug/CrossPlatCpp/DebuggerRunner.cs index 0ae05f008..adacc6632 100644 --- a/test/CppTests/OpenDebug/CrossPlatCpp/DebuggerRunner.cs +++ b/test/CppTests/OpenDebug/CrossPlatCpp/DebuggerRunner.cs @@ -62,7 +62,8 @@ private DebuggerRunner(ILoggingComponent logger, ITestSettings testSettings, IEn pauseForDebugger: false, isVsDbg: this.DebuggerSettings.DebuggerType == SupportedDebugger.VsDbg, isNative: true, - responseTimeout: 5000); + responseTimeout: 5000, + additionalEnvironmentVariables: this.DebuggerSettings.EnvironmentVariables); if (callbackHandlers != null) { diff --git a/test/CppTests/TestConfigurations/config_msys_gdb.xml b/test/CppTests/TestConfigurations/config_msys_gdb.xml index 695f520f7..d6ee086ad 100644 --- a/test/CppTests/TestConfigurations/config_msys_gdb.xml +++ b/test/CppTests/TestConfigurations/config_msys_gdb.xml @@ -17,7 +17,11 @@ Name="GdbMingw64" Type="Gdb_MinGW" Path="D:\a\_temp\msys64\mingw64\bin\gdb.exe" - AdapterPath="OpenDebugAD7.exe" - /> + AdapterPath="OpenDebugAD7.exe"> + + + + + \ No newline at end of file diff --git a/test/DebuggerTesting/Attribution/DebuggerSettings.cs b/test/DebuggerTesting/Attribution/DebuggerSettings.cs index 3e1c9036b..50fdf2a79 100644 --- a/test/DebuggerTesting/Attribution/DebuggerSettings.cs +++ b/test/DebuggerTesting/Attribution/DebuggerSettings.cs @@ -20,7 +20,8 @@ public DebuggerSettings( string debuggerAdapterPath, string miMode, SupportedArchitecture debuggeeArchitecture, - IDictionary debuggerProperties) + IDictionary debuggerProperties, + IDictionary environmentVariables = null) { this.DebuggeeArchitecture = debuggeeArchitecture; this.DebuggerName = debuggerName; @@ -30,6 +31,7 @@ public DebuggerSettings( if (!string.IsNullOrWhiteSpace(miMode)) this.MIMode = miMode; this.Properties = debuggerProperties ?? new Dictionary(StringComparer.Ordinal); + this.EnvironmentVariables = environmentVariables ?? new Dictionary(StringComparer.OrdinalIgnoreCase); } #endregion @@ -119,6 +121,8 @@ public override string ToString() public IDictionary Properties { get; private set; } + public IDictionary EnvironmentVariables { get; private set; } + #endregion } } diff --git a/test/DebuggerTesting/Attribution/TestSettings.cs b/test/DebuggerTesting/Attribution/TestSettings.cs index e393f1f08..37476de75 100644 --- a/test/DebuggerTesting/Attribution/TestSettings.cs +++ b/test/DebuggerTesting/Attribution/TestSettings.cs @@ -24,10 +24,11 @@ internal TestSettings( string debuggerPath, string debuggerAdapterPath, string miMode, - IDictionary debuggerProperties) + IDictionary debuggerProperties, + IDictionary environmentVariables = null) { this.CompilerSettings = new CompilerSettings(compilerName, compilerType, compilerPath, debuggeeArchitecture, compilerProperties); - this.DebuggerSettings = new DebuggerSettings(debuggerName, debuggerType, debuggerPath, debuggerAdapterPath, miMode, debuggeeArchitecture, debuggerProperties); + this.DebuggerSettings = new DebuggerSettings(debuggerName, debuggerType, debuggerPath, debuggerAdapterPath, miMode, debuggeeArchitecture, debuggerProperties, environmentVariables); } private TestSettings(ITestSettings original, string name) diff --git a/test/DebuggerTesting/Attribution/TestSettingsHelper.cs b/test/DebuggerTesting/Attribution/TestSettingsHelper.cs index 7ca8db5c9..bda80b539 100644 --- a/test/DebuggerTesting/Attribution/TestSettingsHelper.cs +++ b/test/DebuggerTesting/Attribution/TestSettingsHelper.cs @@ -158,7 +158,8 @@ public static IEnumerable LoadSettingsFromConfig(string configPat Path = x.GetAttributeValue("Path"), AdapterPath = x.GetAttributeValue("AdapterPath"), MIMode = x.GetAttributeValue("MIMode"), - Properties = x.GetPropertiesDictionary() + Properties = x.GetPropertiesDictionary(), + EnvironmentVariables = x.GetEnvironmentDictionary() }); Assert.True(null != debuggers && debuggers.Count != 0, "Object loaded from '{0}' is not a TestMachineConfiguration. Missing Debuggers.".FormatInvariantWithArgs(configPath)); @@ -197,7 +198,8 @@ into config SafeExpandEnvironmentVariables(config.Debugger.Path), SafeExpandEnvironmentVariables(config.Debugger.AdapterPath), config.Debugger.MIMode, - config.Debugger.Properties); + config.Debugger.Properties, + config.Debugger.EnvironmentVariables); // Force evaluation return testSettings.ToArray(); @@ -215,6 +217,23 @@ private static IDictionary GetPropertiesDictionary(this XElement ?.ToDictionary(p => p.GetAttributeValue("Name"), p => SafeExpandEnvironmentVariables(p.Value), StringComparer.Ordinal); } + /// + /// Reads the Environment element and returns its child element names and values as a dictionary. + /// For example: value yields {"Path": "value"} + /// + private static IDictionary GetEnvironmentDictionary(this XElement element) + { + var envElement = element.Element("Environment"); + if (envElement == null) + return null; + + Dictionary result = envElement + .Elements() + .ToDictionary(e => e.Name.LocalName, e => SafeExpandEnvironmentVariables(e.Value), StringComparer.OrdinalIgnoreCase); + + return result.Count > 0 ? result : null; + } + private static bool Matches(this IEnumerable compilers, ICompilerSettings settings) { return compilers.Any(ca => ca.Compiler.HasFlag(settings.CompilerType) && ca.Architecture.HasFlag(settings.DebuggeeArchitecture)); diff --git a/test/DebuggerTesting/IDebuggerSettings.cs b/test/DebuggerTesting/IDebuggerSettings.cs index 0117f3579..d9a4e3079 100644 --- a/test/DebuggerTesting/IDebuggerSettings.cs +++ b/test/DebuggerTesting/IDebuggerSettings.cs @@ -26,6 +26,8 @@ public interface IDebuggerSettings IDictionary Properties { get; } + IDictionary EnvironmentVariables { get; } + #endregion } } From 2014d665cea0cac59961c66fdf3b488d11591c28 Mon Sep 17 00:00:00 2001 From: Andrew Wang Date: Thu, 16 Jul 2026 09:21:38 -0700 Subject: [PATCH 19/25] Merge pull request #1596 from microsoft/dev/waan/2987836 Fix UI-thread hang when resuming the target during launch --- src/MIDebugEngine/AD7.Impl/AD7Engine.cs | 28 +- .../Engine.Impl/OperationThread.cs | 250 +++++++++++++++--- 2 files changed, 228 insertions(+), 50 deletions(-) diff --git a/src/MIDebugEngine/AD7.Impl/AD7Engine.cs b/src/MIDebugEngine/AD7.Impl/AD7Engine.cs index ffb8146b8..65dcf5681 100755 --- a/src/MIDebugEngine/AD7.Impl/AD7Engine.cs +++ b/src/MIDebugEngine/AD7.Impl/AD7Engine.cs @@ -319,28 +319,24 @@ public int ContinueFromSynchronousEvent(IDebugEvent2 eventObject) { if (eventObject is AD7ProgramCreateEvent) { - Exception exception = null; - try { _engineCallback.OnLoadComplete(); - // At this point breakpoints and exception settings have been sent down, so we can resume the target - _pollThread.RunOperation(() => - { - return _debuggedProcess.ResumeFromLaunch(); - }); + + // Resume the target on the worker thread without blocking the UI thread this runs on. + // Resume faults are reported via onError, since the SDM drops errors returned from here. + _pollThread.PostAsyncOperation( + () => _debuggedProcess.ResumeFromLaunch(), + (exception) => + { + SendStartDebuggingError(exception); + _debuggedProcess.Terminate(); + }); } catch (Exception e) { - exception = e; - // Return from the catch block so that we can let the exception unwind - the stack can get kind of big - } - - if (exception != null) - { - // If something goes wrong, report the error and then stop debugging. The SDM will drop errors - // from ContinueFromSynchronousEvent, so we want to deal with them ourself. - SendStartDebuggingError(exception); + // Report synchronous failures ourselves, since the SDM drops errors returned from here. + SendStartDebuggingError(e); _debuggedProcess.Terminate(); } diff --git a/src/MIDebugEngine/Engine.Impl/OperationThread.cs b/src/MIDebugEngine/Engine.Impl/OperationThread.cs index 2c5d71a24..706f0d626 100755 --- a/src/MIDebugEngine/Engine.Impl/OperationThread.cs +++ b/src/MIDebugEngine/Engine.Impl/OperationThread.cs @@ -28,6 +28,7 @@ internal class WorkerThread : IDisposable private readonly ManualResetEvent _runningOpCompleteEvent; // fired when either m_syncOp finishes, or the kick off of m_async private readonly Object _eventLock = new object(); // Locking on an event directly can cause Mono to stop responding. private readonly Queue _postedOperations; // queue of fire-and-forget operations + private readonly Queue<(AsyncOperation Operation, Action OnError)> _postedAsyncOperations; // queue of fire-and-forget async operations public event EventHandler PostedOperationErrorEvent; @@ -37,6 +38,13 @@ private class OperationDescriptor /// Delegate that was added via 'RunOperation'. Is of type 'Operation' or 'AsyncOperation' /// public readonly Delegate Target; + + /// + /// Handler invoked if the operation faults. For async operations, this may be invoked on a non-worker thread. + /// Only set for PostAsyncOperation. + /// + public Action ErrorHandler; + public ExceptionDispatchInfo ExceptionDispatchInfo; public Task Task; private bool _isStarted; @@ -81,6 +89,7 @@ public WorkerThread(Logger logger) _opSet = new AutoResetEvent(false); _runningOpCompleteEvent = new ManualResetEvent(true); _postedOperations = new Queue(); + _postedAsyncOperations = new Queue<(AsyncOperation Operation, Action OnError)>(); _thread = new Thread(new ThreadStart(ThreadFunc)); _thread.Name = "MIDebugger.PollThread"; @@ -121,6 +130,30 @@ public void RunOperation(string text, CancellationTokenSource canTokenSource, As SetOperationInternalWithProgress(op, text, canTokenSource); } + /// + /// Queue an async operation to run on the worker thread and return immediately, without waiting for the + /// operation to start or finish. Posted async operations run one at a time, in the order posted, and + /// serialize behind any in-flight operation. Faults are reported to . + /// + public void PostAsyncOperation(AsyncOperation op, Action onError) + { + if (op == null) + throw new ArgumentNullException(nameof(op)); + if (onError == null) + throw new ArgumentNullException(nameof(onError)); + + if (_isClosed) + throw new ObjectDisposedException("WorkerThread"); + + lock (_postedAsyncOperations) + { + if (_isClosed) + throw new ObjectDisposedException("WorkerThread"); + + _postedAsyncOperations.Enqueue((op, onError)); + _opSet.Set(); + } + } public void Close() { @@ -150,6 +183,22 @@ public void Close() } } } + + // Fail any queued async operations that will never run now that we are closed, instead of + // dropping them silently. The poll thread won't promote them once _isClosed is set. + while (true) + { + Action onError; + lock (_postedAsyncOperations) + { + if (_postedAsyncOperations.Count == 0) + break; + + onError = _postedAsyncOperations.Dequeue().OnError; + } + + InvokeErrorHandler(onError, new ObjectDisposedException("WorkerThread")); + } } internal void SetOperationInternal(Delegate op) @@ -189,6 +238,7 @@ internal void SetOperationInternalWithProgress(AsyncProgressOperation op, string } } } + public void PostOperation(Operation op) { if (op == null) @@ -212,71 +262,154 @@ public void PostOperation(Operation op) private bool TrySetOperationInternal(Delegate op) { - lock (_eventLock) + bool claimed = false; + try { - if (_isClosed) - throw new ObjectDisposedException("WorkerThread"); - - if (_runningOp == null) + lock (_eventLock) { - _runningOpCompleteEvent.Reset(); + if (_isClosed) + throw new ObjectDisposedException("WorkerThread"); - OperationDescriptor runningOp = new OperationDescriptor(op); - _runningOp = runningOp; + if (_runningOp == null) + { + _runningOpCompleteEvent.Reset(); - _opSet.Set(); + OperationDescriptor runningOp = new OperationDescriptor(op); + _runningOp = runningOp; - _runningOpCompleteEvent.WaitOne(); + _opSet.Set(); - Debug.Assert(runningOp.IsComplete, "Why isn't the running op complete?"); + _runningOpCompleteEvent.WaitOne(); + claimed = true; - if (runningOp.ExceptionDispatchInfo != null) - { - runningOp.ExceptionDispatchInfo.Throw(); + Debug.Assert(runningOp.IsComplete, "Why isn't the running op complete?"); + + if (runningOp.ExceptionDispatchInfo != null) + { + runningOp.ExceptionDispatchInfo.Throw(); + } + + return true; } + } - return true; + return false; + } + finally + { + // The running-op slot was just freed and _eventLock is now released. If promotion on the poll + // thread lost the TryEnter race against this method, re-arm _opSet so a queued async operation + // is promoted immediately instead of stranded until the next unrelated wakeup. + if (claimed && HasPostedAsyncOperation()) + { + _opSet.Set(); } } - - return false; } private bool TrySetOperationInternalWithProgress(AsyncProgressOperation op, string text, CancellationTokenSource canTokenSource) { var waitLoop = new HostWaitLoop(text); - lock (_eventLock) + bool claimed = false; + try { - if (_isClosed) - throw new ObjectDisposedException("WorkerThread"); - - if (_runningOp == null) + lock (_eventLock) { - _runningOpCompleteEvent.Reset(); + if (_isClosed) + throw new ObjectDisposedException("WorkerThread"); - OperationDescriptor runningOp = new OperationDescriptor(new AsyncOperation(() => { return op(waitLoop); })); - _runningOp = runningOp; + if (_runningOp == null) + { + _runningOpCompleteEvent.Reset(); - _opSet.Set(); + OperationDescriptor runningOp = new OperationDescriptor(new AsyncOperation(() => { return op(waitLoop); })); + _runningOp = runningOp; - waitLoop.Wait(_runningOpCompleteEvent, canTokenSource); + _opSet.Set(); - Debug.Assert(runningOp.IsComplete, "Why isn't the running op complete?"); + waitLoop.Wait(_runningOpCompleteEvent, canTokenSource); + claimed = true; - if (runningOp.ExceptionDispatchInfo != null) - { - runningOp.ExceptionDispatchInfo.Throw(); + Debug.Assert(runningOp.IsComplete, "Why isn't the running op complete?"); + + if (runningOp.ExceptionDispatchInfo != null) + { + runningOp.ExceptionDispatchInfo.Throw(); + } + + return true; } + } - return true; + return false; + } + finally + { + // The running-op slot was just freed and _eventLock is now released. If promotion on the poll + // thread lost the TryEnter race against this method, re-arm _opSet so a queued async operation + // is promoted immediately instead of stranded until the next unrelated wakeup. + if (claimed && HasPostedAsyncOperation()) + { + _opSet.Set(); } } - - return false; } + // Called on the poll thread to promote the next posted async operation into the running-op slot when it + // is free. Returns true if an operation was moved into the slot. + private bool TryStartPostedAsyncOperation() + { + Debug.Assert(IsPollThread(), "TryStartPostedAsyncOperation must run on the poll thread."); + + // Cheap early-out so the poll loop does not contend on _eventLock when there is nothing to promote. + lock (_postedAsyncOperations) + { + if (_isClosed || _postedAsyncOperations.Count == 0) + return false; + } + + // Never block on _eventLock here: a client in TrySetOperationInternal holds it across + // _runningOpCompleteEvent.WaitOne() until its operation completes, and only the poll thread can + // complete that operation, so blocking here would deadlock. If a client is mid-set, skip promotion; + // it will be retried on a later poll-loop iteration or wakeup. + if (!Monitor.TryEnter(_eventLock)) + return false; + + try + { + if (_isClosed || _runningOp != null) + return false; + + (AsyncOperation Operation, Action OnError) posted; + lock (_postedAsyncOperations) + { + if (_postedAsyncOperations.Count == 0) + return false; + + posted = _postedAsyncOperations.Dequeue(); + } + + _runningOpCompleteEvent.Reset(); + + // Unlike TrySetOperationInternal, no one waits for completion; faults are routed to the handler. + _runningOp = new OperationDescriptor(posted.Operation) { ErrorHandler = posted.OnError }; + + return true; + } + finally + { + Monitor.Exit(_eventLock); + } + } + private bool HasPostedAsyncOperation() + { + lock (_postedAsyncOperations) + { + return _postedAsyncOperations.Count > 0; + } + } // Thread routine for the poll loop. It handles calls coming in from the debug engine as well as polling for debug events. private void ThreadFunc() @@ -292,6 +425,11 @@ private void ThreadFunc() { ranOperation = false; + if (_runningOp == null) + { + TryStartPostedAsyncOperation(); + } + OperationDescriptor runningOp = _runningOp; if (runningOp != null && !runningOp.IsStarted) { @@ -333,11 +471,20 @@ private void ThreadFunc() if (!completeAsync) { + // Capture the fault before clearing the slot so a synchronous throw is still reported. + Action errorHandler = runningOp.ErrorHandler; + ExceptionDispatchInfo exceptionDispatchInfo = runningOp.ExceptionDispatchInfo; + runningOp.MarkComplete(); Debug.Assert(_runningOp == runningOp, "How did m_runningOp change?"); _runningOp = null; _runningOpCompleteEvent.Set(); + + if (errorHandler != null && exceptionDispatchInfo != null) + { + InvokeErrorHandler(errorHandler, exceptionDispatchInfo.SourceException); + } } } @@ -389,8 +536,43 @@ internal void OnAsyncRunningOpComplete(Task t) } } _runningOp.MarkComplete(); + + // Capture the fault before clearing the slot so it is routed to the handler, not discarded. + Action errorHandler = _runningOp.ErrorHandler; + ExceptionDispatchInfo exceptionDispatchInfo = _runningOp.ExceptionDispatchInfo; + + // Invoke the handler while the running-op slot is still held so it does not run concurrently + // with the next queued operation. This may still run on the task's completion thread. + if (errorHandler != null && exceptionDispatchInfo != null) + { + InvokeErrorHandler(errorHandler, exceptionDispatchInfo.SourceException); + } + _runningOp = null; _runningOpCompleteEvent.Set(); + + lock (_postedAsyncOperations) + { + if (_postedAsyncOperations.Count > 0) + { + _opSet.Set(); + } + } + } + + private void InvokeErrorHandler(Action errorHandler, Exception exception) + { + try + { + errorHandler(exception); + } + catch (Exception e) when (ExceptionHelper.BeforeCatch(e, Logger, reportOnlyCorrupting: false)) + { + if (PostedOperationErrorEvent != null) + { + PostedOperationErrorEvent(this, e); + } + } } internal bool IsPollThread() From 92ea95260de411d2121af3748951c6d2fcb44457 Mon Sep 17 00:00:00 2001 From: Andrew Wang Date: Thu, 13 Aug 2026 10:26:30 -0700 Subject: [PATCH 20/25] Merge pull request #1614 from microsoft/dev/waan/podmanErrorMessaging Improve missing Podman error message --- src/SSHDebugPS/Podman/PodmanHelper.cs | 25 +++++++++++++++-------- src/SSHDebugPS/UI/UIResources.Designer.cs | 9 ++++++++ src/SSHDebugPS/UI/UIResources.resx | 4 ++++ 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/src/SSHDebugPS/Podman/PodmanHelper.cs b/src/SSHDebugPS/Podman/PodmanHelper.cs index 5d25de158..d81ee780c 100644 --- a/src/SSHDebugPS/Podman/PodmanHelper.cs +++ b/src/SSHDebugPS/Podman/PodmanHelper.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Diagnostics; using System.Linq; using System.Text; @@ -17,7 +18,7 @@ public class PodmanHelper { private const string podmanPSCommand = "ps"; private const string podmanPSArgs = "-f status=running --no-trunc --format \"{{json .}}\""; - + private const int win32ErrorFileNotFound = 2; internal static IEnumerable GetLocalPodmanContainers(string hostname, out int totalContainers) { @@ -28,17 +29,25 @@ internal static IEnumerable GetLocalPodmanContainers(st PodmanCommandSettings settings = new PodmanCommandSettings(hostname, false); settings.SetCommand(podmanPSCommand, podmanPSArgs); - DockerHelper.RunContainerCommand(settings, delegate (string args) + try { - if (args.Trim()[0] == '{') + DockerHelper.RunContainerCommand(settings, delegate (string args) { - if (PodmanContainerInstance.TryCreate(args, out PodmanContainerInstance containerInstance)) + if (args.Trim()[0] == '{') { - containers.Add(containerInstance); + if (PodmanContainerInstance.TryCreate(args, out PodmanContainerInstance containerInstance)) + { + containers.Add(containerInstance); + } + containerCount++; } - containerCount++; - } - }); + }); + } + catch (CommandFailedException ex) when (ex.InnerException is Win32Exception win32Exception && + win32Exception.NativeErrorCode == win32ErrorFileNotFound) + { + throw new CommandFailedException(UIResources.PodmanExecutableNotFound, ex); + } totalContainers = containerCount; return containers; diff --git a/src/SSHDebugPS/UI/UIResources.Designer.cs b/src/SSHDebugPS/UI/UIResources.Designer.cs index fce792a7a..64b0b08a0 100644 --- a/src/SSHDebugPS/UI/UIResources.Designer.cs +++ b/src/SSHDebugPS/UI/UIResources.Designer.cs @@ -302,6 +302,15 @@ public static string Podman_ConnectionToolTip { return ResourceManager.GetString("Podman_ConnectionToolTip", resourceCulture); } } + + /// + /// Looks up a localized string similar to Podman was not found. Install Podman, ensure podman.exe is available on PATH, and restart Visual Studio.. + /// + public static string PodmanExecutableNotFound { + get { + return ResourceManager.GetString("PodmanExecutableNotFound", resourceCulture); + } + } /// /// Looks up a localized string similar to Optional Podman Host name. diff --git a/src/SSHDebugPS/UI/UIResources.resx b/src/SSHDebugPS/UI/UIResources.resx index e06689608..7c7d7e5e5 100644 --- a/src/SSHDebugPS/UI/UIResources.resx +++ b/src/SSHDebugPS/UI/UIResources.resx @@ -252,6 +252,10 @@ Location from which to run the Podman CLI. To manage remote connections, in the menu go to Tools -> Options and find Cross Platform -> Connection Manager. + + Podman was not found. Install Podman, ensure podman.exe is available on PATH, and restart Visual Studio. + {Locked="Podman"} {Locked="podman.exe"} {Locked="PATH"} {Locked="Visual Studio"} + Optional Podman Host name From 1028edbeb00a120cbf2823af21243de04ad49cd2 Mon Sep 17 00:00:00 2001 From: "CSIGS@microsoft.com" Date: Sat, 15 Aug 2026 09:08:06 -0700 Subject: [PATCH 21/25] LEGO: Pull request from lego/hb_d72c5677-3f00-4225-b18e-0a1e8a8f5f0e_20260815094702654 to main (#1615) Juno: check in to lego/hb_d72c5677-3f00-4225-b18e-0a1e8a8f5f0e_20260815094702654. --- loc/lcl/CHS/Microsoft.SSHDebugPS.dll.lcl | 9 +++++++++ loc/lcl/CHT/Microsoft.SSHDebugPS.dll.lcl | 9 +++++++++ loc/lcl/CSY/Microsoft.SSHDebugPS.dll.lcl | 9 +++++++++ loc/lcl/DEU/Microsoft.SSHDebugPS.dll.lcl | 9 +++++++++ loc/lcl/ESN/Microsoft.SSHDebugPS.dll.lcl | 9 +++++++++ loc/lcl/FRA/Microsoft.SSHDebugPS.dll.lcl | 9 +++++++++ loc/lcl/ITA/Microsoft.SSHDebugPS.dll.lcl | 9 +++++++++ loc/lcl/PTB/Microsoft.SSHDebugPS.dll.lcl | 9 +++++++++ loc/lcl/RUS/Microsoft.SSHDebugPS.dll.lcl | 9 +++++++++ loc/lcl/TRK/Microsoft.SSHDebugPS.dll.lcl | 9 +++++++++ 10 files changed, 90 insertions(+) diff --git a/loc/lcl/CHS/Microsoft.SSHDebugPS.dll.lcl b/loc/lcl/CHS/Microsoft.SSHDebugPS.dll.lcl index 0e731b967..e8dc6cb73 100644 --- a/loc/lcl/CHS/Microsoft.SSHDebugPS.dll.lcl +++ b/loc/lcl/CHS/Microsoft.SSHDebugPS.dll.lcl @@ -817,6 +817,15 @@ + + + + + + + + + diff --git a/loc/lcl/CHT/Microsoft.SSHDebugPS.dll.lcl b/loc/lcl/CHT/Microsoft.SSHDebugPS.dll.lcl index cd4a53326..baa50e4e3 100644 --- a/loc/lcl/CHT/Microsoft.SSHDebugPS.dll.lcl +++ b/loc/lcl/CHT/Microsoft.SSHDebugPS.dll.lcl @@ -817,6 +817,15 @@ + + + + + + + + + diff --git a/loc/lcl/CSY/Microsoft.SSHDebugPS.dll.lcl b/loc/lcl/CSY/Microsoft.SSHDebugPS.dll.lcl index f0124f73b..85d2fc6f4 100644 --- a/loc/lcl/CSY/Microsoft.SSHDebugPS.dll.lcl +++ b/loc/lcl/CSY/Microsoft.SSHDebugPS.dll.lcl @@ -817,6 +817,15 @@ + + + + + + + + + diff --git a/loc/lcl/DEU/Microsoft.SSHDebugPS.dll.lcl b/loc/lcl/DEU/Microsoft.SSHDebugPS.dll.lcl index 62891d658..c5dc1bb96 100644 --- a/loc/lcl/DEU/Microsoft.SSHDebugPS.dll.lcl +++ b/loc/lcl/DEU/Microsoft.SSHDebugPS.dll.lcl @@ -817,6 +817,15 @@ + + + + + + + + + diff --git a/loc/lcl/ESN/Microsoft.SSHDebugPS.dll.lcl b/loc/lcl/ESN/Microsoft.SSHDebugPS.dll.lcl index 40a4d86d7..4a858c612 100644 --- a/loc/lcl/ESN/Microsoft.SSHDebugPS.dll.lcl +++ b/loc/lcl/ESN/Microsoft.SSHDebugPS.dll.lcl @@ -817,6 +817,15 @@ + + + + + + + + + diff --git a/loc/lcl/FRA/Microsoft.SSHDebugPS.dll.lcl b/loc/lcl/FRA/Microsoft.SSHDebugPS.dll.lcl index eb7049a08..3c14cb564 100644 --- a/loc/lcl/FRA/Microsoft.SSHDebugPS.dll.lcl +++ b/loc/lcl/FRA/Microsoft.SSHDebugPS.dll.lcl @@ -817,6 +817,15 @@ + + + + + + + + + diff --git a/loc/lcl/ITA/Microsoft.SSHDebugPS.dll.lcl b/loc/lcl/ITA/Microsoft.SSHDebugPS.dll.lcl index dae7bb32d..8517e5626 100644 --- a/loc/lcl/ITA/Microsoft.SSHDebugPS.dll.lcl +++ b/loc/lcl/ITA/Microsoft.SSHDebugPS.dll.lcl @@ -817,6 +817,15 @@ + + + + + + + + + diff --git a/loc/lcl/PTB/Microsoft.SSHDebugPS.dll.lcl b/loc/lcl/PTB/Microsoft.SSHDebugPS.dll.lcl index 18d11eb8e..998d7c40a 100644 --- a/loc/lcl/PTB/Microsoft.SSHDebugPS.dll.lcl +++ b/loc/lcl/PTB/Microsoft.SSHDebugPS.dll.lcl @@ -817,6 +817,15 @@ + + + + + + + + + diff --git a/loc/lcl/RUS/Microsoft.SSHDebugPS.dll.lcl b/loc/lcl/RUS/Microsoft.SSHDebugPS.dll.lcl index d5ba360f9..ac1892b21 100644 --- a/loc/lcl/RUS/Microsoft.SSHDebugPS.dll.lcl +++ b/loc/lcl/RUS/Microsoft.SSHDebugPS.dll.lcl @@ -817,6 +817,15 @@ + + + + + + + + + diff --git a/loc/lcl/TRK/Microsoft.SSHDebugPS.dll.lcl b/loc/lcl/TRK/Microsoft.SSHDebugPS.dll.lcl index 6619b27b9..1b66dcfa6 100644 --- a/loc/lcl/TRK/Microsoft.SSHDebugPS.dll.lcl +++ b/loc/lcl/TRK/Microsoft.SSHDebugPS.dll.lcl @@ -817,6 +817,15 @@ + + + + + + + + + From 72067f74d2b494549b08fa4ce820c895848495e7 Mon Sep 17 00:00:00 2001 From: "CSIGS@microsoft.com" Date: Mon, 17 Aug 2026 16:10:30 -0700 Subject: [PATCH 22/25] LEGO: Pull request from lego/hb_d72c5677-3f00-4225-b18e-0a1e8a8f5f0e_20260816094627420 to main (#1616) Juno: check in to lego/hb_d72c5677-3f00-4225-b18e-0a1e8a8f5f0e_20260816094627420. --- loc/lcl/JPN/Microsoft.SSHDebugPS.dll.lcl | 9 +++++++++ loc/lcl/KOR/Microsoft.SSHDebugPS.dll.lcl | 9 +++++++++ loc/lcl/PLK/Microsoft.SSHDebugPS.dll.lcl | 9 +++++++++ 3 files changed, 27 insertions(+) diff --git a/loc/lcl/JPN/Microsoft.SSHDebugPS.dll.lcl b/loc/lcl/JPN/Microsoft.SSHDebugPS.dll.lcl index 01a808ff5..d18df088a 100644 --- a/loc/lcl/JPN/Microsoft.SSHDebugPS.dll.lcl +++ b/loc/lcl/JPN/Microsoft.SSHDebugPS.dll.lcl @@ -817,6 +817,15 @@ + + + + + + + + + diff --git a/loc/lcl/KOR/Microsoft.SSHDebugPS.dll.lcl b/loc/lcl/KOR/Microsoft.SSHDebugPS.dll.lcl index e53e5e42c..9b99335d9 100644 --- a/loc/lcl/KOR/Microsoft.SSHDebugPS.dll.lcl +++ b/loc/lcl/KOR/Microsoft.SSHDebugPS.dll.lcl @@ -817,6 +817,15 @@ + + + + + + + + + diff --git a/loc/lcl/PLK/Microsoft.SSHDebugPS.dll.lcl b/loc/lcl/PLK/Microsoft.SSHDebugPS.dll.lcl index 7a382298a..a6fc743f9 100644 --- a/loc/lcl/PLK/Microsoft.SSHDebugPS.dll.lcl +++ b/loc/lcl/PLK/Microsoft.SSHDebugPS.dll.lcl @@ -817,6 +817,15 @@ + + + + + + + + + From cb9702dc18e9dbffacfc62686254119a75d6b0de Mon Sep 17 00:00:00 2001 From: Dan Fiedler <151573964+danfiedler-msft@users.noreply.github.com> Date: Mon, 24 Aug 2026 12:40:15 -0400 Subject: [PATCH 23/25] Pin GitHub Actions to full-length commit SHAs (#1619) ## Summary This PR pins GitHub Actions to full-length commit SHAs for improved security and reproducibility and adds a 7 day cooldown to Dependabot configuration for GitHub Actions. This work is described in more detail at https://aka.ms/action-pinning. ## Why? Pinning actions to commit SHAs prevents supply-chain attacks where a tag could be moved to point to malicious code. This is a recommended security best practice per the [GitHub Actions security hardening guide](https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions#using-third-party-actions). This change mitigates the risk of tag retargeting to malicious code as seen in incidents like the [tj-actions/changed-files action compromise](https://www.stepsecurity.io/blog/harden-runner-detection-tj-actions-changed-files-action-is-compromised) or [codfish/semantic-release-action compromise](https://www.stepsecurity.io/blog/supply-chain-compromise-codfish-semantic-release-action) and improves the integrity and reproducibility of the CI/CD pipeline. ## What changed? **Action pinning:** Third-party action references in `.github/workflows/` that used mutable tag-based references (e.g., `actions/checkout@v4`) have been updated to full-length commit SHAs with a version comment (e.g., `actions/checkout@ # v4`) using the [pinact](https://github.com/suzuki-shunsuke/pinact) tool. References that were already pinned to a SHA, or that used immutable release tags, were left unchanged. **Dependabot configuration:** `.github/dependabot.yml` has been updated to ensure a `github-actions` package-ecosystem section is present with a `cooldown` configuration (`default-days: 7`). If the file did not exist, it was created. If a `github-actions` section already existed, only the `cooldown` block was added or its `default-days` value was increased to 7 if it was lower. The 7-day cooldown provides a window for the community to detect and report compromised releases before they are automatically proposed as updates, reducing exposure to supply-chain attacks via newly published malicious versions. ## Is this safe to merge? Yes. The pinned SHAs correspond to the same commits that the existing tags pointed to. No behavioral changes in action execution are introduced. You can verify the pinned SHA value using the GitHub REST API (e.g., the commit hash for `actions/checkout@v7` can be found in the `sha` property in the JSON response for `GET https://api.github.com/repos/actions/checkout/commits/v7`). ## Additional Information For more information, please see https://aka.ms/action-pinning --- .github/dependabot.yml | 11 +++++++++ .github/workflows/Build-And-Test.yml | 34 ++++++++++++++-------------- 2 files changed, 28 insertions(+), 17 deletions(-) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..2c48305b7 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + groups: + github-actions: + patterns: ["*"] + schedule: + interval: "weekly" + cooldown: + default-days: 7 diff --git a/.github/workflows/Build-And-Test.yml b/.github/workflows/Build-And-Test.yml index 6f176ff78..87cd26ac1 100644 --- a/.github/workflows/Build-And-Test.yml +++ b/.github/workflows/Build-And-Test.yml @@ -22,20 +22,20 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: fetch-depth: 0 - name: Install .NET Core - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 with: dotnet-version: 8.0.x - name: Setup MSBuild.exe - uses: microsoft/setup-msbuild@v2 + uses: microsoft/setup-msbuild@6fb02220983dee41ce7ae257b6f4d8f9bf5ed4ce # v2.0.0 - name: Setup NuGet.exe for use with actions - uses: NuGet/setup-nuget@v2 + uses: NuGet/setup-nuget@d105a947828025cd7a980103c35ba2bfae586d0f # v2.0.2 - name: Build MIDebugEngine run: | @@ -44,7 +44,7 @@ jobs: Configuration: ${{ matrix.configuration }} - name: Setup VSTest.console.exe - uses: darenm/Setup-VSTest@v1.3 + uses: darenm/Setup-VSTest@3a16d909a1f3bbc65b52f8270d475d905e7d3e44 # v1.3 - name: Run VS Extension tests run: vstest.console.exe ${{ github.workspace }}\bin\${{ matrix.configuration }}\MICoreUnitTests.dll ${{ github.workspace }}\bin\${{ matrix.configuration }}\JDbgUnitTests.dll ${{ github.workspace }}\bin\${{ matrix.configuration }}\SSHDebugTests.dll ${{ github.workspace }}\bin\${{ matrix.configuration }}\MIDebugEngineUnitTests.dll @@ -54,20 +54,20 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: fetch-depth: 0 - name: Install .NET Core - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 with: dotnet-version: 8.0.x - name: Setup MSBuild.exe - uses: microsoft/setup-msbuild@v2 + uses: microsoft/setup-msbuild@6fb02220983dee41ce7ae257b6f4d8f9bf5ed4ce # v2.0.0 - name: Setup NuGet.exe for use with actions - uses: NuGet/setup-nuget@v2 + uses: NuGet/setup-nuget@d105a947828025cd7a980103c35ba2bfae586d0f # v2.0.2 - name: Build MIDebugEngine run: | @@ -78,7 +78,7 @@ jobs: copy ${{ github.workspace }}\bin\DebugAdapterProtocolTests\Debug\CppTests\TestConfigurations\config_msys_gdb.xml ${{ github.workspace }}\bin\DebugAdapterProtocolTests\Debug\CppTests\config.xml - name: Setup MSYS2 - uses: msys2/setup-msys2@v2 + uses: msys2/setup-msys2@66cd2cce69caa17b53920067426061ca1de3a884 # v2.32.0 with: msystem: MINGW64 path-type: inherit @@ -102,7 +102,7 @@ jobs: dotnet test $CppTestsPath --logger "trx;LogFileName=$ResultsPath" - name: 'Upload Test Results' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 if: ${{ always() }} with: name: win_msys2_x64_results @@ -112,12 +112,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: fetch-depth: 0 - name: Install .NET Core - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 with: dotnet-version: 8.0.x @@ -142,7 +142,7 @@ jobs: ${{ github.workspace }}/eng/Scripts/CI-Test.sh - name: 'Upload Test Results' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 if: ${{ always() }} with: name: linux_x64_results @@ -152,12 +152,12 @@ jobs: runs-on: macos-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: fetch-depth: 0 - name: Install .NET Core - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 with: dotnet-version: 8.0.x @@ -173,7 +173,7 @@ jobs: # ${{ github.workspace }}/eng/Scripts/CI-Test.sh - name: 'Upload Test Results' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 if: ${{ always() }} with: name: osx_x64_results From 292bc80d96175afd9f935540a675fd374569f586 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Aug 2026 15:57:43 -0700 Subject: [PATCH 24/25] Bump the github-actions group with 5 updates (#1620) Bumps the github-actions group with 5 updates: | Package | From | To | | --- | --- | --- | | [actions/checkout](https://github.com/actions/checkout) | `4.4.0` | `7.0.1` | | [actions/setup-dotnet](https://github.com/actions/setup-dotnet) | `4.3.1` | `6.0.0` | | [microsoft/setup-msbuild](https://github.com/microsoft/setup-msbuild) | `2.0.0` | `3.0.0` | | [NuGet/setup-nuget](https://github.com/nuget/setup-nuget) | `2.0.2` | `3.1.0` | | [actions/upload-artifact](https://github.com/actions/upload-artifact) | `4.6.2` | `7.0.1` | Updates `actions/checkout` from 4.4.0 to 7.0.1 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/11d5960a326750d5838078e36cf38b85af677262...3d3c42e5aac5ba805825da76410c181273ba90b1) Updates `actions/setup-dotnet` from 4.3.1 to 6.0.0 - [Release notes](https://github.com/actions/setup-dotnet/releases) - [Commits](https://github.com/actions/setup-dotnet/compare/67a3573c9a986a3f9c594539f4ab511d57bb3ce9...a98b56852c35b8e3190ac28c8c2271da59106c68) Updates `microsoft/setup-msbuild` from 2.0.0 to 3.0.0 - [Release notes](https://github.com/microsoft/setup-msbuild/releases) - [Commits](https://github.com/microsoft/setup-msbuild/compare/6fb02220983dee41ce7ae257b6f4d8f9bf5ed4ce...30375c66a4eea26614e0d39710365f22f8b0af57) Updates `NuGet/setup-nuget` from 2.0.2 to 3.1.0 - [Release notes](https://github.com/nuget/setup-nuget/releases) - [Commits](https://github.com/nuget/setup-nuget/compare/d105a947828025cd7a980103c35ba2bfae586d0f...b26b823c478ee115be5c9403e62c90b0bf943843) Updates `actions/upload-artifact` from 4.6.2 to 7.0.1 - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/ea165f8d65b6e75b540449e92b4886f43607fa02...043fb46d1a93c77aae656e7c1c64a875d1fc6a0a) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 7.0.1 dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: actions/setup-dotnet dependency-version: 6.0.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: microsoft/setup-msbuild dependency-version: 3.0.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: NuGet/setup-nuget dependency-version: 3.1.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: actions/upload-artifact dependency-version: 7.0.1 dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/Build-And-Test.yml | 30 ++++++++++++++-------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/Build-And-Test.yml b/.github/workflows/Build-And-Test.yml index 87cd26ac1..ce09dde7c 100644 --- a/.github/workflows/Build-And-Test.yml +++ b/.github/workflows/Build-And-Test.yml @@ -22,20 +22,20 @@ jobs: steps: - name: Checkout - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 - name: Install .NET Core - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 + uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 with: dotnet-version: 8.0.x - name: Setup MSBuild.exe - uses: microsoft/setup-msbuild@6fb02220983dee41ce7ae257b6f4d8f9bf5ed4ce # v2.0.0 + uses: microsoft/setup-msbuild@30375c66a4eea26614e0d39710365f22f8b0af57 # v3.0.0 - name: Setup NuGet.exe for use with actions - uses: NuGet/setup-nuget@d105a947828025cd7a980103c35ba2bfae586d0f # v2.0.2 + uses: NuGet/setup-nuget@b26b823c478ee115be5c9403e62c90b0bf943843 # v3.1.0 - name: Build MIDebugEngine run: | @@ -54,20 +54,20 @@ jobs: steps: - name: Checkout - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 - name: Install .NET Core - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 + uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 with: dotnet-version: 8.0.x - name: Setup MSBuild.exe - uses: microsoft/setup-msbuild@6fb02220983dee41ce7ae257b6f4d8f9bf5ed4ce # v2.0.0 + uses: microsoft/setup-msbuild@30375c66a4eea26614e0d39710365f22f8b0af57 # v3.0.0 - name: Setup NuGet.exe for use with actions - uses: NuGet/setup-nuget@d105a947828025cd7a980103c35ba2bfae586d0f # v2.0.2 + uses: NuGet/setup-nuget@b26b823c478ee115be5c9403e62c90b0bf943843 # v3.1.0 - name: Build MIDebugEngine run: | @@ -102,7 +102,7 @@ jobs: dotnet test $CppTestsPath --logger "trx;LogFileName=$ResultsPath" - name: 'Upload Test Results' - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 if: ${{ always() }} with: name: win_msys2_x64_results @@ -112,12 +112,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 - name: Install .NET Core - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 + uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 with: dotnet-version: 8.0.x @@ -142,7 +142,7 @@ jobs: ${{ github.workspace }}/eng/Scripts/CI-Test.sh - name: 'Upload Test Results' - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 if: ${{ always() }} with: name: linux_x64_results @@ -152,12 +152,12 @@ jobs: runs-on: macos-latest steps: - name: Checkout - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 - name: Install .NET Core - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 + uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 with: dotnet-version: 8.0.x @@ -173,7 +173,7 @@ jobs: # ${{ github.workspace }}/eng/Scripts/CI-Test.sh - name: 'Upload Test Results' - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 if: ${{ always() }} with: name: osx_x64_results From 8ffc66d42067463d6582c8b1e3911a89b2069297 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 27 Aug 2026 08:25:02 -0700 Subject: [PATCH 25/25] Bump NuGet/setup-nuget from 3.1.0 to 4 in the github-actions group (#1621) Bumps the github-actions group with 1 update: [NuGet/setup-nuget](https://github.com/nuget/setup-nuget). Updates `NuGet/setup-nuget` from 3.1.0 to 4 - [Release notes](https://github.com/nuget/setup-nuget/releases) - [Commits](https://github.com/nuget/setup-nuget/compare/b26b823c478ee115be5c9403e62c90b0bf943843...fd55a6f3b34392fa83fde1454582407d8c714123) --- updated-dependencies: - dependency-name: NuGet/setup-nuget dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/Build-And-Test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/Build-And-Test.yml b/.github/workflows/Build-And-Test.yml index ce09dde7c..4132cca27 100644 --- a/.github/workflows/Build-And-Test.yml +++ b/.github/workflows/Build-And-Test.yml @@ -35,7 +35,7 @@ jobs: uses: microsoft/setup-msbuild@30375c66a4eea26614e0d39710365f22f8b0af57 # v3.0.0 - name: Setup NuGet.exe for use with actions - uses: NuGet/setup-nuget@b26b823c478ee115be5c9403e62c90b0bf943843 # v3.1.0 + uses: NuGet/setup-nuget@fd55a6f3b34392fa83fde1454582407d8c714123 # v4.0 - name: Build MIDebugEngine run: | @@ -67,7 +67,7 @@ jobs: uses: microsoft/setup-msbuild@30375c66a4eea26614e0d39710365f22f8b0af57 # v3.0.0 - name: Setup NuGet.exe for use with actions - uses: NuGet/setup-nuget@b26b823c478ee115be5c9403e62c90b0bf943843 # v3.1.0 + uses: NuGet/setup-nuget@fd55a6f3b34392fa83fde1454582407d8c714123 # v4.0 - name: Build MIDebugEngine run: |