dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
14.95k stars 4.65k forks source link

Suggestion: make Process.StartInfo a best-effort API #102766

Open am11 opened 3 months ago

am11 commented 3 months ago

Currently, this API throws if the process is not self or a child of current process.

System.InvalidOperationException: Process was not started by this object, so requested information cannot be determined.

Other tools, such as ps(1), provide information about the process in a "best effort" manner, e.g.

#!/bin/sh

ps ax -o pid,comm,args | while read pid comm args; do
  if [ "$pid" = 1 ]; then
    printf '%s\t%s\t%s\n' "$pid" "$comm" "$args"
  fi
done

gives info about the init process on Linux, macOS and FreeBSD alike, regardless of the current user privileges. In contrast, while this program will output the process name of the non-self, non-children processes:

Process process = Process.GetProcessById(1);
Console.WriteLine(process.ProcessName);

we cannot get the process path or arguments which ps(1) offers.

ps: there is a macOS-sepcific quirk that we can use process.MainModule.FullName to find the path, but MainModule is null for "not owned by us" process on Linux and FreeBSD


FWIW, I was trying to make this implementation of UnixInitSystem detection robust for case where multiple init systems are installed (and one of them is active); using Process:

using System;
using System.IO;

UnixInitSystem initSystem = InitSystemDetector.Detect();
Console.WriteLine($"Init system detected: {initSystem}");

/// <summary>
/// Represents the different types of Unix initialization (init) systems.
/// </summary>
public enum UnixInitSystem
{
    /// <summary>
    /// Init system could not be determined, possibly in a container environment.
    /// </summary>
    Unknown,

    /// <summary>
    /// BSD-style init system.
    /// </summary>
    BSD,

    /// <summary>
    /// BusyBox init system.
    /// </summary>
    BusyBox,

    /// <summary>
    /// EInit init system.
    /// </summary>
    EInit,

    /// <summary>
    /// Launchd init system (macOS).
    /// </summary>
    Launchd,

    /// <summary>
    /// Monit process supervision.
    /// </summary>
    Monit,

    /// <summary>
    /// Mudar init system.
    /// </summary>
    Mudar,

    /// <summary>
    /// OpenRC init system.
    /// </summary>
    OpenRC,

    /// <summary>
    /// Runit init system.
    /// </summary>
    Runit,

    /// <summary>
    /// Service Management Facility, Solaris.
    /// </summary>
    SMF,

    /// <summary>
    /// Systemd init system.
    /// </summary>
    Systemd,

    /// <summary>
    /// System V init system.
    /// </summary>
    SystemV,

    /// <summary>
    /// Upstart init system.
    /// </summary>
    Upstart
}

/// <summary>
/// Provides functionality to detect the Unix initialization (init) system.
/// </summary>
public static class InitSystemDetector
{
    /// <summary>
    /// Detects the Unix init system currently in use.
    /// </summary>
    /// <returns>
    /// A <see cref="UnixInitSystem"/> value representing the detected init system.
    /// </returns>
    public static UnixInitSystem Detect()
    {
        if (File.Exists("/sbin/einit") || File.Exists("/etc/einit/einit.conf"))
        {
            return UnixInitSystem.EInit;
        }

        if (File.Exists("/etc/rc") && File.Exists("/etc/rc.subr"))
        {
            return UnixInitSystem.BSD;
        }

        if (File.Exists("/sbin/init") && new FileInfo("/sbin/init").LinkTarget == "/bin/busybox")
        {
            return UnixInitSystem.BusyBox;
        }

        if (File.Exists("/sbin/launchd") && Directory.Exists("/Library/LaunchDaemons"))
        {
            return UnixInitSystem.Launchd;
        }

        if (File.Exists("/etc/monitrc") || Directory.Exists("/etc/monit.d"))
        {
            return UnixInitSystem.Monit;
        }

        if (File.Exists("/sbin/mudar"))
        {
            return UnixInitSystem.Mudar;
        }

        if (Directory.Exists("/etc/init.d") && File.Exists("/sbin/openrc-init"))
        {
            return UnixInitSystem.OpenRC;
        }

        if (Directory.Exists("/etc/sv") && Directory.Exists("/etc/service") && File.Exists("/sbin/runsvdir"))
        {
            return UnixInitSystem.Runit;
        }

        if (Directory.Exists("/lib/svc") && File.Exists("/usr/sbin/svcadm"))
        {
            return UnixInitSystem.SMF;
        }

        if (Directory.Exists("/run/systemd/system") && File.Exists("/sbin/init") && File.ReadAllText("/proc/1/comm").Trim() == "systemd")
        {
            return UnixInitSystem.Systemd;
        }

        if (File.Exists("/sbin/init") && File.Exists("/etc/inittab"))
        {
            return UnixInitSystem.SystemV;
        }

        if (File.Exists("/sbin/initctl") && Directory.Exists("/etc/init"))
        {
            return UnixInitSystem.Upstart;
        }

        return UnixInitSystem.Unknown;
    }
}

