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
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/.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 db5f95201..4132cca27 100644
--- a/.github/workflows/Build-And-Test.yml
+++ b/.github/workflows/Build-And-Test.yml
@@ -22,29 +22,29 @@ jobs:
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Install .NET Core
- uses: actions/setup-dotnet@v4
+ uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: 8.0.x
- name: Setup MSBuild.exe
- uses: microsoft/setup-msbuild@v2
+ uses: microsoft/setup-msbuild@30375c66a4eea26614e0d39710365f22f8b0af57 # v3.0.0
- name: Setup NuGet.exe for use with actions
- uses: NuGet/setup-nuget@v2
+ uses: NuGet/setup-nuget@fd55a6f3b34392fa83fde1454582407d8c714123 # v4.0
- 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 }}
- 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,31 +54,31 @@ jobs:
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Install .NET Core
- uses: actions/setup-dotnet@v4
+ uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: 8.0.x
- name: Setup MSBuild.exe
- uses: microsoft/setup-msbuild@v2
+ uses: microsoft/setup-msbuild@30375c66a4eea26614e0d39710365f22f8b0af57 # v3.0.0
- name: Setup NuGet.exe for use with actions
- uses: NuGet/setup-nuget@v2
+ uses: NuGet/setup-nuget@fd55a6f3b34392fa83fde1454582407d8c714123 # v4.0
- 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: |
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@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@v4
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Install .NET Core
- uses: actions/setup-dotnet@v4
+ 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@v4
+ 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@v4
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Install .NET Core
- uses: actions/setup-dotnet@v4
+ 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@v4
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: ${{ always() }}
with:
name: osx_x64_results
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/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/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
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..8d2d8edfe
--- /dev/null
+++ b/docs/Building-outside-of-VS-for-AI.md
@@ -0,0 +1,50 @@
+# 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
+```
+
+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"
+$env:Path = (Split-Path $msbuild) + ';' + $env:Path
+```
+
+### Linux / macOS
+
+```bash
+eng/Scripts/CI-Build.sh
+```
+
+This runs `dotnet build src/MIDebugEngine-Unix.sln`
+
+## 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..66611555f
--- /dev/null
+++ b/docs/CodingStandards-CSharp-for-AI.md
@@ -0,0 +1,56 @@
+# 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.
+
+## 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/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..9f4da6529
--- /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 # 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.csproj`. Pick the right template from `test\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/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/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
diff --git a/loc/lcl/CHS/Microsoft.SSHDebugPS.dll.lcl b/loc/lcl/CHS/Microsoft.SSHDebugPS.dll.lcl
index f7bcef49c..e8dc6cb73 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,60 @@
+ -
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+ -
+
+ Options and find Cross Platform -> Connection Manager.]]>
+
+ “选项”,并查找“跨平台”->“连接管理器”。]]>
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
-
@@ -868,7 +958,7 @@
-
-
+
diff --git a/loc/lcl/CHT/Microsoft.SSHDebugPS.dll.lcl b/loc/lcl/CHT/Microsoft.SSHDebugPS.dll.lcl
index 94075ea4f..baa50e4e3 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,60 @@
+ -
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+ -
+
+ Options and find Cross Platform -> Connection Manager.]]>
+
+ [選項]5D;,然後找到 [跨平台]5D; -> [連線管理員]5D;。]]>
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
-
@@ -868,7 +958,7 @@
-
-
+
diff --git a/loc/lcl/CSY/Microsoft.SSHDebugPS.dll.lcl b/loc/lcl/CSY/Microsoft.SSHDebugPS.dll.lcl
index 8f5db7fab..85d2fc6f4 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,60 @@
+ -
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+ -
+
+ Options and find Cross Platform -> Connection Manager.]]>
+
+ Možnosti a vyhledejte možnost Pro různé platformy > Správce připojení.]]>
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
-
@@ -868,7 +958,7 @@
-
-
+
diff --git a/loc/lcl/DEU/Microsoft.SSHDebugPS.dll.lcl b/loc/lcl/DEU/Microsoft.SSHDebugPS.dll.lcl
index e04c77111..c5dc1bb96 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,60 @@
+ -
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+ -
+
+ Options and find Cross Platform -> Connection Manager.]]>
+
+ „Optionen“ > „Plattformübergreifend“ > „Verbindungs-Manager“.]]>
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
-
@@ -868,7 +958,7 @@
-
-
+
diff --git a/loc/lcl/ESN/Microsoft.SSHDebugPS.dll.lcl b/loc/lcl/ESN/Microsoft.SSHDebugPS.dll.lcl
index 3f0c41e93..4a858c612 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,60 @@
+ -
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+ -
+
+ Options and find Cross Platform -> Connection Manager.]]>
+
+ Opciones y busque Multiplataforma -> Administrador de conexiones.]]>
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
-
@@ -868,7 +958,7 @@
-
-
+
diff --git a/loc/lcl/FRA/Microsoft.SSHDebugPS.dll.lcl b/loc/lcl/FRA/Microsoft.SSHDebugPS.dll.lcl
index 32dc1aadd..3c14cb564 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,60 @@
+ -
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+ -
+
+ Options and find Cross Platform -> Connection Manager.]]>
+
+ Options, puis recherchez Multiplateforme -> Gestionnaire des connexions.]]>
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
-
@@ -868,7 +958,7 @@
-
-
+
diff --git a/loc/lcl/ITA/Microsoft.SSHDebugPS.dll.lcl b/loc/lcl/ITA/Microsoft.SSHDebugPS.dll.lcl
index 126f24abe..8517e5626 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,60 @@
+ -
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+ -
+
+ Options and find Cross Platform -> Connection Manager.]]>
+
+ Opzioni e trovare Multipiattaforma -> Gestione connessioni.]]>
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
-
@@ -868,7 +958,7 @@
-
-
+
diff --git a/loc/lcl/JPN/Microsoft.SSHDebugPS.dll.lcl b/loc/lcl/JPN/Microsoft.SSHDebugPS.dll.lcl
index 5d9f51eae..d18df088a 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,60 @@
+ -
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+ -
+
+ Options and find Cross Platform -> Connection Manager.]]>
+
+ [オプション]5D; に移動し、[クロス プラットフォーム]5D; -> [接続マネージャー]5D; を探します。]]>
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
-
@@ -868,7 +958,7 @@
-
-
+
diff --git a/loc/lcl/KOR/Microsoft.SSHDebugPS.dll.lcl b/loc/lcl/KOR/Microsoft.SSHDebugPS.dll.lcl
index 4dcd06cc2..9b99335d9 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,60 @@
+ -
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+ -
+
+ Options and find Cross Platform -> Connection Manager.]]>
+
+ [옵션]5D;으로 이동하여 [플랫폼 간]5D; > [연결 관리자]5D;를 찾습니다.]]>
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
-
@@ -868,7 +958,7 @@
-
-
+
diff --git a/loc/lcl/PLK/Microsoft.SSHDebugPS.dll.lcl b/loc/lcl/PLK/Microsoft.SSHDebugPS.dll.lcl
index f5c9c8b0e..a6fc743f9 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,60 @@
+ -
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+ -
+
+ Options and find Cross Platform -> Connection Manager.]]>
+
+ Opcje > i znajdź pozycje Wiele platform -> Menedżer połączeń.]]>
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
-
@@ -868,7 +958,7 @@
-
-
+
diff --git a/loc/lcl/PTB/Microsoft.SSHDebugPS.dll.lcl b/loc/lcl/PTB/Microsoft.SSHDebugPS.dll.lcl
index 915c119a8..998d7c40a 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,60 @@
+ -
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+ -
+
+ Options and find Cross Platform -> Connection Manager.]]>
+
+ Opções e localize Multiplataforma -> Gerenciador de Conexões.]]>
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
-
@@ -868,7 +958,7 @@
-
-
+
diff --git a/loc/lcl/RUS/Microsoft.SSHDebugPS.dll.lcl b/loc/lcl/RUS/Microsoft.SSHDebugPS.dll.lcl
index 535ce8576..ac1892b21 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,60 @@
+ -
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+ -
+
+ Options and find Cross Platform -> Connection Manager.]]>
+
+ "Параметры" и найдите раздел "Кроссплатформенные" > "Диспетчер подключений".]]>
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
-
@@ -868,7 +958,7 @@
-
-
+
diff --git a/loc/lcl/TRK/Microsoft.SSHDebugPS.dll.lcl b/loc/lcl/TRK/Microsoft.SSHDebugPS.dll.lcl
index ed3606cc1..1b66dcfa6 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,60 @@
+ -
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+ -
+
+ Options and find Cross Platform -> Connection Manager.]]>
+
+ Seçenekler'e gidin ve Çoklu Platform -> Bağlantı Yöneticisi'ni bulun.]]>
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
-
@@ -868,7 +958,7 @@
-
-
+
diff --git a/src/DebugEngineHost.Common/HostLogChannel.cs b/src/DebugEngineHost.Common/HostLogChannel.cs
index 14bdc1b57..5f4434193 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.
/*
@@ -50,19 +50,19 @@ public interface ILogChannel
public class HostLogChannel : ILogChannel
{
- private readonly Action _log;
- private StreamWriter _logFile;
+ private readonly Action? _log;
+ private StreamWriter? _logFile;
private LogLevel _minLevelToBeLogged;
private readonly object _lock = new object();
private HostLogChannel() { }
- 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..c34e817e5 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();
}
@@ -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();
}
@@ -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();
}
@@ -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();
}
@@ -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/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.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..75190588c 100644
--- a/src/DebugEngineHost.VSCode/HostLogger.cs
+++ b/src/DebugEngineHost.VSCode/HostLogger.cs
@@ -7,39 +7,39 @@ 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);
}
}
- public static void EnableHostLogging(Action callback, LogLevel level = LogLevel.Verbose)
+ 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..62faf45de 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,17 +17,17 @@ public static class HostRunInTerminal
///
public static bool IsRunInTerminalAvailable()
{
- return s_runInTerminalCallback != null;
+ return s_runInTerminalCallback is not null;
}
///
/// 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 != null)
+ 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);
@@ -42,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.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/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.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..271fcf718 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)
+ 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..243857d8d 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;
@@ -19,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/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/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/DebugEngineHost/RegistryMonitor.cs b/src/DebugEngineHost/RegistryMonitor.cs
index 0281bc371..2fdc26e34 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;
@@ -73,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";
@@ -98,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();
@@ -112,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)
{
@@ -126,7 +144,7 @@ private void Monitor()
_nativsLogger?.WriteLine(LogLevel.Error, Resource.Error_WatchRegistry, errorCode);
break;
}
- RegChanged?.Invoke(this, null);
+ RegChanged?.Invoke(this, EventArgs.Empty);
}
}
}
@@ -134,9 +152,11 @@ private void Monitor()
}
finally
{
- _stoppedEvent.Dispose();
- _stoppedEvent = null;
-
+ lock (_stopLock)
+ {
+ _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/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 07de24092..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;
@@ -48,7 +47,7 @@ public enum AsyncBreakSignal
public abstract class MICommandFactory
{
- protected Debugger _debugger;
+ protected readonly Debugger _debugger;
public MIMode Mode { get; private set; }
@@ -56,6 +55,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,21 +67,20 @@ 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;
}
- public static string SpanNextAddr(string line, out ulong addr)
+ public static string? SpanNextAddr(string line, out ulong addr)
{
addr = 0;
char[] endOfNum = { ' ', '\t', '\"' };
@@ -479,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);
@@ -529,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)
@@ -559,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;
}
@@ -628,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();
}
@@ -704,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 83100ab3e..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;
@@ -19,6 +18,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"; }
@@ -52,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
{
@@ -98,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
{
@@ -188,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;
@@ -276,7 +280,7 @@ public override TargetArchitecture ParseTargetArchitectureResult(string result)
{
while (true)
{
- string resultLine = stringReader.ReadLine();
+ string? resultLine = stringReader.ReadLine();
if (resultLine == null)
break;
@@ -322,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 b430f3257..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;
@@ -16,6 +15,11 @@ namespace MICore
{
internal class LlldbMICommandFactory : MICommandFactory
{
+ public LlldbMICommandFactory(Debugger debugger)
+ : base(debugger)
+ {
+ }
+
public override string Name
{
get { return "LLDB"; }
@@ -111,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()
@@ -131,7 +135,7 @@ public override TargetArchitecture ParseTargetArchitectureResult(string result)
{
while (true)
{
- string resultLine = stringReader.ReadLine();
+ string? resultLine = stringReader.ReadLine();
if (resultLine == null)
break;
@@ -211,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 710d6d70c..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;
@@ -72,7 +71,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")]
@@ -80,10 +79,10 @@ public bool IsClosed
public LaunchOptions LaunchOptions { get { return this._launchOptions; } }
private Queue> _internalBreakActions = new Queue>();
- private TaskCompletionSource
[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;
diff --git a/src/MIDebugEngine-Unix.sln b/src/MIDebugEngine-Unix.sln
index 2fb7d689c..8dfd0c2a1 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
@@ -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 ed5df3b55..7e1c4e476 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
@@ -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
@@ -103,6 +100,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 +306,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/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/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 804709bc2..27edfad98 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);
@@ -1990,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)
{
@@ -2001,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;
@@ -2013,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"))
{
@@ -2051,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/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()
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/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/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}
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/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]"
+ }
}
}
diff --git a/src/SSHDebugPS/ConnectionManager.cs b/src/SSHDebugPS/ConnectionManager.cs
index 0222926b0..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;
@@ -38,7 +39,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);
@@ -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();
@@ -146,11 +187,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..612c91035
--- /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
+ {
+ Docker,
+ Podman
+ }
+}
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/ContainerInstance.cs b/src/SSHDebugPS/Docker/ContainerInstance.cs
new file mode 100644
index 000000000..1e830d4cb
--- /dev/null
+++ b/src/SSHDebugPS/Docker/ContainerInstance.cs
@@ -0,0 +1,137 @@
+// 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 Microsoft.DebugEngineHost;
+using Microsoft.SSHDebugPS.Utilities;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace Microsoft.SSHDebugPS.Docker
+{
+ public class ContainerInstance : IContainerInstance
+ {
+ ///
+ /// Create a ContainerInstance from the results of docker ps in JSON format
+ ///
+ public static bool TryCreate(string json, out ContainerInstance instance)
+ {
+ instance = null;
+ try
+ {
+ JObject obj = JObject.Parse(json);
+ instance = obj.ToObject();
+ }
+ catch (Exception e)
+ {
+ HostTelemetry.SendEvent(TelemetryHelper.Event_DockerPSParseFailure, new KeyValuePair[] {
+ new KeyValuePair(TelemetryHelper.Property_ExceptionName, e.GetType().Name)
+ });
+
+ string error = e.ToString();
+ VsOutputWindowWrapper.WriteLine(StringResources.Error_DockerPSParseFailed.FormatCurrentCultureWithArgs(json, error), StringResources.Docker_PSName);
+ Debug.Fail(error);
+ }
+ return instance != null;
+ }
+
+ protected ContainerInstance() { }
+
+ #region JsonProperties
+
+ [JsonProperty("ID")]
+ public virtual string Id { get; set; }
+
+ [JsonProperty("Names")]
+ public virtual string Name { get; set; }
+
+ [JsonProperty(nameof(Image))]
+ public virtual string Image { get; protected set; }
+
+ [JsonProperty(nameof(Ports))]
+ public virtual string Ports { get; set; }
+
+ [JsonProperty(nameof(Command))]
+ public virtual string Command { get; protected set; }
+
+ [JsonProperty(nameof(Status))]
+ public virtual string Status { get; protected set; }
+
+ [JsonProperty("CreatedAt")]
+ public virtual string Created { get; protected set; }
+
+ [JsonIgnore]
+ public string Platform { get; set; }
+
+ #endregion
+
+ #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 (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
+
+ // 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 d152e61ac..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("="))
@@ -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
deleted file mode 100644
index 810975773..000000000
--- a/src/SSHDebugPS/Docker/DockerContainerInstance.cs
+++ /dev/null
@@ -1,90 +0,0 @@
-// 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 Microsoft.DebugEngineHost;
-using Microsoft.SSHDebugPS.Utilities;
-using Newtonsoft.Json;
-using Newtonsoft.Json.Linq;
-
-namespace Microsoft.SSHDebugPS.Docker
-{
- public class DockerContainerInstance : ContainerInstance
- {
- ///
- /// Create a DockerContainerInstance from the results of docker ps in JSON format
- ///
- public static bool TryCreate(string json, out DockerContainerInstance instance)
- {
- instance = null;
- try
- {
- JObject obj = JObject.Parse(json);
- instance = obj.ToObject();
- }
- catch (Exception e)
- {
- HostTelemetry.SendEvent(TelemetryHelper.Event_DockerPSParseFailure, new KeyValuePair[] {
- new KeyValuePair(TelemetryHelper.Property_ExceptionName, e.GetType().Name)
- });
-
- string error = e.ToString();
- VsOutputWindowWrapper.WriteLine(StringResources.Error_DockerPSParseFailed.FormatCurrentCultureWithArgs(json, error), StringResources.Docker_PSName);
- Debug.Fail(error);
- }
- return instance != null;
- }
-
- private DockerContainerInstance() { }
-
- #region JsonProperties
-
- [JsonProperty("ID")]
- public override string Id { get; set; }
-
- [JsonProperty("Names")]
- public override string Name { get; set; }
-
- [JsonProperty(nameof(Image))]
- public string Image { get; private set; }
-
- [JsonProperty(nameof(Ports))]
- public string Ports { get; set; }
-
- [JsonProperty(nameof(Command))]
- public string Command { get; private set; }
-
- [JsonProperty(nameof(Status))]
- public string Status { get; private set; }
-
- [JsonProperty("CreatedAt")]
- public string Created { get; private set; }
-
- [JsonIgnore]
- public string Platform { get; set; }
-
- #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)
- {
- if (instance is DockerContainerInstance other)
- {
- // 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 false;
- }
-
- protected override 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();
- }
- }
-}
diff --git a/src/SSHDebugPS/Docker/DockerDiscoveryStrategy.cs b/src/SSHDebugPS/Docker/DockerDiscoveryStrategy.cs
new file mode 100644
index 000000000..e6847967d
--- /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 (ContainerInstance 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 (ContainerInstance container in containers)
+ {
+ container.Platform = platform;
+ }
+ }
+ }
+ else
+ {
+ foreach (ContainerInstance 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..c433cb9d6 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;
});
@@ -174,20 +174,20 @@ 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);
- RunDockerCommand(settings, delegate (string args)
+ RunContainerCommand(settings, delegate (string args)
{
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/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..c64cff6ec
--- /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/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..d81ee780c
--- /dev/null
+++ b/src/SSHDebugPS/Podman/PodmanHelper.cs
@@ -0,0 +1,164 @@
+// 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;
+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 .}}\"";
+ private const int win32ErrorFileNotFound = 2;
+
+ 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);
+
+ try
+ {
+ DockerHelper.RunContainerCommand(settings, delegate (string args)
+ {
+ if (args.Trim()[0] == '{')
+ {
+ if (PodmanContainerInstance.TryCreate(args, out PodmanContainerInstance containerInstance))
+ {
+ containers.Add(containerInstance);
+ }
+ containerCount++;
+ }
+ });
+ }
+ catch (CommandFailedException ex) when (ex.InnerException is Win32Exception win32Exception &&
+ win32Exception.NativeErrorCode == win32ErrorFileNotFound)
+ {
+ throw new CommandFailedException(UIResources.PodmanExecutableNotFound, ex);
+ }
+
+ 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/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/UIResources.Designer.cs b/src/SSHDebugPS/UI/UIResources.Designer.cs
index 12e90b5c2..64b0b08a0 100644
--- a/src/SSHDebugPS/UI/UIResources.Designer.cs
+++ b/src/SSHDebugPS/UI/UIResources.Designer.cs
@@ -267,6 +267,60 @@ 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 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.
+ ///
+ 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..7c7d7e5e5 100644
--- a/src/SSHDebugPS/UI/UIResources.resx
+++ b/src/SSHDebugPS/UI/UIResources.resx
@@ -239,6 +239,26 @@
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.
+
+
+ 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
+
Container List
diff --git a/src/SSHDebugPS/UI/ViewModels/ContainerPickerViewModel.cs b/src/SSHDebugPS/UI/ViewModels/ContainerPickerViewModel.cs
index 2fefd0890..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;
@@ -22,10 +23,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 +144,31 @@ 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();
+ case ContainerRuntimeType.Podman:
+ return new PodmanDiscoveryStrategy();
+ 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 +178,19 @@ private void RefreshContainersListInternal()
IContainerViewModel selectedContainer = SelectedContainerInstance;
SelectedContainerInstance = null;
- IEnumerable containers;
+ 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 +201,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)
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";
}
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
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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/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.");
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
}
}
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)