-
Notifications
You must be signed in to change notification settings - Fork 5.4k
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.