guess I can shell out to ps(1) directly for this.

dotnet-policy-service[bot] commented 3 months ago

Tagging subscribers to this area: @dotnet/area-system-serviceprocess See info in area-owners.md if you want to be subscribed.

dotnet-policy-service[bot] commented 2 months ago

Tagging subscribers to this area: @dotnet/area-system-diagnostics-process See info in area-owners.md if you want to be subscribed.

adamsitnik commented 1 month ago

Triage: If it's possible to provide more information rather than just throw, we should improve our implementation and just do that. Since I have no free cycles right now the best I can do is to mark it as "help wanted".

tmds commented 1 month ago

The current design is that ProcessStartInfo gathers the information needed to start a process, and information about a running process is retrieved through properties of the Process class. I think we should stick to those responsibilities.

@am11 what information do you need that is currently not accessible through Process properties. I assume it is mostly about arguments? What could the API look like on the Process class?

ps: there is a macOS-sepcific quirk that we can use process.MainModule.FullName to find the path, but MainModule is null for "not owned by us" process on Linux and FreeBSD

Should/can we address this?

am11 commented 1 month ago

The current design is that ProcessStartInfo gathers the information needed to start a process, and information about a running process is retrieved through properties of the Process class. I think we should stick to those responsibilities.

ProcessStartInfo StartInfo is also a property on Process class. Eagerly throwing exception where OS and other tools have no problem, is not a sound design worth defending. I think if we can improve it, we should.

tmds commented 1 month ago

Currently ProcessStartInfo works as an argument list for Start and it can only be retrieved if the process was started by the Process instance. The semantics are clear.

I think it would be good to understand what information users are looking for and how well we can support it on different OSes. Then we can know if ProcessStartInfo is a good fit.

am11 commented 1 month ago

I think it would be good to understand what information users are looking for and how well we can support it on different OSes. Then we can know if ProcessStartInfo is a good fit.

I was seeking the name and arguments of external process (pid:1 init in my case). If certain information is inaccessible on a platform, the corresponding API should throw an exception, as it currently does. This issue might also occur if the user is running the app in a resource-restricted environment on a platform that typically supports accessing the information. Therefore, handling this at runtime on a best-effort basis is the appropriate approach.

tmds commented 1 month ago

ps: there is a macOS-sepcific quirk that we can use process.MainModule.FullName to find the path, but MainModule is null for "not owned by us" process on Linux and FreeBSD

If Linux would behave similar to macOS, you'd be able to do what you need?

am11 commented 1 month ago

Yup, it seems ps ax -o pid,comm,args is returning the expected values so if we can make it return the process path and arguments (when possible) that would be enough. macOS also return process path and arguments, but we currently throw from StartInfo.Arguments:

$ uname -a
Darwin 87-92-225-103.rev.dnainternet.fi 23.5.0 Darwin Kernel Version 23.5.0: Wed May  1 20:12:58 PDT 2024; root:xnu-10063.121.3~5/RELEASE_ARM64_T6000 arm64

$  ps ax -o pid,comm,args | while read pid comm args; do
  if [ "$pid" = 1 ]; then
    printf '%s\t%s\t%s\n' "$pid" "$comm" "$args"
  fi
done

1   /sbin/launchd   /sbin/launchd

vs.

$ cat Program.cs
using System.Diagnostics;

Process initProcess = Process.GetProcessById(1);
Console.WriteLine($"ProcessPath: {initProcess.MainModule?.FileName}");
Console.WriteLine($"ArgumentsLength: {initProcess.StartInfo.Arguments.Length}");

$ dotnet run
ProcessPath: /sbin/launchd
Unhandled exception. System.InvalidOperationException: Process was not started by this object, so requested information cannot be determined.
   at System.Diagnostics.Process.get_StartInfo()
   at Program.<Main>$(String[] args) in /Users/am11/projects/startinfo-shenanigans/Program.cs:line 5