Linux: determine ProcessName using /proc cmdline to avoid truncated names (dotnet...
authorTom Deseyn <tom.deseyn@gmail.com>
Thu, 25 Apr 2019 15:36:23 +0000 (17:36 +0200)
committerStephen Toub <stoub@microsoft.com>
Thu, 25 Apr 2019 15:36:22 +0000 (11:36 -0400)
* Linux: determine ProcessName using /proc cmdline to avoid truncated names

* PR feedback

* LongProcessNamesAreSupported test: ensure Process gets killed when Assert throws

Commit migrated from https://github.com/dotnet/corefx/commit/4214b09864bea0416f17727ffa39a55d950035d1

src/libraries/Common/src/Interop/Linux/procfs/Interop.ProcFsStat.cs
src/libraries/Common/src/System/IO/StringParser.cs
src/libraries/Common/tests/Tests/Interop/procfsTests.cs
src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Linux.cs
src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessManager.Linux.cs
src/libraries/System.Diagnostics.Process/tests/ProcessTestBase.cs
src/libraries/System.Diagnostics.Process/tests/ProcessTests.cs

index c41415a..66637a3 100644 (file)
@@ -15,12 +15,14 @@ internal static partial class Interop
     {
         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 static partial class Interop
             // 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 partial class Interop
             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 partial class Interop
             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
index d79d7ce..b94a700 100644 (file)
@@ -98,7 +98,7 @@ namespace System.IO
         /// in the string.  The extracted value will be everything between (not including) those parentheses.
         /// </summary>
         /// <returns></returns>
-        public string MoveAndExtractNextInOuterParens()
+        public string MoveAndExtractNextInOuterParens(bool extractValue = true)
         {
             // Move to the next position
             MoveNextOrFail();
@@ -118,7 +118,7 @@ namespace System.IO
             }
 
             // 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;
index 53aa6ae..7fd7647 100644 (file)
@@ -49,7 +49,6 @@ namespace Common.Tests
                 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);
index eca3afd..013418a 100644 (file)
@@ -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 @@ namespace System.Diagnostics
             var processes = new List<Process>();
             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 @@ namespace System.Diagnostics
             return Interop.Sys.ReadLink(exeFilePath);
         }
 
+        /// <summary>Gets the name that was used to start the process, or null if it could not be retrieved.</summary>
+        /// <param name="processId">The pid for the target process, or -1 for the current process.</param>
+        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<byte> 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<byte>.Shared.Rent((int)newLength);
+                            buffer.CopyTo(tmp);
+                            byte[] toReturn = rentedArray;
+                            buffer = rentedArray = tmp;
+                            if (rentedArray != null)
+                            {
+                                ArrayPool<byte>.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<byte>.Shared.Return(rentedArray);
+                }
+            }
+        }
+
         // ----------------------------------
         // ---- Unix PAL layer ends here ----
         // ----------------------------------
index 69800f1..53e2c45 100644 (file)
@@ -108,7 +108,7 @@ namespace System.Diagnostics
         /// <summary>
         /// Creates a ProcessInfo from the specified process ID.
         /// </summary>
-        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 @@ namespace System.Diagnostics
 
             Interop.procfs.ParsedStat stat;
             return Interop.procfs.TryReadStatFile(pid, out stat, reusableReader) ?
-                CreateProcessInfo(stat, reusableReader) :
+                CreateProcessInfo(stat, reusableReader, processName) :
                 null;
         }
 
         /// <summary>
         /// Creates a ProcessInfo from the data parsed from a /proc/pid/stat file and the associated tasks directory.
         /// </summary>
-        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,
index 8fc6801..9b15ab9 100644 (file)
@@ -101,6 +101,16 @@ namespace System.Diagnostics.Tests
         /// <returns></returns>
         protected static bool IsProgramInstalled(string program)
         {
+            return GetProgramPath(program) != null;
+        }
+
+        /// <summary>
+        /// Return program path
+        /// </summary>
+        /// <param name="program"></param>
+        /// <returns></returns>
+        protected static string GetProgramPath(string program)
+        {
             string path;
             string pathEnvVar = Environment.GetEnvironmentVariable("PATH");
             char separator = PlatformDetection.IsWindows ? ';' : ':';
@@ -113,11 +123,11 @@ namespace System.Diagnostics.Tests
                     path = Path.Combine(subPath, program);
                     if (File.Exists(path))
                     {
-                        return true;
+                        return path;
                     }
                 }
             }
-            return false;
+            return null;
         }
     }
 }
index fe0897f..191830b 100644 (file)
@@ -1875,6 +1875,37 @@ namespace System.Diagnostics.Tests
             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";