Repository navigation
[Process]: Start detached process #124334
Description
Background and motivation
There is currently no easy, cross-platform way to start a detached process in .NET. A detached process is one that:
- Does not belong to the parent process's process group
- Continues running after the parent process exits
- Has its standard input/output/error disconnected from the parent
This is a common requirement for scenarios such as:
- Starting long-running server applications or daemons
- Launching GUI applications from console tools
- Creating background services that outlive the parent process
Currently, achieving this requires platform-specific workarounds like invoking shell commands like nohup <command> & through /bin/sh, which requires concatenating arguments as strings and loses the ability to obtain the child process ID (see #104210)
These workarounds are error-prone, not portable, and don't provide consistent behavior across platforms.
API Proposal
The proposal is to extend ProcessStartOptions with a new StartDetached property that indicates the process should be started in a detached manner. The proposal also includes moving StartSuspended method to the option bag and making it a property as well (so both can be composed).
namespace System.Diagnostics;
{
public sealed class ProcessStartOption
{
// Starts a new detached process with standard input, output, and error redirected to NUL.
// On Windows, the process is started with DETACHED_PROCESS and CREATE_NEW_PROCESS_GROUP flags.
// On macOS, the process is started with POSIX_SPAWN_SETSID.
// On other Unix systems, the process calls setsid() after fork and before exec.
+ public bool StartDetached { get; set; }
// Starts the process in a suspended state.
+ public bool StartSuspended { get; set; }
}
}
namespace Microsoft.Win32.SafeHandles
{
public partial class SafeProcessHandle
{
public static SafeProcessHandle Start(ProcessStartOptions options, SafeFileHandle? input, SafeFileHandle? output, SafeFileHandle? error);
- public static SafeProcessHandle StartSuspended(ProcessStartOptions options, SafeFileHandle? input, SafeFileHandle? output, SafeFileHandle? error);
public void Resume();
}
}Usage Example
The following example demonstrates how to start a detached process:
using Microsoft.Win32.SafeHandles;
using System.Diagnostics;
ProcessStartOptions options = new("myserver")
{
Arguments = { "--port", "8080" },
StartDetached = true
};
SafeFileHandle nullHandle = File.OpenNullHandle();
SafeProcessHandle handle = SafeProcessHandle.Start(options, input: nullHandle, output: nullHandle, error: nullHandle);Alternative Designs
My main goals for ProcessStartOptions is to keep it simple and consistent across different operating systems.
But some of the features of process creation are specific to certain platforms. For example, Windows has the DETACHED_PROCESS flag, while Unix-like systems use setsid(). And at the same time, I really don't want this type to become a mess like ProcessStartInfo with a lot of properties that only apply to certain platforms.
So please consider following alternative designs for exposing more advanced OS-specific options for process creation, while keeping the main API simple and cross-platform.
Platform-specific derived option bags
The idea is following:
- keep all cross-platform options in
ProcessStartOptions(likeArguments,WorkingDirectory, etc.) - provide two separate types for advanced OS-specific options. One for Unix and another for Windows.
- the Unix option bag has to expose all the features that can be configured only from the calling process (for example
setsid) so we can call them after fork and before exec.
namespace System.Diagnostics;
public class ProcessStartOptions
{
// Cross-platform options for process creation
public string FileName { get; }
public IList<string> Arguments { get; set; }
public IDictionary<string, string?> Environment { get; }
public string? WorkingDirectory { get; set; }
public bool KillOnParentExit { get; set; }
public IList<SafeHandle> InheritedHandles { get; set; }
public bool StartSuspended { get; set; }
public ProcessStartOptions(string fileName);
}
[UnsupportedOSPlatform("windows")]
public sealed class UnixProcessStartOptions : ProcessStartOptions
{
public bool CreateNewProcessGroup { get; set; } // setpgid
public bool StartNewSession { get; set; } // setsid
// In the future, we could add more Unix-specific options here, such as:
public int? UserId { get; set; } // setuid
public int? GroupId { get; set; } // setgid
// Linux-specific options, could be added here as well.
[SupportedOSPlatform("linux")]]
public string? RootDirectory { get; set; } // chroot
public UnixProcessStartOptions(string fileName);
public UnixProcessStartOptions(ProcessStartOptions source);
}
[SupportedOSPlatform("windows")]
public sealed class WindowsProcessStartOptions : ProcessStartOptions
{
public WindowsProcessCreationFlags CreationFlags { get; set; }
// In the future, we could add more Windows-specific options here, such as:
public IList<SafeHandle> JobHandles { get; set; }
// And provide input for UpdateProcThreadAttribute
public List<(WindowsProcessAttribute attribute, GCHandle value, nint byteSize)> ProcessAttributes { get; set; }
public WindowsProcessStartOptions(string fileName);
public WindowsProcessStartOptions(ProcessStartOptions source);
}
[Flags]
public enum WindowsProcessCreationFlags // #71515
{
CREATE_BREAKAWAY_FROM_JOB,
CREATE_DEFAULT_ERROR_MODE,
CREATE_NEW_CONSOLE, // #71515
CREATE_NEW_PROCESS_GROUP, // ProcessStartInfo.CreateNewProcessGroup
CREATE_NO_WINDOW, // ProcessStartInfo.CreateNoWindow
CREATE_PROTECTED_PROCESS,
CREATE_PRESERVE_CODE_AUTHZ_LEVEL,
CREATE_SECURE_PROCESS,
CREATE_SEPARATE_WOW_VDM,
CREATE_SUSPENDED, // #94127
CREATE_UNICODE_ENVIRONMENT,
DEBUG_ONLY_THIS_PROCESS, // #71515
DEBUG_PROCESS, // #71515
DETACHED_PROCESS, // this issue
EXTENDED_STARTUPINFO_PRESENT,
INHERIT_PARENT_AFFINITY
}
public enum WindowsProcessAttribute
{
PROC_THREAD_ATTRIBUTE_GROUP_AFFINITY,
// PROC_THREAD_ATTRIBUTE_HANDLE_LIST, exposed via ProcessStartOptions.InheritedHandles
PROC_THREAD_ATTRIBUTE_IDEAL_PROCESSOR,
PROC_THREAD_ATTRIBUTE_MACHINE_TYPE,
PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY,
PROC_THREAD_ATTRIBUTE_PARENT_PROCESS,
PROC_THREAD_ATTRIBUTE_PREFERRED_NODE,
PROC_THREAD_ATTRIBUTE_UMS_THREAD,
PROC_THREAD_ATTRIBUTE_SECURITY_CAPABILITIES,
PROC_THREAD_ATTRIBUTE_PROTECTION_LEVEL,
PROC_THREAD_ATTRIBUTE_CHILD_PROCESS_POLICY,
PROC_THREAD_ATTRIBUTE_DESKTOP_APP_POLICY,
// PROC_THREAD_ATTRIBUTE_JOB_LIST, exposed via WindowsProcessStartOptions.JobHandles
PROC_THREAD_ATTRIBUTE_ENABLE_OPTIONAL_XSTATE_FEATURES
}Sample usage:
ProcessStartOptions options = new("myserver") { Arguments = { "--port", "8080" } };
options = OperatingSystem.IsWindows()
? new WindowsProcessStartOptions(options)
{
CreationFlags = WindowsProcessCreationFlags.DETACHED_PROCESS
}
: new UnixProcessStartOptions(options)
{
StartNewSession = true
};
SafeFileHandle nullHandle = File.OpenNullHandle();
SafeProcessHandle handle = SafeProcessHandle.Start(options, input: nullHandle, output: nullHandle, error: nullHandle);This design is more complex and less discoverable than the previous one, but it has the advantage of keeping the main ProcessStartOptions type clean and focused on cross-platform features. Also, all the information can be easily obtained through the getters.
List of advanced options
An abstract class, with public static factory methods for creating instances of AdvancedProcessStartOptions with specific features enabled. For example:
Details
namespace System.Diagnostics;
public sealed class ProcessStartOptions
{
// Simple and cross-platform
public string FileName { get; }
public IList<string> Arguments { get; set; }
public IDictionary<string, string?> Environment { get; }
public string? WorkingDirectory { get; set; }
public bool KillOnParentExit { get; set; }
public IList<AdvancedProcessStartOptions> AdvancedOptions { get; set; }
public ProcessStartOptions(string fileName);
}
public abstract class AdvancedProcessStartOptions
{
// Factory methods for creating internal types that derive from AdvancedProcessStartOptions
public static AdvancedProcessStartOptions InheritHandles(IList<SafeHandle> additionalHandles);
public static AdvancedProcessStartOptions StartSuspended();
public static AdvancedProcessStartOptions StartDetached();
[SupportedOSPlatform("windows")]
public static AdvancedProcessStartOptions WindowsCreationFlags(WindowsProcessCreationFlags flags);
[UnsupportedOSPlatform("windows")]
public static AdvancedProcessStartOptions CreateNewProcessGroup();
[UnsupportedOSPlatform("windows")]
public static AdvancedProcessStartOptions StartNewSession();
[SupportedOSPlatform("linux")]
public static AdvancedProcessStartOptions RootDirectory(string chroot);
}Usage:
ProcessStartOptions options = new("myserver")
{
Arguments = { "--port", "8080" },
AdvancedOptions =
{
AdvancedProcessStartOptions.StartSuspended(),
AdvancedProcessStartOptions.InheritHandles(new[] { someHandle })
}
};
SafeFileHandle nullHandle = File.OpenNullHandle();
SafeProcessHandle handle = SafeProcessHandle.Start(options, input: nullHandle, output: nullHandle, error: nullHandle);The API would be more discoverable than the previous design, and it would allow for better composition of different advanced options. However, it would require creating and JITing a lot of small internal types that derive from AdvancedProcessStartOptions. And it would just move the complexity from one place to another (with the benefit of hiding it from 95% of the users). Not to mention the lack of ability to read the advanced options back from the ProcessStartOptions instance, which could be a problem for some scenarios.
Builder for advanced options
A builder type that allows users to fluently configure advanced options for process creation.
Details
namespace System.Diagnostics;
public sealed class ProcessStartOptions
{
// Simple and cross-platform
public string FileName { get; }
public IList<string> Arguments { get; set; }
public IDictionary<string, string?> Environment { get; }
public string? WorkingDirectory { get; set; }
public bool KillOnParentExit { get; set; }
public AdvancedProcessStartOptionsBuilder Advanced { get; set; }
public ProcessStartOptions(string fileName);
}
public class AdvancedProcessStartOptionsBuilder
{
// Factory methods for creating internal types that derive from AdvancedProcessStartOptions
public AdvancedProcessStartOptionsBuilder InheritHandles(IList<SafeHandle> additionalHandles);
public AdvancedProcessStartOptionsBuilder StartSuspended();
[SupportedOSPlatform("windows")]
public AdvancedProcessStartOptionsBuilder WindowsCreationFlags(WindowsProcessCreationFlags flags);
[UnsupportedOSPlatform("windows")]
public AdvancedProcessStartOptionsBuilder CreateNewProcessGroup();
[UnsupportedOSPlatform("windows")]
public AdvancedProcessStartOptionsBuilder StartNewSession();
[SupportedOSPlatform("linux")]
public AdvancedProcessStartOptionsBuilder RootDirectory(string chroot);
}Usage:
ProcessStartOptions options = new("myserver") { Arguments = { "--port", "8080" } };
options.Advanced
.StartSuspended()
.InheritHandles(new[] { someHandle });
SafeFileHandle nullHandle = File.OpenNullHandle();
SafeProcessHandle handle = SafeProcessHandle.Start(options, input: nullHandle, output: nullHandle, error: nullHandle);The ProcessStartOptions would still be clean and focused on cross-platform features, while the advanced options would be discoverable through the builder. The builder would most likely require fewer internal types than the previous design, as it could be implemented as a single type with properties for each advanced option. But there would be no public API for reading these options, which could be a problem for some scenarios.
Risks
Starting a detached process is advanced scenario, when used incorrectly, it can lead to issues such as orphaned processes that continue running indefinitely.
Activity
adamsitnik commentedon Feb 12, 2026
cc @davidfowl
dotnet-policy-service commentedon Feb 12, 2026
Tagging subscribers to this area: @dotnet/area-system-diagnostics-process
See info in area-owners.md if you want to be subscribed.
jkotas commentedon Feb 12, 2026
Should StartSuspended, StartDetached, ... really be separate methods? It feels like these should be properties in ProcessStartOptions, so you can e.g. start a process that is both suspended and detached.
adamsitnik commentedon Feb 12, 2026
Great question!
This was my initial design, but when I started providing the high-level helpers for most common-scenarios (#123959), I realized that each of these helpers would need to check if the option bag does not request a
Suspendedprocess and just throw in such case.Moreover, exposing these APIs as part of the option bag would "pollute" the API surface for the 99% of users who don't need to support these two niche (but valid and IMO important) scenarios.
jkotas commentedon Feb 12, 2026
ProcessStartOptions has other very niche features like InheritedHandles that 99% of users won't ever use. I think we struggling with deciding whether ProcessStartOptions should or should not have the niche features. It is a mix right now that leads to poor overall design.
adamsitnik commentedon Feb 12, 2026
I agree
InheritedHandlesare niche, but I can see some valid scenarios where they would be useful in high level helpers.FWIW I've mentioned this in the API proposal: #123380
jkotas commentedon Feb 12, 2026
Right, you can always find a case where somebody might want to use a niche feature together with high level helpers.
adamsitnik commentedon Feb 12, 2026
I was not sure myself, that is why I've mentioned this in the API proposal. I was hoping that we discuss it during the API review. Should I discuss it with API review board again? For example along with the current issue (StartDetached)?
stephentoub commentedon Feb 12, 2026
In general, the goal of API review isn't to question fundamental assertions about a feature being needed (though we often end up discussing that); rather, it's assumed that the area owners are asserting that such functionality is necessary, and then the discussion is about the right way to expose it.
jkotas commentedon Feb 12, 2026
I think StartSuspended should be re-reviewed together with StartDetached. StartSuspended + StartDetached is not composable pattern. I think it is flawed design. API composability is more important than whether we need to have an extra check in the implementation or whether somebody will see an extra property in IntelliSense.
adamsitnik commentedon Feb 18, 2026
StartDetachedis very specific, as it must not inherit any handles (the detached process could live way longer than the parent and hold plenty of resources opened, there are some security aspects involved as well).I agree that composability is very important, but I don't believe that we should expose two additional boolean properties that would be used by 0.01% of the users (and most likely almost never used together) at the cost of polluting the option bag api surface for 99.99% of other users. I really want to avoid the situation where people want to try to use
ProcessStartOptionsfor the first time and they thinks it's just anotherProcessStartInfowith a lipstick on a pig.Let's bring it to the API review and discuss in detail.
7 remaining items
jkotas commentedon Mar 16, 2026
It looks odd to me to separate Windows and Unix, and lump Linux and macOS together. If we are not doing specific types for every OS, I think we should go with one type for all advances options.
We can expose this callback and require people to implement this callback in unmanaged code. We have existing APIs like that. For example,. callbacks provided for
ObjectiveCMarshal.Initializemust be implemented in unmanaged code.Cross-platform does not mean latest Windows/Linux/macOS only. Does it really well work cross-platform, including other Unix flavors? I think you need specialized syscalls like Linux
close_rangeto make it work well.adamsitnik commentedon Mar 16, 2026
I agree, it's definitely not a perfect solution. I would love to hear more from others.
It does, because the goal of this API is to ensure that the new process is going to derive the provided handles. When it comes to not deriving other handles, it's best effort attempt (works on every macOS, modern Linux, modern Android and modern FreeBSD) and will be documented as such. FWIW all modern programming languages (golang, rust) do that already.
For my education. how
delegate* unmanagedworks exactly? If we have code like this:Can we somehow ensure that
RequireUnreferencedCodeis pre-compiled and invoking it does not introduce any JIT or allocations? Or do we somehow need to load the native library and get a reference to its method?jkotas commentedon Mar 16, 2026
We can ensure that we will crash deterministically when you run your code snippet.
There is no way to write a code like that in C# today. When I said unmanaged code, I meant unmanaged code (like C/C++).
bartonjs commentedon Mar 17, 2026
Video
The discussion on this made us seem like we're already regretting ProcessStartOptions, and maybe it should go back to ProcessStartInfo and be rethought from there.
adamsitnik commentedon Mar 25, 2026
Update: the API got approved in #125838 (comment)
It's blocked by #13943
Coming soon (famous last words)
artdedeco commentedon Apr 7, 2026
Hey folks, I was reading up on this and I'm a bit confused from following the discussion so far; is the intention of this new API to have
StartDetachedstart the new process such that its lifetime is independent of the parent process, in a cross-platform way? The docs for the property in the OP suggests the answer is yes, but from reading the discussion I'm not sure.adamsitnik commentedon Apr 7, 2026
Yes, this is the intention.