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);
+ }
+ }
+}