< Summary - Results for net9.0, Release

Information
Class: LockCheck.Linux.ProcFileSystem
Assembly: LockCheck
File(s): /home/runner/work/LockCheck/LockCheck/src/LockCheck/Linux/ProcFileSystem.cs
Tag: 96_11660771111
Line coverage
92%
Covered lines: 156
Uncovered lines: 13
Coverable lines: 169
Total lines: 587
Line coverage: 92.3%
Branch coverage
88%
Covered branches: 127
Total branches: 144
Branch coverage: 88.1%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
GetAllProcesses()0%620%
GetProcessesByWorkingDirectory(...)100%88100%
EnumerateProcessIds()83.33%6.02692.31%
GetLockingProcessInfos(...)87.5%16.031695%
GetInodeToPaths(...)100%44100%
get_ProcMatchesPidNamespace()83.33%1212100%
TryGetProcPid(...)75%4.25475%
GetProcCmdline(...)100%22100%
GetProcExe(...)100%22100%
GetProcCwd(...)100%22100%
GetProcStat(...)100%22100%
GetProcDir(...)100%22100%
Exists(...)50%22100%
get_ParentProcessId()100%11100%
set_ParentProcessId(...)100%11100%
get_SessionId()100%11100%
set_SessionId(...)100%11100%
get_IsKernelThread()100%11100%
set_IsKernelThread(...)100%11100%
GetStat(...)95%20.062094.74%
GetProcessOwner(...)66.67%6.17683.33%
GetProcessStartTime(...)75%4.03487.5%
GetProcessCurrentDirectory(...)50%4.59466.67%
GetProcessCommandLineArgs(...)100%1010100%
ConvertToArgs(...)100%2222100%
GetProcessExecutablePath(...)50%4.59466.67%
GetProcessExecutablePathFromCmdLine(...)100%44100%
GetField(...)100%66100%

File(s)

/home/runner/work/LockCheck/LockCheck/src/LockCheck/Linux/ProcFileSystem.cs

