File: Processes\IsolatedProcess.Unix.cs
Web Access
Project: src\src\Aspire.Cli\Aspire.Cli.csproj (aspire)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Diagnostics;
using System.Globalization;
 
namespace Aspire.Cli.Processes;
 
internal sealed partial class IsolatedProcess
{
    private static async Task<StartedProcess> StartDetachedUnixAsync(
        IsolatedProcessStartInfo startInfo,
        CancellationToken cancellationToken)
    {
        if (startInfo.DetachedUnixLauncherPath is null)
        {
            throw new InvalidOperationException("Unix detached process launch requires a DCP executable path.");
        }
 
        var dcpStartInfo = new ProcessStartInfo
        {
            FileName = startInfo.DetachedUnixLauncherPath,
            UseShellExecute = false,
            CreateNoWindow = true,
            RedirectStandardOutput = true,
            RedirectStandardError = true,
            RedirectStandardInput = false,
            WorkingDirectory = startInfo.WorkingDirectory
        };
 
        dcpStartInfo.ArgumentList.Add("fork-process");
        dcpStartInfo.ArgumentList.Add("--monitor");
        dcpStartInfo.ArgumentList.Add(Environment.ProcessId.ToString(CultureInfo.InvariantCulture));
        dcpStartInfo.ArgumentList.Add("--monitor-identity-time");
        dcpStartInfo.ArgumentList.Add(ProcessTreeGracefulShutdownService.FormatDcpProcessStartTime(GetCurrentProcessDcpMonitorStartTime()));
        dcpStartInfo.ArgumentList.Add("--");
        dcpStartInfo.ArgumentList.Add(startInfo.FileName);
        foreach (var arg in startInfo.ArgumentList)
        {
            dcpStartInfo.ArgumentList.Add(arg);
        }
 
        ProcessEnvironment.ApplyTo(dcpStartInfo, startInfo.GetEnvironmentForSpawn());
 
        cancellationToken.ThrowIfCancellationRequested();
 
        var dcpProcess = Process.Start(dcpStartInfo)
            ?? throw new InvalidOperationException("Failed to start DCP fork-process.");
 
        var stderrTask = dcpProcess.StandardError.ReadToEndAsync(CancellationToken.None);
        var stdoutLineTask = dcpProcess.StandardOutput.ReadLineAsync(CancellationToken.None).AsTask();
 
        try
        {
            // Once DCP has started, wait for it to report the detached child PID even if the caller
            // cancels. Without the PID, callers cannot clean up a child that was already forked.
            var stdoutLine = await stdoutLineTask.ConfigureAwait(false);
            if (stdoutLine is null)
            {
                await dcpProcess.WaitForExitAsync(CancellationToken.None).ConfigureAwait(false);
                var stderr = await stderrTask.ConfigureAwait(false);
                throw new InvalidOperationException($"DCP fork-process did not return a child process ID. DCP fork-process exited with code {dcpProcess.ExitCode}. stderr: '{stderr.Trim()}'");
            }
 
            var trimmedStdout = stdoutLine.Trim();
            // DCP fork-process writes only the detached child PID followed by a newline, for example:
            //   12345
            if (!int.TryParse(trimmedStdout, NumberStyles.None, CultureInfo.InvariantCulture, out var childPid))
            {
                throw new InvalidOperationException($"DCP fork-process did not return a valid child process ID. stdout: '{trimmedStdout}'");
            }
 
            ObserveDcpForkProcessStderr(stderrTask);
 
            Process? childProcess;
            try
            {
                childProcess = Process.GetProcessById(childPid);
            }
            catch (ArgumentException)
            {
                // A short-lived detached child can exit and be reaped by the DCP monitor between
                // DCP printing its PID and this parent opening a Process handle. In that case the
                // monitor process is the only remaining handle that can report the child's exit code.
                await dcpProcess.WaitForExitAsync(CancellationToken.None).ConfigureAwait(false);
                return new StartedProcess(
                    dcpProcess,
                    TextReader.Null,
                    TextReader.Null,
                    ExtraDispose: null,
                    ExitCodeProvider: () => dcpProcess.ExitCode,
                    HasExitedProvider: () => true,
                    WaitForExitProvider: dcpProcess.WaitForExitAsync,
                    UseProvidedStartTime: true,
                    ProcessId: childPid);
            }
 
            return new StartedProcess(
                childProcess,
                TextReader.Null,
                TextReader.Null,
                ExtraDispose: () =>
                {
                    dcpProcess.Dispose();
                    return ValueTask.CompletedTask;
                },
                ExitCodeProvider: () => dcpProcess.HasExited ? dcpProcess.ExitCode : childProcess.ExitCode,
                HasExitedProvider: () => dcpProcess.HasExited,
                WaitForExitProvider: dcpProcess.WaitForExitAsync,
                StartTime: GetStartTime(childProcess),
                UseProvidedStartTime: true,
                ProcessId: childPid);
        }
        catch
        {
            if (!dcpProcess.HasExited)
            {
                dcpProcess.Kill(entireProcessTree: true);
                await dcpProcess.WaitForExitAsync(CancellationToken.None).ConfigureAwait(false);
            }
 
            dcpProcess.Dispose();
            throw;
        }
    }
 
    private static void ObserveDcpForkProcessStderr(Task<string> stderrTask)
    {
        _ = stderrTask.ContinueWith(
            static (completedTask, state) =>
            {
                if (!completedTask.IsCompletedSuccessfully)
                {
                    _ = completedTask.Exception;
                    return;
                }
 
                _ = completedTask.Result;
            },
            state: null,
            CancellationToken.None,
            TaskContinuationOptions.ExecuteSynchronously,
            TaskScheduler.Default);
    }
 
    internal static DateTimeOffset GetCurrentProcessDcpMonitorStartTime()
    {
        if (OperatingSystem.IsLinux())
        {
            // DCP's Linux process identity is the /proc/<pid>/stat start tick converted to
            // milliseconds since boot and represented as Go's zero time plus that duration.
            // Build the same timestamp instead of using Process.StartTime, which is derived from
            // wall-clock boot time and can drift after clock adjustments.
            return DateTimeOffset.MinValue.AddMilliseconds(ProcessStartTimeHelper.GetCurrentProcessStartTimeUnixMilliseconds());
        }
 
        return ProcessStartTimeHelper.GetCurrentProcessStartTime();
    }
}