Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions plugins/dotnet11/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ Skills focused on new APIs and language features introduced in .NET 11.

## Skills

- process-api-net11
- system-text-json-net11
179 changes: 179 additions & 0 deletions plugins/dotnet11/skills/process-api-net11/SKILL.md
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}");
```
140 changes: 140 additions & 0 deletions tests/dotnet11/process-api-net11/eval.yaml
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"
Comment thread
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

Comment thread
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
Comment thread
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