#LineLine coverage
 1using System;
 2using System.Buffers;
 3using System.Collections.Generic;
 4using System.Diagnostics;
 5using System.Diagnostics.CodeAnalysis;
 6using System.Globalization;
 7using System.IO;
 8using System.Linq;
 9using System.Reflection.Metadata;
 10using System.Text;
 11using System.Threading;
 12
 13namespace LockCheck.Linux;
 14
 15internal static class ProcFileSystem
 16{
 17    private static volatile int s_procMatchesPidNamespace;
 18
 19    internal static HashSet<ILinuxProcessDetails> GetAllProcesses()
 20    {
 021        var result = new HashSet<ILinuxProcessDetails>();
 022        foreach (int processId in EnumerateProcessIds())
 23        {
 024            result.Add(new ProcInfo(processId));
 25        }
 026        return result;
 27    }
 28
 29    internal static Dictionary<(int, DateTime), ProcessInfo> GetProcessesByWorkingDirectory(List<string> directories)
 30    {
 231        var result = new Dictionary<(int, DateTime), ProcessInfo>();
 32
 233        foreach (int processId in EnumerateProcessIds())
 34        {
 235            var pi = new ProcInfo(processId);
 36
 237            if (!pi.HasError && !string.IsNullOrEmpty(pi.CurrentDirectory))
 38            {
 39                // If the process' current directory is the search path itself, or it is somewhere nested below it,
 40                // we have to take it into account. This will also account for differences in the two when the
 41                // search path does not end with a '/'.
 242                if (directories.FindIndex(d => pi.CurrentDirectory.StartsWith(d, StringComparison.Ordinal)) != -1)
 43                {
 244                    result[(processId, pi.StartTime)] = ProcessInfoLinux.Create(pi);
 45                }
 46            }
 47        }
 48
 249        return result;
 50    }
 51
 52    private static IEnumerable<int> EnumerateProcessIds()
 53    {
 54        // This is an attempt to handle what was outlined in "https://github.com/dotnet/runtime/pull/100076".
 55        // Basically, when a process runs inside a root-less container, it can happen that the PID namespaces
 56        // of the process itself and /proc don't match up. In this case, we cannot reliably get information
 57        // about other processes.
 258        if (!ProcMatchesPidNamespace)
 59        {
 060            yield return Environment.ProcessId;
 61        }
 62
 263        var options = new EnumerationOptions
 264        {
 265            IgnoreInaccessible = true,
 266            MatchCasing = MatchCasing.PlatformDefault,
 267            RecurseSubdirectories = false,
 268            ReturnSpecialDirectories = false
 269        };
 70
 271        foreach (string fullPath in Directory.EnumerateDirectories("/proc", "*", options))
 72        {
 273            if (int.TryParse(Path.GetFileName(fullPath.AsSpan()), NumberStyles.Integer, CultureInfo.InvariantCulture, ou
 74            {
 275                yield return processId;
 76            }
 77        }
 278    }
 79
 80    public static HashSet<ProcessInfo> GetLockingProcessInfos(string[] paths, [NotNullIfNotNull(nameof(directories))] re
 81    {
 282        if (paths == null)
 083            throw new ArgumentNullException(nameof(paths));
 84
 285        Dictionary<long, string>? inodesToPaths = null;
 286        var result = new HashSet<ProcessInfo>();
 87
 288        var xpaths = new HashSet<string>(paths.Length, StringComparer.Ordinal);
 89
 290        foreach (string path in paths)
 91        {
 92            // Get directories, but don't exclude them from lookup via procfs (in contrast to Windows).
 93            // On Linux /proc/locks may also contain directory locks.
 294            if (Directory.Exists(path))
 95            {
 296                directories?.Add(path);
 97            }
 98
 299            xpaths.Add(path);
 100        }
 101
 2102        using (var reader = new StreamReader("/proc/locks"))
 103        {
 104            string? line;
 2105            while ((line = reader.ReadLine()) != null)
 106            {
 2107                if (inodesToPaths == null)
 108                {
 2109                    inodesToPaths = GetInodeToPaths(xpaths);
 110                }
 111
 2112                var lockInfo = LockInfo.ParseLine(line);
 2113                if (inodesToPaths.ContainsKey(lockInfo.InodeInfo.INodeNumber))
 114                {
 2115                    var processInfo = ProcessInfoLinux.Create(lockInfo);
 2116                    if (processInfo != null)
 117                    {
 2118                        result.Add(processInfo);
 119                    }
 120                }
 121            }
 2122        }
 123
 2124        return result;
 125    }
 126
 127    private static Dictionary<long, string> GetInodeToPaths(HashSet<string> paths)
 128    {
 2129        var inodesToPaths = new Dictionary<long, string>();
 130
 2131        foreach (string path in paths)
 132        {
 2133            long inode = NativeMethods.GetInode(path);
 2134            if (inode != -1)
 135            {
 2136                inodesToPaths.Add(inode, path);
 137            }
 138        }
 139
 2140        return inodesToPaths;
 141    }
 142
 143    // Idea for handling of proc/pid-namespace mismatch is largely copied from dotnet/runtime.
 144
 145    internal static bool ProcMatchesPidNamespace
 146    {
 147        get
 148        {
 149            // s_procMatchesPidNamespace is set to:
 150            // - 0: when uninitialized,
 151            // - 1: '/proc' and the process pid namespace match,
 152            // - 2: when they don't match.
 2153            if (s_procMatchesPidNamespace == 0)
 154            {
 155                // '/proc/self' is a symlink to the pid used by '/proc' for the current process.
 156                // We compare it with the pid of the current process to see if the '/proc' and pid namespace match up.
 157
 2158                int? procSelfPid = null;
 159
 2160                if (Directory.ResolveLinkTarget("/proc/self", false)?.FullName is string target &&
 2161                    int.TryParse(Path.GetFileName(target), out int pid))
 162                {
 2163                    procSelfPid = pid;
 164                }
 165
 166                Debug.Assert(procSelfPid.HasValue);
 167
 2168                s_procMatchesPidNamespace = !procSelfPid.HasValue || procSelfPid == Environment.ProcessId ? 1 : 2;
 169            }
 2170            return s_procMatchesPidNamespace == 1;
 171        }
 172    }
 173
 174    internal enum ProcPid : int
 175    {
 176        Invalid = -1,
 177        Self = 0, // Current process: this will also work in root less containers, if accessed via /proc/self/...
 178        // Actual PIDs from /proc, cast as "ProcPid"
 179    }
 180
 181    internal static bool TryGetProcPid(int pid, out ProcPid procPid)
 182    {
 2183        if (pid == Environment.ProcessId)
 184        {
 185            // Use '/proc/self' for the current process.
 2186            procPid = ProcPid.Self;
 2187            return true;
 188        }
 189
 2190        if (ProcMatchesPidNamespace)
 191        {
 192            // Since namespaces match, we can handle any process.
 2193            procPid = (ProcPid)pid;
 2194            return true;
 195        }
 196
 197        // Cannot handle arbitrary processes when namespaces do not match.
 0198        procPid = ProcPid.Invalid;
 0199        return false;
 200    }
 201
 2202    private static string GetProcCmdline(ProcPid procPid) => procPid == ProcPid.Self ? "/proc/self/cmdline" : string.Cre
 2203    private static string GetProcExe(ProcPid procPid) => procPid == ProcPid.Self ? "/proc/self/exe" : string.Create(null
 2204    private static string GetProcCwd(ProcPid procPid) => procPid == ProcPid.Self ? "/proc/self/cwd" : string.Create(null
 2205    private static string GetProcStat(ProcPid procPid) => procPid == ProcPid.Self ? "/proc/self/stat" : string.Create(nu
 2206    private static string GetProcDir(ProcPid procPid) => procPid == ProcPid.Self ? "/proc/self" : string.Create(null, st
 207
 2208    internal static bool Exists(int processId) => TryGetProcPid(processId, out var procPid) && Directory.Exists(GetProcD
 209
 210    // TODO: As of now, we are reading the stat file for a process 3 times (sid, ppid and kthread).
 211    // We should really read the file only once. This would mean that the IsKernelThread(), GetParentProcessId()
 212    // and GetSessionId() methods need to be merged into a single one.
 213
 214    internal struct Stat
 215    {
 2216        public int? ParentProcessId { get; set; }
 2217        public int SessionId { get; set; }
 2218        public bool? IsKernelThread { get; set; }
 219    }
 220
 221    private enum StatFields
 222    {
 223        Ppid = 3,
 224        Sid = 5,
 225        Flags = 8,
 226
 227        Last = Flags
 228    }
 229
 230    internal static Stat GetStat(int processId)
 231    {
 2232        var result = new Stat();
 233
 2234        if (TryGetProcPid(processId, out var procPid))
 235        {
 2236            var stat = File.ReadAllText(GetProcStat(procPid)).AsSpan().Trim();
 237
 238#if NET9_0_OR_GREATER
 2239            int index = 0;
 2240            foreach (var range in stat.Split(' '))
 241            {
 2242                switch ((StatFields)index)
 243                {
 244                    case StatFields.Ppid:
 2245                        if (int.TryParse(stat[range], CultureInfo.InvariantCulture, out int ppid))
 246                        {
 2247                            result.ParentProcessId = ppid;
 248                        }
 2249                        break;
 250                    case StatFields.Sid:
 2251                        if (int.TryParse(stat[range], CultureInfo.InvariantCulture, out int sid))
 252                        {
 2253                            result.SessionId = sid;
 254                        }
 2255                        break;
 256                    case StatFields.Flags:
 2257                        if (int.TryParse(stat[range], CultureInfo.InvariantCulture, out int flags))
 258                        {
 259                            const int PF_KTHREAD = 0x0020_0000;
 2260                            result.IsKernelThread = (flags & PF_KTHREAD) == PF_KTHREAD;
 261                        }
 262                        break;
 263                }
 264
 2265                index++;
 2266                if (index > 8)
 267                {
 268                    break;
 269                }
 270            }
 271
 2272            if (index < (int)StatFields.Last)
 273            {
 0274                throw new IOException($"Expected at least {(int)StatFields.Last + 1} fields in /proc/{processId}/stat.")
 275            }
 276#else
 277            int fieldCount = stat.Count(' ') + 1;
 278            if (fieldCount < (int)StatFields.Last + 1)
 279            {
 280                throw new IOException($"Expected at least {(int)StatFields.Last + 1} fields in /proc/{processId}/stat.")
 281            }
 282
 283            // Need only as much ranges as we have fields. Rest of stat can be squished into a single, trailing range
 284            // which we'll never read.
 285            int rangeCount = (int)StatFields.Last + 2;
 286            Span<Range> ranges = rangeCount < 128 ? stackalloc Range[rangeCount] : new Range[rangeCount];
 287            int num = MemoryExtensions.Split(stat, ranges, ' ');
 288
 289            // Shouldn't trigger, because of pre-checks done above.
 290            Debug.Assert(num == rangeCount);
 291
 292            if (int.TryParse(stat[ranges[(int)StatFields.Ppid]], CultureInfo.InvariantCulture, out int ppid))
 293            {
 294                result.ParentProcessId = ppid;
 295            }
 296
 297            if (int.TryParse(stat[ranges[(int)StatFields.Sid]], CultureInfo.InvariantCulture, out int sid))
 298            {
 299                result.SessionId = sid;
 300            }
 301
 302            if (int.TryParse(stat[ranges[(int)StatFields.Flags]], CultureInfo.InvariantCulture, out int flags))
 303            {
 304                const int PF_KTHREAD = 0x0020_0000;
 305                result.IsKernelThread = (flags & PF_KTHREAD) == PF_KTHREAD;
 306            }
 307#endif
 308        }
 309
 2310        return result;
 311    }
 312
 313    //internal static bool? IsKernelThread(int processId)
 314    //{
 315    //    if (TryGetProcPid(processId, out var procPid))
 316    //    {
 317    //        if (procPid == ProcPid.Self)
 318    //        {
 319    //            // We are for sure not a kernel thread.
 320    //            return false;
 321    //        }
 322
 323    //        var content = File.ReadAllText(GetProcStat(procPid)).AsSpan().Trim();
 324    //        if (int.TryParse(GetField(content, ' ', 8).Trim(), CultureInfo.InvariantCulture, out int flags))
 325    //        {
 326    //            const int PF_KTHREAD = 0x0020_0000;
 327    //            return (flags & PF_KTHREAD) == PF_KTHREAD;
 328    //        }
 329    //    }
 330
 331    //    return null;
 332    //}
 333
 334    //internal static int? GetParentProcessId(int processId)
 335    //{
 336    //    if (TryGetProcPid(processId, out var procPid))
 337    //    {
 338    //        var content = File.ReadAllText(GetProcStat(procPid)).AsSpan().Trim();
 339    //        if (int.TryParse(GetField(content, ' ', 3).Trim(), CultureInfo.InvariantCulture, out int parentProcessId))
 340    //        {
 341    //            return parentProcessId;
 342    //        }
 343    //    }
 344
 345    //    return null;
 346    //}
 347
 348    //internal static int GetProcessSessionId(int processId)
 349    //{
 350    //    int sessionId = -1;
 351    //    if (TryGetProcPid(processId, out ProcPid procPid))
 352    //    {
 353    //        var content = File.ReadAllText(GetProcStat(procPid)).AsSpan().Trim();
 354    //        if (int.TryParse(GetField(content, ' ', 5).Trim(), CultureInfo.InvariantCulture, out int sid))
 355    //        {
 356    //            sessionId = sid;
 357    //        }
 358    //    }
 359
 360    //    return sessionId;
 361    //}
 362
 363    internal static string? GetProcessOwner(int processId)
 364    {
 2365        if (TryGetProcPid(processId, out var procPid))
 366        {
 2367            if (procPid == ProcPid.Self)
 368            {
 2369                return Environment.UserName;
 370            }
 371
 2372            if (NativeMethods.TryGetUid(GetProcDir(procPid), out uint uid))
 373            {
 2374                return NativeMethods.GetUserName(uid);
 375            }
 376        }
 377
 0378        return null;
 379    }
 380
 381    internal static DateTime GetProcessStartTime(int processId)
 382    {
 2383        if (TryGetProcPid(processId, out ProcPid procPid))
 384        {
 385            // Apparently it is currently impossible to fully recreate the time that Process.StartTime is.
 386            // Also see https://github.com/dotnet/runtime/issues/108959.
 387
 2388            if (procPid == ProcPid.Self)
 389            {
 2390                using var process = Process.GetCurrentProcess();
 2391                return process.StartTime;
 392            }
 393            else
 394            {
 2395                using var process = Process.GetProcessById(processId);
 2396                return process.StartTime;
 397            }
 398        }
 399
 0400        return default;
 2401    }
 402
 403    internal static string? GetProcessCurrentDirectory(int processId)
 404    {
 2405        if (TryGetProcPid(processId, out ProcPid procPid))
 406        {
 2407            return Directory.ResolveLinkTarget(GetProcCwd(procPid), true)?.FullName;
 408        }
 409
 0410        return null;
 411    }
 412
 413    internal static string[]? GetProcessCommandLineArgs(int processId, int maxArgs = -1)
 414    {
 2415        if (TryGetProcPid(processId, out ProcPid procPid))
 416        {
 2417            byte[]? rentedBuffer = null;
 418            try
 419            {
 2420                using (var file = new FileStream(GetProcCmdline(procPid), FileMode.Open, FileAccess.Read, FileShare.Read
 421                {
 2422                    Span<byte> buffer = stackalloc byte[256];
 2423                    int bytesRead = 0;
 424                    while (true)
 425                    {
 2426                        if (bytesRead == buffer.Length)
 427                        {
 428                            // Resize buffer
 2429                            uint newLength = (uint)buffer.Length * 2;
 430                            // Store what was read into new buffer
 2431                            byte[] tmp = ArrayPool<byte>.Shared.Rent((int)newLength);
 2432                            buffer.CopyTo(tmp);
 433                            // Remember current "rented" buffer (might be null)
 2434                            byte[]? lastRentedBuffer = rentedBuffer;
 435                            // From now on, we did rent a buffer. And it will be used for further reads.
 2436                            buffer = tmp;
 2437                            rentedBuffer = tmp;
 438                            // Return previously rented buffer, if any.
 2439                            if (lastRentedBuffer != null)
 440                            {
 2441                                ArrayPool<byte>.Shared.Return(lastRentedBuffer);
 442                            }
 443                        }
 444
 445                        Debug.Assert(bytesRead < buffer.Length);
 2446                        int n = file.Read(buffer.Slice(bytesRead));
 2447                        bytesRead += n;
 2448                        if (n == 0)
 449                        {
 450                            break;
 451                        }
 452                    }
 453
 2454                    return ConvertToArgs(ref buffer, maxArgs);
 455                }
 456            }
 2457            catch (IOException)
 458            {
 2459            }
 460            finally
 461            {
 2462                if (rentedBuffer != null)
 463                {
 2464                    ArrayPool<byte>.Shared.Return(rentedBuffer);
 465                }
 2466            }
 467        }
 468
 2469        return null;
 2470    }
 471
 472    internal static string[] ConvertToArgs(ref Span<byte> buffer, int maxArgs = -1)
 473    {
 2474        if (buffer.IsEmpty || maxArgs == 0)
 475        {
 2476            return [];
 477        }
 478
 479        // Removing ending '\0\0' from buffer. That is how /proc/<pid>/cmdline is documented to end.
 480        // We need to strip those to not get a phony, empty, trailing argv[argc-1] element.
 2481        if (buffer[^1] == '\0' && buffer[^2] == '\0')
 482        {
 2483            buffer = buffer.Slice(0, buffer.Length - 2);
 484        }
 485
 2486        if (buffer.IsEmpty)
 487        {
 2488            return [];
 489        }
 490
 491        // Individual argv elements in the buffer are separated by a null byte.
 2492        int actual = buffer.Count((byte)'\0') + 1;
 2493        int count = maxArgs > 0 ? Math.Min(maxArgs, actual) : actual;
 2494        string[] args = new string[count];
 2495        int start = 0;
 2496        int p = 0;
 2497        for (int i = 0; i < buffer.Length; i++)
 498        {
 2499            if (buffer[i] == '\0')
 500            {
 2501                args[p++] = Encoding.UTF8.GetString(buffer.Slice(start, i - start));
 2502                start = i + 1;
 503
 2504                if (maxArgs > 0 && maxArgs == p)
 505                {
 2506                    return args;
 507                }
 508            }
 509        }
 510
 2511        if (start < buffer.Length)
 512        {
 2513            args[p++] = Encoding.UTF8.GetString(buffer.Slice(start));
 514        }
 515
 2516        return args;
 517    }
 518
 519    internal static string? GetProcessExecutablePath(int processId)
 520    {
 2521        if (TryGetProcPid(processId, out ProcPid procPid))
 522        {
 2523            return File.ResolveLinkTarget(GetProcExe(procPid), true)?.FullName;
 524        }
 525
 0526        return null;
 527    }
 528
 529    internal static string? GetProcessExecutablePathFromCmdLine(int processId)
 530    {
 531        // This is a little more expensive than a specific function only reading up to argv[0] from /proc/<pid>/cmdline
 532        // would be - GetProcessCommandLineArgs() reads all arguments, but then only converts "maxArgs" of them to an
 533        // actual System.String. On the other hand it saves quite some code duplication.
 2534        string[]? args = GetProcessCommandLineArgs(processId, maxArgs: 1);
 2535        return args?.Length > 0 ? args[0] : null;
 536    }
 537
 538    internal static ReadOnlySpan<char> GetField(ReadOnlySpan<char> content, char delimiter, int index)
 539    {
 2540        if (index < 0)
 541        {
 2542            throw new ArgumentOutOfRangeException(nameof(index), index, $"Field index cannot be negative.");
 543        }
 544
 545#if NET9_0_OR_GREATER
 546        // PERF NOTE: For larger index values this will perform actually worse than the manual usage of
 547        // Count()/MemoryExtensions.Split() below. However, currently we use rather small indexes (5 out of 52)
 548        // where this performs actually better.
 549        // Also, this is cleaner an less error prone.
 550        // In .NET 10+ the ref struct enumerator returned here will implement IEnumerable<> so that we could
 551        // try using LINQ here, to make things even more simple (and possibly performant, as LINQ is getting
 552        // improved also!)
 553
 2554        int count = 0;
 2555        foreach (var range in content.Split(delimiter))
 556        {
 2557            if (count < index)
 558            {
 2559                count++;
 2560                continue;
 561            }
 2562            return content[range];
 563        }
 564
 2565        throw new ArgumentOutOfRangeException(nameof(index), index, $"Cannot access field at index {index}, only {count}
 566#else
 567        int fieldCount = content.Count(delimiter) + 1;
 568        if (fieldCount <= index)
 569        {
 570            throw new ArgumentOutOfRangeException(nameof(index), index, $"Cannot access field at index {index}, only {fi
 571        }
 572
 573        // We need to split into N+1 fields, where N is the field denoted by the index.
 574        // The extra field will receive the remainder of content, that doesn't need to
 575        // be split further, because we're not interested. That also means, that if we
 576        // are supposed to read the last field of content, we don't need that extra field.
 577        int rangeCount = index == fieldCount - 1 ? index + 1 : index + 2;
 578        Span<Range> ranges = rangeCount < 128 ? stackalloc Range[rangeCount] : new Range[rangeCount];
 579        int num = MemoryExtensions.Split(content, ranges, delimiter);
 580
 581        // Shouldn't trigger, because of pre-checks done above.
 582        Debug.Assert(num == rangeCount);
 583
 584        return content[ranges[index]];
 585#endif
 586    }
 587}