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
215 changes: 215 additions & 0 deletions plugins/dotnet11/skills/process-api-net11/SKILL.md
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 lightweight, low-overhead process creation via `SafeProcessHandle` for better performance characteristics across all form-factors and runtimes.
- 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:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

None of them is a record.

Suggested change
Before using the new convenience methods, note the following return and record structures:
Before using the new convenience methods, note the following return and structures:


- **`ProcessExitStatus`**: Represents the outcome of a completed process.
```csharp
public readonly record struct ProcessExitStatus(int ExitCode)
{
public bool Success => ExitCode == 0;
}
Comment on lines +43 to +46

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is wrong:

  • ProcessExitStatus consists of 3 properties
  • We don't provide Success on purpose, as we can't assume what exit codes are used to represent success/failure for every command line app. For example, somebody can return -1 on success.
Suggested change
public readonly record struct ProcessExitStatus(int ExitCode)
{
public bool Success => ExitCode == 0;
}
public readonly record struct ProcessExitStatus(int ExitCode, bool Canceled, PosixSignal? Signal = null);

```
- **`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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public readonly record struct ProcessTextOutput(ProcessExitStatus ExitStatus, string StandardOutput, string StandardError);
public readonly record struct ProcessTextOutput(ProcessExitStatus ExitStatus, string StandardOutput, string StandardError, int ProcessId);

```
- **`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)
```

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to add a note saying that on timeout/cancellation the process is killed.

##### `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)
```

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to add a note saying that when using the ProcessStartInfo overloads, the user is responsible for setting output and error redirected via the boolean flags (because BCL APIs can't modify the arguments they were given).

##### `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)
```

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another note needed: by default, when output/error redirection was not specified, the StartAndForget method will redirect all standard handles to NUL device.

#### 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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Ensures that the spawned child process is terminated when the current (parent) process exits. Works across Windows, Linux, and Android.
Ensures that the spawned child process is terminated when the current (parent) process exits (including fatal crash and being force killed). 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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to describe few important things:

  • std handles are always included (no need to add to the list)
  • list set to empty list == only std handles get inerited
  • only SafeFileHandle and SafePipeHandle instances are allowes as of today.
  • no global lock used when spawning new process on Windows (important to tune projects that spawn multiple processes in parallel)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Provides precise control over which file/kernel handles are inherited by the child process, preventing accidental resource leaks.
Provides precise control over which handles (file descriptors) are inherited by the child process, preventing accidental resource leaks.

- 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; }
```

Comment on lines +122 to +127

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ProcessStartInfo does not offer such property, it's just an argument for the Run[Async] methods

Suggested change
#### `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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When set to true, the std handles are by default redirected to NUL device. So the child process does not keep the parent process console/terminal resources alive.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added note explaining standard handles redirection to the NUL device when Silent is true.

```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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed the example sets KillOnParentExit to IsWindows() or IsLinux() while the section just above says the property works on Windows, Linux, and Android, so on Android and any other OS the copied example silently leaves the child unprotected and risks orphan processes. It also fails your own eval, which requires a literal assignment to true. I would set KillOnParentExit to true unconditionally and let the runtime handle platform support.

};

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}");
}
}
Comment on lines +192 to +201

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a PITA, but Process.Start can return null only when we set UseShellExecute = true.

Suggested change
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}");
}
}
using Process process = Process.Start(startInfo)!;
// 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:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ran this eval through the repo quality gate and it counts zero stimuli because the file uses the retired scenarios/assertions shape instead of the current stimuli/graders/defaults shape, so it can never produce a verdict and the gate reports 0 preference stimuli needing 5 more. The sibling system-text-json-net11 eval shows the expected layout with exit-success and output-matches graders carrying a config pattern. Please rewrite this file to that schema, converting each scenario to a stimulus with graders and shared defaults.

# --- 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
Loading