Skip to content

Commit

Permalink
several improvements & fixes (#24)
Browse files Browse the repository at this point in the history
* several improvements & fixes

* add THIRD-PARTY-NOTICES.txt
  • Loading branch information
cklutz authored Oct 17, 2024
1 parent 023b5c4 commit 1117693
Show file tree
Hide file tree
Showing 10 changed files with 294 additions and 197 deletions.
58 changes: 58 additions & 0 deletions THIRD-PARTY-NOTICES.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
LockCheck uses third-party material as listed below.

The attached notices are provided for informational purposes only.


License for .NET runtime (https://github.com/dotnet/runtime)
------------------------------------------------------------

The MIT License (MIT)

Copyright (c) .NET Foundation and Contributors

All rights reserved.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


License for .NET Framework Reference Source (https://github.com/microsoft/referencesource)
------------------------------------------------------------------------------------------

The MIT License (MIT)

Copyright (c) Microsoft Corporation

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

13 changes: 9 additions & 4 deletions src/LockCheck/IHasErrorState.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;

namespace LockCheck
{
Expand All @@ -8,14 +8,19 @@ namespace LockCheck
public interface IHasErrorState
{
/// <summary>
/// Get a value that indicates if the entity is errnous.
/// Get a value that indicates if the entity is erroneous.
/// </summary>
bool HasError { get; }

/// <summary>
/// Set the error state of the entity..
/// Set the error state of the entity.
/// </summary>
/// <remarks>
/// Implementations shall only consider the first call to this method, setting <see cref="HasError"/>
/// to <c>true</c>. Further calls to this function shall be ignored.
/// </remarks>
/// <param name="ex">An optional exception that caused the error.</param>
void SetError(Exception ex = null);
/// <param name="errorCode">An option error code that describes the error.</param>
void SetError(Exception ex = null, int errorCode = 0);
}
}
64 changes: 0 additions & 64 deletions src/LockCheck/Linux/NativeMethods.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
using System;
using System.Buffers;
using System.IO;
using System.Reflection.Metadata;
using System.Runtime.InteropServices;
using System.Text;

#pragma warning disable IDE1006 // Naming Styles - off here, because we want to use native names

Expand Down Expand Up @@ -91,9 +88,6 @@ private static unsafe bool TryGetUserName(uint uid, byte* buf, int bufLen, out s
throw new IOException($"Couldn't get user name for '{uid}' (errno = {error})");
}

// Copied (and modified) from github.com/dotnet/runtime, MIT licensed.
// ---BEGIN---------------------------------------------------------------------------------

private const string SystemNative = "System.Native";

[StructLayout(LayoutKind.Sequential)]
Expand Down Expand Up @@ -136,63 +130,5 @@ internal unsafe struct Passwd

[LibraryImport(SystemNative, EntryPoint = "SystemNative_GetPwUidR", SetLastError = false)]
internal static unsafe partial int GetPwUidR(uint uid, out Passwd pwd, byte* buf, int bufLen);

[LibraryImport(SystemNative, EntryPoint = "SystemNative_ReadLink", SetLastError = true)]
private static partial int ReadLink(ref byte path, ref byte buffer, int bufferSize);

internal static string ReadLink(ReadOnlySpan<char> path)
{
const int StackBufferSize = 256;

Span<byte> spanBuffer = stackalloc byte[StackBufferSize];
byte[] arrayBuffer = null;

// Convert path to UTF-8 bytes, zero terminated - CLR code used internal ValueUtf8Converter for this.
int maxSize = checked(Encoding.UTF8.GetMaxByteCount(path.Length) + 1);
Span<byte> pathBytes = maxSize <= StackBufferSize ? stackalloc byte[maxSize] : new byte[maxSize];
int pathBytesCount = Encoding.UTF8.GetBytes(path, pathBytes);
pathBytes[pathBytesCount] = 0;
pathBytes = pathBytes.Slice(0, pathBytesCount + 1);
ref byte pathReference = ref MemoryMarshal.GetReference(pathBytes);

while (true)
{
int error = 0;
try
{
int resultLength = ReadLink(ref pathReference, ref MemoryMarshal.GetReference(spanBuffer), spanBuffer.Length);

if (resultLength < 0)
{
// error
error = Marshal.GetLastPInvokeError();
return null;
}
else if (resultLength < spanBuffer.Length)
{
// success
return Encoding.UTF8.GetString(spanBuffer.Slice(0, resultLength));
}
}
finally
{
if (arrayBuffer != null)
{
ArrayPool<byte>.Shared.Return(arrayBuffer);
}

if (error > 0)
{
Marshal.SetLastPInvokeError(error);
}
}

// Output buffer was too small, loop around again and try with a larger buffer.
arrayBuffer = ArrayPool<byte>.Shared.Rent(spanBuffer.Length * 2);
spanBuffer = arrayBuffer;
}
}

// ---END-----------------------------------------------------------------------------------
}
}
122 changes: 40 additions & 82 deletions src/LockCheck/Linux/ProcFileSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.IO.Pipes;
using System.Linq;
using System.Text;

namespace LockCheck.Linux
Expand Down Expand Up @@ -133,7 +133,7 @@ internal static bool ProcMatchesPidNamespace
{
get
{
// _procMatchesPidNamespace is set to:
// s_procMatchesPidNamespace is set to:
// - 0: when uninitialized,
// - 1: '/proc' and the process pid namespace match,
// - 2: when they don't match.
Expand All @@ -143,8 +143,9 @@ internal static bool ProcMatchesPidNamespace
// We compare it with the pid of the current process to see if the '/proc' and pid namespace match up.

int? procSelfPid = null;
if (NativeMethods.ReadLink("/proc/self") is string target &&
int.TryParse(target, out int pid))

if (Directory.ResolveLinkTarget("/proc/self", false)?.FullName is string target &&
int.TryParse(Path.GetFileName(target), out int pid))
{
procSelfPid = pid;
}
Expand Down Expand Up @@ -185,11 +186,11 @@ internal static bool TryGetProcPid(int pid, out ProcPid procPid)
return false;
}

private static string GetProcCmdline(ProcPid procPid) => procPid == ProcPid.Self ? "/proc/self/cmdline" : string.Create(null, stackalloc char[256], $"/proc/{(int)procPid}/cmdline");
private static string GetProcExe(ProcPid procPid) => procPid == ProcPid.Self ? "/proc/self/exe" : string.Create(null, stackalloc char[256], $"/proc/{(int)procPid}/exe");
private static string GetProcCwd(ProcPid procPid) => procPid == ProcPid.Self ? "/proc/self/cwd" : string.Create(null, stackalloc char[256], $"/proc/{(int)procPid}/cwd");
private static string GetProcStat(ProcPid procPid) => procPid == ProcPid.Self ? "/proc/self/stat" : string.Create(null, stackalloc char[256], $"/proc/{(int)procPid}/stat");
private static string GetProcDir(ProcPid procPid) => procPid == ProcPid.Self ? "/proc/self" : string.Create(null, stackalloc char[256], $"/proc/{(int)procPid}");
private static string GetProcCmdline(ProcPid procPid) => procPid == ProcPid.Self ? "/proc/self/cmdline" : string.Create(null, stackalloc char[128], $"/proc/{(int)procPid}/cmdline");
private static string GetProcExe(ProcPid procPid) => procPid == ProcPid.Self ? "/proc/self/exe" : string.Create(null, stackalloc char[128], $"/proc/{(int)procPid}/exe");
private static string GetProcCwd(ProcPid procPid) => procPid == ProcPid.Self ? "/proc/self/cwd" : string.Create(null, stackalloc char[128], $"/proc/{(int)procPid}/cwd");
private static string GetProcStat(ProcPid procPid) => procPid == ProcPid.Self ? "/proc/self/stat" : string.Create(null, stackalloc char[128], $"/proc/{(int)procPid}/stat");
private static string GetProcDir(ProcPid procPid) => procPid == ProcPid.Self ? "/proc/self" : string.Create(null, stackalloc char[128], $"/proc/{(int)procPid}");

internal static bool Exists(int processId) => TryGetProcPid(processId, out var procPid) && Directory.Exists(GetProcDir(procPid));

Expand All @@ -213,17 +214,21 @@ internal static string GetProcessOwner(int processId)

internal static DateTime GetProcessStartTime(int processId)
{
if (TryGetProcPid(processId, out _))
if (TryGetProcPid(processId, out ProcPid procPid))
{
// Apparently it is impossible to fully recreate the time that Process.StartTime calculates in
// the background. It uses clock_gettime(CLOCK_BOOTTIME) (see https://github.com/dotnet/runtime/pull/83966)
// internally and calculates the start time relative to that using /proc/<pid>/stat.
// However we shave the yack, we get a different time than what Process.StartTime would return.
// Debugging has it, that this is due to the fact that we get a different "boot time" (later time).
// I'm not sure if that is a bug in the CLR or just a fact of life on Linux.
// In any case, we need to get the exact same Timestamp for our hash keys to work properly.
using var process = Process.GetProcessById(processId);
return process.StartTime;
// Apparently it is currently impossible to fully recreate the time that Process.StartTime is.
// Also see https://github.com/dotnet/runtime/issues/108959.

if (procPid == ProcPid.Self)
{
using var process = Process.GetCurrentProcess();
return process.StartTime;
}
else
{
using var process = Process.GetProcessById(processId);
return process.StartTime;
}
}

return default;
Expand Down Expand Up @@ -254,7 +259,7 @@ internal static string GetProcessCurrentDirectory(int processId)
return null;
}

internal static string[] GetProcessCommandLineArgs(int processId)
internal static string[] GetProcessCommandLineArgs(int processId, int maxArgs = -1)
{
if (TryGetProcPid(processId, out ProcPid procPid))
{
Expand Down Expand Up @@ -295,7 +300,7 @@ internal static string[] GetProcessCommandLineArgs(int processId)
}
}

return ConvertToArgs(ref buffer);
return ConvertToArgs(ref buffer, maxArgs);
}
}
catch (IOException)
Expand All @@ -313,9 +318,9 @@ internal static string[] GetProcessCommandLineArgs(int processId)
return null;
}

