Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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`).

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.

nit: we provide sync and async overloads

Suggested change
- Needing to start a process, wait for it to exit, and capture its output/error streams without risking deadlocks (`Process.RunAndCaptureTextAsync`).
- Needing to start a process, wait for it to exit, and capture its output/error streams without risking deadlocks (`Process.RunAndCaptureText[Async]`).

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.

Updated bullet to mention Process.RunAndCaptureText[Async].

- Wanting to ensure child processes are automatically terminated when the parent process exits (`KillOnParentExit`).
- Requiring trimmer-friendly and NativeAOT-compatible process creation via `SafeProcessHandle`.

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.

The wording needs a bit more specific. Process itself is NativeAOT-compatible, it's just that with SafeProcessHandle the user gets the smallest possible size on disk (so it's more of an optimization)

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.

Clarified that SafeProcessHandle is an optimization for minimal disk footprint with NativeAOT.

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

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.

I am not a native speaker, so please take it with a grain of salt. But overall I do believe that the new APIs simplify the code a lot, so I would recommend them even for simple shells.

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.

Removed the bullet against simple shells.

- 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

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.

Each of these methods is a new API, some are also convenience methods. I would just call it New APIs

Suggested change
## New APIs & Convenience Methods
## New APIs

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.

Renamed section heading to ## New APIs.


### High-Level Convenience APIs (Static Methods)

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.

I would suggest we use following separation:

  • New APIs
    • Convenience APIs
      • Static Methods
      • Instance Methods

As some of the instance methods are also convenience methods (at least in my opinion).

Suggested change
### High-Level Convenience APIs (Static Methods)
### High-Level Convenience APIs
#### Static Methods

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.

Reorganized sections under ### High-Level Convenience APIs with #### Static Methods and #### Instance 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.

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 mention that it also allows the users to discard the output/error by providing silent: true and internally redirecting std handles to NUL device.

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.

Mentioned that silent: true discards output/error by internally redirecting std handles to NUL.

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

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.

Let's provide the reasoning behind this method. You can re-use the text from our blog post:

Suggested change
Launches a process and immediately releases the system handle resources, returning only the process ID (PID).
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 releases all resources associated with it

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 the rationale citing Process.Dispose behavior vs process killing, and why Process.StartAndForget was introduced.

```csharp
public static int StartAndForget(string fileName, IList<string>? arguments = null)

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.

In .NET 11 Preview 7 we have changed these methods to accept IEnumerable<string> for the sake of consistency with pre-existing Process APIs (dotnet/runtime#130630)

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

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

Launches a process and immediately releases the system handle resources, returning only the process ID (PID).

public static int StartAndForget(string fileName, IEnumerable<string>? arguments = null)

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.

Updated all method overloads to accept IEnumerable<string>? for arguments to align with .NET 11 Preview 7 changes (dotnet/runtime#130630).

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

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 mention what ProcessOutputLine is, but we don't mention ProcessExitStatus nor ProcessTextOutput. I think it's best to just provide their definition just before they appear for the first time.

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 definitions for ProcessExitStatus, ProcessTextOutput, and ProcessOutputLine under ### Types.


### ProcessStartInfo Properties

#### `KillOnParentExit`
Ensures that the spawned child process is terminated when the current (parent) process exits. Works across both Windows and Unix platforms.

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.

Not every Unix is supported. Only Win, Lin and Android.

Suggested change
Ensures that the spawned child process is terminated when the current (parent) process exits. Works across both Windows and Unix platforms.
Ensures that the spawned child process is terminated when the current (parent) process exits. Works across Windows, Linux and Android.

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.

Updated to specify 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.

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

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:

```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"]);

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.

People tend to use async for apps that absolutely don't benefit from it (scalability, cancellation support etc). I think it's better to use non-async overload for such simple examples. So AI-written code is simple and does not pay for the price of using async

Suggested change
ProcessTextOutput result = await Process.RunAndCaptureTextAsync("git", ["status"]);
ProcessTextOutput result = Process.RunAndCaptureText("git", ["status"]);

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.

Fixed. Updated the example to use the synchronous Process.RunAndCaptureText overload.


if (result.ExitStatus.ExitCode == 0)
{
Console.WriteLine($"Git Output: {result.StandardOutput.Trim()}");

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.

There is no need to use Trim here.

Suggested change
Console.WriteLine($"Git Output: {result.StandardOutput.Trim()}");
Console.WriteLine($"Git Output: {result.StandardOutput}");

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.

Fixed. Removed .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"])

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.

nit: subjective: no need to use var

Suggested change
var startInfo = new ProcessStartInfo("dotnet", ["run", "--project", "BackgroundWorker.csproj"])
ProcessStartInfo startInfo = new("dotnet", ["run", "--project", "BackgroundWorker.csproj"])

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.

Fixed. Updated the snippet to explicitly declare ProcessStartInfo.

{
KillOnParentExit = true // Auto-teardown when this parent process exits

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
KillOnParentExit = true // Auto-teardown when this parent process exits
KillOnParentExit = OperatingSystem.IsWindows() || OperatingSystem.IsLinux() // Auto-teardown when this parent process exits

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.

Fixed. Added OS platform checks: KillOnParentExit = OperatingSystem.IsWindows() || OperatingSystem.IsLinux().

};

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}");
```
57 changes: 57 additions & 0 deletions tests/dotnet11/process-api-net11/eval.yaml
Original file line number Diff line number Diff line change
@@ -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"
Comment thread
AbhitejJohn marked this conversation as resolved.
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: 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"
Comment thread
AbhitejJohn marked this conversation as resolved.
Outdated
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.