diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d70201ba..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 @@ -71,7 +100,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 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..0e7f732d --- /dev/null +++ b/src/StatsdClient/NativeMethods.cs @@ -0,0 +1,160 @@ +#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 + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + 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 + { + // P/Invoke failed + } + + // P/Invoke failed or unsupported OS + inode = 0; + 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 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 StatStructMacOS + { + [FieldOffset(0)] + 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; // File serial number (inode) (8 bytes) + + [FieldOffset(16)] + public uint st_uid; // User ID (4 bytes) + + [FieldOffset(20)] + public uint st_gid; // Group ID (4 bytes) + + [FieldOffset(24)] + public int st_rdev; // Device number (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 long st_atimespec_sec; // Access time seconds (8 bytes) + + [FieldOffset(40)] + public long st_atimespec_nsec; // Access time nanoseconds (8 bytes) + + [FieldOffset(48)] + public long st_mtimespec_sec; // Modification time seconds (8 bytes) + + [FieldOffset(56)] + public long st_mtimespec_nsec; // Modification time nanoseconds (8 bytes) + + [FieldOffset(64)] + public long st_ctimespec_sec; // Status change time seconds (8 bytes) + + [FieldOffset(72)] + public long st_ctimespec_nsec; // Status change time nanoseconds (8 bytes) + + [FieldOffset(80)] + public long st_birthtimespec_sec; // Birth time seconds (8 bytes) - macOS specific + + [FieldOffset(88)] + public long st_birthtimespec_nsec; // Birth time nanoseconds (8 bytes) - macOS specific + + [FieldOffset(96)] + public long st_size; // File size in bytes (8 bytes) + + [FieldOffset(104)] + public long st_blocks; // Blocks allocated (8 bytes) + + [FieldOffset(112)] + 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) + + // Remaining fields up to 144 bytes + } +} + +#endif diff --git a/src/StatsdClient/StatsdClient.csproj b/src/StatsdClient/StatsdClient.csproj index ab1d0b2e..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 @@ -21,7 +22,6 @@ HAS_SPAN - runtime; build; native; contentfiles; analyzers; buildtransitive 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); + } + } +}