diff --git a/src/Common/src/Interop/Linux/procfs/Interop.ProcFsStat.cs b/src/Common/src/Interop/Linux/procfs/Interop.ProcFsStat.cs index c41415a7ec65..66637a3a1457 100644 --- a/src/Common/src/Interop/Linux/procfs/Interop.ProcFsStat.cs +++ b/src/Common/src/Interop/Linux/procfs/Interop.ProcFsStat.cs @@ -15,12 +15,14 @@ internal static partial class procfs { internal const string RootPath = "/proc/"; private const string ExeFileName = "/exe"; + private const string CmdLineFileName = "/cmdline"; private const string StatFileName = "/stat"; private const string MapsFileName = "/maps"; private const string FileDescriptorDirectoryName = "/fd/"; private const string TaskDirectoryName = "/task/"; internal const string SelfExeFilePath = RootPath + "self" + ExeFileName; + internal const string SelfCmdLineFilePath = RootPath + "self" + CmdLineFileName; internal const string ProcStatFilePath = RootPath + "stat"; internal struct ParsedStat @@ -31,7 +33,7 @@ internal struct ParsedStat // the MoveNext() with the appropriate ParseNext* call and assignment. internal int pid; - internal string comm; + // internal string comm; internal char state; internal int ppid; //internal int pgrp; @@ -87,6 +89,11 @@ internal static string GetExeFilePathForProcess(int pid) return RootPath + pid.ToString(CultureInfo.InvariantCulture) + ExeFileName; } + internal static string GetCmdLinePathForProcess(int pid) + { + return RootPath + pid.ToString(CultureInfo.InvariantCulture) + CmdLineFileName; + } + internal static string GetStatFilePathForProcess(int pid) { return RootPath + pid.ToString(CultureInfo.InvariantCulture) + StatFileName; @@ -231,7 +238,7 @@ internal static bool TryParseStatFile(string statFilePath, out ParsedStat result var results = default(ParsedStat); results.pid = parser.ParseNextInt32(); - results.comm = parser.MoveAndExtractNextInOuterParens(); + parser.MoveAndExtractNextInOuterParens(extractValue: false); // comm results.state = parser.ParseNextChar(); results.ppid = parser.ParseNextInt32(); parser.MoveNextOrFail(); // pgrp diff --git a/src/Common/src/System/IO/StringParser.cs b/src/Common/src/System/IO/StringParser.cs index d79d7ce2e82e..b94a7000d876 100644 --- a/src/Common/src/System/IO/StringParser.cs +++ b/src/Common/src/System/IO/StringParser.cs @@ -98,7 +98,7 @@ public string MoveAndExtractNext() /// in the string. The extracted value will be everything between (not including) those parentheses. /// /// - public string MoveAndExtractNextInOuterParens() + public string MoveAndExtractNextInOuterParens(bool extractValue = true) { // Move to the next position MoveNextOrFail(); @@ -118,7 +118,7 @@ public string MoveAndExtractNextInOuterParens() } // Extract the contents of the parens, then move our ending position to be after the paren - string result = _buffer.Substring(_startIndex + 1, lastParen - _startIndex - 1); + string result = extractValue ? _buffer.Substring(_startIndex + 1, lastParen - _startIndex - 1) : null; _endIndex = lastParen + 1; return result; diff --git a/src/Common/tests/Tests/Interop/procfsTests.cs b/src/Common/tests/Tests/Interop/procfsTests.cs index 53aa6ae8d421..7fd7647410fd 100644 --- a/src/Common/tests/Tests/Interop/procfsTests.cs +++ b/src/Common/tests/Tests/Interop/procfsTests.cs @@ -49,7 +49,6 @@ public static void ParseValidStatFiles_Success( Assert.True(Interop.procfs.TryParseStatFile(path, out result, new ReusableTextReader())); Assert.Equal(expectedPid, result.pid); - Assert.Equal(expectedComm, result.comm); Assert.Equal(expectedState, result.state); Assert.Equal(expectedSession, result.session); Assert.Equal(expectedUtime, result.utime); diff --git a/src/System.Diagnostics.Process/src/System/Diagnostics/Process.Linux.cs b/src/System.Diagnostics.Process/src/System/Diagnostics/Process.Linux.cs index eca3afd10600..013418acf92d 100644 --- a/src/System.Diagnostics.Process/src/System/Diagnostics/Process.Linux.cs +++ b/src/System.Diagnostics.Process/src/System/Diagnostics/Process.Linux.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Buffers; using System.Collections.Generic; using System.ComponentModel; using System.Globalization; @@ -29,11 +30,9 @@ public static Process[] GetProcessesByName(string processName, string machineNam var processes = new List(); foreach (int pid in ProcessManager.EnumerateProcessIds()) { - Interop.procfs.ParsedStat parsedStat; - if (Interop.procfs.TryReadStatFile(pid, out parsedStat, reusableReader) && - string.Equals(processName, parsedStat.comm, StringComparison.OrdinalIgnoreCase)) + if (string.Equals(processName, Process.GetProcessName(pid), StringComparison.OrdinalIgnoreCase)) { - ProcessInfo processInfo = ProcessManager.CreateProcessInfo(parsedStat, reusableReader); + ProcessInfo processInfo = ProcessManager.CreateProcessInfo(pid, reusableReader, processName); processes.Add(new Process(machineName, false, processInfo.ProcessId, processInfo)); } } @@ -256,6 +255,74 @@ internal static string GetExePath(int processId = -1) return Interop.Sys.ReadLink(exeFilePath); } + /// Gets the name that was used to start the process, or null if it could not be retrieved. + /// The pid for the target process, or -1 for the current process. + internal static string GetProcessName(int processId = -1) + { + string cmdLineFilePath = processId == -1 ? + Interop.procfs.SelfCmdLineFilePath : + Interop.procfs.GetCmdLinePathForProcess(processId); + + byte[] rentedArray = null; + try + { + // bufferSize == 1 used to avoid unnecessary buffer in FileStream + using (var fs = new FileStream(cmdLineFilePath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 1, useAsync: false)) + { + Span buffer = stackalloc byte[512]; + int bytesRead = 0; + while (true) + { + // Resize buffer if it was too small. + if (bytesRead == buffer.Length) + { + uint newLength = (uint)buffer.Length * 2; + + byte[] tmp = ArrayPool.Shared.Rent((int)newLength); + buffer.CopyTo(tmp); + byte[] toReturn = rentedArray; + buffer = rentedArray = tmp; + if (rentedArray != null) + { + ArrayPool.Shared.Return(toReturn); + } + } + + Debug.Assert(bytesRead < buffer.Length); + int n = fs.Read(buffer.Slice(bytesRead)); + bytesRead += n; + + // cmdline contains the argv array separated by '\0' bytes. + // we determine the process name using argv[0]. + int argv0End = buffer.Slice(0, bytesRead).IndexOf((byte)'\0'); + if (argv0End != -1) + { + // Strip directory names from argv[0]. + int nameStart = buffer.Slice(0, argv0End).LastIndexOf((byte)'/') + 1; + + return Encoding.UTF8.GetString(buffer.Slice(nameStart, argv0End - nameStart)); + } + + if (n == 0) + { + return null; + } + } + } + } + catch (IOException) + { + return null; + } + finally + { + if (rentedArray != null) + { + ArrayPool.Shared.Return(rentedArray); + } + } + } + // ---------------------------------- // ---- Unix PAL layer ends here ---- // ---------------------------------- diff --git a/src/System.Diagnostics.Process/src/System/Diagnostics/ProcessManager.Linux.cs b/src/System.Diagnostics.Process/src/System/Diagnostics/ProcessManager.Linux.cs index 69800f10b0fb..53e2c4586f97 100644 --- a/src/System.Diagnostics.Process/src/System/Diagnostics/ProcessManager.Linux.cs +++ b/src/System.Diagnostics.Process/src/System/Diagnostics/ProcessManager.Linux.cs @@ -108,7 +108,7 @@ internal static ProcessModuleCollection GetModules(int processId) /// /// Creates a ProcessInfo from the specified process ID. /// - internal static ProcessInfo CreateProcessInfo(int pid, ReusableTextReader reusableReader = null) + internal static ProcessInfo CreateProcessInfo(int pid, ReusableTextReader reusableReader = null, string processName = null) { if (reusableReader == null) { @@ -117,21 +117,21 @@ internal static ProcessInfo CreateProcessInfo(int pid, ReusableTextReader reusab Interop.procfs.ParsedStat stat; return Interop.procfs.TryReadStatFile(pid, out stat, reusableReader) ? - CreateProcessInfo(stat, reusableReader) : + CreateProcessInfo(stat, reusableReader, processName) : null; } /// /// Creates a ProcessInfo from the data parsed from a /proc/pid/stat file and the associated tasks directory. /// - internal static ProcessInfo CreateProcessInfo(Interop.procfs.ParsedStat procFsStat, ReusableTextReader reusableReader) + internal static ProcessInfo CreateProcessInfo(Interop.procfs.ParsedStat procFsStat, ReusableTextReader reusableReader, string processName) { int pid = procFsStat.pid; var pi = new ProcessInfo() { ProcessId = pid, - ProcessName = procFsStat.comm, + ProcessName = processName ?? Process.GetProcessName(pid) ?? string.Empty, BasePriority = (int)procFsStat.nice, VirtualBytes = (long)procFsStat.vsize, WorkingSet = procFsStat.rss * Environment.SystemPageSize, diff --git a/src/System.Diagnostics.Process/tests/ProcessTestBase.cs b/src/System.Diagnostics.Process/tests/ProcessTestBase.cs index 8fc68010c392..9b15ab99324e 100644 --- a/src/System.Diagnostics.Process/tests/ProcessTestBase.cs +++ b/src/System.Diagnostics.Process/tests/ProcessTestBase.cs @@ -100,6 +100,16 @@ protected void StartSleepKillWait(Process p) /// /// protected static bool IsProgramInstalled(string program) + { + return GetProgramPath(program) != null; + } + + /// + /// Return program path + /// + /// + /// + protected static string GetProgramPath(string program) { string path; string pathEnvVar = Environment.GetEnvironmentVariable("PATH"); @@ -113,11 +123,11 @@ protected static bool IsProgramInstalled(string program) path = Path.Combine(subPath, program); if (File.Exists(path)) { - return true; + return path; } } } - return false; + return null; } } } diff --git a/src/System.Diagnostics.Process/tests/ProcessTests.cs b/src/System.Diagnostics.Process/tests/ProcessTests.cs index fe0897f4129b..191830beb5c2 100644 --- a/src/System.Diagnostics.Process/tests/ProcessTests.cs +++ b/src/System.Diagnostics.Process/tests/ProcessTests.cs @@ -1875,6 +1875,37 @@ public void TestLongProcessIsWorking() Assert.True(p.HasExited); } + [PlatformSpecific(TestPlatforms.AnyUnix)] + [ActiveIssue(37054, TestPlatforms.OSX)] + [Fact] + public void LongProcessNamesAreSupported() + { + string programPath = GetProgramPath("sleep"); + + if (programPath == null) + { + return; + } + + const string LongProcessName = "123456789012345678901234567890"; + string sleepCommandPathFileName = Path.Combine(TestDirectory, LongProcessName); + File.Copy(programPath, sleepCommandPathFileName); + + using (Process px = Process.Start(sleepCommandPathFileName, "600")) + { + Process[] runningProcesses = Process.GetProcesses(); + try + { + Assert.Contains(runningProcesses, p => p.ProcessName == LongProcessName); + } + finally + { + px.Kill(); + px.WaitForExit(); + } + } + } + private string GetCurrentProcessName() { return $"{Process.GetCurrentProcess().ProcessName}.exe";