From 29286e77538651618ab9e3042d035d949ee29c51 Mon Sep 17 00:00:00 2001 From: SHARUKH SHAIKH Date: Tue, 7 Jul 2026 20:05:26 +0800 Subject: [PATCH 1/3] feat(dotnet11): add process-api-net11 skill for new process APIs --- plugins/dotnet11/README.md | 1 + .../skills/process-api-net11/SKILL.md | 172 ++++++++++++++++++ tests/dotnet11/process-api-net11/eval.yaml | 57 ++++++ 3 files changed, 230 insertions(+) create mode 100644 plugins/dotnet11/skills/process-api-net11/SKILL.md create mode 100644 tests/dotnet11/process-api-net11/eval.yaml diff --git a/plugins/dotnet11/README.md b/plugins/dotnet11/README.md index 7a85a71559..b706f171e1 100644 --- a/plugins/dotnet11/README.md +++ b/plugins/dotnet11/README.md @@ -4,4 +4,5 @@ Skills focused on new APIs and language features introduced in .NET 11. ## Skills +- process-api-net11 - system-text-json-net11 diff --git a/plugins/dotnet11/skills/process-api-net11/SKILL.md b/plugins/dotnet11/skills/process-api-net11/SKILL.md new file mode 100644 index 0000000000..39d48dd247 --- /dev/null +++ b/plugins/dotnet11/skills/process-api-net11/SKILL.md @@ -0,0 +1,172 @@ +--- +name: process-api-net11 +description: > + Provides guidance on the new System.Diagnostics.Process APIs introduced in .NET 11. + It covers high-level convenience methods (Process.Run, Process.RunAndCaptureText, Process.StartAndForget), + reliable deadlock-free output reading (Process.ReadAllText/Bytes/Lines), and lifecycle/handle management + (KillOnParentExit, InheritedHandles, StartDetached). + Use when starting, orchestrating, or capturing output from external processes in .NET 11 applications. +license: MIT +--- + +# Process API Improvements — .NET 11 + +New APIs added to `System.Diagnostics.Process` in .NET 11 simplify process management, eliminate boilerplate, and prevent common deadlock patterns when capturing output. + +## When to Use + +- Running or orchestrating external processes in a .NET 11 (or later) project. +- Needing to start a process, wait for it to exit, and capture its output/error streams without risking deadlocks (`Process.RunAndCaptureTextAsync`). +- Wanting to ensure child processes are automatically terminated when the parent process exits (`KillOnParentExit`). +- Requiring trimmer-friendly and NativeAOT-compatible process creation via `SafeProcessHandle`. +- Requiring fine-grained control over handle inheritance (`InheritedHandles`) or starting detached processes (`StartDetached`). + +## When Not to Use + +- The project targets .NET 10 or earlier — these APIs are not available before .NET 11. +- Running simple shells where custom execution code is unnecessary. +- The default `Process.Start()` is sufficient and does not require output capturing or advanced lifecycle rules. + +## Target Framework + +```xml +net11.0 +``` + +## New APIs & Convenience Methods + +### High-Level Convenience APIs + +#### `Process.Run[Async]` +Starts a process and waits for it to exit, returning the exit status. Does not capture standard output or error. +```csharp +public static ProcessExitStatus Run(string fileName, string arguments = "", ProcessStartInfo? startInfo = null) +public static Task RunAsync(string fileName, string arguments = "", ProcessStartInfo? startInfo = null, CancellationToken cancellationToken = default) +``` + +#### `Process.RunAndCaptureText[Async]` +Starts a process, captures both standard output and error, and waits for it to exit. Extremely useful for avoiding deadlocks on stream redirection. +```csharp +public static ProcessTextOutput RunAndCaptureText(string fileName, string arguments = "", ProcessStartInfo? startInfo = null) +public static Task RunAndCaptureTextAsync(string fileName, string arguments = "", ProcessStartInfo? startInfo = null, CancellationToken cancellationToken = default) +``` + +#### `Process.StartAndForget` +Launches a process and immediately releases the system handle resources, returning only the process ID (PID). +```csharp +public static int StartAndForget(string fileName, string arguments = "", ProcessStartInfo? startInfo = null) +``` + +### Reliable Output Reading APIs +These methods allow direct reading from stdout and stderr without manually handling events or async streams, ensuring no OS pipe buffer overflow deadlocks. + +```csharp +public string ReadAllText() +public Task ReadAllTextAsync(CancellationToken cancellationToken = default) +public byte[] ReadAllBytes() +public Task ReadAllBytesAsync(CancellationToken cancellationToken = default) +public string[] ReadAllLines() +public Task ReadAllLinesAsync(CancellationToken cancellationToken = default) +``` + +### ProcessStartInfo Properties + +#### `KillOnParentExit` +Ensures that the spawned child process is terminated when the current (parent) process exits. Works across both Windows and Unix platforms. +```csharp +public bool KillOnParentExit { get; set; } +``` + +#### `InheritedHandles` +Provides precise control over which file/kernel handles are inherited by the child process, preventing accidental resource leaks. +```csharp +public ProcessHandleInheritance InheritedHandles { get; set; } +``` + +#### `StartDetached` +Starts the process detached from the parent's terminal or job session, ensuring it survives the parent's exit. +```csharp +public bool StartDetached { get; set; } +``` + +--- + +## Examples + +### 1. One-Line Run and Capture Output + +Run a process and safely read all output text without stream deadlock risks: + +```csharp +using System; +using System.Diagnostics; +using System.Threading.Tasks; + +// Run 'git --version' and capture output +ProcessTextOutput result = await Process.RunAndCaptureTextAsync("git", "--version"); + +if (result.ExitStatus.ExitCode == 0) +{ + Console.WriteLine($"Git Version: {result.StandardOutput.Trim()}"); +} +else +{ + Console.WriteLine($"Failed with exit code: {result.ExitStatus.ExitCode}"); + Console.WriteLine($"Error: {result.StandardError}"); +} +``` + +### 2. Auto-Killing Child Processes on Parent Exit + +Ensure a long-running background worker process is killed when the main application terminates: + +```csharp +using System.Diagnostics; + +var startInfo = new ProcessStartInfo("dotnet", "run --project BackgroundWorker.csproj") +{ + KillOnParentExit = true // Auto-teardown when this parent process exits +}; + +using var process = Process.Start(startInfo); +// The background worker is now tied to this process's lifecycle +``` + +### 3. Read All Lines From Output + +Start a process and read its output lines safely: + +```csharp +using System; +using System.Diagnostics; +using System.Threading.Tasks; + +var startInfo = new ProcessStartInfo("ping", "127.0.0.1") +{ + RedirectStandardOutput = true +}; + +using var process = Process.Start(startInfo); +if (process != null) +{ + // Read all output lines safely and asynchronously + string[] lines = await process.ReadAllLinesAsync(); + foreach (var line in lines) + { + Console.WriteLine($"> {line}"); + } +} +``` + +### 4. Start and Forget (Fire & Forget) + +Launch a helper tool or browser without holding onto system handle structures: + +```csharp +using System; +using System.Diagnostics; + +// Fire and forget, getting back only the process ID +int pid = Process.StartAndForget("notepad.exe"); +Console.WriteLine($"Notepad started with PID: {pid}"); +``` diff --git a/tests/dotnet11/process-api-net11/eval.yaml b/tests/dotnet11/process-api-net11/eval.yaml new file mode 100644 index 0000000000..793de2fff2 --- /dev/null +++ b/tests/dotnet11/process-api-net11/eval.yaml @@ -0,0 +1,57 @@ +scenarios: + # --- Scenario 1: Run and capture output in .NET 11 --- + - name: "Run and capture process output in .NET 11" + prompt: | + I'm writing a .NET 11 command-line tool that needs to run an external CLI (e.g. 'git status') and capture both stdout and stderr. + I want to make sure I don't run into any OS pipe deadlock issues, and I want to write it in the cleanest way possible using new .NET 11 features. + Show me the C# program and the `.csproj` XML targeting `net11.0` for this tool. Print both in your response. + assertions: + - type: "exit_success" + - type: "output_matches" + pattern: "Process\\.RunAndCaptureText(Async)?" + - type: "output_matches" + pattern: "net11\\.0" + - type: "output_not_matches" + pattern: "RedirectStandardOutput\\s*=\\s*true" + rubric: + - "Uses the new built-in Process.RunAndCaptureText or Process.RunAndCaptureTextAsync static method" + - "Targets net11.0 in the project file" + - "Avoids manual redirection setup boilerplate (like RedirectStandardOutput = true, BeginOutputReadLine, etc.)" + timeout: 180 + + # --- Scenario 2: Auto-teardown child processes on parent exit --- + - name: "Auto-teardown child processes on parent exit in .NET 11" + prompt: | + In a .NET 11 application, I need to spawn a background daemon process. + To prevent orphan processes, I want to ensure that this daemon process is automatically killed by the operating system if my main parent process crashes or exits. + How do I configure this in .NET 11 using ProcessStartInfo? Show me a minimal example, and show the `.csproj` XML targeting `net11.0` in your response. + assertions: + - type: "exit_success" + - type: "output_matches" + pattern: "KillOnParentExit\\s*=\\s*true" + - type: "output_matches" + pattern: "net11\\.0" + rubric: + - "Uses the new KillOnParentExit property on ProcessStartInfo set to true" + - "Targets net11.0" + timeout: 180 + + # --- Scenario 3: Negative — skill should NOT activate --- + - name: "Non-activation: Running a process on .NET 8" + prompt: | + I have a .NET 8 console app and I need to start a process 'notepad.exe'. + Show me a minimal C# program and the `.csproj` XML targeting `net8.0` that starts this process using the traditional Process.Start. Print both in your response. + expect_activation: false + assertions: + - type: "exit_success" + - type: "output_matches" + pattern: "net8\\.0" + - type: "output_matches" + pattern: "Process\\.Start\\(" + - type: "output_not_matches" + pattern: "Process\\.RunAndCaptureText" + rubric: + - "Solves the task using standard pre-.NET 11 APIs (Process.Start)" + - "Does NOT load or reference the process-api-net11 skill" + - "Targets net8.0" + timeout: 180 From 2ae80518911e44768bacc6585ef969c5c612680e Mon Sep 17 00:00:00 2001 From: SHARUKH SHAIKH Date: Sat, 11 Jul 2026 08:26:52 +0800 Subject: [PATCH 2/3] docs(dotnet11): align process-api-net11 signatures and examples with ref assemblies --- .../skills/process-api-net11/SKILL.md | 59 +++++++++++-------- 1 file changed, 33 insertions(+), 26 deletions(-) diff --git a/plugins/dotnet11/skills/process-api-net11/SKILL.md b/plugins/dotnet11/skills/process-api-net11/SKILL.md index 39d48dd247..891285a6ec 100644 --- a/plugins/dotnet11/skills/process-api-net11/SKILL.md +++ b/plugins/dotnet11/skills/process-api-net11/SKILL.md @@ -35,39 +35,45 @@ New APIs added to `System.Diagnostics.Process` in .NET 11 simplify process manag ## New APIs & Convenience Methods -### High-Level Convenience APIs +### High-Level Convenience APIs (Static Methods) -#### `Process.Run[Async]` +#### `Process.Run` / `Process.RunAsync` Starts a process and waits for it to exit, returning the exit status. Does not capture standard output or error. ```csharp -public static ProcessExitStatus Run(string fileName, string arguments = "", ProcessStartInfo? startInfo = null) -public static Task RunAsync(string fileName, string arguments = "", ProcessStartInfo? startInfo = null, CancellationToken cancellationToken = default) +public static ProcessExitStatus Run(string fileName, IList? arguments = null, bool silent = false, TimeSpan? timeout = null) +public static Task RunAsync(string fileName, IList? arguments = null, bool silent = false, CancellationToken cancellationToken = default) +public static ProcessExitStatus Run(ProcessStartInfo startInfo, TimeSpan? timeout = null) +public static Task RunAsync(ProcessStartInfo startInfo, CancellationToken cancellationToken = default) ``` -#### `Process.RunAndCaptureText[Async]` +#### `Process.RunAndCaptureText` / `Process.RunAndCaptureTextAsync` Starts a process, captures both standard output and error, and waits for it to exit. Extremely useful for avoiding deadlocks on stream redirection. ```csharp -public static ProcessTextOutput RunAndCaptureText(string fileName, string arguments = "", ProcessStartInfo? startInfo = null) -public static Task RunAndCaptureTextAsync(string fileName, string arguments = "", ProcessStartInfo? startInfo = null, CancellationToken cancellationToken = default) +public static ProcessTextOutput RunAndCaptureText(string fileName, IList? arguments = null, TimeSpan? timeout = null) +public static Task RunAndCaptureTextAsync(string fileName, IList? arguments = null, CancellationToken cancellationToken = default) +public static ProcessTextOutput RunAndCaptureText(ProcessStartInfo startInfo, TimeSpan? timeout = null) +public static Task RunAndCaptureTextAsync(ProcessStartInfo startInfo, CancellationToken cancellationToken = default) ``` #### `Process.StartAndForget` Launches a process and immediately releases the system handle resources, returning only the process ID (PID). ```csharp -public static int StartAndForget(string fileName, string arguments = "", ProcessStartInfo? startInfo = null) +public static int StartAndForget(string fileName, IList? arguments = null) +public static int StartAndForget(ProcessStartInfo startInfo) ``` -### Reliable Output Reading APIs -These methods allow direct reading from stdout and stderr without manually handling events or async streams, ensuring no OS pipe buffer overflow deadlocks. +### Reliable Output Reading APIs (Instance Methods) +These methods are called on a `Process` instance to directly read stdout and stderr, guaranteeing no OS pipe buffer overflow deadlocks. ```csharp -public string ReadAllText() -public Task ReadAllTextAsync(CancellationToken cancellationToken = default) -public byte[] ReadAllBytes() -public Task ReadAllBytesAsync(CancellationToken cancellationToken = default) -public string[] ReadAllLines() -public Task ReadAllLinesAsync(CancellationToken cancellationToken = default) +public (string StandardOutput, string StandardError) ReadAllText(TimeSpan? timeout = null) +public Task<(string StandardOutput, string StandardError)> ReadAllTextAsync(CancellationToken cancellationToken = default) +public (byte[] StandardOutput, byte[] StandardError) ReadAllBytes(TimeSpan? timeout = null) +public Task<(byte[] StandardOutput, byte[] StandardError)> ReadAllBytesAsync(CancellationToken cancellationToken = default) +public IEnumerable ReadAllLines(TimeSpan? timeout = null) +public IAsyncEnumerable ReadAllLinesAsync(CancellationToken cancellationToken = default) ``` +*Note: `ProcessOutputLine` is a readonly struct containing `string Content` and `bool StandardError` properties.* ### ProcessStartInfo Properties @@ -80,7 +86,7 @@ public bool KillOnParentExit { get; set; } #### `InheritedHandles` Provides precise control over which file/kernel handles are inherited by the child process, preventing accidental resource leaks. ```csharp -public ProcessHandleInheritance InheritedHandles { get; set; } +public IList? InheritedHandles { get; set; } ``` #### `StartDetached` @@ -102,12 +108,12 @@ using System; using System.Diagnostics; using System.Threading.Tasks; -// Run 'git --version' and capture output -ProcessTextOutput result = await Process.RunAndCaptureTextAsync("git", "--version"); +// Run 'git status' and capture output (arguments passed as list) +ProcessTextOutput result = await Process.RunAndCaptureTextAsync("git", ["status"]); if (result.ExitStatus.ExitCode == 0) { - Console.WriteLine($"Git Version: {result.StandardOutput.Trim()}"); + Console.WriteLine($"Git Output: {result.StandardOutput.Trim()}"); } else { @@ -123,7 +129,7 @@ Ensure a long-running background worker process is killed when the main applicat ```csharp using System.Diagnostics; -var startInfo = new ProcessStartInfo("dotnet", "run --project BackgroundWorker.csproj") +var startInfo = new ProcessStartInfo("dotnet", ["run", "--project", "BackgroundWorker.csproj"]) { KillOnParentExit = true // Auto-teardown when this parent process exits }; @@ -141,19 +147,20 @@ using System; using System.Diagnostics; using System.Threading.Tasks; -var startInfo = new ProcessStartInfo("ping", "127.0.0.1") +var startInfo = new ProcessStartInfo("ping", ["127.0.0.1"]) { - RedirectStandardOutput = true + RedirectStandardOutput = true, + RedirectStandardError = true }; using var process = Process.Start(startInfo); if (process != null) { // Read all output lines safely and asynchronously - string[] lines = await process.ReadAllLinesAsync(); - foreach (var line in lines) + await foreach (ProcessOutputLine line in process.ReadAllLinesAsync()) { - Console.WriteLine($"> {line}"); + string prefix = line.StandardError ? "[Err]" : "[Out]"; + Console.WriteLine($"{prefix} > {line.Content}"); } } ``` From 0eaf5fd5855dd5572342122ad056a7e1e10abfe5 Mon Sep 17 00:00:00 2001 From: SHARUKH SHAIKH Date: Sun, 19 Jul 2026 08:48:46 +0800 Subject: [PATCH 3/3] test(dotnet11): harden eval scenarios and add negative non-activation tests per reviewer suggestions --- tests/dotnet11/process-api-net11/eval.yaml | 87 +++++++++++++++++++++- 1 file changed, 85 insertions(+), 2 deletions(-) diff --git a/tests/dotnet11/process-api-net11/eval.yaml b/tests/dotnet11/process-api-net11/eval.yaml index 793de2fff2..1791924c10 100644 --- a/tests/dotnet11/process-api-net11/eval.yaml +++ b/tests/dotnet11/process-api-net11/eval.yaml @@ -11,8 +11,15 @@ scenarios: pattern: "Process\\.RunAndCaptureText(Async)?" - type: "output_matches" pattern: "net11\\.0" + # The prompt asks for BOTH stdout AND stderr — require both members. + - type: "output_matches" + pattern: "\\.StandardOutput\\b" + - type: "output_matches" + pattern: "\\.StandardError\\b" - type: "output_not_matches" pattern: "RedirectStandardOutput\\s*=\\s*true" + - type: "output_not_matches" + pattern: "BeginOutputReadLine|OutputDataReceived" rubric: - "Uses the new built-in Process.RunAndCaptureText or Process.RunAndCaptureTextAsync static method" - "Targets net11.0 in the project file" @@ -36,7 +43,62 @@ scenarios: - "Targets net11.0" timeout: 180 - # --- Scenario 3: Negative — skill should NOT activate --- + # --- Scenario 3: deadlock-free full read via instance ReadAllText (tuple return) --- + - name: "Deadlock-free full-output read via instance ReadAllText in .NET 11" + prompt: | + In a .NET 11 app I start a child process myself with `Process.Start(...)` because I + need to configure `ProcessStartInfo` first. After it starts I want to read its ENTIRE + standard output AND standard error to completion, without the classic deadlock where + one buffer fills while I'm draining the other. Give me the simplest first-party .NET 11 + way to get both back at once. Show a minimal program and the `net11.0` `.csproj`. + assertions: + - type: "exit_success" + - type: "output_matches" + pattern: "net11\\.0" + - type: "output_matches" + pattern: "ReadAllText(Async)?\\s*\\(" + # Correct: destructure the (StandardOutput, StandardError) tuple (sync or async form). + - type: "output_matches" + pattern: "\\)\\s*=\\s*(await\\s+)?\\w+\\.ReadAllText(Async)?\\s*\\(" + # Wrong: assigning the tuple-returning call to a single string. + - type: "output_not_matches" + pattern: "string\\s+\\w+\\s*=\\s*(await\\s+)?\\w+\\.ReadAllText(Async)?\\s*\\(" + rubric: + - "Uses the new instance method Process.ReadAllText/ReadAllTextAsync" + - "Destructures the (string StandardOutput, string StandardError) tuple — not a single string" + - "Does not fall back to manual StandardOutput.ReadToEndAsync() as the primary mechanism" + - "Targets net11.0" + timeout: 180 + + # --- Scenario 4: stream tagged output lines via ReadAllLinesAsync --- + - name: "Stream tagged process output lines in .NET 11" + prompt: | + I have a long-running child process in a .NET 11 app. As it runs I want to handle each + output line the moment it is produced — not buffer everything to the end — and for each + line I need to know whether it came from stdout or stderr. I want the cleanest first-party + .NET 11 approach, with no event-callback plumbing and no deadlock risk. Show a minimal + program and the `net11.0` `.csproj`. + assertions: + - type: "exit_success" + - type: "output_matches" + pattern: "net11\\.0" + - type: "output_matches" + pattern: "ReadAllLines(Async)?\\s*\\(" + - type: "output_matches" + pattern: "await\\s+foreach" + - type: "output_matches" + pattern: "\\.Content\\b|ProcessOutputLine" + - type: "output_matches" + pattern: "\\.StandardError\\b" + - type: "output_not_matches" + pattern: "OutputDataReceived|BeginOutputReadLine" + rubric: + - "Uses Process.ReadAllLinesAsync consumed with `await foreach` over IAsyncEnumerable" + - "Reads ProcessOutputLine.Content and distinguishes stdout vs stderr via .StandardError" + - "Does NOT use the old OutputDataReceived/BeginOutputReadLine event model; targets net11.0" + timeout: 180 + + # --- Scenario 5: Negative — skill should NOT activate on .NET 8 --- - name: "Non-activation: Running a process on .NET 8" prompt: | I have a .NET 8 console app and I need to start a process 'notepad.exe'. @@ -49,9 +111,30 @@ scenarios: - type: "output_matches" pattern: "Process\\.Start\\(" - type: "output_not_matches" - pattern: "Process\\.RunAndCaptureText" + pattern: "RunAndCaptureText|ReadAllTextAsync|ReadAllLinesAsync|KillOnParentExit" rubric: - "Solves the task using standard pre-.NET 11 APIs (Process.Start)" - "Does NOT load or reference the process-api-net11 skill" - "Targets net8.0" timeout: 180 + + # --- Scenario 6: subtle non-activation — capture output on .NET 10 (APIs are net11-only) --- + - name: "Non-activation: capture process output on .NET 10" + prompt: | + I'm on a `.NET 10` project (`net10.0`). I need to run `git` with the argument `status` + and capture its standard output into a string. Show a minimal C# program and the + `.csproj` XML targeting `net10.0`, and print both. + expect_activation: false + assertions: + - type: "exit_success" + - type: "output_matches" + pattern: "net10\\.0" + - type: "output_not_matches" + pattern: "RunAndCaptureText|ReadAllTextAsync|ReadAllLinesAsync" + - type: "output_matches" + pattern: "RedirectStandardOutput|ReadToEnd" + rubric: + - "Solves the task with pre-.NET 11 APIs (RedirectStandardOutput + ReadToEnd/ReadToEndAsync)" + - "Recognizes the new .NET 11 Process convenience APIs are unavailable on net10.0 and does not use them" + - "Does NOT load the process-api-net11 skill; targets net10.0" + timeout: 180