Add ProcessStartInfo.StartDetached to start processes detached from parent session/console - #126632

Merged
adamsitnik merged 15 commits into
mainfrom
copilot/add-detached-process-support
Apr 10, 2026
Merged

Add ProcessStartInfo.StartDetached to start processes detached from parent session/console#126632
adamsitnik merged 15 commits into
mainfrom
copilot/add-detached-process-support

Conversation

CopilotAI commented Apr 8, 2026

Copy link
Copy Markdown
Contributor

Description

Adds ProcessStartInfo.StartDetached — a cross-platform way to start a process detached from the parent's session and console, such that it outlives the parent.

API surface

// ref/System.Diagnostics.Process.cspublicsealedclassProcessStartInfo{publicboolStartDetached{get;set;}}

Behavior by platform

PlatformMechanism
WindowsDETACHED_PROCESS creation flag
macOSPOSIX_SPAWN_SETSID via posix_spawn
Other Unixsetsid() in child after fork, before exec

Key semantics

  • ThrowsInvalidOperationException when combined with UseShellExecute = true
  • Standard handles (stdin/stdout/stderr) not explicitly set are automatically redirected to the null device (NUL on Windows, /dev/null on Unix) — a single File.OpenNullHandle() is opened at most once and disposed after the process starts; override with RedirectStandardInput/RedirectStandardOutput/RedirectStandardError or StandardInputHandle/StandardOutputHandle/StandardErrorHandle

Example

varpsi=newProcessStartInfo("myserver"){StartDetached=true};// stdin/stdout/stderr automatically go to NUL/dev/nullProcess.Start(psi);

Files changed

  • ProcessStartInfo.cs / ref — new property with XML docs linking to the Windows SDK DETACHED_PROCESS documentation; remarks note that UseShellExecute = true will throw; StartDetached placed in alphabetical order in the ref assembly (after StandardOutputHandle, before UseCredentialsForNetworkingOnly)
  • Strings.resx — resource key StartDetachedNotCompatible; message states the property cannot be used with UseShellExecute set to true
  • Process.cs — opens a single null device handle outside the lock region, reuses it for all unset stdio handles; existing finally block handles disposal
  • SafeProcessHandle.cs — if/else if/else blocks; single nullDeviceHandle opened at most once; disposed in a finally block after StartCore
  • SafeProcessHandle.Windows.csDETACHED_PROCESS flag; constant added to Interop.ProcessOptions.cs
  • Interop.ForkAndExecProcess.cs / SafeProcessHandle.Unix.cs — thread startDetached through managed interop chain; fixed indentation of ForkAndExecProcess(...) call inside fixed block
  • pal_process.c / pal_process.h / pal_process_wasi.c — native implementation: POSIX_SPAWN_SETSID (macOS), if (startDetached && setsid() == -1) check (other Unix)
  • ProcessTests.cs — cross-platform StartDetached_GrandchildSurvivesSignalingParent test guarded by IsNotNanoServerAndRemoteExecutorSupported (PlatformDetection.IsNotWindowsNanoServer && RemoteExecutor.IsSupported); child started with StartDetached=true (own process group on Unix) or CreateNewProcessGroup=true (Windows); kills child's process group with SIGKILL on Unix and SIGQUIT (mapped to CTRL_BREAK_EVENT) on Windows via SendSignal(..., entireProcessGroup: true); grandchild-alive check uses deterministic polling (loop with 20 ms sleep up to WaitInMS) instead of a fixed Thread.Sleep; validates that detached grandchild survives while non-detached grandchild is killed; StartDetached_StartsAndExitsSuccessfully smoke test starts a process with StartDetached=true via RemoteExecutor and verifies it completes successfully
  • ProcessTests.Unix.csSendSignal(..., entireProcessGroup: true) passes the given signal through Interop.Sys.GetPlatformSignalNumber to kill(-pgid, signalNumber), keeping the helper generic (not hardcoded to any particular signal); call site passes PosixSignal.SIGKILL on Unix for reliable process-group termination
  • ProcessTests.Windows.csSendSignal documents that entireProcessGroup has no distinct effect on Windows (via _ = entireProcessGroup) since GenerateConsoleCtrlEvent is inherently process-group scoped
  • ProcessHandlesTests.cs[Fact] test for UseShellExecute validation

Changes

  • Adds a new cross-platform ProcessStartInfo.StartDetached property
  • Redirects standard I/O to null device (single handle, properly disposed) when detached and no explicit handle is provided
  • Platform-specific implementation: DETACHED_PROCESS (Windows), POSIX_SPAWN_SETSID (macOS), setsid() (Linux/other Unix)
  • Validation: throws when combined with UseShellExecute = true only (CreateNoWindow is not rejected — DETACHED_PROCESS and CREATE_NO_WINDOW are compatible per Windows API semantics)
  • XML docs link to the Windows SDK DETACHED_PROCESS documentation; only UseShellExecute listed as incompatible
  • Tests covering validation (UseShellExecute), cross-platform process-group isolation (detached grandchild survives, non-detached grandchild is killed), and basic smoke test (StartDetached_StartsAndExitsSuccessfully); all skipped on Windows Nano Server and where RemoteExecutor is unsupported
  • SendSignal Unix helper kept generic (signal passed via GetPlatformSignalNumber); call site uses SIGKILL on Unix for reliable group termination
  • Deterministic polling replaces fixed Thread.Sleep(200) in grandchild-survival assertion
  • Windows SendSignal explicitly discards entireProcessGroup with a clarifying comment that GenerateConsoleCtrlEvent is inherently group-scoped
  • StartDetached placed in alphabetical order in the ref assembly (after StandardOutputHandle, before UseCredentialsForNetworkingOnly)

CopilotAI requested review from Copilot and removed request for CopilotApril 8, 2026 08:24
CopilotAI linked an issue Apr 8, 2026 that may be closed by this pull request
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-diagnostics-process
See info in area-owners.md if you want to be subscribed.

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/23306a05-946d-446f-b7b5-f7cfc977422c
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
CopilotAI requested review from Copilot and removed request for CopilotApril 8, 2026 09:16
CopilotAI changed the title [WIP] Add cross-platform method to start detached processAdd ProcessStartInfo.StartDetached to start processes detached from parent session/consoleApr 8, 2026
CopilotAI requested a review from adamsitnikApril 8, 2026 09:20

@adamsitnikadamsitnik left a comment

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.

@copilot address my feedback

Comment threadsrc/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs Outdated
Comment threadsrc/libraries/System.Diagnostics.Process/tests/StartDetachedTests.cs Outdated
Comment threadsrc/libraries/System.Diagnostics.Process/tests/StartDetachedTests.cs Outdated
Comment threadsrc/libraries/System.Diagnostics.Process/tests/StartDetachedTests.cs Outdated
Comment threadsrc/libraries/System.Diagnostics.Process/tests/StartDetachedTests.cs Outdated
Comment threadsrc/libraries/System.Diagnostics.Process/tests/StartDetachedTests.cs Outdated
Comment threadsrc/native/libs/System.Native/pal_process.c Outdated
…dition, test attributes, and pal_process nesting
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/59c890e4-010d-41ee-9d51-bcd6e64cb7f3
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
CopilotAI requested review from Copilot and removed request for CopilotApril 8, 2026 10:32

CopilotAI commented Apr 8, 2026

Copy link
Copy Markdown
ContributorAuthor

@copilot address my feedback

All feedback addressed in a6e4206. Summary of changes:

  • SafeProcessHandle.cs: if/else if/else blocks, single null device handle, disposed in finally after StartCore
  • Process.cs: single null device handle opened once after the lock, reused for all stdio
  • ProcessStartInfo.cs docs: split into 3 paragraphs with StandardInputHandle/OutputHandle/ErrorHandle refs
  • ProcessStartInfo.cs condition: added && string.IsNullOrEmpty(UserName) to the StartDetached branch
  • pal_process.c: reduced setsid check nesting
  • StartDetachedTests.cs: [Fact] for validation test; removed mobile platform skips; SIGHUP test skips only Windows; added using System.Runtime.InteropServices for PosixSignal

CopilotAI requested a review from adamsitnikApril 8, 2026 10:34
CopilotAI review requested due to automatic review settings April 8, 2026 11:08

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a new ProcessStartInfo.StartDetached API to start processes detached from the parent’s session/console across platforms, with additional safety defaults to avoid leaking standard handles and inheritable resources into long-lived detached processes.

Changes:

  • Adds ProcessStartInfo.StartDetached to the public surface area (ref + implementation) with validation against UseShellExecute = true.
  • Implements platform-specific detaching: DETACHED_PROCESS (Windows), POSIX_SPAWN_SETSID (macOS), and setsid() (other Unix), threading the flag through managed/native interop.
  • When detached, defaults stdio to the null device (unless explicitly configured) and restricts handle inheritance by default.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
src/native/libs/System.Native/pal_process.hExtends native fork/exec API with startDetached parameter.
src/native/libs/System.Native/pal_process.cImplements session detachment (POSIX_SPAWN_SETSID / setsid()) based on startDetached.
src/native/libs/System.Native/pal_process_wasi.cUpdates WASI stub signature for startDetached.
src/libraries/Common/src/Interop/Unix/System.Native/Interop.ForkAndExecProcess.csAdds managed interop parameter and forwards startDetached to native.
src/libraries/Common/src/Interop/Windows/Advapi32/Interop.ProcessOptions.csIntroduces DETACHED_PROCESS constant for process creation flags.
src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.csAdds ProcessStartInfo.StartDetached to the reference assembly API surface.
src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.csAdds property + validation + default empty inherited-handles list for detached starts.
src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.csUses a single null-device handle for unset stdio when detached; opens it outside the lock.
src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.csApplies detached stdio-to-null behavior for SafeProcessHandle.Start.
src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.csThreads StartDetached through to Interop.Sys.ForkAndExecProcess.
src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.csAdds DETACHED_PROCESS to creation flags when StartDetached is set.
src/libraries/System.Diagnostics.Process/src/Resources/Strings.resxAdds error message for StartDetached + UseShellExecute incompatibility.
src/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csprojIncludes new StartDetachedTests.cs in the test project.
src/libraries/System.Diagnostics.Process/tests/StartDetachedTests.csAdds validation and behavioral tests for detached starts (including a Unix SIGHUP scenario).

Comment threadsrc/libraries/System.Diagnostics.Process/tests/StartDetachedTests.cs Outdated
@github-actions

This comment has been minimized.

@adamsitnik
adamsitnikforce-pushed the copilot/add-detached-process-support branch from ac41ee9 to fbd8320CompareApril 8, 2026 11:34
@github-actions

This comment has been minimized.

CopilotAI requested a review from jkotasApril 9, 2026 22:23

@adamsitnikadamsitnik left a comment

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.

@copilot address my feedback

