-
Notifications
You must be signed in to change notification settings - Fork 414
feat(dotnet11): add process-api-net11 skill for new process APIs #866
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 7 commits
29286e7
2ae8051
0eaf5fd
630086d
fb89d28
d821f78
ab73b73
3eb02b5
77ee4b0
5c789a6
020887a
70ddbff
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,215 @@ | ||
| --- | ||
| 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.RunAndCaptureText[Async]`). | ||
| - Wanting to ensure child processes are automatically terminated when the parent process exits (`KillOnParentExit`). | ||
| - Requiring trimmer-friendly and NativeAOT-optimized process creation via `SafeProcessHandle` for the smallest possible disk footprint. | ||
| - 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. | ||
| - The default `Process.Start()` is sufficient and does not require output capturing or advanced lifecycle rules. | ||
|
|
||
| ## Target Framework | ||
|
|
||
| ```xml | ||
| <TargetFramework>net11.0</TargetFramework> | ||
| ``` | ||
|
|
||
| ## New APIs | ||
|
|
||
| ### Types | ||
|
|
||
| Before using the new convenience methods, note the following return and record structures: | ||
|
|
||
| - **`ProcessExitStatus`**: Represents the outcome of a completed process. | ||
| ```csharp | ||
| public readonly record struct ProcessExitStatus(int ExitCode) | ||
| { | ||
| public bool Success => ExitCode == 0; | ||
| } | ||
| ``` | ||
| - **`ProcessTextOutput`**: Contains the exit status along with all captured standard output and standard error text. | ||
| ```csharp | ||
| public readonly record struct ProcessTextOutput(ProcessExitStatus ExitStatus, string StandardOutput, string StandardError); | ||
| ``` | ||
| - **`ProcessOutputLine`**: Represents a single output line tagged with its stream source. | ||
| ```csharp | ||
| public readonly struct ProcessOutputLine | ||
| { | ||
| public string Content { get; } | ||
| public bool StandardError { get; } | ||
| } | ||
| ``` | ||
|
|
||
| ### High-Level Convenience APIs | ||
|
|
||
| #### Static Methods | ||
|
|
||
| ##### `Process.Run` / `Process.RunAsync` | ||
| Starts a process and waits for it to exit, returning the exit status. Does not capture standard output or error. Passing `silent: true` discards standard output and error by internally redirecting standard handles to the `NUL` device. | ||
| ```csharp | ||
| public static ProcessExitStatus Run(string fileName, IEnumerable<string>? arguments = null, bool silent = false, TimeSpan? timeout = null) | ||
| public static Task<ProcessExitStatus> RunAsync(string fileName, IEnumerable<string>? arguments = null, bool silent = false, CancellationToken cancellationToken = default) | ||
| public static ProcessExitStatus Run(ProcessStartInfo startInfo, TimeSpan? timeout = null) | ||
| public static Task<ProcessExitStatus> RunAsync(ProcessStartInfo startInfo, CancellationToken cancellationToken = default) | ||
| ``` | ||
|
|
||
| ##### `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, IEnumerable<string>? arguments = null, TimeSpan? timeout = null) | ||
| public static Task<ProcessTextOutput> RunAndCaptureTextAsync(string fileName, IEnumerable<string>? arguments = null, CancellationToken cancellationToken = default) | ||
| public static ProcessTextOutput RunAndCaptureText(ProcessStartInfo startInfo, TimeSpan? timeout = null) | ||
| public static Task<ProcessTextOutput> RunAndCaptureTextAsync(ProcessStartInfo startInfo, CancellationToken cancellationToken = default) | ||
| ``` | ||
|
|
||
| ##### `Process.StartAndForget` | ||
| There is a common misconception that when a process is disposed, it's also being killed. This is not the case, as `Process.Dispose` only releases the resources associated with the process, but does not kill it. | ||
|
|
||
| To make it easier to start a process without the need to worry about disposing it, `Process.StartAndForget` was introduced. The method starts a process, returns its ID, and immediately releases all handle resources associated with it. | ||
| ```csharp | ||
| public static int StartAndForget(string fileName, IEnumerable<string>? arguments = null) | ||
| public static int StartAndForget(ProcessStartInfo startInfo) | ||
| ``` | ||
|
|
||
| #### 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 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<ProcessOutputLine> ReadAllLines(TimeSpan? timeout = null) | ||
| public IAsyncEnumerable<ProcessOutputLine> ReadAllLinesAsync(CancellationToken cancellationToken = default) | ||
| ``` | ||
|
|
||
| ### ProcessStartInfo Properties | ||
|
|
||
| #### `KillOnParentExit` | ||
| Ensures that the spawned child process is terminated when the current (parent) process exits. Works across Windows, Linux, and Android. | ||
| ```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. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need to describe few important things:
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added details on standard handles inclusion, empty list behavior, supported handle types, and Windows parallel spawning without global lock. |
||
| - Standard handles (`stdin`, `stdout`, `stderr`) are always included (no need to add them to the list). | ||
| - Setting the list to an empty list means only standard handles get inherited. | ||
| - Only `SafeFileHandle` and `SafePipeHandle` instances are allowed as of today. | ||
| - No global lock is used when spawning new processes on Windows (important for tuning projects that spawn multiple processes in parallel). | ||
| ```csharp | ||
| public IList<SafeHandle>? InheritedHandles { get; set; } | ||
| ``` | ||
|
|
||
| #### `Silent` | ||
| When set to `true`, the standard handles are by default redirected to the `NUL` device, ensuring the child process does not keep parent console or terminal resources alive. | ||
| ```csharp | ||
| public bool Silent { get; set; } | ||
| ``` | ||
|
|
||
| #### `StartDetached` | ||
| Starts the process detached from the parent's terminal or job session, ensuring it survives the parent's exit. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When set to
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added note explaining standard handles redirection to the |
||
| ```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. For simple CLI operations that don't need cancellation or async scalability, prefer the synchronous overload: | ||
|
|
||
| ```csharp | ||
| using System; | ||
| using System.Diagnostics; | ||
|
|
||
| // Run 'git status' and capture output | ||
| ProcessTextOutput result = Process.RunAndCaptureText("git", ["status"]); | ||
|
|
||
| if (result.ExitStatus.ExitCode == 0) | ||
| { | ||
| Console.WriteLine($"Git Output: {result.StandardOutput}"); | ||
| } | ||
| 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; | ||
| using System.Diagnostics; | ||
|
|
||
| ProcessStartInfo startInfo = new("dotnet", ["run", "--project", "BackgroundWorker.csproj"]) | ||
| { | ||
| KillOnParentExit = OperatingSystem.IsWindows() || OperatingSystem.IsLinux() // Auto-teardown when this parent process exits | ||
| }; | ||
|
|
||
| using Process? 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; | ||
|
|
||
| ProcessStartInfo startInfo = new("ping", ["127.0.0.1"]) | ||
| { | ||
| RedirectStandardOutput = true, | ||
| RedirectStandardError = true | ||
| }; | ||
|
|
||
| using Process? process = Process.Start(startInfo); | ||
| if (process != null) | ||
| { | ||
| // Read all output lines safely and asynchronously | ||
| await foreach (ProcessOutputLine line in process.ReadAllLinesAsync()) | ||
| { | ||
| string prefix = line.StandardError ? "[Err]" : "[Out]"; | ||
| Console.WriteLine($"{prefix} > {line.Content}"); | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ### 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}"); | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,140 @@ | ||
| 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" | ||
| # 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" | ||
|
AbhitejJohn marked this conversation as resolved.
|
||
| - 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" | ||
| - "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 | ||
|
|
||
|
AbhitejJohn marked this conversation as resolved.
|
||
| # --- 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<ProcessOutputLine>" | ||
| - "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'. | ||
| 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: "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 | ||
|
AbhitejJohn marked this conversation as resolved.
|
||
|
|
||
| # --- 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 | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SafeProcessHandle is more lightweight API than full Process. It has better performance characteristics on all form-factors. It is not a NativeAOT specific optimization.