-
Notifications
You must be signed in to change notification settings - Fork 352
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
Open
shaikhsharukh
wants to merge
3
commits into
dotnet:main
Choose a base branch
from
shaikhsharukh:contrib/process-api-net11
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+320
−0
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
29286e7
feat(dotnet11): add process-api-net11 skill for new process APIs
shaikhsharukh 2ae8051
docs(dotnet11): align process-api-net11 signatures and examples with …
shaikhsharukh 0eaf5fd
test(dotnet11): harden eval scenarios and add negative non-activation…
shaikhsharukh File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,179 @@ | ||
| --- | ||
| 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 | ||
| <TargetFramework>net11.0</TargetFramework> | ||
| ``` | ||
|
|
||
| ## New APIs & Convenience Methods | ||
|
|
||
| ### 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. | ||
| ```csharp | ||
| public static ProcessExitStatus Run(string fileName, IList<string>? arguments = null, bool silent = false, TimeSpan? timeout = null) | ||
| public static Task<ProcessExitStatus> RunAsync(string fileName, IList<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, IList<string>? arguments = null, TimeSpan? timeout = null) | ||
| public static Task<ProcessTextOutput> RunAndCaptureTextAsync(string fileName, IList<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` | ||
| Launches a process and immediately releases the system handle resources, returning only the process ID (PID). | ||
| ```csharp | ||
| public static int StartAndForget(string fileName, IList<string>? arguments = null) | ||
| public static int StartAndForget(ProcessStartInfo startInfo) | ||
| ``` | ||
|
|
||
| ### 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 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) | ||
| ``` | ||
| *Note: `ProcessOutputLine` is a readonly struct containing `string Content` and `bool StandardError` properties.* | ||
|
|
||
| ### 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 IList<SafeHandle>? 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 status' and capture output (arguments passed as list) | ||
| ProcessTextOutput result = await Process.RunAndCaptureTextAsync("git", ["status"]); | ||
|
|
||
| if (result.ExitStatus.ExitCode == 0) | ||
| { | ||
| Console.WriteLine($"Git Output: {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, | ||
| RedirectStandardError = true | ||
| }; | ||
|
|
||
| using var 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}"); | ||
| ``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" | ||
| - 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 | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.