Skip to content

[Process]: Start detached process #124334

@adamsitnik

Description

@adamsitnik

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 (like Arguments, 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.

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions