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

Commit c034b13

Browse files
committed
Merge pull request #2188 from stephentoub/linux_modules
Implement basic System.Diagnostics.Process.Modules support on Linux
2 parents 010d9b9 + 301de75 commit c034b13

File tree

6 files changed

+163
-22
lines changed

6 files changed

+163
-22
lines changed

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

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
// Copyright (c) Microsoft. All rights reserved.
22
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
33

4+
using System;
5+
using System.Collections.Generic;
6+
using System.ComponentModel;
7+
using System.Diagnostics;
48
using System.Globalization;
59
using System.IO;
610

@@ -12,6 +16,7 @@ internal static partial class procfs
1216
internal const string SelfExeFilePath = RootPath + "self/exe";
1317
internal const string ProcUptimeFilePath = RootPath + "uptime";
1418
private const string StatFileName = "/stat";
19+
private const string MapsFileName = "/maps";
1520
private const string TaskDirectoryName = "/task/";
1621

1722
internal struct ParsedStat
@@ -67,16 +72,104 @@ internal struct ParsedStat
6772
//internal long cguest_time;
6873
}
6974

75+
internal struct ParsedMapsModule
76+
{
77+
internal string FileName;
78+
internal KeyValuePair<long, long> AddressRange;
79+
}
80+
7081
internal static string GetStatFilePathForProcess(int pid)
7182
{
7283
return RootPath + pid.ToString(CultureInfo.InvariantCulture) + StatFileName;
7384
}
7485

86+
internal static string GetMapsFilePathForProcess(int pid)
87+
{
88+
return RootPath + pid.ToString(CultureInfo.InvariantCulture) + MapsFileName;
89+
}
90+
7591
internal static string GetTaskDirectoryPathForProcess(int pid)
7692
{
7793
return RootPath + pid.ToString(CultureInfo.InvariantCulture) + TaskDirectoryName;
7894
}
7995

96+
internal static IEnumerable<ParsedMapsModule> ParseMapsModules(int pid)
97+
{
98+
try
99+
{
100+
return ParseMapsModulesCore(File.ReadLines(GetMapsFilePathForProcess(pid)));
101+
}
102+
catch (FileNotFoundException) { }
103+
catch (DirectoryNotFoundException) { }
104+
catch (UnauthorizedAccessException) { }
105+
106+
return Array.Empty<ParsedMapsModule>();
107+
}
108+
109+
private static IEnumerable<ParsedMapsModule> ParseMapsModulesCore(IEnumerable<string> lines)
110+
{
111+
Debug.Assert(lines != null);
112+
113+
// Parse each line from the maps file into a ParsedMapsModule result
114+
foreach (string line in lines)
115+
{
116+
// Use a StringParser to avoid string.Split costs
117+
var parser = new StringParser(line, separator: ' ', skipEmpty: true);
118+
119+
// Parse the address range
120+
KeyValuePair<long, long> addressRange =
121+
parser.ParseRaw(delegate (string s, ref int start, ref int end)
122+
{
123+
long startingAddress = 0, endingAddress = 0;
124+
int pos = s.IndexOf('-', start, end - start);
125+
if (pos > 0)
126+
{
127+
string startingString = s.Substring(start, pos);
128+
if (long.TryParse(startingString, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out startingAddress))
129+
{
130+
string endingString = s.Substring(pos + 1, end - (pos + 1));
131+
long.TryParse(endingString, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out endingAddress);
132+
}
133+
}
134+
return new KeyValuePair<long, long>(startingAddress, endingAddress);
135+
});
136+
137+
// Parse the permissions (we only care about entries with 'r' and 'x' set)
138+
if (!parser.ParseRaw(delegate (string s, ref int start, ref int end)
139+
{
140+
bool sawRead = false, sawExec = false;
141+
for (int i = start; i < end; i++)
142+
{
143+
if (s[i] == 'r')
144+
sawRead = true;
145+
else if (s[i] == 'x')
146+
sawExec = true;
147+
}
148+
return sawRead & sawExec;
149+
}))
150+
{
151+
continue;
152+
}
153+
154+
// Skip past the offset, dev, and inode fields
155+
parser.MoveNext();
156+
parser.MoveNext();
157+
parser.MoveNext();
158+
159+
// Parse the pathname
160+
if (!parser.MoveNext())
161+
{
162+
continue;
163+
}
164+
string pathname = parser.ExtractCurrent();
165+
166+
// We only get here if a we have a non-empty pathname and
167+
// the permissions included both readability and executability.
168+
// Yield the result.
169+
yield return new ParsedMapsModule { FileName = pathname, AddressRange = addressRange };
170+
}
171+
}
172+
80173
private static string GetStatFilePathForThread(int pid, int tid)
81174
{
82175
// Perf note: Calling GetTaskDirectoryPathForProcess will allocate a string,

src/System.Diagnostics.Process/src/System/Diagnostics/Process.Unix.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,11 @@ public bool WaitForExitCore(int milliseconds)
8585
/// <summary>Gets the main module for the associated process.</summary>
8686
public ProcessModule MainModule
8787
{
88-
get { throw new PlatformNotSupportedException(); }
88+
get
89+
{
90+
ProcessModuleCollection pmc = Modules;
91+
return pmc.Count > 0 ? pmc[0] : null;
92+
}
8993
}
9094

9195
/// <summary>Checks whether the process has exited and updates state accordingly.</summary>

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

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
33

44
using System.Collections.Generic;
5+
using System.ComponentModel;
56
using System.Globalization;
67
using System.IO;
78

@@ -28,6 +29,52 @@ public static int[] GetProcessIds()
2829
return pids.ToArray();
2930
}
3031