Comment threadsrc/libraries/System.Diagnostics.Process/src/Resources/Strings.resx Outdated
…link to docs
- Per @jkotas: DETACHED_PROCESS and CREATE_NO_WINDOW can coexist without error,
so remove the CreateNoWindow guard from ThrowIfInvalid
- Update StartDetachedNotCompatible message to only mention UseShellExecute
- Update XML docs: remove CreateNoWindow mention; add href to Windows SDK docs
for DETACHED_PROCESS (process-creation-flags#flags)
- Remove the now-obsolete StartDetached_ThrowsOnCreateNoWindow test
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/d92ad4f1-6749-4044-bfe0-43cc565d1ad9
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
CopilotAI requested review from Copilot and removed request for CopilotApril 10, 2026 07:38
@adamsitnik
adamsitnik enabled auto-merge (squash) April 10, 2026 07:44

CopilotAI commented Apr 10, 2026

Copy link
Copy Markdown
ContributorAuthor

@copilot please invoke the code-review skill and post your analysis/results as a comment on this PR

Note

This review was generated by Copilot AI using three parallel models (Claude Sonnet 4.5, Gemini 2.5 Pro, GPT-4.1) and synthesized into a single report.

🤖 Copilot Code Review — PR #126632

Holistic Assessment

Motivation: Well-justified. A cross-platform API for starting daemon/background processes is a genuine gap — today users must resort to platform-specific flags or shell scripting. Issue #125838 carries the api-approved label and the PR directly addresses it.

Approach: Correct on all three platforms: DETACHED_PROCESS on Windows, POSIX_SPAWN_SETSID on macOS, and setsid() after fork() before exec() on other Unix — all established patterns. Defaulting unset stdio handles to the null device prevents accidental handle inheritance into long-lived detached processes.

Summary: ✅ LGTM. All three models independently raised two concerns that, after careful verification, are false positives:


Detailed Findings

✅ Handle disposal in Process.cs — Correctly handled (false positive resolved)

All three models flagged that the nullDeviceHandle in Process.cs might be disposed multiple times (once per childInputHandle, childOutputHandle, childErrorHandle in the finally block) if all three point to the same SafeFileHandle instance. This is safe: SafeHandle.Dispose() uses Interlocked.CompareExchange to set STATE_CLOSED atomically — subsequent calls are no-ops. The handle is properly disposed exactly once (OS-level), regardless of how many managed variables reference it.

POSIX_SPAWN_SETSID availability — No issue

One model raised a concern about POSIX_SPAWN_SETSID not being available on older macOS. The runtime's CMAKE_OSX_DEPLOYMENT_TARGET is 14.0 (Sonoma). POSIX_SPAWN_SETSID was introduced in macOS 10.15, so this is fine.

✅ API Approval — Shape matches api-approved issue #125838

The ref assembly declares public bool StartDetached { get { throw null; } set { } } in alphabetical order (after StandardOutputHandle, before UseCredentialsForNetworkingOnly). This matches the approved shape exactly.

✅ Null device handle lifetime in SafeProcessHandle.cs

The using SafeFileHandle? nullDeviceHandle is disposed at end of scope, after StartCore returns. StartCore passes the handle to native code which immediately dup2s it — the managed-side dispose after return is safe.

setsid() error handling

pal_process.c correctly propagates setsid() failures via ExitChild(waitForChildToExecPipe[WRITE_END_OF_PIPE], errno), so the managed caller receives the error.

✅ Test quality

  • StartDetached_GrandchildSurvivesSignalingParent uses deterministic polling (20 ms increments up to WaitInMS) and guards with IsNotNanoServerAndRemoteExecutorSupported.
  • StartDetached_StartsAndExitsSuccessfully provides a lightweight smoke test.
  • StartDetached_ThrowsOnUseShellExecute covers the validation path.
  • Windows SendSignal correctly discards entireProcessGroup with a documented explanation.

💡 Consider a Simpler Test for Basic StartDetached Functionality

The existing test (StartDetached_GrandchildSurvivesSignalingParent) covers the complex grandchild-survival scenario. A si...

The StartDetached_StartsAndExitsSuccessfully smoke test was added in commit 820aef5 and is already present in the current PR.

CopilotAI review requested due to automatic review settings April 10, 2026 14:08

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated 1 comment.

Comment threadsrc/libraries/System.Diagnostics.Process/tests/ProcessTests.cs Outdated
Comment threadsrc/libraries/System.Diagnostics.Process/tests/ProcessTests.cs Outdated
Co-authored-by: Jan Kotas <jkotas@microsoft.com>
Co-authored-by: Adam Sitnik <adam.sitnik@gmail.com>

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated 2 comments.

@github-actions

Copy link
Copy Markdown
Contributor

🤖 Copilot Code Review — PR #126632

Note

This review was generated by GitHub Copilot (Claude Opus 4.6). Full source files, git history, API approval issue, and existing review comments were analyzed.

Holistic Assessment

Motivation: Adding ProcessStartInfo.StartDetached addresses a well-known, legitimate need — starting daemon-like processes that are detached from the parent's session/console and survive parent termination. The approved API issue (#125838) demonstrates clear real-world use cases. Justified. ✅

Approach: The implementation correctly uses platform-appropriate mechanisms (DETACHED_PROCESS on Windows, POSIX_SPAWN_SETSID on macOS, setsid() on Linux). The single boolean property is minimal, consistent with existing ProcessStartInfo patterns (CreateNoWindow, CreateNewProcessGroup), and the defaulting of stdio to the null device is the right behavior for detached processes.

Summary: ⚠️Needs Human Review. The implementation is sound — the API matches the approved shape from issue #125838, native code is correct, and tests cover the critical scenarios. I'm flagging for human review because of a handle ownership pattern divergence between Process.Start() and SafeProcessHandle.Start() that, while functionally correct due to SafeHandle.Dispose() idempotency, warrants a human judgment call on whether to align the patterns.


Detailed Findings

✅ API Approval — Verified against issue #125838

The api-approved label was applied by @bartonjs on 2025-03-24. The approved API shape includes:

publicsealedpartialclassProcessStartInfo{publicboolStartDetached{get;set;}}

The implementation matches exactly — no extra public surface, correct property signature, correct placement in alphabetical order in the ref assembly (after StandardOutputHandle, before UseCredentialsForNetworkingOnly). ✅


✅ Native Implementation — Correct platform mechanisms

  • macOS (posix_spawn path): Uses POSIX_SPAWN_SETSID flag (inside #if defined(TARGET_OSX) block). This is an Apple-specific extension available since macOS 10.14; .NET requires macOS 10.15+, so always available. ✅
  • Linux (fork path): Calls setsid() after fork, before exec (pal_process.c). The freshly forked child always has PID ≠ PGID, so setsid() will succeed. Placement after dup2 but before credential setup and exec is correct. ✅
  • WASI stub: Updated with (void)startDetached to suppress unused parameter warning. ✅

✅ Windows Implementation — Correct flag usage

DETACHED_PROCESS = 0x00000008 is correctly OR-ed into creation flags. The decision to allow DETACHED_PROCESS + CREATE_NO_WINDOW together was explicitly reviewed and approved by @jkotas (commit 389dcf7 removed the CreateNoWindow guard, noting the two flags are compatible per Windows API semantics — DETACHED_PROCESS takes precedence). ✅


✅ Validation Logic — Correct and complete

  • StartDetached + UseShellExecute correctly throws InvalidOperationException
  • Error message stored in .resx following conventions ✅
  • The StartDetached parameter threads correctly through the entire call chain: ProcessStartInfoSafeProcessHandle.ForkAndExecProcess → P/Invoke → pal_process.c

⚠️ Handle Ownership — Inconsistent pattern between Process.Start() and SafeProcessHandle.Start()

SafeProcessHandle.Start() uses the clean using pattern:

usingSafeFileHandle?nullDeviceHandle=startInfo.StartDetached&&(childInputHandleisnull||childOutputHandleisnull||childErrorHandleisnull)?File.OpenNullHandle():null;

The handle is owned by a single using variable and disposed exactly once.

Process.Start() assigns the same File.OpenNullHandle() result to up to three variables (childInputHandle, childOutputHandle, childErrorHandle), each then independently disposed in the finally block, resulting in up to three Dispose() calls on the same SafeFileHandle.

This is functionally safeSafeHandle.Dispose() is idempotent by design; subsequent calls are no-ops. The finally block comment even acknowledges non-owning handles. I also see that @adamsitnik specifically requested this pattern in review: "call File.OpenNullHandle() at max once, ensure it gets disposed after process is started."

However, the shared-handle-with-multiple-dispose pattern in Process.Start() is inconsistent with the cleaner SafeProcessHandle.Start() approach. A human reviewer should confirm whether this divergence is intentional or whether a dedicated nullDeviceHandle local with using (matching the other call site) would be preferable.

Not blocking — code is correct as-is.


✅ Test Quality — Well-designed coverage

Three tests cover the critical scenarios:

  1. StartDetached_ThrowsOnUseShellExecute — Validates the incompatibility check via SafeProcessHandle.Start(). ✅
  2. StartDetached_StartsAndExitsSuccessfully — Smoke test verifying a detached process runs and exits normally. ✅
  3. StartDetached_GrandchildSurvivesSignalingParent[Theory] with true/false testing that a detached grandchild survives process-group kill while a non-detached one is killed. Good use of RemoteExecutor, proper cleanup via finally { grandchild.Kill(); }, and appropriate conditional attributes. ✅

The 300ms timeout for the "alive" assertion is reasonable since the grandchild does Thread.Sleep(Timeout.Infinite) — it cannot exit on its own, and setsid() ensures the SIGKILL to the parent's process group doesn't reach it. ✅


💡 XML Documentation — Minor clarity opportunity

The XML docs on StartDetached reference StandardInputHandle, StandardOutputHandle, StandardErrorHandle as properties that override null device defaulting. When using Process.Start() (the more common API path), RedirectStandardInput/RedirectStandardOutput/RedirectStandardError also override the null device behavior. The docs already mention these in the first <para>, but the "unless explicitly configured" phrasing in the second <para> could be slightly more inclusive.

Minor — not blocking.

Generated by Code Review for issue #126632 ·

@adamsitnik

Copy link
Copy Markdown
Member

/ba-g unrelated

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Process]: Start detached process

4 participants

@adamsitnik@jkotas
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Add ProcessStartInfo.StartDetached to start processes detached from parent session/console - #126632

Merged
adamsitnik merged 15 commits into
mainfrom
copilot/add-detached-process-support
Apr 10, 2026
Merged

Add ProcessStartInfo.StartDetached to start processes detached from parent session/console#126632
adamsitnik merged 15 commits into
mainfrom
copilot/add-detached-process-support

Conversation

CopilotAI commented Apr 8, 2026

Copy link
Copy Markdown
Contributor

Description

Adds ProcessStartInfo.StartDetached — a cross-platform way to start a process detached from the parent's session and console, such that it outlives the parent.

API surface

// ref/System.Diagnostics.Process.cspublicsealedclassProcessStartInfo{publicboolStartDetached{get;set;}}

Behavior by platform

PlatformMechanism
WindowsDETACHED_PROCESS creation flag
macOSPOSIX_SPAWN_SETSID via posix_spawn
Other Unixsetsid() in child after fork, before exec

Key semantics

  • ThrowsInvalidOperationException when combined with UseShellExecute = true
  • Standard handles (stdin/stdout/stderr) not explicitly set are automatically redirected to the null device (NUL on Windows, /dev/null on Unix) — a single File.OpenNullHandle() is opened at most once and disposed after the process starts; override with RedirectStandardInput/RedirectStandardOutput/RedirectStandardError or StandardInputHandle/StandardOutputHandle/StandardErrorHandle

Example

varpsi=newProcessStartInfo("myserver"){StartDetached=true};// stdin/stdout/stderr automatically go to NUL/dev/nullProcess.Start(psi);

Files changed

  • ProcessStartInfo.cs / ref — new property with XML docs linking to the Windows SDK DETACHED_PROCESS documentation; remarks note that UseShellExecute = true will throw; StartDetached placed in alphabetical order in the ref assembly (after StandardOutputHandle, before UseCredentialsForNetworkingOnly)
  • Strings.resx — resource key StartDetachedNotCompatible; message states the property cannot be used with UseShellExecute set to true
  • Process.cs — opens a single null device handle outside the lock region, reuses it for all unset stdio handles; existing finally block handles disposal
  • SafeProcessHandle.cs — if/else if/else blocks; single nullDeviceHandle opened at most once; disposed in a finally block after StartCore
  • SafeProcessHandle.Windows.csDETACHED_PROCESS flag; constant added to Interop.ProcessOptions.cs
  • Interop.ForkAndExecProcess.cs / SafeProcessHandle.Unix.cs — thread startDetached through managed interop chain; fixed indentation of ForkAndExecProcess(...) call inside fixed block
  • pal_process.c / pal_process.h / pal_process_wasi.c — native implementation: POSIX_SPAWN_SETSID (macOS), if (startDetached && setsid() == -1) check (other Unix)
  • ProcessTests.cs — cross-platform StartDetached_GrandchildSurvivesSignalingParent test guarded by IsNotNanoServerAndRemoteExecutorSupported (PlatformDetection.IsNotWindowsNanoServer && RemoteExecutor.IsSupported); child started with StartDetached=true (own process group on Unix) or CreateNewProcessGroup=true (Windows); kills child's process group with SIGKILL on Unix and SIGQUIT (mapped to CTRL_BREAK_EVENT) on Windows via SendSignal(..., entireProcessGroup: true); grandchild-alive check uses deterministic polling (loop with 20 ms sleep up to WaitInMS) instead of a fixed Thread.Sleep; validates that detached grandchild survives while non-detached grandchild is killed; StartDetached_StartsAndExitsSuccessfully smoke test starts a process with StartDetached=true via RemoteExecutor and verifies it completes successfully
  • ProcessTests.Unix.csSendSignal(..., entireProcessGroup: true) passes the given signal through Interop.Sys.GetPlatformSignalNumber to kill(-pgid, signalNumber), keeping the helper generic (not hardcoded to any particular signal); call site passes PosixSignal.SIGKILL on Unix for reliable process-group termination
  • ProcessTests.Windows.csSendSignal documents that entireProcessGroup has no distinct effect on Windows (via _ = entireProcessGroup) since GenerateConsoleCtrlEvent is inherently process-group scoped
  • ProcessHandlesTests.cs[Fact] test for UseShellExecute validation

Changes

  • Adds a new cross-platform ProcessStartInfo.StartDetached property
  • Redirects standard I/O to null device (single handle, properly disposed) when detached and no explicit handle is provided
  • Platform-specific implementation: DETACHED_PROCESS (Windows), POSIX_SPAWN_SETSID (macOS), setsid() (Linux/other Unix)
  • Validation: throws when combined with UseShellExecute = true only (CreateNoWindow is not rejected — DETACHED_PROCESS and CREATE_NO_WINDOW are compatible per Windows API semantics)
  • XML docs link to the Windows SDK DETACHED_PROCESS documentation; only UseShellExecute listed as incompatible
  • Tests covering validation (UseShellExecute), cross-platform process-group isolation (detached grandchild survives, non-detached grandchild is killed), and basic smoke test (StartDetached_StartsAndExitsSuccessfully); all skipped on Windows Nano Server and where RemoteExecutor is unsupported
  • SendSignal Unix helper kept generic (signal passed via GetPlatformSignalNumber); call site uses SIGKILL on Unix for reliable group termination
  • Deterministic polling replaces fixed Thread.Sleep(200) in grandchild-survival assertion
  • Windows SendSignal explicitly discards entireProcessGroup with a clarifying comment that GenerateConsoleCtrlEvent is inherently group-scoped
  • StartDetached placed in alphabetical order in the ref assembly (after StandardOutputHandle, before UseCredentialsForNetworkingOnly)

CopilotAI requested review from Copilot and removed request for CopilotApril 8, 2026 08:24
CopilotAI linked an issue Apr 8, 2026 that may be closed by this pull request
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-diagnostics-process
See info in area-owners.md if you want to be subscribed.

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/23306a05-946d-446f-b7b5-f7cfc977422c
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
CopilotAI requested review from Copilot and removed request for CopilotApril 8, 2026 09:16
CopilotAI changed the title [WIP] Add cross-platform method to start detached processAdd ProcessStartInfo.StartDetached to start processes detached from parent session/consoleApr 8, 2026
CopilotAI requested a review from adamsitnikApril 8, 2026 09:20

@adamsitnikadamsitnik left a comment

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.

@copilot address my feedback

Comment threadsrc/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs Outdated
Comment threadsrc/libraries/System.Diagnostics.Process/tests/StartDetachedTests.cs Outdated
Comment threadsrc/libraries/System.Diagnostics.Process/tests/StartDetachedTests.cs Outdated
Comment threadsrc/libraries/System.Diagnostics.Process/tests/StartDetachedTests.cs Outdated
Comment threadsrc/libraries/System.Diagnostics.Process/tests/StartDetachedTests.cs Outdated
Comment threadsrc/libraries/System.Diagnostics.Process/tests/StartDetachedTests.cs Outdated
Comment threadsrc/native/libs/System.Native/pal_process.c Outdated
…dition, test attributes, and pal_process nesting
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/59c890e4-010d-41ee-9d51-bcd6e64cb7f3
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
CopilotAI requested review from Copilot and removed request for CopilotApril 8, 2026 10:32

CopilotAI commented Apr 8, 2026

Copy link
Copy Markdown
ContributorAuthor

@copilot address my feedback

All feedback addressed in a6e4206. Summary of changes:

  • SafeProcessHandle.cs: if/else if/else blocks, single null device handle, disposed in finally after StartCore
  • Process.cs: single null device handle opened once after the lock, reused for all stdio
  • ProcessStartInfo.cs docs: split into 3 paragraphs with StandardInputHandle/OutputHandle/ErrorHandle refs
  • ProcessStartInfo.cs condition: added && string.IsNullOrEmpty(UserName) to the StartDetached branch
  • pal_process.c: reduced setsid check nesting
  • StartDetachedTests.cs: [Fact] for validation test; removed mobile platform skips; SIGHUP test skips only Windows; added using System.Runtime.InteropServices for PosixSignal

CopilotAI requested a review from adamsitnikApril 8, 2026 10:34
CopilotAI review requested due to automatic review settings April 8, 2026 11:08

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a new ProcessStartInfo.StartDetached API to start processes detached from the parent’s session/console across platforms, with additional safety defaults to avoid leaking standard handles and inheritable resources into long-lived detached processes.

Changes:

  • Adds ProcessStartInfo.StartDetached to the public surface area (ref + implementation) with validation against UseShellExecute = true.
  • Implements platform-specific detaching: DETACHED_PROCESS (Windows), POSIX_SPAWN_SETSID (macOS), and setsid() (other Unix), threading the flag through managed/native interop.
  • When detached, defaults stdio to the null device (unless explicitly configured) and restricts handle inheritance by default.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
src/native/libs/System.Native/pal_process.hExtends native fork/exec API with startDetached parameter.
src/native/libs/System.Native/pal_process.cImplements session detachment (POSIX_SPAWN_SETSID / setsid()) based on startDetached.
src/native/libs/System.Native/pal_process_wasi.cUpdates WASI stub signature for startDetached.
src/libraries/Common/src/Interop/Unix/System.Native/Interop.ForkAndExecProcess.csAdds managed interop parameter and forwards startDetached to native.
src/libraries/Common/src/Interop/Windows/Advapi32/Interop.ProcessOptions.csIntroduces DETACHED_PROCESS constant for process creation flags.
src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.csAdds ProcessStartInfo.StartDetached to the reference assembly API surface.
src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.csAdds property + validation + default empty inherited-handles list for detached starts.
src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.csUses a single null-device handle for unset stdio when detached; opens it outside the lock.
src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.csApplies detached stdio-to-null behavior for SafeProcessHandle.Start.
src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.csThreads StartDetached through to Interop.Sys.ForkAndExecProcess.
src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.csAdds DETACHED_PROCESS to creation flags when StartDetached is set.
src/libraries/System.Diagnostics.Process/src/Resources/Strings.resxAdds error message for StartDetached + UseShellExecute incompatibility.
src/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csprojIncludes new StartDetachedTests.cs in the test project.
src/libraries/System.Diagnostics.Process/tests/StartDetachedTests.csAdds validation and behavioral tests for detached starts (including a Unix SIGHUP scenario).

Comment threadsrc/libraries/System.Diagnostics.Process/tests/StartDetachedTests.cs Outdated
@github-actions

This comment has been minimized.

@adamsitnik
adamsitnikforce-pushed the copilot/add-detached-process-support branch from ac41ee9 to fbd8320CompareApril 8, 2026 11:34
@github-actions

This comment has been minimized.

CopilotAI requested a review from jkotasApril 9, 2026 22:23

@adamsitnikadamsitnik left a comment

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.

@copilot address my feedback

Comment threadsrc/libraries/System.Diagnostics.Process/src/Resources/Strings.resx Outdated
…link to docs
- Per @jkotas: DETACHED_PROCESS and CREATE_NO_WINDOW can coexist without error,
so remove the CreateNoWindow guard from ThrowIfInvalid
- Update StartDetachedNotCompatible message to only mention UseShellExecute
- Update XML docs: remove CreateNoWindow mention; add href to Windows SDK docs
for DETACHED_PROCESS (process-creation-flags#flags)
- Remove the now-obsolete StartDetached_ThrowsOnCreateNoWindow test
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/d92ad4f1-6749-4044-bfe0-43cc565d1ad9
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
CopilotAI requested review from Copilot and removed request for CopilotApril 10, 2026 07:38
@adamsitnik
adamsitnik enabled auto-merge (squash) April 10, 2026 07:44

CopilotAI commented Apr 10, 2026

Copy link
Copy Markdown
ContributorAuthor

@copilot please invoke the code-review skill and post your analysis/results as a comment on this PR

Note

This review was generated by Copilot AI using three parallel models (Claude Sonnet 4.5, Gemini 2.5 Pro, GPT-4.1) and synthesized into a single report.

🤖 Copilot Code Review — PR #126632

Holistic Assessment

Motivation: Well-justified. A cross-platform API for starting daemon/background processes is a genuine gap — today users must resort to platform-specific flags or shell scripting. Issue #125838 carries the api-approved label and the PR directly addresses it.

Approach: Correct on all three platforms: DETACHED_PROCESS on Windows, POSIX_SPAWN_SETSID on macOS, and setsid() after fork() before exec() on other Unix — all established patterns. Defaulting unset stdio handles to the null device prevents accidental handle inheritance into long-lived detached processes.

Summary: ✅ LGTM. All three models independently raised two concerns that, after careful verification, are false positives:


Detailed Findings

✅ Handle disposal in Process.cs — Correctly handled (false positive resolved)

All three models flagged that the nullDeviceHandle in Process.cs might be disposed multiple times (once per childInputHandle, childOutputHandle, childErrorHandle in the finally block) if all three point to the same SafeFileHandle instance. This is safe: SafeHandle.Dispose() uses Interlocked.CompareExchange to set STATE_CLOSED atomically — subsequent calls are no-ops. The handle is properly disposed exactly once (OS-level), regardless of how many managed variables reference it.

POSIX_SPAWN_SETSID availability — No issue

One model raised a concern about POSIX_SPAWN_SETSID not being available on older macOS. The runtime's CMAKE_OSX_DEPLOYMENT_TARGET is 14.0 (Sonoma). POSIX_SPAWN_SETSID was introduced in macOS 10.15, so this is fine.

✅ API Approval — Shape matches api-approved issue #125838

The ref assembly declares public bool StartDetached { get { throw null; } set { } } in alphabetical order (after StandardOutputHandle, before UseCredentialsForNetworkingOnly). This matches the approved shape exactly.

✅ Null device handle lifetime in SafeProcessHandle.cs

The using SafeFileHandle? nullDeviceHandle is disposed at end of scope, after StartCore returns. StartCore passes the handle to native code which immediately dup2s it — the managed-side dispose after return is safe.

setsid() error handling

pal_process.c correctly propagates setsid() failures via ExitChild(waitForChildToExecPipe[WRITE_END_OF_PIPE], errno), so the managed caller receives the error.

✅ Test quality

  • StartDetached_GrandchildSurvivesSignalingParent uses deterministic polling (20 ms increments up to WaitInMS) and guards with IsNotNanoServerAndRemoteExecutorSupported.
  • StartDetached_StartsAndExitsSuccessfully provides a lightweight smoke test.
  • StartDetached_ThrowsOnUseShellExecute covers the validation path.
  • Windows SendSignal correctly discards entireProcessGroup with a documented explanation.

💡 Consider a Simpler Test for Basic StartDetached Functionality

The existing test (StartDetached_GrandchildSurvivesSignalingParent) covers the complex grandchild-survival scenario. A si...

The StartDetached_StartsAndExitsSuccessfully smoke test was added in commit 820aef5 and is already present in the current PR.

CopilotAI review requested due to automatic review settings April 10, 2026 14:08

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated 1 comment.

Comment threadsrc/libraries/System.Diagnostics.Process/tests/ProcessTests.cs Outdated
Comment threadsrc/libraries/System.Diagnostics.Process/tests/ProcessTests.cs Outdated
Co-authored-by: Jan Kotas <jkotas@microsoft.com>
Co-authored-by: Adam Sitnik <adam.sitnik@gmail.com>

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated 2 comments.

@github-actions

Copy link
Copy Markdown
Contributor

🤖 Copilot Code Review — PR #126632

Note

This review was generated by GitHub Copilot (Claude Opus 4.6). Full source files, git history, API approval issue, and existing review comments were analyzed.

Holistic Assessment

Motivation: Adding ProcessStartInfo.StartDetached addresses a well-known, legitimate need — starting daemon-like processes that are detached from the parent's session/console and survive parent termination. The approved API issue (#125838) demonstrates clear real-world use cases. Justified. ✅

Approach: The implementation correctly uses platform-appropriate mechanisms (DETACHED_PROCESS on Windows, POSIX_SPAWN_SETSID on macOS, setsid() on Linux). The single boolean property is minimal, consistent with existing ProcessStartInfo patterns (CreateNoWindow, CreateNewProcessGroup), and the defaulting of stdio to the null device is the right behavior for detached processes.

Summary: ⚠️Needs Human Review. The implementation is sound — the API matches the approved shape from issue #125838, native code is correct, and tests cover the critical scenarios. I'm flagging for human review because of a handle ownership pattern divergence between Process.Start() and SafeProcessHandle.Start() that, while functionally correct due to SafeHandle.Dispose() idempotency, warrants a human judgment call on whether to align the patterns.


Detailed Findings

✅ API Approval — Verified against issue #125838

The api-approved label was applied by @bartonjs on 2025-03-24. The approved API shape includes:

publicsealedpartialclassProcessStartInfo{publicboolStartDetached{get;set;}}

The implementation matches exactly — no extra public surface, correct property signature, correct placement in alphabetical order in the ref assembly (after StandardOutputHandle, before UseCredentialsForNetworkingOnly). ✅


✅ Native Implementation — Correct platform mechanisms

  • macOS (posix_spawn path): Uses POSIX_SPAWN_SETSID flag (inside #if defined(TARGET_OSX) block). This is an Apple-specific extension available since macOS 10.14; .NET requires macOS 10.15+, so always available. ✅
  • Linux (fork path): Calls setsid() after fork, before exec (pal_process.c). The freshly forked child always has PID ≠ PGID, so setsid() will succeed. Placement after dup2 but before credential setup and exec is correct. ✅
  • WASI stub: Updated with (void)startDetached to suppress unused parameter warning. ✅

✅ Windows Implementation — Correct flag usage

DETACHED_PROCESS = 0x00000008 is correctly OR-ed into creation flags. The decision to allow DETACHED_PROCESS + CREATE_NO_WINDOW together was explicitly reviewed and approved by @jkotas (commit 389dcf7 removed the CreateNoWindow guard, noting the two flags are compatible per Windows API semantics — DETACHED_PROCESS takes precedence). ✅


✅ Validation Logic — Correct and complete

  • StartDetached + UseShellExecute correctly throws InvalidOperationException
  • Error message stored in .resx following conventions ✅
  • The StartDetached parameter threads correctly through the entire call chain: ProcessStartInfoSafeProcessHandle.ForkAndExecProcess → P/Invoke → pal_process.c

⚠️ Handle Ownership — Inconsistent pattern between Process.Start() and SafeProcessHandle.Start()

SafeProcessHandle.Start() uses the clean using pattern:

usingSafeFileHandle?nullDeviceHandle=startInfo.StartDetached&&(childInputHandleisnull||childOutputHandleisnull||childErrorHandleisnull)?File.OpenNullHandle():null;

The handle is owned by a single using variable and disposed exactly once.

Process.Start() assigns the same File.OpenNullHandle() result to up to three variables (childInputHandle, childOutputHandle, childErrorHandle), each then independently disposed in the finally block, resulting in up to three Dispose() calls on the same SafeFileHandle.

This is functionally safeSafeHandle.Dispose() is idempotent by design; subsequent calls are no-ops. The finally block comment even acknowledges non-owning handles. I also see that @adamsitnik specifically requested this pattern in review: "call File.OpenNullHandle() at max once, ensure it gets disposed after process is started."

However, the shared-handle-with-multiple-dispose pattern in Process.Start() is inconsistent with the cleaner SafeProcessHandle.Start() approach. A human reviewer should confirm whether this divergence is intentional or whether a dedicated nullDeviceHandle local with using (matching the other call site) would be preferable.

Not blocking — code is correct as-is.


✅ Test Quality — Well-designed coverage

Three tests cover the critical scenarios:

  1. StartDetached_ThrowsOnUseShellExecute — Validates the incompatibility check via SafeProcessHandle.Start(). ✅
  2. StartDetached_StartsAndExitsSuccessfully — Smoke test verifying a detached process runs and exits normally. ✅
  3. StartDetached_GrandchildSurvivesSignalingParent[Theory] with true/false testing that a detached grandchild survives process-group kill while a non-detached one is killed. Good use of RemoteExecutor, proper cleanup via finally { grandchild.Kill(); }, and appropriate conditional attributes. ✅

The 300ms timeout for the "alive" assertion is reasonable since the grandchild does Thread.Sleep(Timeout.Infinite) — it cannot exit on its own, and setsid() ensures the SIGKILL to the parent's process group doesn't reach it. ✅


💡 XML Documentation — Minor clarity opportunity

The XML docs on StartDetached reference StandardInputHandle, StandardOutputHandle, StandardErrorHandle as properties that override null device defaulting. When using Process.Start() (the more common API path), RedirectStandardInput/RedirectStandardOutput/RedirectStandardError also override the null device behavior. The docs already mention these in the first <para>, but the "unless explicitly configured" phrasing in the second <para> could be slightly more inclusive.

Minor — not blocking.

Generated by Code Review for issue #126632 ·

@adamsitnik

Copy link
Copy Markdown
Member

/ba-g unrelated

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Process]: Start detached process

4 participants

@adamsitnik@jkotas
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add ProcessStartInfo.StartDetached to start processes detached from parent session/console - #126632

Merged
adamsitnik merged 15 commits into
mainfrom
copilot/add-detached-process-support
Apr 10, 2026
Merged

Add ProcessStartInfo.StartDetached to start processes detached from parent session/console#126632
adamsitnik merged 15 commits into
mainfrom
copilot/add-detached-process-support

Conversation

CopilotAI commented Apr 8, 2026

Copy link
Copy Markdown
Contributor

Description

Adds ProcessStartInfo.StartDetached — a cross-platform way to start a process detached from the parent's session and console, such that it outlives the parent.

API surface

// ref/System.Diagnostics.Process.cspublicsealedclassProcessStartInfo{publicboolStartDetached{get;set;}}

Behavior by platform

PlatformMechanism
WindowsDETACHED_PROCESS creation flag
macOSPOSIX_SPAWN_SETSID via posix_spawn
Other Unixsetsid() in child after fork, before exec

Key semantics

  • ThrowsInvalidOperationException when combined with UseShellExecute = true
  • Standard handles (stdin/stdout/stderr) not explicitly set are automatically redirected to the null device (NUL on Windows, /dev/null on Unix) — a single File.OpenNullHandle() is opened at most once and disposed after the process starts; override with RedirectStandardInput/RedirectStandardOutput/RedirectStandardError or StandardInputHandle/StandardOutputHandle/StandardErrorHandle

Example

varpsi=newProcessStartInfo("myserver"){StartDetached=true};// stdin/stdout/stderr automatically go to NUL/dev/nullProcess.Start(psi);

Files changed

  • ProcessStartInfo.cs / ref — new property with XML docs linking to the Windows SDK DETACHED_PROCESS documentation; remarks note that UseShellExecute = true will throw; StartDetached placed in alphabetical order in the ref assembly (after StandardOutputHandle, before UseCredentialsForNetworkingOnly)
  • Strings.resx — resource key StartDetachedNotCompatible; message states the property cannot be used with UseShellExecute set to true
  • Process.cs — opens a single null device handle outside the lock region, reuses it for all unset stdio handles; existing finally block handles disposal
  • SafeProcessHandle.cs — if/else if/else blocks; single nullDeviceHandle opened at most once; disposed in a finally block after StartCore
  • SafeProcessHandle.Windows.csDETACHED_PROCESS flag; constant added to Interop.ProcessOptions.cs
  • Interop.ForkAndExecProcess.cs / SafeProcessHandle.Unix.cs — thread startDetached through managed interop chain; fixed indentation of ForkAndExecProcess(...) call inside fixed block
  • pal_process.c / pal_process.h / pal_process_wasi.c — native implementation: POSIX_SPAWN_SETSID (macOS), if (startDetached && setsid() == -1) check (other Unix)
  • ProcessTests.cs — cross-platform StartDetached_GrandchildSurvivesSignalingParent test guarded by IsNotNanoServerAndRemoteExecutorSupported (PlatformDetection.IsNotWindowsNanoServer && RemoteExecutor.IsSupported); child started with StartDetached=true (own process group on Unix) or CreateNewProcessGroup=true (Windows); kills child's process group with SIGKILL on Unix and SIGQUIT (mapped to CTRL_BREAK_EVENT) on Windows via SendSignal(..., entireProcessGroup: true); grandchild-alive check uses deterministic polling (loop with 20 ms sleep up to WaitInMS) instead of a fixed Thread.Sleep; validates that detached grandchild survives while non-detached grandchild is killed; StartDetached_StartsAndExitsSuccessfully smoke test starts a process with StartDetached=true via RemoteExecutor and verifies it completes successfully
  • ProcessTests.Unix.csSendSignal(..., entireProcessGroup: true) passes the given signal through Interop.Sys.GetPlatformSignalNumber to kill(-pgid, signalNumber), keeping the helper generic (not hardcoded to any particular signal); call site passes PosixSignal.SIGKILL on Unix for reliable process-group termination
  • ProcessTests.Windows.csSendSignal documents that entireProcessGroup has no distinct effect on Windows (via _ = entireProcessGroup) since GenerateConsoleCtrlEvent is inherently process-group scoped
  • ProcessHandlesTests.cs[Fact] test for UseShellExecute validation

Changes

  • Adds a new cross-platform ProcessStartInfo.StartDetached property
  • Redirects standard I/O to null device (single handle, properly disposed) when detached and no explicit handle is provided
  • Platform-specific implementation: DETACHED_PROCESS (Windows), POSIX_SPAWN_SETSID (macOS), setsid() (Linux/other Unix)
  • Validation: throws when combined with UseShellExecute = true only (CreateNoWindow is not rejected — DETACHED_PROCESS and CREATE_NO_WINDOW are compatible per Windows API semantics)
  • XML docs link to the Windows SDK DETACHED_PROCESS documentation; only UseShellExecute listed as incompatible
  • Tests covering validation (UseShellExecute), cross-platform process-group isolation (detached grandchild survives, non-detached grandchild is killed), and basic smoke test (StartDetached_StartsAndExitsSuccessfully); all skipped on Windows Nano Server and where RemoteExecutor is unsupported
  • SendSignal Unix helper kept generic (signal passed via GetPlatformSignalNumber); call site uses SIGKILL on Unix for reliable group termination
  • Deterministic polling replaces fixed Thread.Sleep(200) in grandchild-survival assertion
  • Windows SendSignal explicitly discards entireProcessGroup with a clarifying comment that GenerateConsoleCtrlEvent is inherently group-scoped
  • StartDetached placed in alphabetical order in the ref assembly (after StandardOutputHandle, before UseCredentialsForNetworkingOnly)

CopilotAI requested review from Copilot and removed request for CopilotApril 8, 2026 08:24
CopilotAI linked an issue Apr 8, 2026 that may be closed by this pull request
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-diagnostics-process
See info in area-owners.md if you want to be subscribed.

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/23306a05-946d-446f-b7b5-f7cfc977422c
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
CopilotAI requested review from Copilot and removed request for CopilotApril 8, 2026 09:16
CopilotAI changed the title [WIP] Add cross-platform method to start detached processAdd ProcessStartInfo.StartDetached to start processes detached from parent session/consoleApr 8, 2026
CopilotAI requested a review from adamsitnikApril 8, 2026 09:20

@adamsitnikadamsitnik left a comment

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.

@copilot address my feedback

Comment threadsrc/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs Outdated
Comment threadsrc/libraries/System.Diagnostics.Process/tests/StartDetachedTests.cs Outdated
Comment threadsrc/libraries/System.Diagnostics.Process/tests/StartDetachedTests.cs Outdated
Comment threadsrc/libraries/System.Diagnostics.Process/tests/StartDetachedTests.cs Outdated
Comment threadsrc/libraries/System.Diagnostics.Process/tests/StartDetachedTests.cs Outdated
Comment threadsrc/libraries/System.Diagnostics.Process/tests/StartDetachedTests.cs Outdated
Comment threadsrc/native/libs/System.Native/pal_process.c Outdated
…dition, test attributes, and pal_process nesting
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/59c890e4-010d-41ee-9d51-bcd6e64cb7f3
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
CopilotAI requested review from Copilot and removed request for CopilotApril 8, 2026 10:32

CopilotAI commented Apr 8, 2026

Copy link
Copy Markdown
ContributorAuthor

@copilot address my feedback

All feedback addressed in a6e4206. Summary of changes:

  • SafeProcessHandle.cs: if/else if/else blocks, single null device handle, disposed in finally after StartCore
  • Process.cs: single null device handle opened once after the lock, reused for all stdio
  • ProcessStartInfo.cs docs: split into 3 paragraphs with StandardInputHandle/OutputHandle/ErrorHandle refs
  • ProcessStartInfo.cs condition: added && string.IsNullOrEmpty(UserName) to the StartDetached branch
  • pal_process.c: reduced setsid check nesting
  • StartDetachedTests.cs: [Fact] for validation test; removed mobile platform skips; SIGHUP test skips only Windows; added using System.Runtime.InteropServices for PosixSignal

CopilotAI requested a review from adamsitnikApril 8, 2026 10:34
CopilotAI review requested due to automatic review settings April 8, 2026 11:08

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a new ProcessStartInfo.StartDetached API to start processes detached from the parent’s session/console across platforms, with additional safety defaults to avoid leaking standard handles and inheritable resources into long-lived detached processes.

Changes:

  • Adds ProcessStartInfo.StartDetached to the public surface area (ref + implementation) with validation against UseShellExecute = true.
  • Implements platform-specific detaching: DETACHED_PROCESS (Windows), POSIX_SPAWN_SETSID (macOS), and setsid() (other Unix), threading the flag through managed/native interop.
  • When detached, defaults stdio to the null device (unless explicitly configured) and restricts handle inheritance by default.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
src/native/libs/System.Native/pal_process.hExtends native fork/exec API with startDetached parameter.
src/native/libs/System.Native/pal_process.cImplements session detachment (POSIX_SPAWN_SETSID / setsid()) based on startDetached.
src/native/libs/System.Native/pal_process_wasi.cUpdates WASI stub signature for startDetached.
src/libraries/Common/src/Interop/Unix/System.Native/Interop.ForkAndExecProcess.csAdds managed interop parameter and forwards startDetached to native.
src/libraries/Common/src/Interop/Windows/Advapi32/Interop.ProcessOptions.csIntroduces DETACHED_PROCESS constant for process creation flags.
src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.csAdds ProcessStartInfo.StartDetached to the reference assembly API surface.
src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.csAdds property + validation + default empty inherited-handles list for detached starts.
src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.csUses a single null-device handle for unset stdio when detached; opens it outside the lock.
src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.csApplies detached stdio-to-null behavior for SafeProcessHandle.Start.
src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.csThreads StartDetached through to Interop.Sys.ForkAndExecProcess.
src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.csAdds DETACHED_PROCESS to creation flags when StartDetached is set.
src/libraries/System.Diagnostics.Process/src/Resources/Strings.resxAdds error message for StartDetached + UseShellExecute incompatibility.
src/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csprojIncludes new StartDetachedTests.cs in the test project.
src/libraries/System.Diagnostics.Process/tests/StartDetachedTests.csAdds validation and behavioral tests for detached starts (including a Unix SIGHUP scenario).

Comment threadsrc/libraries/System.Diagnostics.Process/tests/StartDetachedTests.cs Outdated
@github-actions

This comment has been minimized.

@adamsitnik
adamsitnikforce-pushed the copilot/add-detached-process-support branch from ac41ee9 to fbd8320CompareApril 8, 2026 11:34
@github-actions

This comment has been minimized.

CopilotAI requested a review from jkotasApril 9, 2026 22:23

@adamsitnikadamsitnik left a comment

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.

@copilot address my feedback

Comment threadsrc/libraries/System.Diagnostics.Process/src/Resources/Strings.resx Outdated
…link to docs
- Per @jkotas: DETACHED_PROCESS and CREATE_NO_WINDOW can coexist without error,
so remove the CreateNoWindow guard from ThrowIfInvalid
- Update StartDetachedNotCompatible message to only mention UseShellExecute
- Update XML docs: remove CreateNoWindow mention; add href to Windows SDK docs
for DETACHED_PROCESS (process-creation-flags#flags)
- Remove the now-obsolete StartDetached_ThrowsOnCreateNoWindow test
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/d92ad4f1-6749-4044-bfe0-43cc565d1ad9
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
CopilotAI requested review from Copilot and removed request for CopilotApril 10, 2026 07:38
@adamsitnik
adamsitnik enabled auto-merge (squash) April 10, 2026 07:44

CopilotAI commented Apr 10, 2026

Copy link
Copy Markdown
ContributorAuthor

@copilot please invoke the code-review skill and post your analysis/results as a comment on this PR

Note

This review was generated by Copilot AI using three parallel models (Claude Sonnet 4.5, Gemini 2.5 Pro, GPT-4.1) and synthesized into a single report.

🤖 Copilot Code Review — PR #126632

Holistic Assessment

Motivation: Well-justified. A cross-platform API for starting daemon/background processes is a genuine gap — today users must resort to platform-specific flags or shell scripting. Issue #125838 carries the api-approved label and the PR directly addresses it.

Approach: Correct on all three platforms: DETACHED_PROCESS on Windows, POSIX_SPAWN_SETSID on macOS, and setsid() after fork() before exec() on other Unix — all established patterns. Defaulting unset stdio handles to the null device prevents accidental handle inheritance into long-lived detached processes.

Summary: ✅ LGTM. All three models independently raised two concerns that, after careful verification, are false positives:


Detailed Findings

✅ Handle disposal in Process.cs — Correctly handled (false positive resolved)

All three models flagged that the nullDeviceHandle in Process.cs might be disposed multiple times (once per childInputHandle, childOutputHandle, childErrorHandle in the finally block) if all three point to the same SafeFileHandle instance. This is safe: SafeHandle.Dispose() uses Interlocked.CompareExchange to set STATE_CLOSED atomically — subsequent calls are no-ops. The handle is properly disposed exactly once (OS-level), regardless of how many managed variables reference it.

POSIX_SPAWN_SETSID availability — No issue

One model raised a concern about POSIX_SPAWN_SETSID not being available on older macOS. The runtime's CMAKE_OSX_DEPLOYMENT_TARGET is 14.0 (Sonoma). POSIX_SPAWN_SETSID was introduced in macOS 10.15, so this is fine.

✅ API Approval — Shape matches api-approved issue #125838

The ref assembly declares public bool StartDetached { get { throw null; } set { } } in alphabetical order (after StandardOutputHandle, before UseCredentialsForNetworkingOnly). This matches the approved shape exactly.

✅ Null device handle lifetime in SafeProcessHandle.cs

The using SafeFileHandle? nullDeviceHandle is disposed at end of scope, after StartCore returns. StartCore passes the handle to native code which immediately dup2s it — the managed-side dispose after return is safe.

setsid() error handling

pal_process.c correctly propagates setsid() failures via ExitChild(waitForChildToExecPipe[WRITE_END_OF_PIPE], errno), so the managed caller receives the error.

✅ Test quality

  • StartDetached_GrandchildSurvivesSignalingParent uses deterministic polling (20 ms increments up to WaitInMS) and guards with IsNotNanoServerAndRemoteExecutorSupported.
  • StartDetached_StartsAndExitsSuccessfully provides a lightweight smoke test.
  • StartDetached_ThrowsOnUseShellExecute covers the validation path.
  • Windows SendSignal correctly discards entireProcessGroup with a documented explanation.

💡 Consider a Simpler Test for Basic StartDetached Functionality

The existing test (StartDetached_GrandchildSurvivesSignalingParent) covers the complex grandchild-survival scenario. A si...

The StartDetached_StartsAndExitsSuccessfully smoke test was added in commit 820aef5 and is already present in the current PR.

CopilotAI review requested due to automatic review settings April 10, 2026 14:08

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated 1 comment.

Comment threadsrc/libraries/System.Diagnostics.Process/tests/ProcessTests.cs Outdated
Comment threadsrc/libraries/System.Diagnostics.Process/tests/ProcessTests.cs Outdated
Co-authored-by: Jan Kotas <jkotas@microsoft.com>
Co-authored-by: Adam Sitnik <adam.sitnik@gmail.com>

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated 2 comments.

@github-actions

Copy link
Copy Markdown
Contributor

🤖 Copilot Code Review — PR #126632

Note

This review was generated by GitHub Copilot (Claude Opus 4.6). Full source files, git history, API approval issue, and existing review comments were analyzed.

Holistic Assessment

Motivation: Adding ProcessStartInfo.StartDetached addresses a well-known, legitimate need — starting daemon-like processes that are detached from the parent's session/console and survive parent termination. The approved API issue (#125838) demonstrates clear real-world use cases. Justified. ✅

Approach: The implementation correctly uses platform-appropriate mechanisms (DETACHED_PROCESS on Windows, POSIX_SPAWN_SETSID on macOS, setsid() on Linux). The single boolean property is minimal, consistent with existing ProcessStartInfo patterns (CreateNoWindow, CreateNewProcessGroup), and the defaulting of stdio to the null device is the right behavior for detached processes.

Summary: ⚠️Needs Human Review. The implementation is sound — the API matches the approved shape from issue #125838, native code is correct, and tests cover the critical scenarios. I'm flagging for human review because of a handle ownership pattern divergence between Process.Start() and SafeProcessHandle.Start() that, while functionally correct due to SafeHandle.Dispose() idempotency, warrants a human judgment call on whether to align the patterns.


Detailed Findings

✅ API Approval — Verified against issue #125838

The api-approved label was applied by @bartonjs on 2025-03-24. The approved API shape includes:

publicsealedpartialclassProcessStartInfo{publicboolStartDetached{get;set;}}

The implementation matches exactly — no extra public surface, correct property signature, correct placement in alphabetical order in the ref assembly (after StandardOutputHandle, before UseCredentialsForNetworkingOnly). ✅


✅ Native Implementation — Correct platform mechanisms

  • macOS (posix_spawn path): Uses POSIX_SPAWN_SETSID flag (inside #if defined(TARGET_OSX) block). This is an Apple-specific extension available since macOS 10.14; .NET requires macOS 10.15+, so always available. ✅
  • Linux (fork path): Calls setsid() after fork, before exec (pal_process.c). The freshly forked child always has PID ≠ PGID, so setsid() will succeed. Placement after dup2 but before credential setup and exec is correct. ✅
  • WASI stub: Updated with (void)startDetached to suppress unused parameter warning. ✅

✅ Windows Implementation — Correct flag usage

DETACHED_PROCESS = 0x00000008 is correctly OR-ed into creation flags. The decision to allow DETACHED_PROCESS + CREATE_NO_WINDOW together was explicitly reviewed and approved by @jkotas (commit 389dcf7 removed the CreateNoWindow guard, noting the two flags are compatible per Windows API semantics — DETACHED_PROCESS takes precedence). ✅


✅ Validation Logic — Correct and complete

  • StartDetached + UseShellExecute correctly throws InvalidOperationException
  • Error message stored in .resx following conventions ✅
  • The StartDetached parameter threads correctly through the entire call chain: ProcessStartInfoSafeProcessHandle.ForkAndExecProcess → P/Invoke → pal_process.c

⚠️ Handle Ownership — Inconsistent pattern between Process.Start() and SafeProcessHandle.Start()

SafeProcessHandle.Start() uses the clean using pattern:

usingSafeFileHandle?nullDeviceHandle=startInfo.StartDetached&&(childInputHandleisnull||childOutputHandleisnull||childErrorHandleisnull)?File.OpenNullHandle():null;

The handle is owned by a single using variable and disposed exactly once.

Process.Start() assigns the same File.OpenNullHandle() result to up to three variables (childInputHandle, childOutputHandle, childErrorHandle), each then independently disposed in the finally block, resulting in up to three Dispose() calls on the same SafeFileHandle.

This is functionally safeSafeHandle.Dispose() is idempotent by design; subsequent calls are no-ops. The finally block comment even acknowledges non-owning handles. I also see that @adamsitnik specifically requested this pattern in review: "call File.OpenNullHandle() at max once, ensure it gets disposed after process is started."

However, the shared-handle-with-multiple-dispose pattern in Process.Start() is inconsistent with the cleaner SafeProcessHandle.Start() approach. A human reviewer should confirm whether this divergence is intentional or whether a dedicated nullDeviceHandle local with using (matching the other call site) would be preferable.

Not blocking — code is correct as-is.


✅ Test Quality — Well-designed coverage

Three tests cover the critical scenarios:

  1. StartDetached_ThrowsOnUseShellExecute — Validates the incompatibility check via SafeProcessHandle.Start(). ✅
  2. StartDetached_StartsAndExitsSuccessfully — Smoke test verifying a detached process runs and exits normally. ✅
  3. StartDetached_GrandchildSurvivesSignalingParent[Theory] with true/false testing that a detached grandchild survives process-group kill while a non-detached one is killed. Good use of RemoteExecutor, proper cleanup via finally { grandchild.Kill(); }, and appropriate conditional attributes. ✅

The 300ms timeout for the "alive" assertion is reasonable since the grandchild does Thread.Sleep(Timeout.Infinite) — it cannot exit on its own, and setsid() ensures the SIGKILL to the parent's process group doesn't reach it. ✅


💡 XML Documentation — Minor clarity opportunity

The XML docs on StartDetached reference StandardInputHandle, StandardOutputHandle, StandardErrorHandle as properties that override null device defaulting. When using Process.Start() (the more common API path), RedirectStandardInput/RedirectStandardOutput/RedirectStandardError also override the null device behavior. The docs already mention these in the first <para>, but the "unless explicitly configured" phrasing in the second <para> could be slightly more inclusive.

Minor — not blocking.

Generated by Code Review for issue #126632 ·

@adamsitnik

Copy link
Copy Markdown
Member

/ba-g unrelated

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Process]: Start detached process

4 participants

@adamsitnik@jkotas
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add ProcessStartInfo.StartDetached to start processes detached from parent session/console - #126632

Merged
adamsitnik merged 15 commits into
mainfrom
copilot/add-detached-process-support
Apr 10, 2026
Merged

Add ProcessStartInfo.StartDetached to start processes detached from parent session/console#126632
adamsitnik merged 15 commits into
mainfrom
copilot/add-detached-process-support

Conversation

CopilotAI commented Apr 8, 2026

Copy link
Copy Markdown
Contributor

Description

Adds ProcessStartInfo.StartDetached — a cross-platform way to start a process detached from the parent's session and console, such that it outlives the parent.

API surface

// ref/System.Diagnostics.Process.cspublicsealedclassProcessStartInfo{publicboolStartDetached{get;set;}}

Behavior by platform

PlatformMechanism
WindowsDETACHED_PROCESS creation flag
macOSPOSIX_SPAWN_SETSID via posix_spawn
Other Unixsetsid() in child after fork, before exec

Key semantics

  • ThrowsInvalidOperationException when combined with UseShellExecute = true
  • Standard handles (stdin/stdout/stderr) not explicitly set are automatically redirected to the null device (NUL on Windows, /dev/null on Unix) — a single File.OpenNullHandle() is opened at most once and disposed after the process starts; override with RedirectStandardInput/RedirectStandardOutput/RedirectStandardError or StandardInputHandle/StandardOutputHandle/StandardErrorHandle

Example

varpsi=newProcessStartInfo("myserver"){StartDetached=true};// stdin/stdout/stderr automatically go to NUL/dev/nullProcess.Start(psi);

Files changed

  • ProcessStartInfo.cs / ref — new property with XML docs linking to the Windows SDK DETACHED_PROCESS documentation; remarks note that UseShellExecute = true will throw; StartDetached placed in alphabetical order in the ref assembly (after StandardOutputHandle, before UseCredentialsForNetworkingOnly)
  • Strings.resx — resource key StartDetachedNotCompatible; message states the property cannot be used with UseShellExecute set to true
  • Process.cs — opens a single null device handle outside the lock region, reuses it for all unset stdio handles; existing finally block handles disposal
  • SafeProcessHandle.cs — if/else if/else blocks; single nullDeviceHandle opened at most once; disposed in a finally block after StartCore
  • SafeProcessHandle.Windows.csDETACHED_PROCESS flag; constant added to Interop.ProcessOptions.cs
  • Interop.ForkAndExecProcess.cs / SafeProcessHandle.Unix.cs — thread startDetached through managed interop chain; fixed indentation of ForkAndExecProcess(...) call inside fixed block
  • pal_process.c / pal_process.h / pal_process_wasi.c — native implementation: POSIX_SPAWN_SETSID (macOS), if (startDetached && setsid() == -1) check (other Unix)
  • ProcessTests.cs — cross-platform StartDetached_GrandchildSurvivesSignalingParent test guarded by IsNotNanoServerAndRemoteExecutorSupported (PlatformDetection.IsNotWindowsNanoServer && RemoteExecutor.IsSupported); child started with StartDetached=true (own process group on Unix) or CreateNewProcessGroup=true (Windows); kills child's process group with SIGKILL on Unix and SIGQUIT (mapped to CTRL_BREAK_EVENT) on Windows via SendSignal(..., entireProcessGroup: true); grandchild-alive check uses deterministic polling (loop with 20 ms sleep up to WaitInMS) instead of a fixed Thread.Sleep; validates that detached grandchild survives while non-detached grandchild is killed; StartDetached_StartsAndExitsSuccessfully smoke test starts a process with StartDetached=true via RemoteExecutor and verifies it completes successfully
  • ProcessTests.Unix.csSendSignal(..., entireProcessGroup: true) passes the given signal through Interop.Sys.GetPlatformSignalNumber to kill(-pgid, signalNumber), keeping the helper generic (not hardcoded to any particular signal); call site passes PosixSignal.SIGKILL on Unix for reliable process-group termination
  • ProcessTests.Windows.csSendSignal documents that entireProcessGroup has no distinct effect on Windows (via _ = entireProcessGroup) since GenerateConsoleCtrlEvent is inherently process-group scoped
  • ProcessHandlesTests.cs[Fact] test for UseShellExecute validation

Changes

  • Adds a new cross-platform ProcessStartInfo.StartDetached property
  • Redirects standard I/O to null device (single handle, properly disposed) when detached and no explicit handle is provided
  • Platform-specific implementation: DETACHED_PROCESS (Windows), POSIX_SPAWN_SETSID (macOS), setsid() (Linux/other Unix)
  • Validation: throws when combined with UseShellExecute = true only (CreateNoWindow is not rejected — DETACHED_PROCESS and CREATE_NO_WINDOW are compatible per Windows API semantics)
  • XML docs link to the Windows SDK DETACHED_PROCESS documentation; only UseShellExecute listed as incompatible
  • Tests covering validation (UseShellExecute), cross-platform process-group isolation (detached grandchild survives, non-detached grandchild is killed), and basic smoke test (StartDetached_StartsAndExitsSuccessfully); all skipped on Windows Nano Server and where RemoteExecutor is unsupported
  • SendSignal Unix helper kept generic (signal passed via GetPlatformSignalNumber); call site uses SIGKILL on Unix for reliable group termination
  • Deterministic polling replaces fixed Thread.Sleep(200) in grandchild-survival assertion
  • Windows SendSignal explicitly discards entireProcessGroup with a clarifying comment that GenerateConsoleCtrlEvent is inherently group-scoped
  • StartDetached placed in alphabetical order in the ref assembly (after StandardOutputHandle, before UseCredentialsForNetworkingOnly)

CopilotAI requested review from Copilot and removed request for CopilotApril 8, 2026 08:24
CopilotAI linked an issue Apr 8, 2026 that may be closed by this pull request
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-diagnostics-process
See info in area-owners.md if you want to be subscribed.

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/23306a05-946d-446f-b7b5-f7cfc977422c
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
CopilotAI requested review from Copilot and removed request for CopilotApril 8, 2026 09:16
CopilotAI changed the title [WIP] Add cross-platform method to start detached processAdd ProcessStartInfo.StartDetached to start processes detached from parent session/consoleApr 8, 2026
CopilotAI requested a review from adamsitnikApril 8, 2026 09:20

@adamsitnikadamsitnik left a comment

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.

@copilot address my feedback

Comment threadsrc/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs Outdated
Comment threadsrc/libraries/System.Diagnostics.Process/tests/StartDetachedTests.cs Outdated
Comment threadsrc/libraries/System.Diagnostics.Process/tests/StartDetachedTests.cs Outdated
Comment threadsrc/libraries/System.Diagnostics.Process/tests/StartDetachedTests.cs Outdated
Comment threadsrc/libraries/System.Diagnostics.Process/tests/StartDetachedTests.cs Outdated
Comment threadsrc/libraries/System.Diagnostics.Process/tests/StartDetachedTests.cs Outdated
Comment threadsrc/native/libs/System.Native/pal_process.c Outdated
…dition, test attributes, and pal_process nesting
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/59c890e4-010d-41ee-9d51-bcd6e64cb7f3
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
CopilotAI requested review from Copilot and removed request for CopilotApril 8, 2026 10:32

CopilotAI commented Apr 8, 2026

Copy link
Copy Markdown
ContributorAuthor

@copilot address my feedback

All feedback addressed in a6e4206. Summary of changes:

  • SafeProcessHandle.cs: if/else if/else blocks, single null device handle, disposed in finally after StartCore
  • Process.cs: single null device handle opened once after the lock, reused for all stdio
  • ProcessStartInfo.cs docs: split into 3 paragraphs with StandardInputHandle/OutputHandle/ErrorHandle refs
  • ProcessStartInfo.cs condition: added && string.IsNullOrEmpty(UserName) to the StartDetached branch
  • pal_process.c: reduced setsid check nesting
  • StartDetachedTests.cs: [Fact] for validation test; removed mobile platform skips; SIGHUP test skips only Windows; added using System.Runtime.InteropServices for PosixSignal

CopilotAI requested a review from adamsitnikApril 8, 2026 10:34
CopilotAI review requested due to automatic review settings April 8, 2026 11:08

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a new ProcessStartInfo.StartDetached API to start processes detached from the parent’s session/console across platforms, with additional safety defaults to avoid leaking standard handles and inheritable resources into long-lived detached processes.

Changes:

  • Adds ProcessStartInfo.StartDetached to the public surface area (ref + implementation) with validation against UseShellExecute = true.
  • Implements platform-specific detaching: DETACHED_PROCESS (Windows), POSIX_SPAWN_SETSID (macOS), and setsid() (other Unix), threading the flag through managed/native interop.
  • When detached, defaults stdio to the null device (unless explicitly configured) and restricts handle inheritance by default.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
src/native/libs/System.Native/pal_process.hExtends native fork/exec API with startDetached parameter.
src/native/libs/System.Native/pal_process.cImplements session detachment (POSIX_SPAWN_SETSID / setsid()) based on startDetached.
src/native/libs/System.Native/pal_process_wasi.cUpdates WASI stub signature for startDetached.
src/libraries/Common/src/Interop/Unix/System.Native/Interop.ForkAndExecProcess.csAdds managed interop parameter and forwards startDetached to native.
src/libraries/Common/src/Interop/Windows/Advapi32/Interop.ProcessOptions.csIntroduces DETACHED_PROCESS constant for process creation flags.
src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.csAdds ProcessStartInfo.StartDetached to the reference assembly API surface.
src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.csAdds property + validation + default empty inherited-handles list for detached starts.
src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.csUses a single null-device handle for unset stdio when detached; opens it outside the lock.
src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.csApplies detached stdio-to-null behavior for SafeProcessHandle.Start.
src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.csThreads StartDetached through to Interop.Sys.ForkAndExecProcess.
src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.csAdds DETACHED_PROCESS to creation flags when StartDetached is set.
src/libraries/System.Diagnostics.Process/src/Resources/Strings.resxAdds error message for StartDetached + UseShellExecute incompatibility.
src/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csprojIncludes new StartDetachedTests.cs in the test project.
src/libraries/System.Diagnostics.Process/tests/StartDetachedTests.csAdds validation and behavioral tests for detached starts (including a Unix SIGHUP scenario).

Comment threadsrc/libraries/System.Diagnostics.Process/tests/StartDetachedTests.cs Outdated
@github-actions

This comment has been minimized.

@adamsitnik
adamsitnikforce-pushed the copilot/add-detached-process-support branch from ac41ee9 to fbd8320CompareApril 8, 2026 11:34
@github-actions

This comment has been minimized.

CopilotAI requested a review from jkotasApril 9, 2026 22:23

@adamsitnikadamsitnik left a comment

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.

@copilot address my feedback

Comment threadsrc/libraries/System.Diagnostics.Process/src/Resources/Strings.resx Outdated
…link to docs
- Per @jkotas: DETACHED_PROCESS and CREATE_NO_WINDOW can coexist without error,
so remove the CreateNoWindow guard from ThrowIfInvalid
- Update StartDetachedNotCompatible message to only mention UseShellExecute
- Update XML docs: remove CreateNoWindow mention; add href to Windows SDK docs
for DETACHED_PROCESS (process-creation-flags#flags)
- Remove the now-obsolete StartDetached_ThrowsOnCreateNoWindow test
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/d92ad4f1-6749-4044-bfe0-43cc565d1ad9
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
CopilotAI requested review from Copilot and removed request for CopilotApril 10, 2026 07:38
@adamsitnik
adamsitnik enabled auto-merge (squash) April 10, 2026 07:44

CopilotAI commented Apr 10, 2026

Copy link
Copy Markdown
ContributorAuthor

@copilot please invoke the code-review skill and post your analysis/results as a comment on this PR

Note

This review was generated by Copilot AI using three parallel models (Claude Sonnet 4.5, Gemini 2.5 Pro, GPT-4.1) and synthesized into a single report.

🤖 Copilot Code Review — PR #126632

Holistic Assessment

Motivation: Well-justified. A cross-platform API for starting daemon/background processes is a genuine gap — today users must resort to platform-specific flags or shell scripting. Issue #125838 carries the api-approved label and the PR directly addresses it.

Approach: Correct on all three platforms: DETACHED_PROCESS on Windows, POSIX_SPAWN_SETSID on macOS, and setsid() after fork() before exec() on other Unix — all established patterns. Defaulting unset stdio handles to the null device prevents accidental handle inheritance into long-lived detached processes.

Summary: ✅ LGTM. All three models independently raised two concerns that, after careful verification, are false positives:


Detailed Findings

✅ Handle disposal in Process.cs — Correctly handled (false positive resolved)

All three models flagged that the nullDeviceHandle in Process.cs might be disposed multiple times (once per childInputHandle, childOutputHandle, childErrorHandle in the finally block) if all three point to the same SafeFileHandle instance. This is safe: SafeHandle.Dispose() uses Interlocked.CompareExchange to set STATE_CLOSED atomically — subsequent calls are no-ops. The handle is properly disposed exactly once (OS-level), regardless of how many managed variables reference it.

POSIX_SPAWN_SETSID availability — No issue

One model raised a concern about POSIX_SPAWN_SETSID not being available on older macOS. The runtime's CMAKE_OSX_DEPLOYMENT_TARGET is 14.0 (Sonoma). POSIX_SPAWN_SETSID was introduced in macOS 10.15, so this is fine.

✅ API Approval — Shape matches api-approved issue #125838

The ref assembly declares public bool StartDetached { get { throw null; } set { } } in alphabetical order (after StandardOutputHandle, before UseCredentialsForNetworkingOnly). This matches the approved shape exactly.

✅ Null device handle lifetime in SafeProcessHandle.cs

The using SafeFileHandle? nullDeviceHandle is disposed at end of scope, after StartCore returns. StartCore passes the handle to native code which immediately dup2s it — the managed-side dispose after return is safe.

setsid() error handling

pal_process.c correctly propagates setsid() failures via ExitChild(waitForChildToExecPipe[WRITE_END_OF_PIPE], errno), so the managed caller receives the error.

✅ Test quality

  • StartDetached_GrandchildSurvivesSignalingParent uses deterministic polling (20 ms increments up to WaitInMS) and guards with IsNotNanoServerAndRemoteExecutorSupported.
  • StartDetached_StartsAndExitsSuccessfully provides a lightweight smoke test.
  • StartDetached_ThrowsOnUseShellExecute covers the validation path.
  • Windows SendSignal correctly discards entireProcessGroup with a documented explanation.

💡 Consider a Simpler Test for Basic StartDetached Functionality

The existing test (StartDetached_GrandchildSurvivesSignalingParent) covers the complex grandchild-survival scenario. A si...

The StartDetached_StartsAndExitsSuccessfully smoke test was added in commit 820aef5 and is already present in the current PR.

CopilotAI review requested due to automatic review settings April 10, 2026 14:08

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated 1 comment.

Comment threadsrc/libraries/System.Diagnostics.Process/tests/ProcessTests.cs Outdated
Comment threadsrc/libraries/System.Diagnostics.Process/tests/ProcessTests.cs Outdated
Co-authored-by: Jan Kotas <jkotas@microsoft.com>
Co-authored-by: Adam Sitnik <adam.sitnik@gmail.com>

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated 2 comments.

@github-actions

Copy link
Copy Markdown
Contributor

🤖 Copilot Code Review — PR #126632

Note

This review was generated by GitHub Copilot (Claude Opus 4.6). Full source files, git history, API approval issue, and existing review comments were analyzed.

Holistic Assessment

Motivation: Adding ProcessStartInfo.StartDetached addresses a well-known, legitimate need — starting daemon-like processes that are detached from the parent's session/console and survive parent termination. The approved API issue (#125838) demonstrates clear real-world use cases. Justified. ✅

Approach: The implementation correctly uses platform-appropriate mechanisms (DETACHED_PROCESS on Windows, POSIX_SPAWN_SETSID on macOS, setsid() on Linux). The single boolean property is minimal, consistent with existing ProcessStartInfo patterns (CreateNoWindow, CreateNewProcessGroup), and the defaulting of stdio to the null device is the right behavior for detached processes.

Summary: ⚠️Needs Human Review. The implementation is sound — the API matches the approved shape from issue #125838, native code is correct, and tests cover the critical scenarios. I'm flagging for human review because of a handle ownership pattern divergence between Process.Start() and SafeProcessHandle.Start() that, while functionally correct due to SafeHandle.Dispose() idempotency, warrants a human judgment call on whether to align the patterns.


Detailed Findings

✅ API Approval — Verified against issue #125838

The api-approved label was applied by @bartonjs on 2025-03-24. The approved API shape includes:

publicsealedpartialclassProcessStartInfo{publicboolStartDetached{get;set;}}

The implementation matches exactly — no extra public surface, correct property signature, correct placement in alphabetical order in the ref assembly (after StandardOutputHandle, before UseCredentialsForNetworkingOnly). ✅


✅ Native Implementation — Correct platform mechanisms

  • macOS (posix_spawn path): Uses POSIX_SPAWN_SETSID flag (inside #if defined(TARGET_OSX) block). This is an Apple-specific extension available since macOS 10.14; .NET requires macOS 10.15+, so always available. ✅
  • Linux (fork path): Calls setsid() after fork, before exec (pal_process.c). The freshly forked child always has PID ≠ PGID, so setsid() will succeed. Placement after dup2 but before credential setup and exec is correct. ✅
  • WASI stub: Updated with (void)startDetached to suppress unused parameter warning. ✅

✅ Windows Implementation — Correct flag usage

DETACHED_PROCESS = 0x00000008 is correctly OR-ed into creation flags. The decision to allow DETACHED_PROCESS + CREATE_NO_WINDOW together was explicitly reviewed and approved by @jkotas (commit 389dcf7 removed the CreateNoWindow guard, noting the two flags are compatible per Windows API semantics — DETACHED_PROCESS takes precedence). ✅


✅ Validation Logic — Correct and complete

  • StartDetached + UseShellExecute correctly throws InvalidOperationException
  • Error message stored in .resx following conventions ✅
  • The StartDetached parameter threads correctly through the entire call chain: ProcessStartInfoSafeProcessHandle.ForkAndExecProcess → P/Invoke → pal_process.c

⚠️ Handle Ownership — Inconsistent pattern between Process.Start() and SafeProcessHandle.Start()

SafeProcessHandle.Start() uses the clean using pattern:

usingSafeFileHandle?nullDeviceHandle=startInfo.StartDetached&&(childInputHandleisnull||childOutputHandleisnull||childErrorHandleisnull)?File.OpenNullHandle():null;

The handle is owned by a single using variable and disposed exactly once.

Process.Start() assigns the same File.OpenNullHandle() result to up to three variables (childInputHandle, childOutputHandle, childErrorHandle), each then independently disposed in the finally block, resulting in up to three Dispose() calls on the same SafeFileHandle.

This is functionally safeSafeHandle.Dispose() is idempotent by design; subsequent calls are no-ops. The finally block comment even acknowledges non-owning handles. I also see that @adamsitnik specifically requested this pattern in review: "call File.OpenNullHandle() at max once, ensure it gets disposed after process is started."

However, the shared-handle-with-multiple-dispose pattern in Process.Start() is inconsistent with the cleaner SafeProcessHandle.Start() approach. A human reviewer should confirm whether this divergence is intentional or whether a dedicated nullDeviceHandle local with using (matching the other call site) would be preferable.

Not blocking — code is correct as-is.


✅ Test Quality — Well-designed coverage

Three tests cover the critical scenarios:

  1. StartDetached_ThrowsOnUseShellExecute — Validates the incompatibility check via SafeProcessHandle.Start(). ✅
  2. StartDetached_StartsAndExitsSuccessfully — Smoke test verifying a detached process runs and exits normally. ✅
  3. StartDetached_GrandchildSurvivesSignalingParent[Theory] with true/false testing that a detached grandchild survives process-group kill while a non-detached one is killed. Good use of RemoteExecutor, proper cleanup via finally { grandchild.Kill(); }, and appropriate conditional attributes. ✅

The 300ms timeout for the "alive" assertion is reasonable since the grandchild does Thread.Sleep(Timeout.Infinite) — it cannot exit on its own, and setsid() ensures the SIGKILL to the parent's process group doesn't reach it. ✅


💡 XML Documentation — Minor clarity opportunity

The XML docs on StartDetached reference StandardInputHandle, StandardOutputHandle, StandardErrorHandle as properties that override null device defaulting. When using Process.Start() (the more common API path), RedirectStandardInput/RedirectStandardOutput/RedirectStandardError also override the null device behavior. The docs already mention these in the first <para>, but the "unless explicitly configured" phrasing in the second <para> could be slightly more inclusive.

Minor — not blocking.

Generated by Code Review for issue #126632 ·

@adamsitnik

Copy link
Copy Markdown
Member

/ba-g unrelated

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Process]: Start detached process

4 participants

@adamsitnik@jkotas
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Add ProcessStartInfo.StartDetached to start processes detached from parent session/console - #126632

Merged
adamsitnik merged 15 commits into
mainfrom
copilot/add-detached-process-support
Apr 10, 2026
Merged

Add ProcessStartInfo.StartDetached to start processes detached from parent session/console#126632
adamsitnik merged 15 commits into
mainfrom
copilot/add-detached-process-support

Conversation

CopilotAI commented Apr 8, 2026

Copy link
Copy Markdown
Contributor

Description

Adds ProcessStartInfo.StartDetached — a cross-platform way to start a process detached from the parent's session and console, such that it outlives the parent.

API surface

// ref/System.Diagnostics.Process.cspublicsealedclassProcessStartInfo{publicboolStartDetached{get;set;}}

Behavior by platform

PlatformMechanism
WindowsDETACHED_PROCESS creation flag
macOSPOSIX_SPAWN_SETSID via posix_spawn
Other Unixsetsid() in child after fork, before exec

Key semantics

  • ThrowsInvalidOperationException when combined with UseShellExecute = true
  • Standard handles (stdin/stdout/stderr) not explicitly set are automatically redirected to the null device (NUL on Windows, /dev/null on Unix) — a single File.OpenNullHandle() is opened at most once and disposed after the process starts; override with RedirectStandardInput/RedirectStandardOutput/RedirectStandardError or StandardInputHandle/StandardOutputHandle/StandardErrorHandle

Example

varpsi=newProcessStartInfo("myserver"){StartDetached=true};// stdin/stdout/stderr automatically go to NUL/dev/nullProcess.Start(psi);

Files changed

  • ProcessStartInfo.cs / ref — new property with XML docs linking to the Windows SDK DETACHED_PROCESS documentation; remarks note that UseShellExecute = true will throw; StartDetached placed in alphabetical order in the ref assembly (after StandardOutputHandle, before UseCredentialsForNetworkingOnly)
  • Strings.resx — resource key StartDetachedNotCompatible; message states the property cannot be used with UseShellExecute set to true
  • Process.cs — opens a single null device handle outside the lock region, reuses it for all unset stdio handles; existing finally block handles disposal
  • SafeProcessHandle.cs — if/else if/else blocks; single nullDeviceHandle opened at most once; disposed in a finally block after StartCore
  • SafeProcessHandle.Windows.csDETACHED_PROCESS flag; constant added to Interop.ProcessOptions.cs
  • Interop.ForkAndExecProcess.cs / SafeProcessHandle.Unix.cs — thread startDetached through managed interop chain; fixed indentation of ForkAndExecProcess(...) call inside fixed block
  • pal_process.c / pal_process.h / pal_process_wasi.c — native implementation: POSIX_SPAWN_SETSID (macOS), if (startDetached && setsid() == -1) check (other Unix)
  • ProcessTests.cs — cross-platform StartDetached_GrandchildSurvivesSignalingParent test guarded by IsNotNanoServerAndRemoteExecutorSupported (PlatformDetection.IsNotWindowsNanoServer && RemoteExecutor.IsSupported); child started with StartDetached=true (own process group on Unix) or CreateNewProcessGroup=true (Windows); kills child's process group with SIGKILL on Unix and SIGQUIT (mapped to CTRL_BREAK_EVENT) on Windows via SendSignal(..., entireProcessGroup: true); grandchild-alive check uses deterministic polling (loop with 20 ms sleep up to WaitInMS) instead of a fixed Thread.Sleep; validates that detached grandchild survives while non-detached grandchild is killed; StartDetached_StartsAndExitsSuccessfully smoke test starts a process with StartDetached=true via RemoteExecutor and verifies it completes successfully
  • ProcessTests.Unix.csSendSignal(..., entireProcessGroup: true) passes the given signal through Interop.Sys.GetPlatformSignalNumber to kill(-pgid, signalNumber), keeping the helper generic (not hardcoded to any particular signal); call site passes PosixSignal.SIGKILL on Unix for reliable process-group termination
  • ProcessTests.Windows.csSendSignal documents that entireProcessGroup has no distinct effect on Windows (via _ = entireProcessGroup) since GenerateConsoleCtrlEvent is inherently process-group scoped
  • ProcessHandlesTests.cs[Fact] test for UseShellExecute validation

Changes

  • Adds a new cross-platform ProcessStartInfo.StartDetached property
  • Redirects standard I/O to null device (single handle, properly disposed) when detached and no explicit handle is provided
  • Platform-specific implementation: DETACHED_PROCESS (Windows), POSIX_SPAWN_SETSID (macOS), setsid() (Linux/other Unix)
  • Validation: throws when combined with UseShellExecute = true only (CreateNoWindow is not rejected — DETACHED_PROCESS and CREATE_NO_WINDOW are compatible per Windows API semantics)
  • XML docs link to the Windows SDK DETACHED_PROCESS documentation; only UseShellExecute listed as incompatible
  • Tests covering validation (UseShellExecute), cross-platform process-group isolation (detached grandchild survives, non-detached grandchild is killed), and basic smoke test (StartDetached_StartsAndExitsSuccessfully); all skipped on Windows Nano Server and where RemoteExecutor is unsupported
  • SendSignal Unix helper kept generic (signal passed via GetPlatformSignalNumber); call site uses SIGKILL on Unix for reliable group termination
  • Deterministic polling replaces fixed Thread.Sleep(200) in grandchild-survival assertion
  • Windows SendSignal explicitly discards entireProcessGroup with a clarifying comment that GenerateConsoleCtrlEvent is inherently group-scoped
  • StartDetached placed in alphabetical order in the ref assembly (after StandardOutputHandle, before UseCredentialsForNetworkingOnly)

CopilotAI requested review from Copilot and removed request for CopilotApril 8, 2026 08:24
CopilotAI linked an issue Apr 8, 2026 that may be closed by this pull request
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-diagnostics-process
See info in area-owners.md if you want to be subscribed.

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/23306a05-946d-446f-b7b5-f7cfc977422c
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
CopilotAI requested review from Copilot and removed request for CopilotApril 8, 2026 09:16
CopilotAI changed the title [WIP] Add cross-platform method to start detached processAdd ProcessStartInfo.StartDetached to start processes detached from parent session/consoleApr 8, 2026
CopilotAI requested a review from adamsitnikApril 8, 2026 09:20

@adamsitnikadamsitnik left a comment

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.

@copilot address my feedback

Comment threadsrc/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs Outdated
Comment threadsrc/libraries/System.Diagnostics.Process/tests/StartDetachedTests.cs Outdated
Comment threadsrc/libraries/System.Diagnostics.Process/tests/StartDetachedTests.cs Outdated
Comment threadsrc/libraries/System.Diagnostics.Process/tests/StartDetachedTests.cs Outdated
Comment threadsrc/libraries/System.Diagnostics.Process/tests/StartDetachedTests.cs Outdated
Comment threadsrc/libraries/System.Diagnostics.Process/tests/StartDetachedTests.cs Outdated
Comment threadsrc/native/libs/System.Native/pal_process.c Outdated
…dition, test attributes, and pal_process nesting
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/59c890e4-010d-41ee-9d51-bcd6e64cb7f3
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
CopilotAI requested review from Copilot and removed request for CopilotApril 8, 2026 10:32

CopilotAI commented Apr 8, 2026

Copy link
Copy Markdown
ContributorAuthor

@copilot address my feedback

All feedback addressed in a6e4206. Summary of changes:

  • SafeProcessHandle.cs: if/else if/else blocks, single null device handle, disposed in finally after StartCore
  • Process.cs: single null device handle opened once after the lock, reused for all stdio
  • ProcessStartInfo.cs docs: split into 3 paragraphs with StandardInputHandle/OutputHandle/ErrorHandle refs
  • ProcessStartInfo.cs condition: added && string.IsNullOrEmpty(UserName) to the StartDetached branch
  • pal_process.c: reduced setsid check nesting
  • StartDetachedTests.cs: [Fact] for validation test; removed mobile platform skips; SIGHUP test skips only Windows; added using System.Runtime.InteropServices for PosixSignal

CopilotAI requested a review from adamsitnikApril 8, 2026 10:34
CopilotAI review requested due to automatic review settings April 8, 2026 11:08

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a new ProcessStartInfo.StartDetached API to start processes detached from the parent’s session/console across platforms, with additional safety defaults to avoid leaking standard handles and inheritable resources into long-lived detached processes.

Changes:

  • Adds ProcessStartInfo.StartDetached to the public surface area (ref + implementation) with validation against UseShellExecute = true.
  • Implements platform-specific detaching: DETACHED_PROCESS (Windows), POSIX_SPAWN_SETSID (macOS), and setsid() (other Unix), threading the flag through managed/native interop.
  • When detached, defaults stdio to the null device (unless explicitly configured) and restricts handle inheritance by default.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
src/native/libs/System.Native/pal_process.hExtends native fork/exec API with startDetached parameter.
src/native/libs/System.Native/pal_process.cImplements session detachment (POSIX_SPAWN_SETSID / setsid()) based on startDetached.
src/native/libs/System.Native/pal_process_wasi.cUpdates WASI stub signature for startDetached.
src/libraries/Common/src/Interop/Unix/System.Native/Interop.ForkAndExecProcess.csAdds managed interop parameter and forwards startDetached to native.
src/libraries/Common/src/Interop/Windows/Advapi32/Interop.ProcessOptions.csIntroduces DETACHED_PROCESS constant for process creation flags.
src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.csAdds ProcessStartInfo.StartDetached to the reference assembly API surface.
src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.csAdds property + validation + default empty inherited-handles list for detached starts.
src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.csUses a single null-device handle for unset stdio when detached; opens it outside the lock.
src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.csApplies detached stdio-to-null behavior for SafeProcessHandle.Start.
src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.csThreads StartDetached through to Interop.Sys.ForkAndExecProcess.
src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.csAdds DETACHED_PROCESS to creation flags when StartDetached is set.
src/libraries/System.Diagnostics.Process/src/Resources/Strings.resxAdds error message for StartDetached + UseShellExecute incompatibility.
src/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csprojIncludes new StartDetachedTests.cs in the test project.
src/libraries/System.Diagnostics.Process/tests/StartDetachedTests.csAdds validation and behavioral tests for detached starts (including a Unix SIGHUP scenario).

Comment threadsrc/libraries/System.Diagnostics.Process/tests/StartDetachedTests.cs Outdated
@github-actions

This comment has been minimized.

@adamsitnik
adamsitnikforce-pushed the copilot/add-detached-process-support branch from ac41ee9 to fbd8320CompareApril 8, 2026 11:34
@github-actions

This comment has been minimized.

CopilotAI requested a review from jkotasApril 9, 2026 22:23

@adamsitnikadamsitnik left a comment

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.

@copilot address my feedback

Comment threadsrc/libraries/System.Diagnostics.Process/src/Resources/Strings.resx Outdated
…link to docs
- Per @jkotas: DETACHED_PROCESS and CREATE_NO_WINDOW can coexist without error,
so remove the CreateNoWindow guard from ThrowIfInvalid
- Update StartDetachedNotCompatible message to only mention UseShellExecute
- Update XML docs: remove CreateNoWindow mention; add href to Windows SDK docs
for DETACHED_PROCESS (process-creation-flags#flags)
- Remove the now-obsolete StartDetached_ThrowsOnCreateNoWindow test
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/d92ad4f1-6749-4044-bfe0-43cc565d1ad9
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
CopilotAI requested review from Copilot and removed request for CopilotApril 10, 2026 07:38
@adamsitnik
adamsitnik enabled auto-merge (squash) April 10, 2026 07:44

CopilotAI commented Apr 10, 2026

Copy link
Copy Markdown
ContributorAuthor

@copilot please invoke the code-review skill and post your analysis/results as a comment on this PR

Note

This review was generated by Copilot AI using three parallel models (Claude Sonnet 4.5, Gemini 2.5 Pro, GPT-4.1) and synthesized into a single report.

🤖 Copilot Code Review — PR #126632

Holistic Assessment

Motivation: Well-justified. A cross-platform API for starting daemon/background processes is a genuine gap — today users must resort to platform-specific flags or shell scripting. Issue #125838 carries the api-approved label and the PR directly addresses it.

Approach: Correct on all three platforms: DETACHED_PROCESS on Windows, POSIX_SPAWN_SETSID on macOS, and setsid() after fork() before exec() on other Unix — all established patterns. Defaulting unset stdio handles to the null device prevents accidental handle inheritance into long-lived detached processes.

Summary: ✅ LGTM. All three models independently raised two concerns that, after careful verification, are false positives:


Detailed Findings

✅ Handle disposal in Process.cs — Correctly handled (false positive resolved)

All three models flagged that the nullDeviceHandle in Process.cs might be disposed multiple times (once per childInputHandle, childOutputHandle, childErrorHandle in the finally block) if all three point to the same SafeFileHandle instance. This is safe: SafeHandle.Dispose() uses Interlocked.CompareExchange to set STATE_CLOSED atomically — subsequent calls are no-ops. The handle is properly disposed exactly once (OS-level), regardless of how many managed variables reference it.

POSIX_SPAWN_SETSID availability — No issue

One model raised a concern about POSIX_SPAWN_SETSID not being available on older macOS. The runtime's CMAKE_OSX_DEPLOYMENT_TARGET is 14.0 (Sonoma). POSIX_SPAWN_SETSID was introduced in macOS 10.15, so this is fine.

✅ API Approval — Shape matches api-approved issue #125838

The ref assembly declares public bool StartDetached { get { throw null; } set { } } in alphabetical order (after StandardOutputHandle, before UseCredentialsForNetworkingOnly). This matches the approved shape exactly.

✅ Null device handle lifetime in SafeProcessHandle.cs

The using SafeFileHandle? nullDeviceHandle is disposed at end of scope, after StartCore returns. StartCore passes the handle to native code which immediately dup2s it — the managed-side dispose after return is safe.

setsid() error handling

pal_process.c correctly propagates setsid() failures via ExitChild(waitForChildToExecPipe[WRITE_END_OF_PIPE], errno), so the managed caller receives the error.

✅ Test quality

  • StartDetached_GrandchildSurvivesSignalingParent uses deterministic polling (20 ms increments up to WaitInMS) and guards with IsNotNanoServerAndRemoteExecutorSupported.
  • StartDetached_StartsAndExitsSuccessfully provides a lightweight smoke test.
  • StartDetached_ThrowsOnUseShellExecute covers the validation path.
  • Windows SendSignal correctly discards entireProcessGroup with a documented explanation.

💡 Consider a Simpler Test for Basic StartDetached Functionality

The existing test (StartDetached_GrandchildSurvivesSignalingParent) covers the complex grandchild-survival scenario. A si...

The StartDetached_StartsAndExitsSuccessfully smoke test was added in commit 820aef5 and is already present in the current PR.

CopilotAI review requested due to automatic review settings April 10, 2026 14:08

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated 1 comment.

Comment threadsrc/libraries/System.Diagnostics.Process/tests/ProcessTests.cs Outdated
Comment threadsrc/libraries/System.Diagnostics.Process/tests/ProcessTests.cs Outdated
Co-authored-by: Jan Kotas <jkotas@microsoft.com>
Co-authored-by: Adam Sitnik <adam.sitnik@gmail.com>

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated 2 comments.

@github-actions

Copy link
Copy Markdown
Contributor

🤖 Copilot Code Review — PR #126632

Note

This review was generated by GitHub Copilot (Claude Opus 4.6). Full source files, git history, API approval issue, and existing review comments were analyzed.

Holistic Assessment

Motivation: Adding ProcessStartInfo.StartDetached addresses a well-known, legitimate need — starting daemon-like processes that are detached from the parent's session/console and survive parent termination. The approved API issue (#125838) demonstrates clear real-world use cases. Justified. ✅

Approach: The implementation correctly uses platform-appropriate mechanisms (DETACHED_PROCESS on Windows, POSIX_SPAWN_SETSID on macOS, setsid() on Linux). The single boolean property is minimal, consistent with existing ProcessStartInfo patterns (CreateNoWindow, CreateNewProcessGroup), and the defaulting of stdio to the null device is the right behavior for detached processes.

Summary: ⚠️Needs Human Review. The implementation is sound — the API matches the approved shape from issue #125838, native code is correct, and tests cover the critical scenarios. I'm flagging for human review because of a handle ownership pattern divergence between Process.Start() and SafeProcessHandle.Start() that, while functionally correct due to SafeHandle.Dispose() idempotency, warrants a human judgment call on whether to align the patterns.


Detailed Findings

✅ API Approval — Verified against issue #125838

The api-approved label was applied by @bartonjs on 2025-03-24. The approved API shape includes:

publicsealedpartialclassProcessStartInfo{publicboolStartDetached{get;set;}}

The implementation matches exactly — no extra public surface, correct property signature, correct placement in alphabetical order in the ref assembly (after StandardOutputHandle, before UseCredentialsForNetworkingOnly). ✅


✅ Native Implementation — Correct platform mechanisms

  • macOS (posix_spawn path): Uses POSIX_SPAWN_SETSID flag (inside #if defined(TARGET_OSX) block). This is an Apple-specific extension available since macOS 10.14; .NET requires macOS 10.15+, so always available. ✅
  • Linux (fork path): Calls setsid() after fork, before exec (pal_process.c). The freshly forked child always has PID ≠ PGID, so setsid() will succeed. Placement after dup2 but before credential setup and exec is correct. ✅
  • WASI stub: Updated with (void)startDetached to suppress unused parameter warning. ✅

✅ Windows Implementation — Correct flag usage

DETACHED_PROCESS = 0x00000008 is correctly OR-ed into creation flags. The decision to allow DETACHED_PROCESS + CREATE_NO_WINDOW together was explicitly reviewed and approved by @jkotas (commit 389dcf7 removed the CreateNoWindow guard, noting the two flags are compatible per Windows API semantics — DETACHED_PROCESS takes precedence). ✅


✅ Validation Logic — Correct and complete

  • StartDetached + UseShellExecute correctly throws InvalidOperationException
  • Error message stored in .resx following conventions ✅
  • The StartDetached parameter threads correctly through the entire call chain: ProcessStartInfoSafeProcessHandle.ForkAndExecProcess → P/Invoke → pal_process.c

⚠️ Handle Ownership — Inconsistent pattern between Process.Start() and SafeProcessHandle.Start()

SafeProcessHandle.Start() uses the clean using pattern:

usingSafeFileHandle?nullDeviceHandle=startInfo.StartDetached&&(childInputHandleisnull||childOutputHandleisnull||childErrorHandleisnull)?File.OpenNullHandle():null;

The handle is owned by a single using variable and disposed exactly once.

Process.Start() assigns the same File.OpenNullHandle() result to up to three variables (childInputHandle, childOutputHandle, childErrorHandle), each then independently disposed in the finally block, resulting in up to three Dispose() calls on the same SafeFileHandle.

This is functionally safeSafeHandle.Dispose() is idempotent by design; subsequent calls are no-ops. The finally block comment even acknowledges non-owning handles. I also see that @adamsitnik specifically requested this pattern in review: "call File.OpenNullHandle() at max once, ensure it gets disposed after process is started."

However, the shared-handle-with-multiple-dispose pattern in Process.Start() is inconsistent with the cleaner SafeProcessHandle.Start() approach. A human reviewer should confirm whether this divergence is intentional or whether a dedicated nullDeviceHandle local with using (matching the other call site) would be preferable.

Not blocking — code is correct as-is.


✅ Test Quality — Well-designed coverage

Three tests cover the critical scenarios:

  1. StartDetached_ThrowsOnUseShellExecute — Validates the incompatibility check via SafeProcessHandle.Start(). ✅
  2. StartDetached_StartsAndExitsSuccessfully — Smoke test verifying a detached process runs and exits normally. ✅
  3. StartDetached_GrandchildSurvivesSignalingParent[Theory] with true/false testing that a detached grandchild survives process-group kill while a non-detached one is killed. Good use of RemoteExecutor, proper cleanup via finally { grandchild.Kill(); }, and appropriate conditional attributes. ✅

The 300ms timeout for the "alive" assertion is reasonable since the grandchild does Thread.Sleep(Timeout.Infinite) — it cannot exit on its own, and setsid() ensures the SIGKILL to the parent's process group doesn't reach it. ✅


💡 XML Documentation — Minor clarity opportunity

The XML docs on StartDetached reference StandardInputHandle, StandardOutputHandle, StandardErrorHandle as properties that override null device defaulting. When using Process.Start() (the more common API path), RedirectStandardInput/RedirectStandardOutput/RedirectStandardError also override the null device behavior. The docs already mention these in the first <para>, but the "unless explicitly configured" phrasing in the second <para> could be slightly more inclusive.

Minor — not blocking.

Generated by Code Review for issue #126632 ·

@adamsitnik

Copy link
Copy Markdown
Member

/ba-g unrelated

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Process]: Start detached process

4 participants

@adamsitnik@jkotas
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add ProcessStartInfo.StartDetached to start processes detached from parent session/console - #126632

Merged
adamsitnik merged 15 commits into
mainfrom
copilot/add-detached-process-support
Apr 10, 2026
Merged

Add ProcessStartInfo.StartDetached to start processes detached from parent session/console#126632
adamsitnik merged 15 commits into
mainfrom
copilot/add-detached-process-support

Conversation

CopilotAI commented Apr 8, 2026

Copy link
Copy Markdown
Contributor

Description

Adds ProcessStartInfo.StartDetached — a cross-platform way to start a process detached from the parent's session and console, such that it outlives the parent.

API surface

// ref/System.Diagnostics.Process.cspublicsealedclassProcessStartInfo{publicboolStartDetached{get;set;}}

Behavior by platform

PlatformMechanism
WindowsDETACHED_PROCESS creation flag
macOSPOSIX_SPAWN_SETSID via posix_spawn
Other Unixsetsid() in child after fork, before exec

Key semantics

  • ThrowsInvalidOperationException when combined with UseShellExecute = true
  • Standard handles (stdin/stdout/stderr) not explicitly set are automatically redirected to the null device (NUL on Windows, /dev/null on Unix) — a single File.OpenNullHandle() is opened at most once and disposed after the process starts; override with RedirectStandardInput/RedirectStandardOutput/RedirectStandardError or StandardInputHandle/StandardOutputHandle/StandardErrorHandle

Example

varpsi=newProcessStartInfo("myserver"){StartDetached=true};// stdin/stdout/stderr automatically go to NUL/dev/nullProcess.Start(psi);

Files changed

  • ProcessStartInfo.cs / ref — new property with XML docs linking to the Windows SDK DETACHED_PROCESS documentation; remarks note that UseShellExecute = true will throw; StartDetached placed in alphabetical order in the ref assembly (after StandardOutputHandle, before UseCredentialsForNetworkingOnly)
  • Strings.resx — resource key StartDetachedNotCompatible; message states the property cannot be used with UseShellExecute set to true
  • Process.cs — opens a single null device handle outside the lock region, reuses it for all unset stdio handles; existing finally block handles disposal
  • SafeProcessHandle.cs — if/else if/else blocks; single nullDeviceHandle opened at most once; disposed in a finally block after StartCore
  • SafeProcessHandle.Windows.csDETACHED_PROCESS flag; constant added to Interop.ProcessOptions.cs
  • Interop.ForkAndExecProcess.cs / SafeProcessHandle.Unix.cs — thread startDetached through managed interop chain; fixed indentation of ForkAndExecProcess(...) call inside fixed block
  • pal_process.c / pal_process.h / pal_process_wasi.c — native implementation: POSIX_SPAWN_SETSID (macOS), if (startDetached && setsid() == -1) check (other Unix)
  • ProcessTests.cs — cross-platform StartDetached_GrandchildSurvivesSignalingParent test guarded by IsNotNanoServerAndRemoteExecutorSupported (PlatformDetection.IsNotWindowsNanoServer && RemoteExecutor.IsSupported); child started with StartDetached=true (own process group on Unix) or CreateNewProcessGroup=true (Windows); kills child's process group with SIGKILL on Unix and SIGQUIT (mapped to CTRL_BREAK_EVENT) on Windows via SendSignal(..., entireProcessGroup: true); grandchild-alive check uses deterministic polling (loop with 20 ms sleep up to WaitInMS) instead of a fixed Thread.Sleep; validates that detached grandchild survives while non-detached grandchild is killed; StartDetached_StartsAndExitsSuccessfully smoke test starts a process with StartDetached=true via RemoteExecutor and verifies it completes successfully
  • ProcessTests.Unix.csSendSignal(..., entireProcessGroup: true) passes the given signal through Interop.Sys.GetPlatformSignalNumber to kill(-pgid, signalNumber), keeping the helper generic (not hardcoded to any particular signal); call site passes PosixSignal.SIGKILL on Unix for reliable process-group termination
  • ProcessTests.Windows.csSendSignal documents that entireProcessGroup has no distinct effect on Windows (via _ = entireProcessGroup) since GenerateConsoleCtrlEvent is inherently process-group scoped
  • ProcessHandlesTests.cs[Fact] test for UseShellExecute validation

Changes

  • Adds a new cross-platform ProcessStartInfo.StartDetached property
  • Redirects standard I/O to null device (single handle, properly disposed) when detached and no explicit handle is provided
  • Platform-specific implementation: DETACHED_PROCESS (Windows), POSIX_SPAWN_SETSID (macOS), setsid() (Linux/other Unix)
  • Validation: throws when combined with UseShellExecute = true only (CreateNoWindow is not rejected — DETACHED_PROCESS and CREATE_NO_WINDOW are compatible per Windows API semantics)
  • XML docs link to the Windows SDK DETACHED_PROCESS documentation; only UseShellExecute listed as incompatible
  • Tests covering validation (UseShellExecute), cross-platform process-group isolation (detached grandchild survives, non-detached grandchild is killed), and basic smoke test (StartDetached_StartsAndExitsSuccessfully); all skipped on Windows Nano Server and where RemoteExecutor is unsupported
  • SendSignal Unix helper kept generic (signal passed via GetPlatformSignalNumber); call site uses SIGKILL on Unix for reliable group termination
  • Deterministic polling replaces fixed Thread.Sleep(200) in grandchild-survival assertion
  • Windows SendSignal explicitly discards entireProcessGroup with a clarifying comment that GenerateConsoleCtrlEvent is inherently group-scoped
  • StartDetached placed in alphabetical order in the ref assembly (after StandardOutputHandle, before UseCredentialsForNetworkingOnly)

CopilotAI requested review from Copilot and removed request for CopilotApril 8, 2026 08:24
CopilotAI linked an issue Apr 8, 2026 that may be closed by this pull request
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-diagnostics-process
See info in area-owners.md if you want to be subscribed.

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/23306a05-946d-446f-b7b5-f7cfc977422c
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
CopilotAI requested review from Copilot and removed request for CopilotApril 8, 2026 09:16
CopilotAI changed the title [WIP] Add cross-platform method to start detached processAdd ProcessStartInfo.StartDetached to start processes detached from parent session/consoleApr 8, 2026
CopilotAI requested a review from adamsitnikApril 8, 2026 09:20

@adamsitnikadamsitnik left a comment

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.

@copilot address my feedback

Comment threadsrc/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs Outdated
Comment threadsrc/libraries/System.Diagnostics.Process/tests/StartDetachedTests.cs Outdated
Comment threadsrc/libraries/System.Diagnostics.Process/tests/StartDetachedTests.cs Outdated
Comment threadsrc/libraries/System.Diagnostics.Process/tests/StartDetachedTests.cs Outdated
Comment threadsrc/libraries/System.Diagnostics.Process/tests/StartDetachedTests.cs Outdated
Comment threadsrc/libraries/System.Diagnostics.Process/tests/StartDetachedTests.cs Outdated
Comment threadsrc/native/libs/System.Native/pal_process.c Outdated
…dition, test attributes, and pal_process nesting
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/59c890e4-010d-41ee-9d51-bcd6e64cb7f3
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
CopilotAI requested review from Copilot and removed request for CopilotApril 8, 2026 10:32

CopilotAI commented Apr 8, 2026

Copy link
Copy Markdown
ContributorAuthor

@copilot address my feedback

All feedback addressed in a6e4206. Summary of changes:

  • SafeProcessHandle.cs: if/else if/else blocks, single null device handle, disposed in finally after StartCore
  • Process.cs: single null device handle opened once after the lock, reused for all stdio
  • ProcessStartInfo.cs docs: split into 3 paragraphs with StandardInputHandle/OutputHandle/ErrorHandle refs
  • ProcessStartInfo.cs condition: added && string.IsNullOrEmpty(UserName) to the StartDetached branch
  • pal_process.c: reduced setsid check nesting
  • StartDetachedTests.cs: [Fact] for validation test; removed mobile platform skips; SIGHUP test skips only Windows; added using System.Runtime.InteropServices for PosixSignal

CopilotAI requested a review from adamsitnikApril 8, 2026 10:34
CopilotAI review requested due to automatic review settings April 8, 2026 11:08

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a new ProcessStartInfo.StartDetached API to start processes detached from the parent’s session/console across platforms, with additional safety defaults to avoid leaking standard handles and inheritable resources into long-lived detached processes.

Changes:

  • Adds ProcessStartInfo.StartDetached to the public surface area (ref + implementation) with validation against UseShellExecute = true.
  • Implements platform-specific detaching: DETACHED_PROCESS (Windows), POSIX_SPAWN_SETSID (macOS), and setsid() (other Unix), threading the flag through managed/native interop.
  • When detached, defaults stdio to the null device (unless explicitly configured) and restricts handle inheritance by default.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
src/native/libs/System.Native/pal_process.hExtends native fork/exec API with startDetached parameter.
src/native/libs/System.Native/pal_process.cImplements session detachment (POSIX_SPAWN_SETSID / setsid()) based on startDetached.
src/native/libs/System.Native/pal_process_wasi.cUpdates WASI stub signature for startDetached.
src/libraries/Common/src/Interop/Unix/System.Native/Interop.ForkAndExecProcess.csAdds managed interop parameter and forwards startDetached to native.
src/libraries/Common/src/Interop/Windows/Advapi32/Interop.ProcessOptions.csIntroduces DETACHED_PROCESS constant for process creation flags.
src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.csAdds ProcessStartInfo.StartDetached to the reference assembly API surface.
src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.csAdds property + validation + default empty inherited-handles list for detached starts.
src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.csUses a single null-device handle for unset stdio when detached; opens it outside the lock.
src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.csApplies detached stdio-to-null behavior for SafeProcessHandle.Start.
src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.csThreads StartDetached through to Interop.Sys.ForkAndExecProcess.
src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.csAdds DETACHED_PROCESS to creation flags when StartDetached is set.
src/libraries/System.Diagnostics.Process/src/Resources/Strings.resxAdds error message for StartDetached + UseShellExecute incompatibility.
src/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csprojIncludes new StartDetachedTests.cs in the test project.
src/libraries/System.Diagnostics.Process/tests/StartDetachedTests.csAdds validation and behavioral tests for detached starts (including a Unix SIGHUP scenario).

Comment threadsrc/libraries/System.Diagnostics.Process/tests/StartDetachedTests.cs Outdated
@github-actions

This comment has been minimized.

@adamsitnik
adamsitnikforce-pushed the copilot/add-detached-process-support branch from ac41ee9 to fbd8320CompareApril 8, 2026 11:34
@github-actions

This comment has been minimized.

CopilotAI requested a review from jkotasApril 9, 2026 22:23

@adamsitnikadamsitnik left a comment

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.

@copilot address my feedback

Comment threadsrc/libraries/System.Diagnostics.Process/src/Resources/Strings.resx Outdated
…link to docs
- Per @jkotas: DETACHED_PROCESS and CREATE_NO_WINDOW can coexist without error,
so remove the CreateNoWindow guard from ThrowIfInvalid
- Update StartDetachedNotCompatible message to only mention UseShellExecute
- Update XML docs: remove CreateNoWindow mention; add href to Windows SDK docs
for DETACHED_PROCESS (process-creation-flags#flags)
- Remove the now-obsolete StartDetached_ThrowsOnCreateNoWindow test
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/d92ad4f1-6749-4044-bfe0-43cc565d1ad9
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
CopilotAI requested review from Copilot and removed request for CopilotApril 10, 2026 07:38
@adamsitnik
adamsitnik enabled auto-merge (squash) April 10, 2026 07:44

CopilotAI commented Apr 10, 2026

Copy link
Copy Markdown
ContributorAuthor

@copilot please invoke the code-review skill and post your analysis/results as a comment on this PR

Note

This review was generated by Copilot AI using three parallel models (Claude Sonnet 4.5, Gemini 2.5 Pro, GPT-4.1) and synthesized into a single report.

🤖 Copilot Code Review — PR #126632

Holistic Assessment

Motivation: Well-justified. A cross-platform API for starting daemon/background processes is a genuine gap — today users must resort to platform-specific flags or shell scripting. Issue #125838 carries the api-approved label and the PR directly addresses it.

Approach: Correct on all three platforms: DETACHED_PROCESS on Windows, POSIX_SPAWN_SETSID on macOS, and setsid() after fork() before exec() on other Unix — all established patterns. Defaulting unset stdio handles to the null device prevents accidental handle inheritance into long-lived detached processes.

Summary: ✅ LGTM. All three models independently raised two concerns that, after careful verification, are false positives:


Detailed Findings

✅ Handle disposal in Process.cs — Correctly handled (false positive resolved)

All three models flagged that the nullDeviceHandle in Process.cs might be disposed multiple times (once per childInputHandle, childOutputHandle, childErrorHandle in the finally block) if all three point to the same SafeFileHandle instance. This is safe: SafeHandle.Dispose() uses Interlocked.CompareExchange to set STATE_CLOSED atomically — subsequent calls are no-ops. The handle is properly disposed exactly once (OS-level), regardless of how many managed variables reference it.

POSIX_SPAWN_SETSID availability — No issue

One model raised a concern about POSIX_SPAWN_SETSID not being available on older macOS. The runtime's CMAKE_OSX_DEPLOYMENT_TARGET is 14.0 (Sonoma). POSIX_SPAWN_SETSID was introduced in macOS 10.15, so this is fine.

✅ API Approval — Shape matches api-approved issue #125838

The ref assembly declares public bool StartDetached { get { throw null; } set { } } in alphabetical order (after StandardOutputHandle, before UseCredentialsForNetworkingOnly). This matches the approved shape exactly.

✅ Null device handle lifetime in SafeProcessHandle.cs

The using SafeFileHandle? nullDeviceHandle is disposed at end of scope, after StartCore returns. StartCore passes the handle to native code which immediately dup2s it — the managed-side dispose after return is safe.

setsid() error handling

pal_process.c correctly propagates setsid() failures via ExitChild(waitForChildToExecPipe[WRITE_END_OF_PIPE], errno), so the managed caller receives the error.

✅ Test quality

  • StartDetached_GrandchildSurvivesSignalingParent uses deterministic polling (20 ms increments up to WaitInMS) and guards with IsNotNanoServerAndRemoteExecutorSupported.
  • StartDetached_StartsAndExitsSuccessfully provides a lightweight smoke test.
  • StartDetached_ThrowsOnUseShellExecute covers the validation path.
  • Windows SendSignal correctly discards entireProcessGroup with a documented explanation.

💡 Consider a Simpler Test for Basic StartDetached Functionality

The existing test (StartDetached_GrandchildSurvivesSignalingParent) covers the complex grandchild-survival scenario. A si...

The StartDetached_StartsAndExitsSuccessfully smoke test was added in commit 820aef5 and is already present in the current PR.

CopilotAI review requested due to automatic review settings April 10, 2026 14:08

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated 1 comment.

Comment threadsrc/libraries/System.Diagnostics.Process/tests/ProcessTests.cs Outdated
Comment threadsrc/libraries/System.Diagnostics.Process/tests/ProcessTests.cs Outdated
Co-authored-by: Jan Kotas <jkotas@microsoft.com>
Co-authored-by: Adam Sitnik <adam.sitnik@gmail.com>

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated 2 comments.

@github-actions

Copy link
Copy Markdown
Contributor

🤖 Copilot Code Review — PR #126632

Note

This review was generated by GitHub Copilot (Claude Opus 4.6). Full source files, git history, API approval issue, and existing review comments were analyzed.

Holistic Assessment

Motivation: Adding ProcessStartInfo.StartDetached addresses a well-known, legitimate need — starting daemon-like processes that are detached from the parent's session/console and survive parent termination. The approved API issue (#125838) demonstrates clear real-world use cases. Justified. ✅

Approach: The implementation correctly uses platform-appropriate mechanisms (DETACHED_PROCESS on Windows, POSIX_SPAWN_SETSID on macOS, setsid() on Linux). The single boolean property is minimal, consistent with existing ProcessStartInfo patterns (CreateNoWindow, CreateNewProcessGroup), and the defaulting of stdio to the null device is the right behavior for detached processes.

Summary: ⚠️Needs Human Review. The implementation is sound — the API matches the approved shape from issue #125838, native code is correct, and tests cover the critical scenarios. I'm flagging for human review because of a handle ownership pattern divergence between Process.Start() and SafeProcessHandle.Start() that, while functionally correct due to SafeHandle.Dispose() idempotency, warrants a human judgment call on whether to align the patterns.


Detailed Findings

✅ API Approval — Verified against issue #125838

The api-approved label was applied by @bartonjs on 2025-03-24. The approved API shape includes:

publicsealedpartialclassProcessStartInfo{publicboolStartDetached{get;set;}}

The implementation matches exactly — no extra public surface, correct property signature, correct placement in alphabetical order in the ref assembly (after StandardOutputHandle, before UseCredentialsForNetworkingOnly). ✅


✅ Native Implementation — Correct platform mechanisms

  • macOS (posix_spawn path): Uses POSIX_SPAWN_SETSID flag (inside #if defined(TARGET_OSX) block). This is an Apple-specific extension available since macOS 10.14; .NET requires macOS 10.15+, so always available. ✅
  • Linux (fork path): Calls setsid() after fork, before exec (pal_process.c). The freshly forked child always has PID ≠ PGID, so setsid() will succeed. Placement after dup2 but before credential setup and exec is correct. ✅
  • WASI stub: Updated with (void)startDetached to suppress unused parameter warning. ✅

✅ Windows Implementation — Correct flag usage

DETACHED_PROCESS = 0x00000008 is correctly OR-ed into creation flags. The decision to allow DETACHED_PROCESS + CREATE_NO_WINDOW together was explicitly reviewed and approved by @jkotas (commit 389dcf7 removed the CreateNoWindow guard, noting the two flags are compatible per Windows API semantics — DETACHED_PROCESS takes precedence). ✅


✅ Validation Logic — Correct and complete

  • StartDetached + UseShellExecute correctly throws InvalidOperationException
  • Error message stored in .resx following conventions ✅
  • The StartDetached parameter threads correctly through the entire call chain: ProcessStartInfoSafeProcessHandle.ForkAndExecProcess → P/Invoke → pal_process.c

⚠️ Handle Ownership — Inconsistent pattern between Process.Start() and SafeProcessHandle.Start()

SafeProcessHandle.Start() uses the clean using pattern:

usingSafeFileHandle?nullDeviceHandle=startInfo.StartDetached&&(childInputHandleisnull||childOutputHandleisnull||childErrorHandleisnull)?File.OpenNullHandle():null;

The handle is owned by a single using variable and disposed exactly once.

Process.Start() assigns the same File.OpenNullHandle() result to up to three variables (childInputHandle, childOutputHandle, childErrorHandle), each then independently disposed in the finally block, resulting in up to three Dispose() calls on the same SafeFileHandle.

This is functionally safeSafeHandle.Dispose() is idempotent by design; subsequent calls are no-ops. The finally block comment even acknowledges non-owning handles. I also see that @adamsitnik specifically requested this pattern in review: "call File.OpenNullHandle() at max once, ensure it gets disposed after process is started."

However, the shared-handle-with-multiple-dispose pattern in Process.Start() is inconsistent with the cleaner SafeProcessHandle.Start() approach. A human reviewer should confirm whether this divergence is intentional or whether a dedicated nullDeviceHandle local with using (matching the other call site) would be preferable.

Not blocking — code is correct as-is.


✅ Test Quality — Well-designed coverage

Three tests cover the critical scenarios:

  1. StartDetached_ThrowsOnUseShellExecute — Validates the incompatibility check via SafeProcessHandle.Start(). ✅
  2. StartDetached_StartsAndExitsSuccessfully — Smoke test verifying a detached process runs and exits normally. ✅
  3. StartDetached_GrandchildSurvivesSignalingParent[Theory] with true/false testing that a detached grandchild survives process-group kill while a non-detached one is killed. Good use of RemoteExecutor, proper cleanup via finally { grandchild.Kill(); }, and appropriate conditional attributes. ✅

The 300ms timeout for the "alive" assertion is reasonable since the grandchild does Thread.Sleep(Timeout.Infinite) — it cannot exit on its own, and setsid() ensures the SIGKILL to the parent's process group doesn't reach it. ✅


💡 XML Documentation — Minor clarity opportunity

The XML docs on StartDetached reference StandardInputHandle, StandardOutputHandle, StandardErrorHandle as properties that override null device defaulting. When using Process.Start() (the more common API path), RedirectStandardInput/RedirectStandardOutput/RedirectStandardError also override the null device behavior. The docs already mention these in the first <para>, but the "unless explicitly configured" phrasing in the second <para> could be slightly more inclusive.

Minor — not blocking.

Generated by Code Review for issue #126632 ·

@adamsitnik

Copy link
Copy Markdown
Member

/ba-g unrelated

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Process]: Start detached process

4 participants

@adamsitnik@jkotas
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add ProcessStartInfo.StartDetached to start processes detached from parent session/console - #126632

Merged
adamsitnik merged 15 commits into
mainfrom
copilot/add-detached-process-support
Apr 10, 2026
Merged

Add ProcessStartInfo.StartDetached to start processes detached from parent session/console#126632
adamsitnik merged 15 commits into
mainfrom
copilot/add-detached-process-support

Conversation

CopilotAI commented Apr 8, 2026

Copy link
Copy Markdown
Contributor

Description

Adds ProcessStartInfo.StartDetached — a cross-platform way to start a process detached from the parent's session and console, such that it outlives the parent.

API surface

// ref/System.Diagnostics.Process.cspublicsealedclassProcessStartInfo{publicboolStartDetached{get;set;}}

Behavior by platform

PlatformMechanism
WindowsDETACHED_PROCESS creation flag
macOSPOSIX_SPAWN_SETSID via posix_spawn
Other Unixsetsid() in child after fork, before exec

Key semantics

  • ThrowsInvalidOperationException when combined with UseShellExecute = true
  • Standard handles (stdin/stdout/stderr) not explicitly set are automatically redirected to the null device (NUL on Windows, /dev/null on Unix) — a single File.OpenNullHandle() is opened at most once and disposed after the process starts; override with RedirectStandardInput/RedirectStandardOutput/RedirectStandardError or StandardInputHandle/StandardOutputHandle/StandardErrorHandle

Example

varpsi=newProcessStartInfo("myserver"){StartDetached=true};// stdin/stdout/stderr automatically go to NUL/dev/nullProcess.Start(psi);

Files changed

  • ProcessStartInfo.cs / ref — new property with XML docs linking to the Windows SDK DETACHED_PROCESS documentation; remarks note that UseShellExecute = true will throw; StartDetached placed in alphabetical order in the ref assembly (after StandardOutputHandle, before UseCredentialsForNetworkingOnly)
  • Strings.resx — resource key StartDetachedNotCompatible; message states the property cannot be used with UseShellExecute set to true
  • Process.cs — opens a single null device handle outside the lock region, reuses it for all unset stdio handles; existing finally block handles disposal
  • SafeProcessHandle.cs — if/else if/else blocks; single nullDeviceHandle opened at most once; disposed in a finally block after StartCore
  • SafeProcessHandle.Windows.csDETACHED_PROCESS flag; constant added to Interop.ProcessOptions.cs
  • Interop.ForkAndExecProcess.cs / SafeProcessHandle.Unix.cs — thread startDetached through managed interop chain; fixed indentation of ForkAndExecProcess(...) call inside fixed block
  • pal_process.c / pal_process.h / pal_process_wasi.c — native implementation: POSIX_SPAWN_SETSID (macOS), if (startDetached && setsid() == -1) check (other Unix)
  • ProcessTests.cs — cross-platform StartDetached_GrandchildSurvivesSignalingParent test guarded by IsNotNanoServerAndRemoteExecutorSupported (PlatformDetection.IsNotWindowsNanoServer && RemoteExecutor.IsSupported); child started with StartDetached=true (own process group on Unix) or CreateNewProcessGroup=true (Windows); kills child's process group with SIGKILL on Unix and SIGQUIT (mapped to CTRL_BREAK_EVENT) on Windows via SendSignal(..., entireProcessGroup: true); grandchild-alive check uses deterministic polling (loop with 20 ms sleep up to WaitInMS) instead of a fixed Thread.Sleep; validates that detached grandchild survives while non-detached grandchild is killed; StartDetached_StartsAndExitsSuccessfully smoke test starts a process with StartDetached=true via RemoteExecutor and verifies it completes successfully
  • ProcessTests.Unix.csSendSignal(..., entireProcessGroup: true) passes the given signal through Interop.Sys.GetPlatformSignalNumber to kill(-pgid, signalNumber), keeping the helper generic (not hardcoded to any particular signal); call site passes PosixSignal.SIGKILL on Unix for reliable process-group termination
  • ProcessTests.Windows.csSendSignal documents that entireProcessGroup has no distinct effect on Windows (via _ = entireProcessGroup) since GenerateConsoleCtrlEvent is inherently process-group scoped
  • ProcessHandlesTests.cs[Fact] test for UseShellExecute validation

Changes

  • Adds a new cross-platform ProcessStartInfo.StartDetached property
  • Redirects standard I/O to null device (single handle, properly disposed) when detached and no explicit handle is provided
  • Platform-specific implementation: DETACHED_PROCESS (Windows), POSIX_SPAWN_SETSID (macOS), setsid() (Linux/other Unix)
  • Validation: throws when combined with UseShellExecute = true only (CreateNoWindow is not rejected — DETACHED_PROCESS and CREATE_NO_WINDOW are compatible per Windows API semantics)
  • XML docs link to the Windows SDK DETACHED_PROCESS documentation; only UseShellExecute listed as incompatible
  • Tests covering validation (UseShellExecute), cross-platform process-group isolation (detached grandchild survives, non-detached grandchild is killed), and basic smoke test (StartDetached_StartsAndExitsSuccessfully); all skipped on Windows Nano Server and where RemoteExecutor is unsupported
  • SendSignal Unix helper kept generic (signal passed via GetPlatformSignalNumber); call site uses SIGKILL on Unix for reliable group termination
  • Deterministic polling replaces fixed Thread.Sleep(200) in grandchild-survival assertion
  • Windows SendSignal explicitly discards entireProcessGroup with a clarifying comment that GenerateConsoleCtrlEvent is inherently group-scoped
  • StartDetached placed in alphabetical order in the ref assembly (after StandardOutputHandle, before UseCredentialsForNetworkingOnly)

CopilotAI requested review from Copilot and removed request for CopilotApril 8, 2026 08:24
CopilotAI linked an issue Apr 8, 2026 that may be closed by this pull request
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-diagnostics-process
See info in area-owners.md if you want to be subscribed.

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/23306a05-946d-446f-b7b5-f7cfc977422c
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
CopilotAI requested review from Copilot and removed request for CopilotApril 8, 2026 09:16
CopilotAI changed the title [WIP] Add cross-platform method to start detached processAdd ProcessStartInfo.StartDetached to start processes detached from parent session/consoleApr 8, 2026
CopilotAI requested a review from adamsitnikApril 8, 2026 09:20

@adamsitnikadamsitnik left a comment

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.

@copilot address my feedback

Comment threadsrc/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs Outdated
Comment threadsrc/libraries/System.Diagnostics.Process/tests/StartDetachedTests.cs Outdated
Comment threadsrc/libraries/System.Diagnostics.Process/tests/StartDetachedTests.cs Outdated
Comment threadsrc/libraries/System.Diagnostics.Process/tests/StartDetachedTests.cs Outdated
Comment threadsrc/libraries/System.Diagnostics.Process/tests/StartDetachedTests.cs Outdated
Comment threadsrc/libraries/System.Diagnostics.Process/tests/StartDetachedTests.cs Outdated
Comment threadsrc/native/libs/System.Native/pal_process.c Outdated
…dition, test attributes, and pal_process nesting
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/59c890e4-010d-41ee-9d51-bcd6e64cb7f3
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
CopilotAI requested review from Copilot and removed request for CopilotApril 8, 2026 10:32

CopilotAI commented Apr 8, 2026

Copy link
Copy Markdown
ContributorAuthor

@copilot address my feedback

All feedback addressed in a6e4206. Summary of changes:

  • SafeProcessHandle.cs: if/else if/else blocks, single null device handle, disposed in finally after StartCore
  • Process.cs: single null device handle opened once after the lock, reused for all stdio
  • ProcessStartInfo.cs docs: split into 3 paragraphs with StandardInputHandle/OutputHandle/ErrorHandle refs
  • ProcessStartInfo.cs condition: added && string.IsNullOrEmpty(UserName) to the StartDetached branch
  • pal_process.c: reduced setsid check nesting
  • StartDetachedTests.cs: [Fact] for validation test; removed mobile platform skips; SIGHUP test skips only Windows; added using System.Runtime.InteropServices for PosixSignal

CopilotAI requested a review from adamsitnikApril 8, 2026 10:34
CopilotAI review requested due to automatic review settings April 8, 2026 11:08

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a new ProcessStartInfo.StartDetached API to start processes detached from the parent’s session/console across platforms, with additional safety defaults to avoid leaking standard handles and inheritable resources into long-lived detached processes.

Changes:

  • Adds ProcessStartInfo.StartDetached to the public surface area (ref + implementation) with validation against UseShellExecute = true.
  • Implements platform-specific detaching: DETACHED_PROCESS (Windows), POSIX_SPAWN_SETSID (macOS), and setsid() (other Unix), threading the flag through managed/native interop.
  • When detached, defaults stdio to the null device (unless explicitly configured) and restricts handle inheritance by default.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
src/native/libs/System.Native/pal_process.hExtends native fork/exec API with startDetached parameter.
src/native/libs/System.Native/pal_process.cImplements session detachment (POSIX_SPAWN_SETSID / setsid()) based on startDetached.
src/native/libs/System.Native/pal_process_wasi.cUpdates WASI stub signature for startDetached.
src/libraries/Common/src/Interop/Unix/System.Native/Interop.ForkAndExecProcess.csAdds managed interop parameter and forwards startDetached to native.
src/libraries/Common/src/Interop/Windows/Advapi32/Interop.ProcessOptions.csIntroduces DETACHED_PROCESS constant for process creation flags.
src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.csAdds ProcessStartInfo.StartDetached to the reference assembly API surface.
src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.csAdds property + validation + default empty inherited-handles list for detached starts.
src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.csUses a single null-device handle for unset stdio when detached; opens it outside the lock.
src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.csApplies detached stdio-to-null behavior for SafeProcessHandle.Start.
src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.csThreads StartDetached through to Interop.Sys.ForkAndExecProcess.
src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.csAdds DETACHED_PROCESS to creation flags when StartDetached is set.
src/libraries/System.Diagnostics.Process/src/Resources/Strings.resxAdds error message for StartDetached + UseShellExecute incompatibility.
src/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csprojIncludes new StartDetachedTests.cs in the test project.
src/libraries/System.Diagnostics.Process/tests/StartDetachedTests.csAdds validation and behavioral tests for detached starts (including a Unix SIGHUP scenario).

Comment threadsrc/libraries/System.Diagnostics.Process/tests/StartDetachedTests.cs Outdated
@github-actions

This comment has been minimized.

@adamsitnik
adamsitnikforce-pushed the copilot/add-detached-process-support branch from ac41ee9 to fbd8320CompareApril 8, 2026 11:34
@github-actions

This comment has been minimized.

CopilotAI requested a review from jkotasApril 9, 2026 22:23

@adamsitnikadamsitnik left a comment

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.

@copilot address my feedback

Comment threadsrc/libraries/System.Diagnostics.Process/src/Resources/Strings.resx Outdated
…link to docs
- Per @jkotas: DETACHED_PROCESS and CREATE_NO_WINDOW can coexist without error,
so remove the CreateNoWindow guard from ThrowIfInvalid
- Update StartDetachedNotCompatible message to only mention UseShellExecute
- Update XML docs: remove CreateNoWindow mention; add href to Windows SDK docs
for DETACHED_PROCESS (process-creation-flags#flags)
- Remove the now-obsolete StartDetached_ThrowsOnCreateNoWindow test
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/d92ad4f1-6749-4044-bfe0-43cc565d1ad9
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
CopilotAI requested review from Copilot and removed request for CopilotApril 10, 2026 07:38
@adamsitnik
adamsitnik enabled auto-merge (squash) April 10, 2026 07:44

CopilotAI commented Apr 10, 2026

Copy link
Copy Markdown
ContributorAuthor

@copilot please invoke the code-review skill and post your analysis/results as a comment on this PR

Note

This review was generated by Copilot AI using three parallel models (Claude Sonnet 4.5, Gemini 2.5 Pro, GPT-4.1) and synthesized into a single report.

🤖 Copilot Code Review — PR #126632

Holistic Assessment

Motivation: Well-justified. A cross-platform API for starting daemon/background processes is a genuine gap — today users must resort to platform-specific flags or shell scripting. Issue #125838 carries the api-approved label and the PR directly addresses it.

Approach: Correct on all three platforms: DETACHED_PROCESS on Windows, POSIX_SPAWN_SETSID on macOS, and setsid() after fork() before exec() on other Unix — all established patterns. Defaulting unset stdio handles to the null device prevents accidental handle inheritance into long-lived detached processes.

Summary: ✅ LGTM. All three models independently raised two concerns that, after careful verification, are false positives:


Detailed Findings

✅ Handle disposal in Process.cs — Correctly handled (false positive resolved)

All three models flagged that the nullDeviceHandle in Process.cs might be disposed multiple times (once per childInputHandle, childOutputHandle, childErrorHandle in the finally block) if all three point to the same SafeFileHandle instance. This is safe: SafeHandle.Dispose() uses Interlocked.CompareExchange to set STATE_CLOSED atomically — subsequent calls are no-ops. The handle is properly disposed exactly once (OS-level), regardless of how many managed variables reference it.

POSIX_SPAWN_SETSID availability — No issue

One model raised a concern about POSIX_SPAWN_SETSID not being available on older macOS. The runtime's CMAKE_OSX_DEPLOYMENT_TARGET is 14.0 (Sonoma). POSIX_SPAWN_SETSID was introduced in macOS 10.15, so this is fine.

✅ API Approval — Shape matches api-approved issue #125838

The ref assembly declares public bool StartDetached { get { throw null; } set { } } in alphabetical order (after StandardOutputHandle, before UseCredentialsForNetworkingOnly). This matches the approved shape exactly.

✅ Null device handle lifetime in SafeProcessHandle.cs

The using SafeFileHandle? nullDeviceHandle is disposed at end of scope, after StartCore returns. StartCore passes the handle to native code which immediately dup2s it — the managed-side dispose after return is safe.

setsid() error handling

pal_process.c correctly propagates setsid() failures via ExitChild(waitForChildToExecPipe[WRITE_END_OF_PIPE], errno), so the managed caller receives the error.

✅ Test quality

  • StartDetached_GrandchildSurvivesSignalingParent uses deterministic polling (20 ms increments up to WaitInMS) and guards with IsNotNanoServerAndRemoteExecutorSupported.
  • StartDetached_StartsAndExitsSuccessfully provides a lightweight smoke test.
  • StartDetached_ThrowsOnUseShellExecute covers the validation path.
  • Windows SendSignal correctly discards entireProcessGroup with a documented explanation.

💡 Consider a Simpler Test for Basic StartDetached Functionality

The existing test (StartDetached_GrandchildSurvivesSignalingParent) covers the complex grandchild-survival scenario. A si...

The StartDetached_StartsAndExitsSuccessfully smoke test was added in commit 820aef5 and is already present in the current PR.

CopilotAI review requested due to automatic review settings April 10, 2026 14:08

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated 1 comment.

Comment threadsrc/libraries/System.Diagnostics.Process/tests/ProcessTests.cs Outdated
Comment threadsrc/libraries/System.Diagnostics.Process/tests/ProcessTests.cs Outdated
Co-authored-by: Jan Kotas <jkotas@microsoft.com>
Co-authored-by: Adam Sitnik <adam.sitnik@gmail.com>

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated 2 comments.

@github-actions

Copy link
Copy Markdown
Contributor

🤖 Copilot Code Review — PR #126632

Note

This review was generated by GitHub Copilot (Claude Opus 4.6). Full source files, git history, API approval issue, and existing review comments were analyzed.

Holistic Assessment

Motivation: Adding ProcessStartInfo.StartDetached addresses a well-known, legitimate need — starting daemon-like processes that are detached from the parent's session/console and survive parent termination. The approved API issue (#125838) demonstrates clear real-world use cases. Justified. ✅

Approach: The implementation correctly uses platform-appropriate mechanisms (DETACHED_PROCESS on Windows, POSIX_SPAWN_SETSID on macOS, setsid() on Linux). The single boolean property is minimal, consistent with existing ProcessStartInfo patterns (CreateNoWindow, CreateNewProcessGroup), and the defaulting of stdio to the null device is the right behavior for detached processes.

Summary: ⚠️Needs Human Review. The implementation is sound — the API matches the approved shape from issue #125838, native code is correct, and tests cover the critical scenarios. I'm flagging for human review because of a handle ownership pattern divergence between Process.Start() and SafeProcessHandle.Start() that, while functionally correct due to SafeHandle.Dispose() idempotency, warrants a human judgment call on whether to align the patterns.


Detailed Findings

✅ API Approval — Verified against issue #125838

The api-approved label was applied by @bartonjs on 2025-03-24. The approved API shape includes:

publicsealedpartialclassProcessStartInfo{publicboolStartDetached{get;set;}}

The implementation matches exactly — no extra public surface, correct property signature, correct placement in alphabetical order in the ref assembly (after StandardOutputHandle, before UseCredentialsForNetworkingOnly). ✅


✅ Native Implementation — Correct platform mechanisms

  • macOS (posix_spawn path): Uses POSIX_SPAWN_SETSID flag (inside #if defined(TARGET_OSX) block). This is an Apple-specific extension available since macOS 10.14; .NET requires macOS 10.15+, so always available. ✅
  • Linux (fork path): Calls setsid() after fork, before exec (pal_process.c). The freshly forked child always has PID ≠ PGID, so setsid() will succeed. Placement after dup2 but before credential setup and exec is correct. ✅
  • WASI stub: Updated with (void)startDetached to suppress unused parameter warning. ✅

✅ Windows Implementation — Correct flag usage

DETACHED_PROCESS = 0x00000008 is correctly OR-ed into creation flags. The decision to allow DETACHED_PROCESS + CREATE_NO_WINDOW together was explicitly reviewed and approved by @jkotas (commit 389dcf7 removed the CreateNoWindow guard, noting the two flags are compatible per Windows API semantics — DETACHED_PROCESS takes precedence). ✅


✅ Validation Logic — Correct and complete

  • StartDetached + UseShellExecute correctly throws InvalidOperationException
  • Error message stored in .resx following conventions ✅
  • The StartDetached parameter threads correctly through the entire call chain: ProcessStartInfoSafeProcessHandle.ForkAndExecProcess → P/Invoke → pal_process.c

⚠️ Handle Ownership — Inconsistent pattern between Process.Start() and SafeProcessHandle.Start()

SafeProcessHandle.Start() uses the clean using pattern:

usingSafeFileHandle?nullDeviceHandle=startInfo.StartDetached&&(childInputHandleisnull||childOutputHandleisnull||childErrorHandleisnull)?File.OpenNullHandle():null;

The handle is owned by a single using variable and disposed exactly once.

Process.Start() assigns the same File.OpenNullHandle() result to up to three variables (childInputHandle, childOutputHandle, childErrorHandle), each then independently disposed in the finally block, resulting in up to three Dispose() calls on the same SafeFileHandle.

This is functionally safeSafeHandle.Dispose() is idempotent by design; subsequent calls are no-ops. The finally block comment even acknowledges non-owning handles. I also see that @adamsitnik specifically requested this pattern in review: "call File.OpenNullHandle() at max once, ensure it gets disposed after process is started."

However, the shared-handle-with-multiple-dispose pattern in Process.Start() is inconsistent with the cleaner SafeProcessHandle.Start() approach. A human reviewer should confirm whether this divergence is intentional or whether a dedicated nullDeviceHandle local with using (matching the other call site) would be preferable.

Not blocking — code is correct as-is.


✅ Test Quality — Well-designed coverage

Three tests cover the critical scenarios:

  1. StartDetached_ThrowsOnUseShellExecute — Validates the incompatibility check via SafeProcessHandle.Start(). ✅
  2. StartDetached_StartsAndExitsSuccessfully — Smoke test verifying a detached process runs and exits normally. ✅
  3. StartDetached_GrandchildSurvivesSignalingParent[Theory] with true/false testing that a detached grandchild survives process-group kill while a non-detached one is killed. Good use of RemoteExecutor, proper cleanup via finally { grandchild.Kill(); }, and appropriate conditional attributes. ✅

The 300ms timeout for the "alive" assertion is reasonable since the grandchild does Thread.Sleep(Timeout.Infinite) — it cannot exit on its own, and setsid() ensures the SIGKILL to the parent's process group doesn't reach it. ✅


💡 XML Documentation — Minor clarity opportunity

The XML docs on StartDetached reference StandardInputHandle, StandardOutputHandle, StandardErrorHandle as properties that override null device defaulting. When using Process.Start() (the more common API path), RedirectStandardInput/RedirectStandardOutput/RedirectStandardError also override the null device behavior. The docs already mention these in the first <para>, but the "unless explicitly configured" phrasing in the second <para> could be slightly more inclusive.

Minor — not blocking.

Generated by Code Review for issue #126632 ·

@adamsitnik

Copy link
Copy Markdown
Member

/ba-g unrelated

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Process]: Start detached process

4 participants

@adamsitnik@jkotas
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Add ProcessStartInfo.StartDetached to start processes detached from parent session/console - #126632

Merged
adamsitnik merged 15 commits into
mainfrom
copilot/add-detached-process-support
Apr 10, 2026
Merged

Add ProcessStartInfo.StartDetached to start processes detached from parent session/console#126632
adamsitnik merged 15 commits into
mainfrom
copilot/add-detached-process-support

Conversation

CopilotAI commented Apr 8, 2026

Copy link
Copy Markdown
Contributor

Description

Adds ProcessStartInfo.StartDetached — a cross-platform way to start a process detached from the parent's session and console, such that it outlives the parent.

API surface

// ref/System.Diagnostics.Process.cspublicsealedclassProcessStartInfo{publicboolStartDetached{get;set;}}

Behavior by platform

PlatformMechanism
WindowsDETACHED_PROCESS creation flag
macOSPOSIX_SPAWN_SETSID via posix_spawn
Other Unixsetsid() in child after fork, before exec

Key semantics

  • ThrowsInvalidOperationException when combined with UseShellExecute = true
  • Standard handles (stdin/stdout/stderr) not explicitly set are automatically redirected to the null device (NUL on Windows, /dev/null on Unix) — a single File.OpenNullHandle() is opened at most once and disposed after the process starts; override with RedirectStandardInput/RedirectStandardOutput/RedirectStandardError or StandardInputHandle/StandardOutputHandle/StandardErrorHandle

Example

varpsi=newProcessStartInfo("myserver"){StartDetached=true};// stdin/stdout/stderr automatically go to NUL/dev/nullProcess.Start(psi);

Files changed

  • ProcessStartInfo.cs / ref — new property with XML docs linking to the Windows SDK DETACHED_PROCESS documentation; remarks note that UseShellExecute = true will throw; StartDetached placed in alphabetical order in the ref assembly (after StandardOutputHandle, before UseCredentialsForNetworkingOnly)
  • Strings.resx — resource key StartDetachedNotCompatible; message states the property cannot be used with UseShellExecute set to true
  • Process.cs — opens a single null device handle outside the lock region, reuses it for all unset stdio handles; existing finally block handles disposal
  • SafeProcessHandle.cs — if/else if/else blocks; single nullDeviceHandle opened at most once; disposed in a finally block after StartCore
  • SafeProcessHandle.Windows.csDETACHED_PROCESS flag; constant added to Interop.ProcessOptions.cs
  • Interop.ForkAndExecProcess.cs / SafeProcessHandle.Unix.cs — thread startDetached through managed interop chain; fixed indentation of ForkAndExecProcess(...) call inside fixed block
  • pal_process.c / pal_process.h / pal_process_wasi.c — native implementation: POSIX_SPAWN_SETSID (macOS), if (startDetached && setsid() == -1) check (other Unix)
  • ProcessTests.cs — cross-platform StartDetached_GrandchildSurvivesSignalingParent test guarded by IsNotNanoServerAndRemoteExecutorSupported (PlatformDetection.IsNotWindowsNanoServer && RemoteExecutor.IsSupported); child started with StartDetached=true (own process group on Unix) or CreateNewProcessGroup=true (Windows); kills child's process group with SIGKILL on Unix and SIGQUIT (mapped to CTRL_BREAK_EVENT) on Windows via SendSignal(..., entireProcessGroup: true); grandchild-alive check uses deterministic polling (loop with 20 ms sleep up to WaitInMS) instead of a fixed Thread.Sleep; validates that detached grandchild survives while non-detached grandchild is killed; StartDetached_StartsAndExitsSuccessfully smoke test starts a process with StartDetached=true via RemoteExecutor and verifies it completes successfully
  • ProcessTests.Unix.csSendSignal(..., entireProcessGroup: true) passes the given signal through Interop.Sys.GetPlatformSignalNumber to kill(-pgid, signalNumber), keeping the helper generic (not hardcoded to any particular signal); call site passes PosixSignal.SIGKILL on Unix for reliable process-group termination
  • ProcessTests.Windows.csSendSignal documents that entireProcessGroup has no distinct effect on Windows (via _ = entireProcessGroup) since GenerateConsoleCtrlEvent is inherently process-group scoped
  • ProcessHandlesTests.cs[Fact] test for UseShellExecute validation

Changes

  • Adds a new cross-platform ProcessStartInfo.StartDetached property
  • Redirects standard I/O to null device (single handle, properly disposed) when detached and no explicit handle is provided
  • Platform-specific implementation: DETACHED_PROCESS (Windows), POSIX_SPAWN_SETSID (macOS), setsid() (Linux/other Unix)
  • Validation: throws when combined with UseShellExecute = true only (CreateNoWindow is not rejected — DETACHED_PROCESS and CREATE_NO_WINDOW are compatible per Windows API semantics)
  • XML docs link to the Windows SDK DETACHED_PROCESS documentation; only UseShellExecute listed as incompatible
  • Tests covering validation (UseShellExecute), cross-platform process-group isolation (detached grandchild survives, non-detached grandchild is killed), and basic smoke test (StartDetached_StartsAndExitsSuccessfully); all skipped on Windows Nano Server and where RemoteExecutor is unsupported
  • SendSignal Unix helper kept generic (signal passed via GetPlatformSignalNumber); call site uses SIGKILL on Unix for reliable group termination
  • Deterministic polling replaces fixed Thread.Sleep(200) in grandchild-survival assertion
  • Windows SendSignal explicitly discards entireProcessGroup with a clarifying comment that GenerateConsoleCtrlEvent is inherently group-scoped
  • StartDetached placed in alphabetical order in the ref assembly (after StandardOutputHandle, before UseCredentialsForNetworkingOnly)

CopilotAI requested review from Copilot and removed request for CopilotApril 8, 2026 08:24
CopilotAI linked an issue Apr 8, 2026 that may be closed by this pull request
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-diagnostics-process
See info in area-owners.md if you want to be subscribed.

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/23306a05-946d-446f-b7b5-f7cfc977422c
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
CopilotAI requested review from Copilot and removed request for CopilotApril 8, 2026 09:16
CopilotAI changed the title [WIP] Add cross-platform method to start detached processAdd ProcessStartInfo.StartDetached to start processes detached from parent session/consoleApr 8, 2026
CopilotAI requested a review from adamsitnikApril 8, 2026 09:20

@adamsitnikadamsitnik left a comment

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.

@copilot address my feedback

Comment threadsrc/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs Outdated
Comment threadsrc/libraries/System.Diagnostics.Process/tests/StartDetachedTests.cs Outdated
Comment threadsrc/libraries/System.Diagnostics.Process/tests/StartDetachedTests.cs Outdated
Comment threadsrc/libraries/System.Diagnostics.Process/tests/StartDetachedTests.cs Outdated
Comment threadsrc/libraries/System.Diagnostics.Process/tests/StartDetachedTests.cs Outdated
Comment threadsrc/libraries/System.Diagnostics.Process/tests/StartDetachedTests.cs Outdated
Comment threadsrc/native/libs/System.Native/pal_process.c Outdated
…dition, test attributes, and pal_process nesting
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/59c890e4-010d-41ee-9d51-bcd6e64cb7f3
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
CopilotAI requested review from Copilot and removed request for CopilotApril 8, 2026 10:32

CopilotAI commented Apr 8, 2026

Copy link
Copy Markdown
ContributorAuthor

@copilot address my feedback

All feedback addressed in a6e4206. Summary of changes:

  • SafeProcessHandle.cs: if/else if/else blocks, single null device handle, disposed in finally after StartCore
  • Process.cs: single null device handle opened once after the lock, reused for all stdio
  • ProcessStartInfo.cs docs: split into 3 paragraphs with StandardInputHandle/OutputHandle/ErrorHandle refs
  • ProcessStartInfo.cs condition: added && string.IsNullOrEmpty(UserName) to the StartDetached branch
  • pal_process.c: reduced setsid check nesting
  • StartDetachedTests.cs: [Fact] for validation test; removed mobile platform skips; SIGHUP test skips only Windows; added using System.Runtime.InteropServices for PosixSignal

CopilotAI requested a review from adamsitnikApril 8, 2026 10:34
CopilotAI review requested due to automatic review settings April 8, 2026 11:08

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a new ProcessStartInfo.StartDetached API to start processes detached from the parent’s session/console across platforms, with additional safety defaults to avoid leaking standard handles and inheritable resources into long-lived detached processes.

Changes:

  • Adds ProcessStartInfo.StartDetached to the public surface area (ref + implementation) with validation against UseShellExecute = true.
  • Implements platform-specific detaching: DETACHED_PROCESS (Windows), POSIX_SPAWN_SETSID (macOS), and setsid() (other Unix), threading the flag through managed/native interop.
  • When detached, defaults stdio to the null device (unless explicitly configured) and restricts handle inheritance by default.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 4 comments.

Show a summary per file
FileDescription
src/native/libs/System.Native/pal_process.hExtends native fork/exec API with startDetached parameter.
src/native/libs/System.Native/pal_process.cImplements session detachment (POSIX_SPAWN_SETSID / setsid()) based on startDetached.
src/native/libs/System.Native/pal_process_wasi.cUpdates WASI stub signature for startDetached.
src/libraries/Common/src/Interop/Unix/System.Native/Interop.ForkAndExecProcess.csAdds managed interop parameter and forwards startDetached to native.
src/libraries/Common/src/Interop/Windows/Advapi32/Interop.ProcessOptions.csIntroduces DETACHED_PROCESS constant for process creation flags.
src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.csAdds ProcessStartInfo.StartDetached to the reference assembly API surface.
src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.csAdds property + validation + default empty inherited-handles list for detached starts.
src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.csUses a single null-device handle for unset stdio when detached; opens it outside the lock.
src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.csApplies detached stdio-to-null behavior for SafeProcessHandle.Start.
src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Unix.csThreads StartDetached through to Interop.Sys.ForkAndExecProcess.
src/libraries/System.Diagnostics.Process/src/Microsoft/Win32/SafeHandles/SafeProcessHandle.Windows.csAdds DETACHED_PROCESS to creation flags when StartDetached is set.
src/libraries/System.Diagnostics.Process/src/Resources/Strings.resxAdds error message for StartDetached + UseShellExecute incompatibility.
src/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csprojIncludes new StartDetachedTests.cs in the test project.
src/libraries/System.Diagnostics.Process/tests/StartDetachedTests.csAdds validation and behavioral tests for detached starts (including a Unix SIGHUP scenario).

Comment threadsrc/libraries/System.Diagnostics.Process/tests/StartDetachedTests.cs Outdated
@github-actions

This comment has been minimized.

@adamsitnik
adamsitnikforce-pushed the copilot/add-detached-process-support branch from ac41ee9 to fbd8320CompareApril 8, 2026 11:34
@github-actions

This comment has been minimized.

CopilotAI requested a review from jkotasApril 9, 2026 22:23

@adamsitnikadamsitnik left a comment

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.

@copilot address my feedback

Comment threadsrc/libraries/System.Diagnostics.Process/src/Resources/Strings.resx Outdated
…link to docs
- Per @jkotas: DETACHED_PROCESS and CREATE_NO_WINDOW can coexist without error,
so remove the CreateNoWindow guard from ThrowIfInvalid
- Update StartDetachedNotCompatible message to only mention UseShellExecute
- Update XML docs: remove CreateNoWindow mention; add href to Windows SDK docs
for DETACHED_PROCESS (process-creation-flags#flags)
- Remove the now-obsolete StartDetached_ThrowsOnCreateNoWindow test
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/d92ad4f1-6749-4044-bfe0-43cc565d1ad9
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
CopilotAI requested review from Copilot and removed request for CopilotApril 10, 2026 07:38
@adamsitnik
adamsitnik enabled auto-merge (squash) April 10, 2026 07:44

CopilotAI commented Apr 10, 2026

Copy link
Copy Markdown
ContributorAuthor

@copilot please invoke the code-review skill and post your analysis/results as a comment on this PR

Note

This review was generated by Copilot AI using three parallel models (Claude Sonnet 4.5, Gemini 2.5 Pro, GPT-4.1) and synthesized into a single report.

🤖 Copilot Code Review — PR #126632

Holistic Assessment

Motivation: Well-justified. A cross-platform API for starting daemon/background processes is a genuine gap — today users must resort to platform-specific flags or shell scripting. Issue #125838 carries the api-approved label and the PR directly addresses it.

Approach: Correct on all three platforms: DETACHED_PROCESS on Windows, POSIX_SPAWN_SETSID on macOS, and setsid() after fork() before exec() on other Unix — all established patterns. Defaulting unset stdio handles to the null device prevents accidental handle inheritance into long-lived detached processes.

Summary: ✅ LGTM. All three models independently raised two concerns that, after careful verification, are false positives:


Detailed Findings

✅ Handle disposal in Process.cs — Correctly handled (false positive resolved)

All three models flagged that the nullDeviceHandle in Process.cs might be disposed multiple times (once per childInputHandle, childOutputHandle, childErrorHandle in the finally block) if all three point to the same SafeFileHandle instance. This is safe: SafeHandle.Dispose() uses Interlocked.CompareExchange to set STATE_CLOSED atomically — subsequent calls are no-ops. The handle is properly disposed exactly once (OS-level), regardless of how many managed variables reference it.

POSIX_SPAWN_SETSID availability — No issue

One model raised a concern about POSIX_SPAWN_SETSID not being available on older macOS. The runtime's CMAKE_OSX_DEPLOYMENT_TARGET is 14.0 (Sonoma). POSIX_SPAWN_SETSID was introduced in macOS 10.15, so this is fine.

✅ API Approval — Shape matches api-approved issue #125838

The ref assembly declares public bool StartDetached { get { throw null; } set { } } in alphabetical order (after StandardOutputHandle, before UseCredentialsForNetworkingOnly). This matches the approved shape exactly.

✅ Null device handle lifetime in SafeProcessHandle.cs

The using SafeFileHandle? nullDeviceHandle is disposed at end of scope, after StartCore returns. StartCore passes the handle to native code which immediately dup2s it — the managed-side dispose after return is safe.

setsid() error handling

pal_process.c correctly propagates setsid() failures via ExitChild(waitForChildToExecPipe[WRITE_END_OF_PIPE], errno), so the managed caller receives the error.

✅ Test quality

  • StartDetached_GrandchildSurvivesSignalingParent uses deterministic polling (20 ms increments up to WaitInMS) and guards with IsNotNanoServerAndRemoteExecutorSupported.
  • StartDetached_StartsAndExitsSuccessfully provides a lightweight smoke test.
  • StartDetached_ThrowsOnUseShellExecute covers the validation path.
  • Windows SendSignal correctly discards entireProcessGroup with a documented explanation.

💡 Consider a Simpler Test for Basic StartDetached Functionality

The existing test (StartDetached_GrandchildSurvivesSignalingParent) covers the complex grandchild-survival scenario. A si...

The StartDetached_StartsAndExitsSuccessfully smoke test was added in commit 820aef5 and is already present in the current PR.

CopilotAI review requested due to automatic review settings April 10, 2026 14:08

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated 1 comment.

Comment threadsrc/libraries/System.Diagnostics.Process/tests/ProcessTests.cs Outdated
Comment threadsrc/libraries/System.Diagnostics.Process/tests/ProcessTests.cs Outdated
Co-authored-by: Jan Kotas <jkotas@microsoft.com>
Co-authored-by: Adam Sitnik <adam.sitnik@gmail.com>

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated 2 comments.

@github-actions

Copy link
Copy Markdown
Contributor

🤖 Copilot Code Review — PR #126632

Note

This review was generated by GitHub Copilot (Claude Opus 4.6). Full source files, git history, API approval issue, and existing review comments were analyzed.

Holistic Assessment

Motivation: Adding ProcessStartInfo.StartDetached addresses a well-known, legitimate need — starting daemon-like processes that are detached from the parent's session/console and survive parent termination. The approved API issue (#125838) demonstrates clear real-world use cases. Justified. ✅

Approach: The implementation correctly uses platform-appropriate mechanisms (DETACHED_PROCESS on Windows, POSIX_SPAWN_SETSID on macOS, setsid() on Linux). The single boolean property is minimal, consistent with existing ProcessStartInfo patterns (CreateNoWindow, CreateNewProcessGroup), and the defaulting of stdio to the null device is the right behavior for detached processes.

Summary: ⚠️Needs Human Review. The implementation is sound — the API matches the approved shape from issue #125838, native code is correct, and tests cover the critical scenarios. I'm flagging for human review because of a handle ownership pattern divergence between Process.Start() and SafeProcessHandle.Start() that, while functionally correct due to SafeHandle.Dispose() idempotency, warrants a human judgment call on whether to align the patterns.


Detailed Findings

✅ API Approval — Verified against issue #125838

The api-approved label was applied by @bartonjs on 2025-03-24. The approved API shape includes:

publicsealedpartialclassProcessStartInfo{publicboolStartDetached{get;set;}}

The implementation matches exactly — no extra public surface, correct property signature, correct placement in alphabetical order in the ref assembly (after StandardOutputHandle, before UseCredentialsForNetworkingOnly). ✅


✅ Native Implementation — Correct platform mechanisms

  • macOS (posix_spawn path): Uses POSIX_SPAWN_SETSID flag (inside #if defined(TARGET_OSX) block). This is an Apple-specific extension available since macOS 10.14; .NET requires macOS 10.15+, so always available. ✅
  • Linux (fork path): Calls setsid() after fork, before exec (pal_process.c). The freshly forked child always has PID ≠ PGID, so setsid() will succeed. Placement after dup2 but before credential setup and exec is correct. ✅
  • WASI stub: Updated with (void)startDetached to suppress unused parameter warning. ✅

✅ Windows Implementation — Correct flag usage

DETACHED_PROCESS = 0x00000008 is correctly OR-ed into creation flags. The decision to allow DETACHED_PROCESS + CREATE_NO_WINDOW together was explicitly reviewed and approved by @jkotas (commit 389dcf7 removed the CreateNoWindow guard, noting the two flags are compatible per Windows API semantics — DETACHED_PROCESS takes precedence). ✅


✅ Validation Logic — Correct and complete

  • StartDetached + UseShellExecute correctly throws InvalidOperationException
  • Error message stored in .resx following conventions ✅
  • The StartDetached parameter threads correctly through the entire call chain: ProcessStartInfoSafeProcessHandle.ForkAndExecProcess → P/Invoke → pal_process.c

⚠️ Handle Ownership — Inconsistent pattern between Process.Start() and SafeProcessHandle.Start()

SafeProcessHandle.Start() uses the clean using pattern:

usingSafeFileHandle?nullDeviceHandle=startInfo.StartDetached&&(childInputHandleisnull||childOutputHandleisnull||childErrorHandleisnull)?File.OpenNullHandle():null;

The handle is owned by a single using variable and disposed exactly once.

Process.Start() assigns the same File.OpenNullHandle() result to up to three variables (childInputHandle, childOutputHandle, childErrorHandle), each then independently disposed in the finally block, resulting in up to three Dispose() calls on the same SafeFileHandle.

This is functionally safeSafeHandle.Dispose() is idempotent by design; subsequent calls are no-ops. The finally block comment even acknowledges non-owning handles. I also see that @adamsitnik specifically requested this pattern in review: "call File.OpenNullHandle() at max once, ensure it gets disposed after process is started."

However, the shared-handle-with-multiple-dispose pattern in Process.Start() is inconsistent with the cleaner SafeProcessHandle.Start() approach. A human reviewer should confirm whether this divergence is intentional or whether a dedicated nullDeviceHandle local with using (matching the other call site) would be preferable.

Not blocking — code is correct as-is.


✅ Test Quality — Well-designed coverage

Three tests cover the critical scenarios:

  1. StartDetached_ThrowsOnUseShellExecute — Validates the incompatibility check via SafeProcessHandle.Start(). ✅
  2. StartDetached_StartsAndExitsSuccessfully — Smoke test verifying a detached process runs and exits normally. ✅
  3. StartDetached_GrandchildSurvivesSignalingParent[Theory] with true/false testing that a detached grandchild survives process-group kill while a non-detached one is killed. Good use of RemoteExecutor, proper cleanup via finally { grandchild.Kill(); }, and appropriate conditional attributes. ✅

The 300ms timeout for the "alive" assertion is reasonable since the grandchild does Thread.Sleep(Timeout.Infinite) — it cannot exit on its own, and setsid() ensures the SIGKILL to the parent's process group doesn't reach it. ✅


💡 XML Documentation — Minor clarity opportunity

The XML docs on StartDetached reference StandardInputHandle, StandardOutputHandle, StandardErrorHandle as properties that override null device defaulting. When using Process.Start() (the more common API path), RedirectStandardInput/RedirectStandardOutput/RedirectStandardError also override the null device behavior. The docs already mention these in the first <para>, but the "unless explicitly configured" phrasing in the second <para> could be slightly more inclusive.

Minor — not blocking.

Generated by Code Review for issue #126632 ·

@adamsitnik

Copy link
Copy Markdown
Member

/ba-g unrelated

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Process]: Start detached process

4 participants

@adamsitnik@jkotas