internal static string[] ConvertToArgs(ref Span<byte> buffer)
internal static string[] ConvertToArgs(ref Span<byte> buffer, int maxArgs = -1)
{
if (buffer.IsEmpty)
if (buffer.IsEmpty || maxArgs == 0)
{
return [];
}
Expand All @@ -333,7 +338,8 @@ internal static string[] ConvertToArgs(ref Span<byte> buffer)
}

// Individual argv elements in the buffer are separated by a null byte.
int count = buffer.Count((byte)'\0') + 1;
int actual = buffer.Count((byte)'\0') + 1;
int count = maxArgs > 0 ? Math.Min(maxArgs, actual) : actual;
string[] args = new string[count];
int start = 0;
int p = 0;
Expand All @@ -343,6 +349,11 @@ internal static string[] ConvertToArgs(ref Span<byte> buffer)
{
args[p++] = Encoding.UTF8.GetString(buffer.Slice(start, i - start));
start = i + 1;

if (maxArgs > 0 && maxArgs == p)
{
return args;
}
}
}

Expand All @@ -366,64 +377,11 @@ internal static string GetProcessExecutablePath(int processId)

internal static string GetProcessExecutablePathFromCmdLine(int processId)
{
if (TryGetProcPid(processId, out ProcPid procPid))
{
byte[] rentedBuffer = null;
try
{
using (var file = new FileStream(GetProcCmdline(procPid), FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 1, useAsync: false))
{
Span<byte> buffer = stackalloc byte[256];
int bytesRead = 0;
while (true)
{
if (bytesRead == buffer.Length)
{
// Resize buffer
uint newLength = (uint)buffer.Length * 2;
// Store what was read into new buffer
byte[] tmp = ArrayPool<byte>.Shared.Rent((int)newLength);
buffer.CopyTo(tmp);
// Remember current "rented" buffer (might be null)
byte[] lastRentedBuffer = rentedBuffer;
// From now on, we did rent a buffer. And it will be used for further reads.
buffer = tmp;
rentedBuffer = tmp;
// Return previously rented buffer, if any.
if (lastRentedBuffer != null)
{
ArrayPool<byte>.Shared.Return(lastRentedBuffer);
}
}

Debug.Assert(bytesRead < buffer.Length);
int n = file.Read(buffer.Slice(bytesRead));
bytesRead += n;

// "/proc/<pid>/cmdline" contains the original argument vector (argv), where each part is separated by a null byte.
// See if we have read enough for argv[0], which contains the process name.
ReadOnlySpan<byte> argRemainder = buffer.Slice(0, bytesRead);
int argEnd = argRemainder.IndexOf((byte)'\0');
if (argEnd != -1)
{
return Encoding.UTF8.GetString(argRemainder.Slice(0, argEnd));
}
}
}
}
catch (IOException)
{
}
finally
{
if (rentedBuffer != null)
{
ArrayPool<byte>.Shared.Return(rentedBuffer);
}
}
}

return null;
// This is a little more expensive than a specific function only reading up to argv[0] from /proc/<pid>/cmdline
// would be - GetProcessCommandLineArgs() reads all arguments, but then only converts "maxArgs" of them to an
// actual System.String. On the other hand it saves quite some code duplication.
string[] args = GetProcessCommandLineArgs(processId, maxArgs: 1);
return args?.Length > 0 ? args[0] : null;
}

private static ReadOnlySpan<char> GetField(ReadOnlySpan<char> content, char delimiter, int index)
Expand Down
Loading

0 comments on commit 1117693

Please sign in to comment.