32+
/// <summary>Gets an array of module infos for the specified process.</summary>
33+
/// <param name="processId">The ID of the process whose modules should be enumerated.</param>
34+
/// <returns>The array of modules.</returns>
35+
internal static ModuleInfo[] GetModuleInfos(int processId)
36+
{
37+
var modules = new List<ModuleInfo>();
38+
39+
// Process from the parsed maps file each entry representing a module
40+
foreach (Interop.procfs.ParsedMapsModule entry in Interop.procfs.ParseMapsModules(processId))
41+
{
42+
int sizeOfImage = (int)(entry.AddressRange.Value - entry.AddressRange.Key);
43+
44+
// A single module may be split across multiple map entries; consolidate based on
45+
// the name and address ranges of sequential entries.
46+
if (modules.Count > 0)
47+
{
48+
ModuleInfo mi = modules[modules.Count - 1];
49+
if (mi._fileName == entry.FileName &&
50+
((long)mi._baseOfDll + mi._sizeOfImage == entry.AddressRange.Key))
51+
{
52+
// Merge this entry with the previous one
53+
modules[modules.Count - 1]._sizeOfImage += sizeOfImage;
54+
continue;
55+
}
56+
}
57+
58+
// It's not a continuation of a previous entry but a new one: add it.
59+
modules.Add(new ModuleInfo()
60+
{
61+
_fileName = entry.FileName,
62+
_baseName = Path.GetFileName(entry.FileName),
63+
_baseOfDll = new IntPtr(entry.AddressRange.Key),
64+
_sizeOfImage = sizeOfImage,
65+
_entryPoint = IntPtr.Zero // unknown
66+
});
67+
}
68+
69+
// Return the set of modules found
70+
if (modules.Count == 0)
71+
{
72+
// Match Windows behavior when failing to enumerate modules
73+
throw new Win32Exception(SR.EnumProcessModuleFailed);
74+
}
75+
return modules.ToArray();
76+
}
77+
3178
// -----------------------------
3279
// ---- PAL layer ends here ----
3380
// -----------------------------

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,15 @@ private static ProcessInfo CreateProcessInfo(int pid)
7171
return procInfo;
7272
}
7373

74+
/// <summary>Gets an array of module infos for the specified process.</summary>
75+
/// <param name="processId">The ID of the process whose modules should be enumerated.</param>
76+
/// <returns>The array of modules.</returns>
77+
internal static ModuleInfo[] GetModuleInfos(int processId)
78+
{
79+
// We currently don't provide support for modules on OS X.
80+
return Array.Empty<ModuleInfo>();
81+
}
82+
7483
// ----------------------------------
7584
// ---- Unix PAL layer ends here ----
7685
// ----------------------------------

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

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -77,17 +77,6 @@ public static int GetProcessIdFromHandle(SafeProcessHandle processHandle)
7777
return (int)processHandle.DangerousGetHandle(); // not actually dangerous; just wraps a process ID
7878
}
7979

80-
/// <summary>Gets an array of module infos for the specified process.</summary>
81-
/// <param name="processId">The ID of the process whose modules should be enumerated.</param>
82-
/// <returns>The array of modules.</returns>
83-
public static ModuleInfo[] GetModuleInfos(int processId)
84-
{
85-
// Not currently supported, but we can simply return an empty array rather than throwing.
86-
// Could potentially be done via /proc/pid/maps and some heuristics to determine
87-
// which entries correspond to modules.
88-
return Array.Empty<ModuleInfo>();
89-
}
90-
9180
/// <summary>Gets whether the named machine is remote or local.</summary>
9281
/// <param name="machineName">The machine name.</param>
9382
/// <returns>true if the machine is remote; false if it's local.</returns>

src/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests/ProcessTest.cs

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -229,14 +229,11 @@ public void Process_MachineName()
229229
public void Process_MainModule()
230230
{
231231
// Get MainModule property from a Process object
232-
ProcessModule mainModule = null;
233-
if (global::Interop.IsWindows)
234-
{
235-
mainModule = _process.MainModule;
236-
}
237-
else
232+
ProcessModule mainModule = _process.MainModule;
233+
234+
if (!global::Interop.IsOSX) // OS X doesn't currently implement modules support
238235
{
239-
Assert.Throws<PlatformNotSupportedException>(() => _process.MainModule);
236+
Assert.NotNull(mainModule);
240237
}
241238

242239
if (mainModule != null)
@@ -338,11 +335,13 @@ public void Process_Modules()
338335
{
339336
// Validated that we can get a value for each of the following.
340337
Assert.NotNull(pModule);
341-
Assert.NotNull(pModule.BaseAddress);
342-
Assert.NotNull(pModule.EntryPointAddress);
338+
Assert.NotEqual(IntPtr.Zero, pModule.BaseAddress);
343339
Assert.NotNull(pModule.FileName);
344-
int memSize = pModule.ModuleMemorySize;
345340
Assert.NotNull(pModule.ModuleName);
341+
342+
// Just make sure these don't throw
343+
IntPtr addr = pModule.EntryPointAddress;
344+
int memSize = pModule.ModuleMemorySize;
346345
}
347346
}
348347

0 commit comments

Comments
 (0)