Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Commit 4214b09

Browse files
tmdsstephentoub
authored andcommitted
Linux: determine ProcessName using /proc cmdline to avoid truncated names (#37144)
* Linux: determine ProcessName using /proc cmdline to avoid truncated names * PR feedback * LongProcessNamesAreSupported test: ensure Process gets killed when Assert throws
1 parent eb2d732 commit 4214b09

File tree

7 files changed

+129
-15
lines changed

7 files changed

+129
-15
lines changed

src/Common/src/Interop/Linux/procfs/Interop.ProcFsStat.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@ internal static partial class procfs
1515
{
1616
internal const string RootPath = "/proc/";
1717
private const string ExeFileName = "/exe";
18+
private const string CmdLineFileName = "/cmdline";
1819
private const string StatFileName = "/stat";
1920
private const string MapsFileName = "/maps";
2021
private const string FileDescriptorDirectoryName = "/fd/";
2122
private const string TaskDirectoryName = "/task/";
2223

2324
internal const string SelfExeFilePath = RootPath + "self" + ExeFileName;
25+
internal const string SelfCmdLineFilePath = RootPath + "self" + CmdLineFileName;
2426
internal const string ProcStatFilePath = RootPath + "stat";
2527

2628
internal struct ParsedStat
@@ -31,7 +33,7 @@ internal struct ParsedStat
3133
// the MoveNext() with the appropriate ParseNext* call and assignment.
3234

3335
internal int pid;
34-
internal string comm;
36+
// internal string comm;
3537
internal char state;
3638
internal int ppid;
3739
//internal int pgrp;
@@ -87,6 +89,11 @@ internal static string GetExeFilePathForProcess(int pid)
8789
return RootPath + pid.ToString(CultureInfo.InvariantCulture) + ExeFileName;
8890
}
8991

92+
internal static string GetCmdLinePathForProcess(int pid)
93+
{
94+
return RootPath + pid.ToString(CultureInfo.InvariantCulture) + CmdLineFileName;
95+
}
96+
9097
internal static string GetStatFilePathForProcess(int pid)
9198
{
9299
return RootPath + pid.ToString(CultureInfo.InvariantCulture) + StatFileName;
@@ -231,7 +238,7 @@ internal static bool TryParseStatFile(string statFilePath, out ParsedStat result
231238
var results = default(ParsedStat);
232239

233240
results.pid = parser.ParseNextInt32();
234-
results.comm = parser.MoveAndExtractNextInOuterParens();
241+
parser.MoveAndExtractNextInOuterParens(extractValue: false); // comm
235242
results.state = parser.ParseNextChar();
236243
results.ppid = parser.ParseNextInt32();
237244
parser.MoveNextOrFail(); // pgrp

src/Common/src/System/IO/StringParser.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ public string MoveAndExtractNext()
9898
/// in the string. The extracted value will be everything between (not including) those parentheses.
9999
/// </summary>
100100
/// <returns></returns>
101-
public string MoveAndExtractNextInOuterParens()
101+
public string MoveAndExtractNextInOuterParens(bool extractValue = true)
102102
{
103103
// Move to the next position
104104
MoveNextOrFail();
@@ -118,7 +118,7 @@ public string MoveAndExtractNextInOuterParens()
118118
}
119119

120120
// Extract the contents of the parens, then move our ending position to be after the paren
121-
string result = _buffer.Substring(_startIndex + 1, lastParen - _startIndex - 1);
121+
string result = extractValue ? _buffer.Substring(_startIndex + 1, lastParen - _startIndex - 1) : null;
122122
_endIndex = lastParen + 1;
123123

124124
return result;

src/Common/tests/Tests/Interop/procfsTests.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ public static void ParseValidStatFiles_Success(
4949
Assert.True(Interop.procfs.TryParseStatFile(path, out result, new ReusableTextReader()));
5050

5151
Assert.Equal(expectedPid, result.pid);
52-
Assert.Equal(expectedComm, result.comm);
5352
Assert.Equal(expectedState, result.state);
5453
Assert.Equal(expectedSession, result.session);
5554
Assert.Equal(expectedUtime, result.utime);

src/System.Diagnostics.Process/src/System/Diagnostics/Process.Linux.cs

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// See the LICENSE file in the project root for more information.
44

55
using System;
6+
using System.Buffers;
67
using System.Collections.Generic;
78
using System.ComponentModel;
89
using System.Globalization;
@@ -29,11 +30,9 @@ public static Process[] GetProcessesByName(string processName, string machineNam
2930
var processes = new List<Process>();
3031
foreach (int pid in ProcessManager.EnumerateProcessIds())
3132
{
32-
Interop.procfs.ParsedStat parsedStat;
33-
if (Interop.procfs.TryReadStatFile(pid, out parsedStat, reusableReader) &&
34-
string.Equals(processName, parsedStat.comm, StringComparison.OrdinalIgnoreCase))
33+
if (string.Equals(processName, Process.GetProcessName(pid), StringComparison.OrdinalIgnoreCase))
3534
{
36-
ProcessInfo processInfo = ProcessManager.CreateProcessInfo(parsedStat, reusableReader);
35+
ProcessInfo processInfo = ProcessManager.CreateProcessInfo(pid, reusableReader, processName);
3736
processes.Add(new Process(machineName, false, processInfo.ProcessId, processInfo));
3837
}
3938
}
@@ -256,6 +255,74 @@ internal static string GetExePath(int processId = -1)
256255
return Interop.Sys.ReadLink(exeFilePath);
257256
}
258257

258+
/// <summary>Gets the name that was used to start the process, or null if it could not be retrieved.</summary>
259+
/// <param name="processId">The pid for the target process, or -1 for the current process.</param>
260+
internal static string GetProcessName(int processId = -1)
261+
{
262+
string cmdLineFilePath = processId == -1 ?
263+
Interop.procfs.SelfCmdLineFilePath :
264+
Interop.procfs.GetCmdLinePathForProcess(processId);
265+
266+
byte[] rentedArray = null;
267+
try
268+
{
269+
// bufferSize == 1 used to avoid unnecessary buffer in FileStream
270+
using (var fs = new FileStream(cmdLineFilePath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 1, useAsync: false))
271+
{
272+
Span<byte> buffer = stackalloc byte[512];
273+
int bytesRead = 0;
274+
while (true)
275+
{
276+
// Resize buffer if it was too small.
277+
if (bytesRead == buffer.Length)
278+
{
279+
uint newLength = (uint)buffer.Length * 2;
280+
281+
byte[] tmp = ArrayPool<byte>.Shared.Rent((int)newLength);
282+
buffer.CopyTo(tmp);
283+
byte[] toReturn = rentedArray;
284+
buffer = rentedArray = tmp;
285+
if (rentedArray != null)
286+
{
287+
ArrayPool<byte>.Shared.Return(toReturn);
288+
}
289+
}
290+
291+
Debug.Assert(bytesRead < buffer.Length);
292+
int n = fs.Read(buffer.Slice(bytesRead));
293+
bytesRead += n;
294+
295+
// cmdline contains the argv array separated by '\0' bytes.
296+
// we determine the process name using argv[0].
297+
int argv0End = buffer.Slice(0, bytesRead).IndexOf((byte)'\0');
298+
if (argv0End != -1)
299+
{
300+
// Strip directory names from argv[0].
301+
int nameStart = buffer.Slice(0, argv0End).LastIndexOf((byte)'/') + 1;
302+
303+
return Encoding.UTF8.GetString(buffer.Slice(nameStart, argv0End - nameStart));
304+
}
305+
306+
if (n == 0)
307+
{
308+
return null;
309+
}
310+
}
311+
}
312+
}
313+
catch (IOException)
314+
{
315+
return null;
316+
}
317+
finally
318+
{
319+
if (rentedArray != null)
320+
{
321+
ArrayPool<byte>.Shared.Return(rentedArray);
322+
}
323+
}
324+
}
325+
259326
// ----------------------------------
260327
// ---- Unix PAL layer ends here ----
261328
// ----------------------------------

src/System.Diagnostics.Process/src/System/Diagnostics/ProcessManager.Linux.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ internal static ProcessModuleCollection GetModules(int processId)
108108
/// <summary>
109109
/// Creates a ProcessInfo from the specified process ID.
110110
/// </summary>
111-
internal static ProcessInfo CreateProcessInfo(int pid, ReusableTextReader reusableReader = null)
111+
internal static ProcessInfo CreateProcessInfo(int pid, ReusableTextReader reusableReader = null, string processName = null)
112112
{
113113
if (reusableReader == null)
114114
{
@@ -117,21 +117,21 @@ internal static ProcessInfo CreateProcessInfo(int pid, ReusableTextReader reusab
117117

118118
Interop.procfs.ParsedStat stat;
119119
return Interop.procfs.TryReadStatFile(pid, out stat, reusableReader) ?
120-
CreateProcessInfo(stat, reusableReader) :
120+
CreateProcessInfo(stat, reusableReader, processName) :
121121
null;
122122
}
123123

124124
/// <summary>
125125
/// Creates a ProcessInfo from the data parsed from a /proc/pid/stat file and the associated tasks directory.
126126
/// </summary>
127-
internal static ProcessInfo CreateProcessInfo(Interop.procfs.ParsedStat procFsStat, ReusableTextReader reusableReader)
127+
internal static ProcessInfo CreateProcessInfo(Interop.procfs.ParsedStat procFsStat, ReusableTextReader reusableReader, string processName)
128128
{
129129
int pid = procFsStat.pid;
130130

131131
var pi = new ProcessInfo()
132132
{
133133
ProcessId = pid,
134-
ProcessName = procFsStat.comm,
134+
ProcessName = processName ?? Process.GetProcessName(pid) ?? string.Empty,
135135
BasePriority = (int)procFsStat.nice,
136136
VirtualBytes = (long)procFsStat.vsize,
137137
WorkingSet = procFsStat.rss * Environment.SystemPageSize,

src/System.Diagnostics.Process/tests/ProcessTestBase.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,16 @@ protected void StartSleepKillWait(Process p)
100100
/// <param name="program"></param>
101101
/// <returns></returns>
102102
protected static bool IsProgramInstalled(string program)
103+
{
104+
return GetProgramPath(program) != null;
105+
}
106+
107+
/// <summary>
108+
/// Return program path
109+
/// </summary>
110+
/// <param name="program"></param>
111+
/// <returns></returns>
112+
protected static string GetProgramPath(string program)
103113
{
104114
string path;
105115
string pathEnvVar = Environment.GetEnvironmentVariable("PATH");
@@ -113,11 +123,11 @@ protected static bool IsProgramInstalled(string program)
113123
path = Path.Combine(subPath, program);
114124
if (File.Exists(path))
115125
{
116-
return true;
126+
return path;
117127
}
118128
}
119129
}
120-
return false;
130+
return null;
121131
}
122132
}
123133
}

src/System.Diagnostics.Process/tests/ProcessTests.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1875,6 +1875,37 @@ public void TestLongProcessIsWorking()
18751875
Assert.True(p.HasExited);
18761876
}
18771877

1878+
[PlatformSpecific(TestPlatforms.AnyUnix)]
1879+
[ActiveIssue(37054, TestPlatforms.OSX)]
1880+
[Fact]
1881+
public void LongProcessNamesAreSupported()
1882+
{
1883+
string programPath = GetProgramPath("sleep");
1884+
1885+
if (programPath == null)
1886+
{
1887+
return;
1888+
}
1889+
1890+
const string LongProcessName = "123456789012345678901234567890";
1891+
string sleepCommandPathFileName = Path.Combine(TestDirectory, LongProcessName);
1892+
File.Copy(programPath, sleepCommandPathFileName);
1893+
1894+
using (Process px = Process.Start(sleepCommandPathFileName, "600"))
1895+
{
1896+
Process[] runningProcesses = Process.GetProcesses();
1897+
try
1898+
{
1899+
Assert.Contains(runningProcesses, p => p.ProcessName == LongProcessName);
1900+
}
1901+
finally
1902+
{
1903+
px.Kill();
1904+
px.WaitForExit();
1905+
}
1906+
}
1907+
}
1908+
18781909
private string GetCurrentProcessName()
18791910
{
18801911
return $"{Process.GetCurrentProcess().ProcessName}.exe";

0 commit comments

Comments
 (0)