| File: Processes\OrphanDetectionEnvironment.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.Globalization; using Aspire.Hosting; namespace Aspire.Cli.Processes; /// <summary> /// Stamps a launching process's identity (PID plus a stable start-time value) into a child process's /// environment so the child's parent-liveness watchdog / orphan detector can verify the parent by PID /// <em>and</em> start time and therefore survive PID reuse. Centralizes the env-var writing that would /// otherwise be duplicated at every process-launch site. /// </summary> internal static class OrphanDetectionEnvironment { /// <summary> /// Stamps the current CLI process's identity under the given key names, defaulting to the CLI /// orphan-detection keys (<see cref="KnownConfigNames.CliProcessId"/> / /// <see cref="KnownConfigNames.CliProcessStarted"/>). /// </summary> /// <param name="environment">The child environment to stamp.</param> /// <param name="pidKey">The variable name to write the parent PID under.</param> /// <param name="startedKey">The variable name to write the parent start time under.</param> /// <param name="overwrite"> /// When <see langword="true"/> (the default) existing values are replaced. /// When <see langword="false"/> a value the caller already set is preserved. /// </param> public static void ApplyCurrentProcess( IDictionary<string, string> environment, string pidKey = KnownConfigNames.CliProcessId, string startedKey = KnownConfigNames.CliProcessStarted, bool overwrite = true) { // Widening a non-null-valued dictionary to the nullable-valued signature is safe: Apply only // ever writes non-null values, so the caller's non-null contract is never violated. Apply((IDictionary<string, string?>)environment, Environment.ProcessId, ProcessStartTimeHelper.GetCurrentProcessStartTimeUnixMilliseconds(), pidKey, startedKey, overwrite); } /// <summary> /// Stamps a specific process's identity, using an already-resolved <paramref name="stableStartTimeUnixMilliseconds"/>. /// Accepting the start time (rather than resolving it) lets a caller write the same identity under /// several key pairs while only reading the start time once. The nullable value type matches /// <see cref="System.Diagnostics.ProcessStartInfo.Environment"/> so it can be stamped directly. /// </summary> /// <param name="environment">The child environment to stamp.</param> /// <param name="pid">The parent process id.</param> /// <param name="stableStartTimeUnixMilliseconds"> /// The parent's stable start time in Unix milliseconds, or <see langword="null"/> when it could /// not be read. When <see langword="null"/> only the PID is written; the watchdog then falls back /// to a PID-only existence check. /// </param> /// <param name="pidKey">The variable name to write the parent PID under.</param> /// <param name="startedKey">The variable name to write the parent start time under.</param> /// <param name="overwrite"> /// When <see langword="true"/> (the default) existing values are replaced. /// When <see langword="false"/> caller-supplied values are preserved. /// </param> public static void Apply( IDictionary<string, string?> environment, int pid, long? stableStartTimeUnixMilliseconds, string pidKey, string startedKey, bool overwrite = true) { var pidWritten = overwrite || !environment.ContainsKey(pidKey); if (pidWritten) { environment[pidKey] = pid.ToString(CultureInfo.InvariantCulture); } else { // Env var already exists and we are not allowed to overwrite it. return; } var isCliParentIdentity = pidKey == KnownConfigNames.CliProcessId && startedKey == KnownConfigNames.CliProcessStarted; // For the CLI parent identity, ASPIRE_CLI_STARTED is the value AppHosts <= Aspire version 13.4 // verify with their Process.StartTime-based check, so it MUST stay in whole Unix seconds. // Every other identity's primary key carries the stable millisecond value directly. // We need to use correct units here. long? startedValue = null; if (stableStartTimeUnixMilliseconds is { } stableStartedValue) { if (isCliParentIdentity) { startedValue = ProcessStartTimeHelper.TryGetRuntimeProcessStartTimeUnixSeconds(pid); } else { startedValue = stableStartedValue; } } // The start time can be unavailable (target already exited, privileged, etc.). When replacing // the PID, remove any inherited start time that cannot be replaced so the child does not verify // a mismatched PID/start-time identity. if (startedValue is { } started) { if (overwrite || !environment.ContainsKey(startedKey)) { environment[startedKey] = started.ToString(CultureInfo.InvariantCulture); } } else if (overwrite) { environment.Remove(startedKey); } if (isCliParentIdentity) { if (stableStartTimeUnixMilliseconds is { } stableStarted) { if (overwrite || !environment.ContainsKey(KnownConfigNames.CliProcessStartedStable)) { // ASPIRE_CLI_STARTED_STABLE is the millisecond-precision companion current AppHosts prefer: // it survives wall-clock steps and gives exact PID-reuse detection. // AppHosts <= Aspire ver 13.4 ignore it and fall back to the seconds-based ASPIRE_CLI_STARTED above. environment[KnownConfigNames.CliProcessStartedStable] = stableStarted.ToString(CultureInfo.InvariantCulture); } } else if (overwrite) { environment.Remove(KnownConfigNames.CliProcessStartedStable); } } } }