From 1d329f31bc575b25474b63ea7de5987fa1f6292c Mon Sep 17 00:00:00 2001 From: Lucas Pimentel Date: Tue, 25 Nov 2025 16:50:40 -0500 Subject: [PATCH 1/7] Replace Mono.Unix Syscall with libc P/Invoke --- src/StatsdClient/IFileSystem.cs | 16 +++-- src/StatsdClient/NativeMethods.cs | 101 ++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+), 5 deletions(-) create mode 100644 src/StatsdClient/NativeMethods.cs diff --git a/src/StatsdClient/IFileSystem.cs b/src/StatsdClient/IFileSystem.cs index bde45190..88699ad3 100644 --- a/src/StatsdClient/IFileSystem.cs +++ b/src/StatsdClient/IFileSystem.cs @@ -1,6 +1,6 @@ using System; using System.IO; -using Mono.Unix.Native; +using System.Runtime.InteropServices; namespace StatsdClient { @@ -81,15 +81,21 @@ public TextReader OpenText(string path) /// True if the file stat was successful, false otherwise public bool TryStat(string path, out ulong inode) { - if (Environment.OSVersion.Platform == PlatformID.Unix && - Syscall.stat(path, out var stat) > 0) +#if NET461 + // Unix Domain Sockets not supported on .NET Framework (always runs on Windows). + inode = 0; + return false; +#else + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - inode = stat.st_ino; - return true; + // P/Invoke to libc + return NativeMethods.TryStat(path, out inode); } + // Unsupported OS inode = 0; return false; +#endif } } } diff --git a/src/StatsdClient/NativeMethods.cs b/src/StatsdClient/NativeMethods.cs new file mode 100644 index 00000000..cff16cee --- /dev/null +++ b/src/StatsdClient/NativeMethods.cs @@ -0,0 +1,101 @@ +#if !NET461 + +using System.Runtime.InteropServices; + +namespace StatsdClient; + +/// +/// P/Invoke wrapper for Unix system calls +/// +internal static class NativeMethods +{ + /// + /// Attempts to get the inode of the file at the given path + /// + /// File path + /// The inode number if successful, 0 otherwise + /// True if successful, false otherwise + public static bool TryStat(string path, out ulong inode) + { + try + { + int result = Stat(path, out var statBuf); + if (result == 0) + { + inode = statBuf.st_ino; + return true; + } + } + catch + { + // P/Invoke failed + } + + // P/Invoke failed or unsupported OS + inode = 0; + return false; + } + + [DllImport("libc", SetLastError = true, EntryPoint = "stat", CharSet = CharSet.Ansi)] + private static extern int Stat(string pathname, out StatStruct buf); + + [StructLayout(LayoutKind.Explicit, Size = 144)] + private struct StatStruct + { + [FieldOffset(0)] + public ulong st_dev; // device (offset 0, 8 bytes) + + [FieldOffset(8)] + public ulong st_ino; // inode (offset 8, 8 bytes) + + [FieldOffset(16)] + public ulong st_nlink; // number of hard links (offset 16, 8 bytes) + + [FieldOffset(24)] + public uint st_mode; // protection (offset 24, 4 bytes) + + [FieldOffset(28)] + public uint st_uid; // user ID (offset 28, 4 bytes) + + [FieldOffset(32)] + public uint st_gid; // group ID (offset 32, 4 bytes) + + // [FieldOffset(36)] - 4 bytes padding + + [FieldOffset(40)] + public ulong st_rdev; // device type (offset 40, 8 bytes) + + [FieldOffset(48)] + public long st_size; // size (offset 48, 8 bytes) + + [FieldOffset(56)] + public long st_blksize; // block size (offset 56, 8 bytes) + + [FieldOffset(64)] + public long st_blocks; // blocks allocated (offset 64, 8 bytes) + + [FieldOffset(72)] + public long st_atime; // access time (offset 72, 8 bytes) + + [FieldOffset(80)] + public long st_atime_nsec; // access time nsec (offset 80, 8 bytes) + + [FieldOffset(88)] + public long st_mtime; // modification time (offset 88, 8 bytes) + + [FieldOffset(96)] + public long st_mtime_nsec; // modification time nsec (offset 96, 8 bytes) + + [FieldOffset(104)] + public long st_ctime; // status change time (offset 104, 8 bytes) + + [FieldOffset(112)] + public long st_ctime_nsec; // status change time nsec (offset 112, 8 bytes) + + // Total size: 144 bytes (includes 24 bytes reserved at end on x86_64) + // Note: This struct layout matches the Linux x86_64 glibc struct stat layout. + // Using explicit offsets to ensure correct memory marshaling across architectures. + } +} + +#endif From 3b4088c8f8e9fde98d3988597351fa91ca8c354a Mon Sep 17 00:00:00 2001 From: Lucas Pimentel Date: Tue, 25 Nov 2025 17:00:07 -0500 Subject: [PATCH 2/7] restore UnixEndPoint --- src/StatsdClient/UnixEndPoint.cs | 152 +++++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 src/StatsdClient/UnixEndPoint.cs diff --git a/src/StatsdClient/UnixEndPoint.cs b/src/StatsdClient/UnixEndPoint.cs new file mode 100644 index 00000000..3ee6b52b --- /dev/null +++ b/src/StatsdClient/UnixEndPoint.cs @@ -0,0 +1,152 @@ +// From https://raw.githubusercontent.com/mono/mono/master/mcs/class/Mono.Posix/Mono.Unix/UnixEndPoint.cs + +// +// Mono.Unix.UnixEndPoint: EndPoint derived class for AF_UNIX family sockets. +// +// Authors: +// Gonzalo Paniagua Javier (gonzalo@ximian.com) +// +// (C) 2003 Ximian, Inc (http://www.ximian.com) +// + +// +// 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. +// +using System; +using System.Net; +using System.Net.Sockets; +using System.Text; + +namespace Mono.Unix +{ + internal class UnixEndPoint : EndPoint + { + private string filename; + + public UnixEndPoint(string filename) + { + if (filename == null) + { + throw new ArgumentNullException("filename"); + } + + if (filename == string.Empty) + { + throw new ArgumentException("Cannot be empty.", "filename"); + } + + this.filename = filename; + } + + public string Filename + { + get + { + return (filename); + } + + set + { + filename = value; + } + } + + public override AddressFamily AddressFamily + { + get { return AddressFamily.Unix; } + } + + public override EndPoint Create(SocketAddress socketAddress) + { + /* + * Should also check this + * + int addr = (int) AddressFamily.Unix; + if (socketAddress [0] != (addr & 0xFF)) + throw new ArgumentException ("socketAddress is not a unix socket address."); + + if (socketAddress [1] != ((addr & 0xFF00) >> 8)) + throw new ArgumentException ("socketAddress is not a unix socket address."); + */ + + if (socketAddress.Size == 2) + { + // Empty filename. + // Probably from RemoteEndPoint which on linux does not return the file name. + UnixEndPoint uep = new UnixEndPoint("a"); + uep.filename = string.Empty; + return uep; + } + + int size = socketAddress.Size - 2; + byte[] bytes = new byte[size]; + for (int i = 0; i < bytes.Length; i++) + { + bytes[i] = socketAddress[i + 2]; + // There may be junk after the null terminator, so ignore it all. + if (bytes[i] == 0) + { + size = i; + break; + } + } + + string name = Encoding.UTF8.GetString(bytes, 0, size); + return new UnixEndPoint(name); + } + + public override SocketAddress Serialize() + { + byte[] bytes = Encoding.UTF8.GetBytes(filename); + SocketAddress sa = new SocketAddress(AddressFamily, 2 + bytes.Length + 1); + // sa [0] -> family low byte, sa [1] -> family high byte + for (int i = 0; i < bytes.Length; i++) + { + sa[2 + i] = bytes[i]; + } + + // NULL suffix for non-abstract path + sa[2 + bytes.Length] = 0; + + return sa; + } + + public override string ToString() + { + return (filename); + } + + public override int GetHashCode() + { + return filename.GetHashCode(); + } + + public override bool Equals(object o) + { + UnixEndPoint other = o as UnixEndPoint; + if (other == null) + { + return false; + } + + return (other.filename == filename); + } + } +} From 3c31874c598407e98d81a3b06802051891819cbd Mon Sep 17 00:00:00 2001 From: Lucas Pimentel Date: Tue, 25 Nov 2025 17:15:23 -0500 Subject: [PATCH 3/7] remove Mono.Unix dependency --- src/StatsdClient/StatsdClient.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/src/StatsdClient/StatsdClient.csproj b/src/StatsdClient/StatsdClient.csproj index ab1d0b2e..d954a4da 100644 --- a/src/StatsdClient/StatsdClient.csproj +++ b/src/StatsdClient/StatsdClient.csproj @@ -21,7 +21,6 @@ HAS_SPAN - runtime; build; native; contentfiles; analyzers; buildtransitive From c8a741864c4bde42b8e157323f545771e511495b Mon Sep 17 00:00:00 2001 From: Lucas Pimentel Date: Tue, 25 Nov 2025 16:36:19 -0500 Subject: [PATCH 4/7] set C# language version to `latest` --- src/StatsdClient/StatsdClient.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/StatsdClient/StatsdClient.csproj b/src/StatsdClient/StatsdClient.csproj index d954a4da..c7b12f1e 100644 --- a/src/StatsdClient/StatsdClient.csproj +++ b/src/StatsdClient/StatsdClient.csproj @@ -5,6 +5,7 @@ A DogStatsD client for C#. DogStatsD is an extension of the StatsD metric server for use with Datadog. For more information visit http://datadoghq.com. Datadog net461;netstandard2.0;netcoreapp3.1;net6.0 + latest 9.0.0 9.0.0 https://github.com/DataDog/dogstatsd-csharp-client/blob/master/MIT-LICENCE.md From 6467c5012dd40d1c1e00a92eaae67dab064d855b Mon Sep 17 00:00:00 2001 From: Lucas Pimentel Date: Tue, 25 Nov 2025 23:43:49 -0500 Subject: [PATCH 5/7] install .NET SDK 6.0 (no longer included in image?) --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d70201ba..4b17be6e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -71,7 +71,7 @@ jobs: - framework_version: net5.0 sdk_version: 5.0.x - framework_version: net6.0 - sdk_version: skip-install + sdk_version: 6.0.x - framework_version: net7.0 sdk_version: 7.0.x - framework_version: net8.0 From eb5dc9d7e7c26857cb4d5beba5ecc660c4ef9b0c Mon Sep 17 00:00:00 2001 From: Lucas Pimentel Date: Wed, 26 Nov 2025 00:02:18 -0500 Subject: [PATCH 6/7] add native stat structs for macOS --- src/StatsdClient/NativeMethods.cs | 115 ++++++++++++++++++++++-------- 1 file changed, 87 insertions(+), 28 deletions(-) diff --git a/src/StatsdClient/NativeMethods.cs b/src/StatsdClient/NativeMethods.cs index cff16cee..0e7f732d 100644 --- a/src/StatsdClient/NativeMethods.cs +++ b/src/StatsdClient/NativeMethods.cs @@ -19,11 +19,24 @@ public static bool TryStat(string path, out ulong inode) { try { - int result = Stat(path, out var statBuf); - if (result == 0) + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { - inode = statBuf.st_ino; - return true; + int result = StatMacOS(path, out var statBuf); + if (result == 0) + { + inode = statBuf.st_ino; + return true; + } + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + // Linux (and other Unix-like systems) + int result = StatLinux(path, out var statBuf); + if (result == 0) + { + inode = statBuf.st_ino; + return true; + } } } catch @@ -36,65 +49,111 @@ public static bool TryStat(string path, out ulong inode) return false; } + // Linux stat syscall (x86_64 and ARM64/aarch64) + [DllImport("libc", SetLastError = true, EntryPoint = "stat", CharSet = CharSet.Ansi)] + private static extern int StatLinux(string pathname, out StatStructLinux buf); + + // macOS stat syscall (x86_64 and ARM64/Apple Silicon) [DllImport("libc", SetLastError = true, EntryPoint = "stat", CharSet = CharSet.Ansi)] - private static extern int Stat(string pathname, out StatStruct buf); + private static extern int StatMacOS(string pathname, out StatStructMacOS buf); + + // Linux struct stat (asm-generic/stat.h - used by ARM64/aarch64 and x86_64) + // Reference: https://github.com/torvalds/linux/blob/master/include/uapi/asm-generic/stat.h + [StructLayout(LayoutKind.Sequential)] + private struct StatStructLinux + { + public ulong st_dev; // Device + public ulong st_ino; // File serial number (inode) + public uint st_mode; // File mode + public uint st_nlink; // Link count + public uint st_uid; // User ID of the file's owner + public uint st_gid; // Group ID of the file's group + public ulong st_rdev; // Device number, if device + public ulong __pad1; + public long st_size; // Size of file, in bytes + public int st_blksize; // Optimal block size for I/O + public int __pad2; + public long st_blocks; // Number 512-byte blocks allocated + public long st_atime; // Time of last access + public ulong st_atime_nsec; + public long st_mtime; // Time of last modification + public ulong st_mtime_nsec; + public long st_ctime; // Time of last status change + public ulong st_ctime_nsec; + public uint __unused4; + public uint __unused5; + } + // macOS struct stat (sys/stat.h - x86_64 and ARM64) + // Reference: https://stackoverflow.com/questions/39671660 [StructLayout(LayoutKind.Explicit, Size = 144)] - private struct StatStruct + private struct StatStructMacOS { [FieldOffset(0)] - public ulong st_dev; // device (offset 0, 8 bytes) + public int st_dev; // Device (4 bytes) + + [FieldOffset(4)] + public ushort st_mode; // File mode (2 bytes) + + [FieldOffset(6)] + public ushort st_nlink; // Link count (2 bytes) [FieldOffset(8)] - public ulong st_ino; // inode (offset 8, 8 bytes) + public ulong st_ino; // File serial number (inode) (8 bytes) [FieldOffset(16)] - public ulong st_nlink; // number of hard links (offset 16, 8 bytes) + public uint st_uid; // User ID (4 bytes) + + [FieldOffset(20)] + public uint st_gid; // Group ID (4 bytes) [FieldOffset(24)] - public uint st_mode; // protection (offset 24, 4 bytes) + public int st_rdev; // Device number (4 bytes) - [FieldOffset(28)] - public uint st_uid; // user ID (offset 28, 4 bytes) + // Padding to align timespec at offset 32 + // [FieldOffset(28)] - 4 bytes padding + // Four timespec structures (each 16 bytes: 8 bytes tv_sec + 8 bytes tv_nsec) [FieldOffset(32)] - public uint st_gid; // group ID (offset 32, 4 bytes) - - // [FieldOffset(36)] - 4 bytes padding + public long st_atimespec_sec; // Access time seconds (8 bytes) [FieldOffset(40)] - public ulong st_rdev; // device type (offset 40, 8 bytes) + public long st_atimespec_nsec; // Access time nanoseconds (8 bytes) [FieldOffset(48)] - public long st_size; // size (offset 48, 8 bytes) + public long st_mtimespec_sec; // Modification time seconds (8 bytes) [FieldOffset(56)] - public long st_blksize; // block size (offset 56, 8 bytes) + public long st_mtimespec_nsec; // Modification time nanoseconds (8 bytes) [FieldOffset(64)] - public long st_blocks; // blocks allocated (offset 64, 8 bytes) + public long st_ctimespec_sec; // Status change time seconds (8 bytes) [FieldOffset(72)] - public long st_atime; // access time (offset 72, 8 bytes) + public long st_ctimespec_nsec; // Status change time nanoseconds (8 bytes) [FieldOffset(80)] - public long st_atime_nsec; // access time nsec (offset 80, 8 bytes) + public long st_birthtimespec_sec; // Birth time seconds (8 bytes) - macOS specific [FieldOffset(88)] - public long st_mtime; // modification time (offset 88, 8 bytes) + public long st_birthtimespec_nsec; // Birth time nanoseconds (8 bytes) - macOS specific [FieldOffset(96)] - public long st_mtime_nsec; // modification time nsec (offset 96, 8 bytes) + public long st_size; // File size in bytes (8 bytes) [FieldOffset(104)] - public long st_ctime; // status change time (offset 104, 8 bytes) + public long st_blocks; // Blocks allocated (8 bytes) [FieldOffset(112)] - public long st_ctime_nsec; // status change time nsec (offset 112, 8 bytes) + public int st_blksize; // Optimal block size (4 bytes) + + [FieldOffset(116)] + public uint st_flags; // User defined flags - macOS specific (4 bytes) + + [FieldOffset(120)] + public uint st_gen; // File generation number - macOS specific (4 bytes) - // Total size: 144 bytes (includes 24 bytes reserved at end on x86_64) - // Note: This struct layout matches the Linux x86_64 glibc struct stat layout. - // Using explicit offsets to ensure correct memory marshaling across architectures. + // Remaining fields up to 144 bytes } } From 2892d63210f38c39f68aa4b24fe9302d85cb219e Mon Sep 17 00:00:00 2001 From: Lucas Pimentel Date: Wed, 26 Nov 2025 00:02:44 -0500 Subject: [PATCH 7/7] add macOS tests to github workflow --- .github/workflows/test.yml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4b17be6e..879729f9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -50,6 +50,35 @@ jobs: ${{ matrix.dotnet.sdk_version }} - name: Run tests run: dotnet test --blame-hang --blame-hang-dump-type none --blame-hang-timeout 60s --framework ${{ matrix.dotnet.framework_version }} -- tests/StatsdClient.Tests/ + unit-tests-macos: + name: Tests (macOS) + runs-on: macos-latest + permissions: + actions: read + contents: read + strategy: + fail-fast: false + matrix: + dotnet: + - framework_version: net6.0 + sdk_version: 6.0.x + - framework_version: net7.0 + sdk_version: 7.0.x + - framework_version: net8.0 + sdk_version: 8.0.x + - framework_version: net9.0 + sdk_version: 9.0.x + steps: + - name: Checkout repository + uses: actions/checkout@v3 + - name: Setup .NET Core + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 9.0.x + ${{ matrix.dotnet.sdk_version }} + - name: Run tests + run: dotnet test --blame-hang --blame-hang-dump-type none --blame-hang-timeout 60s --framework ${{ matrix.dotnet.framework_version }} -- tests/StatsdClient.Tests/ unit-tests-windows: name: Tests (Windows) runs-on: windows-latest