diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e8d773b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,38 @@ +# Build outputs +**/bin/ +**/obj/ +**/out/ + +# Native build outputs +**/build/ +runtimes/ + +# IDE / Editor +.vs/ +.vscode/ +*.suo +*.user +*.userosscache +*.sln.docstates + +# Test results +TestResults/ +*.trx +*.coverage + +# NuGet +*.nupkg +*.snupkg +packages/ + +# Misc +.git/ +.gitignore +.github/ +*.md +*.log +*.zip + +# Keep these files +!README.md +!CLAUDE.md diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..896a6fb --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +# Set line endings to LF, even on Windows. Otherwise, execution within Docker fails. +# See https://help.github.com/articles/dealing-with-line-endings/ +*.sh text eol=lf +*.bash text eol=lf diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 00a0dfb..fd6fa8b 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -8,9 +8,59 @@ on: workflow_dispatch: jobs: + build-native-linux: + name: Build libfs for ${{ matrix.libc }}-${{ matrix.arch }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - arch: x64 + libc: glibc + container: mcr.microsoft.com/dotnet/sdk:8.0 + install_cmd: apt-get update && apt-get install -y cmake build-essential + - arch: x64 + libc: musl + container: mcr.microsoft.com/dotnet/sdk:8.0-alpine + install_cmd: apk add cmake make gcc g++ musl-dev + - arch: arm64 + libc: glibc + container: mcr.microsoft.com/dotnet/sdk:8.0-arm64v8 + install_cmd: apt-get update && apt-get install -y cmake build-essential + - arch: arm64 + libc: musl + container: mcr.microsoft.com/dotnet/sdk:8.0-alpine-arm64v8 + install_cmd: apk add cmake make gcc g++ musl-dev + + container: ${{ matrix.container }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install build tools + run: ${{ matrix.install_cmd }} + + - name: Build native library + run: | + mkdir -p build && cd build + cmake ../src/StatsdClient.Native + cmake --build . --config Release + cd .. + + mkdir -p runtimes/linux-${{ matrix.libc }}-${{ matrix.arch }}/native + cp build/libfs.so runtimes/linux-${{ matrix.libc }}-${{ matrix.arch }}/native/ + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: native-linux-${{ matrix.libc }}-${{ matrix.arch }} + path: runtimes/ + unit-tests: name: ${{ matrix.framework }} on ${{ matrix.os }} runs-on: ${{ matrix.os }} + needs: build-native-linux permissions: actions: read contents: read @@ -67,6 +117,13 @@ jobs: with: fetch-depth: 0 + - name: Download native libraries + uses: actions/download-artifact@v4 + with: + pattern: native-linux-* + path: runtimes/ + merge-multiple: true + - name: Install libssl on Linux # .NET 5.0 and below want libssl v1, which is not available on Ubuntu 24.04 normally if: runner.os == 'Linux' diff --git a/.gitignore b/.gitignore index a3e3744..2148d1e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ .vs/ .vscode/ .idea/ +runtimes/ +artifacts/ +build/ [bB]in/ [oO]bj/ artifacts/ diff --git a/BUILD.md b/BUILD.md new file mode 100644 index 0000000..c897241 --- /dev/null +++ b/BUILD.md @@ -0,0 +1,253 @@ +# Building and Testing + +This document describes how to build and test the DogStatsD C# client library. + +## Prerequisites + +- [.NET SDK 10.0 or above](https://dotnet.microsoft.com/download) +- [Docker](https://www.docker.com/get-started) (required for building Linux native libraries) + +## Quick Start + +```bash +# Build the .NET library +dotnet build + +# Run tests for a specific framework +dotnet test --framework net8.0 + +# Build all Linux native libraries using Docker +./build-and-test.sh --platform linux + +# Pack the NuGet package with all native libraries +dotnet pack src/StatsdClient/StatsdClient.csproj -c Release +``` + +## Building + +### .NET Library + +Build the main .NET library: + +```bash +# Restore dependencies +dotnet restore + +# Build all projects +dotnet build + +# Build specific project +dotnet build src/StatsdClient/StatsdClient.csproj + +# Build for specific configuration +dotnet build -c Release + +# Build for specific target framework +dotnet build src/StatsdClient/StatsdClient.csproj -f netstandard2.0 +``` + +### Native Library (Linux only) + +The repository includes a native C library (`libfs`) for Linux file system operations. This library must be built for multiple Linux variants to support different distributions. + +#### Using Docker (Recommended) + +Build all 4 Linux variants using Docker: + +```bash +# Build all variants (linux-x64, linux-musl-x64, linux-arm64, linux-musl-arm64) +./build-and-test.sh --platform linux + +# Build and test all variants +./build-and-test.sh --test --platform linux +``` + +**Output:** `runtimes/{rid}/native/libfs.so` + +**Supported RIDs:** +- `linux-x64` - Standard x64 Linux (glibc) +- `linux-musl-x64` - Alpine x64 Linux (musl libc) +- `linux-arm64` - Standard ARM64 Linux (glibc) +- `linux-musl-arm64` - Alpine ARM64 Linux (musl libc) + +#### Local Build (Linux/WSL only) + +Build without Docker on Linux or WSL: + +```bash +# Build for specific RID (outputs to runtimes/{rid}/native/) +./src/StatsdClient.Native/build.sh linux-x64 + +# Build for local development only (outputs to src/StatsdClient.Native/build/) +./src/StatsdClient.Native/build.sh +``` + +**Note:** Local builds compile for the current system architecture only. Docker is required to cross-compile for different architectures (x64/ARM64) or libc variants (glibc/musl). + +## Testing + +### Important: Framework Selection + +**Always specify `--framework` when running tests.** Running tests without a framework specification will run all target frameworks in parallel, causing conflicts due to shared named pipes. + +### Using build-and-test.sh + +```bash +# Test specific framework on native platform (Windows/macOS/Linux) +./build-and-test.sh --test --platform native --framework net8.0 + +# Test all frameworks sequentially on native platform +./build-and-test.sh --test --platform native + +# Test all Linux variants in Docker +./build-and-test.sh --test --platform linux +``` + +### Using dotnet test + +```bash +# Run all tests for a specific framework +dotnet test --framework net8.0 + +# Run tests for specific project and framework +dotnet test tests/StatsdClient.Tests/ --framework net8.0 + +# Run only native library tests +dotnet test --framework net8.0 --filter FullyQualifiedName~NativeLibraryTests + +# Run specific test class +dotnet test --framework net8.0 --filter FullyQualifiedName~DogStatsdServiceMetricsTests + +# Run specific test method +dotnet test --framework net8.0 --filter FullyQualifiedName~DogStatsdServiceMetricsTests.Counter +``` + +### Testing All Frameworks Sequentially + +**Linux/macOS:** +```bash +for tfm in netcoreapp2.1 netcoreapp3.0 netcoreapp3.1 net5.0 net6.0 net7.0 net8.0 net9.0 net10.0; do + dotnet test --framework $tfm +done +``` + +**Windows (includes .NET Framework):** +```bash +for tfm in net48 netcoreapp2.1 netcoreapp3.0 netcoreapp3.1 net5.0 net6.0 net7.0 net8.0 net9.0 net10.0; do + dotnet test --framework $tfm +done +``` + +Or use the build script: +```bash +./build-and-test.sh --test --platform native +``` + +### Supported Target Frameworks + +- .NET Framework 4.8 (Windows only) +- .NET Core 2.1, 3.0, 3.1 +- .NET 5, 6, 7, 8, 9, 10 + +## Packaging + +To create a NuGet package with all native libraries: + +```bash +# Step 1: Build all native library variants +./build-and-test.sh --platform linux + +# Step 2: Pack the NuGet package +dotnet pack src/StatsdClient/StatsdClient.csproj -c Release + +# Output: src/StatsdClient/bin/Release/*.nupkg +``` + +The package will include all 4 Linux native library variants in the correct RID directories. The .NET runtime automatically selects the appropriate variant based on the deployment environment. + +### Verify Package Contents + +**Linux/WSL:** +```bash +unzip -l src/StatsdClient/bin/Release/DogStatsD-CSharp-Client.*.nupkg | grep libfs +``` + +**Windows (PowerShell):** +```powershell +Expand-Archive src/StatsdClient/bin/Release/DogStatsD-CSharp-Client.*.nupkg -DestinationPath temp +Get-ChildItem temp/runtimes/*/native/ +``` + +## Benchmarks + +Run performance benchmarks: + +```bash +dotnet run -c Release --project benchmarks/StatsdClient.Benchmarks/StatsdClient.Benchmarks.csproj +``` + +## Troubleshooting + +### Tests fail with "address already in use" or named pipe conflicts + +Make sure you're specifying `--framework` when running tests. Running multiple frameworks in parallel causes port and named pipe conflicts. + +```bash +# ❌ Wrong - runs all frameworks in parallel +dotnet test + +# ✅ Correct - runs single framework +dotnet test --framework net8.0 +``` + +### Native library not found during tests + +Ensure you've built the native libraries before running native library tests: + +```bash +./build-and-test.sh --platform linux +dotnet test --framework net8.0 --filter FullyQualifiedName~NativeLibraryTests +``` + +### Docker build fails on Windows + +Make sure Docker Desktop is running and configured for Linux containers (not Windows containers). + +### Local native build fails + +Native library builds require: +- CMake 3.10+ +- GCC or Clang +- Standard build tools (make, etc.) + +On Ubuntu/Debian: +```bash +sudo apt-get update +sudo apt-get install -y cmake build-essential +``` + +On Alpine: +```bash +apk add --no-cache cmake make gcc g++ musl-dev +``` + +## Clean Build + +Remove build artifacts: + +```bash +# Clean .NET build outputs +dotnet clean + +# Remove native library outputs +rm -rf runtimes/*/native/libfs.so +rm -rf src/StatsdClient.Native/build/ + +# Remove NuGet packages +rm -rf src/StatsdClient/bin/ src/StatsdClient/obj/ +``` + +## Additional Resources + +- [src/StatsdClient.Native/README.md](src/StatsdClient.Native/README.md) - Native library details +- [.NET RID Catalog](https://learn.microsoft.com/en-us/dotnet/core/rid-catalog) - Runtime identifier documentation diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..9cafd64 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,180 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Repository Overview + +This is the DogStatsD C# client library (https://github.com/DataDog/dogstatsd-csharp-client), which provides a C# implementation of the DogStatsD protocol for sending metrics, events, and service checks to Datadog. + +## Build and Test Commands + +### Building + +#### .NET Library (Cross-platform) +```bash +# Restore dependencies +dotnet restore + +# Build the solution +dotnet build + +# Build specific project +dotnet build src/StatsdClient/StatsdClient.csproj + +# Build for specific target framework +dotnet build src/StatsdClient/StatsdClient.csproj -f netstandard2.0 +``` + +#### Native Library (Linux only) + +The repository includes a small native C library (`libfs`) for Linux inode operations. See `src/StatsdClient.Native/README.md` for details. + +**Using build-and-test.sh (recommended):** +```bash +# Build all 4 Linux variants using Docker (linux-x64, linux-musl-x64, linux-arm64, linux-musl-arm64) +./build-and-test.sh --platform linux + +# Build and test all Linux variants +./build-and-test.sh --test --platform linux + +# Build and test .NET for specific framework (native platform, no Docker) +./build-and-test.sh --test --platform native --framework net8.0 + +# Build and test .NET for all frameworks sequentially (native platform, no Docker) +./build-and-test.sh --test --platform native +``` + +**Local build (Linux/WSL only):** +```bash +# Build for specific RID (outputs to runtimes/{rid}/native/) +./src/StatsdClient.Native/build.sh linux-x64 +./src/StatsdClient.Native/build.sh linux-musl-x64 + +# Build for local development (outputs to src/StatsdClient.Native/build/) +./src/StatsdClient.Native/build.sh +``` + +**Output locations:** +- Docker builds: `runtimes/{rid}/native/libfs.so` +- Local builds with RID: `runtimes/{rid}/native/libfs.so` +- Local builds without RID: `src/StatsdClient.Native/build/libfs.so` + +**Supported RIDs** (per [official .NET RID catalog](https://learn.microsoft.com/en-us/dotnet/core/rid-catalog#linux-rids)): +- `linux-x64` (standard x64 Linux with glibc) +- `linux-musl-x64` (Alpine x64 Linux with musl) +- `linux-arm64` (standard ARM64 Linux with glibc) +- `linux-musl-arm64` (Alpine ARM64 Linux with musl) + +### Testing + +**IMPORTANT**: Always specify `--framework` when running tests. Running tests without a framework will run all target frameworks in parallel, which causes conflicts due to shared named pipes. + +```bash +# Run tests for a specific framework (REQUIRED) +dotnet test tests/StatsdClient.Tests/ --framework net8.0 + +# Run only native library tests +dotnet test tests/StatsdClient.Tests/ --framework net8.0 --filter FullyQualifiedName~NativeLibraryTests + +# Run a single test class +dotnet test tests/StatsdClient.Tests/ --framework net8.0 --filter FullyQualifiedName~DogStatsdServiceMetricsTests + +# Run a single test method +dotnet test tests/StatsdClient.Tests/ --framework net8.0 --filter FullyQualifiedName~DogStatsdServiceMetricsTests.Counter + +# Run all tests sequentially (one framework at a time) +# On Linux/macOS: +for tfm in netcoreapp2.1 netcoreapp3.0 netcoreapp3.1 net5.0 net6.0 net7.0 net8.0 net9.0 net10.0; do + dotnet test tests/StatsdClient.Tests/ --framework $tfm +done + +# On Windows (also includes net48): +for tfm in net48 netcoreapp2.1 netcoreapp3.0 netcoreapp3.1 net5.0 net6.0 net7.0 net8.0 net9.0 net10.0; do + dotnet test tests/StatsdClient.Tests/ --framework $tfm +done +``` + +### Packaging + +To build the NuGet package with all native libraries: + +```bash +# Step 1: Build all native variants using Docker +./build-and-test.sh --platform linux + +# Step 2: Pack the NuGet package +dotnet pack src/StatsdClient/StatsdClient.csproj -c Release + +# Output: src/StatsdClient/bin/Release/*.nupkg +``` + +The package will include all 4 native library variants in the correct RID directories. + +### Benchmarks +```bash +# Run benchmarks +dotnet run -c Release --project benchmarks/StatsdClient.Benchmarks/StatsdClient.Benchmarks.csproj +``` + +## Architecture + +### Core Components + +**DogStatsdService** (`src/StatsdClient/DogStatsdService.cs`): Thread-safe instance-based API for sending metrics. Requires explicit `Configure()` call before use. Must be disposed to flush metrics. + +**DogStatsd** (static class): Static wrapper around DogStatsdService for applications that prefer a single global instance. Shares the same underlying implementation. + +**StatsRouter** (`src/StatsdClient/StatsRouter.cs`): Routes incoming stats to either client-side aggregators (for Count, Gauge, Set) or directly to BufferBuilder (for Histogram, Distribution, Timing). + +**MetricsSender** (`src/StatsdClient/MetricsSender.cs`): Handles serialization and sending of metrics through the StatsRouter. + +### Client-Side Aggregation + +By default, basic metric types (Count, Gauge, Set) are aggregated client-side before sending to reduce network usage and agent load: +- **CountAggregator** (`src/StatsdClient/Aggregator/CountAggregator.cs`): Aggregates counter values +- **GaugeAggregator** (`src/StatsdClient/Aggregator/GaugeAggregator.cs`): Keeps last gauge value +- **SetAggregator** (`src/StatsdClient/Aggregator/SetAggregator.cs`): Tracks unique set values +- **AggregatorFlusher** (`src/StatsdClient/Aggregator/AggregatorFlusher.cs`): Periodically flushes aggregated metrics + +Aggregation window defaults to 2 seconds (configurable via `ClientSideAggregationConfig.FlushInterval`). Disable by setting `StatsdConfig.ClientSideAggregation` to null. + +### Buffering and Transport + +**BufferBuilder** (`src/StatsdClient/Bufferize/BufferBuilder.cs`): Batches multiple metrics into single datagrams up to max packet size (default 1432 bytes for UDP). + +**AsynchronousWorker** (`src/StatsdClient/Worker/AsynchronousWorker.cs`): Manages background worker threads that process the metrics queue asynchronously. Non-blocking except for `Flush()` and `Dispose()`. + +**Transport Layer** (`src/StatsdClient/Transport/`): +- **UDPTransport**: Standard UDP transport to agent +- **UnixDomainSocketTransport**: Unix domain socket transport (not supported on Windows for Dgram sockets) +- **NamedPipeTransport**: Windows named pipe transport (internal) + +### Configuration + +**StatsdConfig** (`src/StatsdClient/StatsdConfig.cs`): Main configuration class with properties: +- `StatsdServerName`: Agent hostname or unix socket path (e.g., "unix:///tmp/dsd.socket") +- `StatsdPort`: Agent port (defaults to 8125) +- `ClientSideAggregation`: Client-side aggregation settings (null to disable) +- Environment variable support: `DD_AGENT_HOST`, `DD_DOGSTATSD_PORT`, `DD_ENTITY_ID`, `DD_SERVICE`, `DD_ENV`, `DD_VERSION` + +## Target Frameworks + +The library supports: +- .NET Standard 2.0+ +- .NET Core 2.1, 3.0, 3.1 +- .NET 5.0, 6.0, 7.0, 8.0, 9.0 +- .NET Framework 4.8 + +Tests run on all supported frameworks via GitHub Actions (Linux and Windows). + +## Key Design Patterns + +1. **Object Pooling**: Uses custom Pool implementation (`src/StatsdClient/Utils/Pool.cs`) to reduce allocations for frequently created objects like buffers and stats. + +2. **Struct-based Stats**: Internal `Stats` structs (`src/StatsdClient/Statistic/`) minimize heap allocations when passing metrics through the pipeline. + +3. **Thread Safety**: Both DogStatsdService and static DogStatsd are thread-safe. Worker handlers must be thread-safe when `workerThreadCount` > 1. + +4. **Non-blocking Operations**: Metric submission methods are non-blocking (enqueue to worker thread). Only `Flush()` and `Dispose()` block. + +5. **Telemetry**: Built-in telemetry (`src/StatsdClient/Telemetry.cs`) tracks client metrics like bytes sent, packets sent, dropped metrics, etc. diff --git a/Dockerfile.linux b/Dockerfile.linux new file mode 100644 index 0000000..8c7c11d --- /dev/null +++ b/Dockerfile.linux @@ -0,0 +1,74 @@ +# Multi-stage Dockerfile for building and testing libfs for all Linux variants +# Usage: See build-and-test.sh + +ARG BASE_IMAGE +ARG DOTNET_TEST_ARGS="" +FROM ${BASE_IMAGE} AS builder + +# Install build tools based on the base image +RUN if command -v apt-get > /dev/null 2>&1; then \ + apt-get update && apt-get install -y cmake build-essential && rm -rf /var/lib/apt/lists/*; \ + elif command -v apk > /dev/null 2>&1; then \ + apk add --no-cache cmake make gcc g++ musl-dev bash; \ + fi + +WORKDIR /build + +# Copy only the native library source +COPY src/StatsdClient.Native/ ./src/StatsdClient.Native/ + +# Build the library using the build script +ARG RID +RUN chmod +x src/StatsdClient.Native/build.sh && \ + bash src/StatsdClient.Native/build.sh "$RID" + +# The output libfs.so is in /build/runtimes/${RID}/native/libfs.so + +# ============================================================ +# Test stage (optional - only built with --target tester) +# ============================================================ +FROM ${BASE_IMAGE} AS tester + +WORKDIR /build + +# Copy only project files for restore (better caching) +COPY src/StatsdClient/StatsdClient.csproj ./src/StatsdClient/ +COPY tests/StatsdClient.Tests/StatsdClient.Tests.csproj ./tests/StatsdClient.Tests/ + +# Restore dependencies (cached separately from source changes) +RUN dotnet restore src/StatsdClient/StatsdClient.csproj +RUN dotnet restore tests/StatsdClient.Tests/StatsdClient.Tests.csproj + +# Copy remaining source files +COPY src/ ./src/ +COPY tests/ ./tests/ +COPY stylecop.json stylecop.ruleset ./ + +# Copy the pre-built native library from builder stage +ARG RID +COPY --from=builder /build/runtimes/${RID}/native/libfs.so /build/runtimes/${RID}/native/libfs.so + +# Build the .NET project (without restore since already done) +RUN dotnet build src/StatsdClient/StatsdClient.csproj -c Release --no-restore + +# Build the test project (without restore since already done) +RUN dotnet build tests/StatsdClient.Tests/StatsdClient.Tests.csproj -c Release --no-restore + +# Copy library to the location where .NET runtime will find it during tests +# The library is already in the correct RID directory (linux-x64, linux-musl-x64, etc.) +# .NET runtime will automatically select the correct variant based on the environment +# As a workaround for testing, also copy to the root of the test output directory +ARG RID +RUN cp /build/tests/StatsdClient.Tests/bin/Release/net8.0/runtimes/${RID}/native/libfs.so \ + /build/tests/StatsdClient.Tests/bin/Release/net8.0/libfs.so || true + +# Run the native library tests +# DOTNET_TEST_ARGS can be used to pass additional arguments (e.g., --filter) +ARG DOTNET_TEST_ARGS="--filter FullyQualifiedName~NativeLibraryTests" +RUN dotnet test tests/StatsdClient.Tests/StatsdClient.Tests.csproj \ + --framework net8.0 \ + -c Release \ + -v normal \ + --no-build \ + --logger 'console;verbosity=normal' \ + ${DOTNET_TEST_ARGS} diff --git a/build-and-test.sh b/build-and-test.sh new file mode 100755 index 0000000..ad0a297 --- /dev/null +++ b/build-and-test.sh @@ -0,0 +1,231 @@ +#!/bin/bash +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Color output +GREEN='\033[0;32m' +BLUE='\033[0;34m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +# Default values +RUN_TESTS=false +PLATFORM="" +FRAMEWORK="" +EXTRA_TEST_ARGS=() + +# Parse arguments +while [[ $# -gt 0 ]]; do + case $1 in + --test) + RUN_TESTS=true + shift + ;; + --platform) + PLATFORM="$2" + shift 2 + ;; + --framework) + FRAMEWORK="$2" + shift 2 + ;; + --) + # Everything after -- is passed to dotnet test + shift + EXTRA_TEST_ARGS=("$@") + break + ;; + --help) + echo "Usage: $0 [--test] [--platform ] [--framework ] [-- ]" + echo "" + echo "Options:" + echo " --test Run tests after building (default: build only)" + echo " --platform Target platform:" + echo " native - Use native dotnet build/test (Windows/macOS)" + echo " linux - Use Docker to build all 4 Linux variants" + echo " (default: auto-detect based on OS)" + echo " --framework Target framework moniker (e.g., net8.0, net48)" + echo " If not specified, tests all frameworks sequentially" + echo " Prevents parallel test execution conflicts" + echo " -- Everything after -- is passed directly to dotnet test" + echo "" + echo "Examples:" + echo " $0 --test --platform native --framework net8.0 # Build and test .NET for net8.0 only" + echo " $0 --test --platform native # Build and test all frameworks sequentially" + echo " $0 --platform linux # Build Linux native libs in Docker" + echo " $0 --test --platform linux # Build and test all Linux variants in Docker" + echo " $0 --test --platform linux -- --filter FullyQualifiedName~NativeLibraryTests" + echo " # Test only NativeLibraryTests in Docker" + echo " $0 # Auto-detect platform, build only" + exit 0 + ;; + *) + echo -e "${RED}Unknown option: $1${NC}" + echo "Run '$0 --help' for usage information" + exit 1 + ;; + esac +done + +# Auto-detect platform if not specified +if [ -z "$PLATFORM" ]; then + if [[ "$OSTYPE" == "linux-gnu"* ]]; then + PLATFORM="linux" + echo -e "${YELLOW}Auto-detected platform: linux${NC}" + elif [[ "$OSTYPE" == "darwin"* ]] || [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]]; then + PLATFORM="native" + echo -e "${YELLOW}Auto-detected platform: native${NC}" + else + echo -e "${RED}Could not auto-detect platform. Please specify --platform ${NC}" + exit 1 + fi +fi + +# Validate platform +if [[ "$PLATFORM" != "native" && "$PLATFORM" != "linux" ]]; then + echo -e "${RED}Invalid platform: $PLATFORM${NC}" + echo "Must be 'native' or 'linux'" + exit 1 +fi + +echo -e "${BLUE}Starting build process...${NC}" +echo "" + +# Native platform (Windows/macOS - .NET only, no native libs) +if [ "$PLATFORM" = "native" ]; then + echo -e "${BLUE}Building .NET project (native platform)...${NC}" + dotnet build src/StatsdClient/StatsdClient.csproj -c Release + + if [ "$RUN_TESTS" = true ]; then + if [ -n "$FRAMEWORK" ]; then + # Test a single framework + echo "" + echo -e "${BLUE}Running tests for framework: $FRAMEWORK${NC}" + dotnet test tests/StatsdClient.Tests/StatsdClient.Tests.csproj \ + --framework "$FRAMEWORK" \ + -c Release \ + --no-build \ + "${EXTRA_TEST_ARGS[@]}" + else + # Test all frameworks sequentially + echo "" + echo -e "${BLUE}Running tests for all frameworks sequentially (to avoid named pipe conflicts)...${NC}" + + # Determine which frameworks to test based on OS + FRAMEWORKS="netcoreapp2.1 netcoreapp3.0 netcoreapp3.1 net5.0 net6.0 net7.0 net8.0 net9.0 net10.0" + + # Add .NET Framework on Windows + if [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]]; then + FRAMEWORKS="net48 $FRAMEWORKS" + fi + + # Run tests for each framework sequentially + for tfm in $FRAMEWORKS; do + echo "" + echo -e "${BLUE}Testing framework: $tfm${NC}" + dotnet test tests/StatsdClient.Tests/StatsdClient.Tests.csproj \ + --framework $tfm \ + -c Release \ + --no-build \ + "${EXTRA_TEST_ARGS[@]}" + done + fi + fi + + echo "" + echo -e "${GREEN}✓ Native build complete!${NC}" + exit 0 +fi + +# Linux platform (Docker - build native libs for all variants) +if [ "$PLATFORM" = "linux" ]; then + # Check if Docker is available + if ! command -v docker &> /dev/null; then + echo -e "${RED}Error: Docker is not installed or not in PATH${NC}" + exit 1 + fi + + export DOCKER_BUILDKIT=1 + echo "Docker version:" + docker --version + echo "" + + build_and_test_variant() { + local arch=$1 + local libc=$2 + local base_image=$3 + + # Construct proper RID based on official RID catalog + # https://learn.microsoft.com/en-us/dotnet/core/rid-catalog#linux-rids + local rid="linux-${arch}" + if [ "$libc" = "musl" ]; then + rid="linux-musl-${arch}" + fi + + echo -e "${BLUE}Building $rid...${NC}" + + # Determine Docker platform + local platform="" + if [ "$arch" = "x64" ]; then + platform="linux/amd64" + elif [ "$arch" = "arm64" ]; then + platform="linux/arm64" + fi + + # Determine build target + # For ARM64: only build native lib (skip tests due to QEMU emulation limitations) + local target="builder" + if [ "$RUN_TESTS" = true ] && [ "$arch" = "x64" ]; then + target="tester" + fi + + if [ "$RUN_TESTS" = true ] && [ "$arch" = "arm64" ]; then + echo -e "${YELLOW}Note: Skipping .NET tests for ARM64 due to QEMU emulation limitations${NC}" + fi + + # Build the Docker image + # Convert array to string for passing to Docker + local test_args_string="${EXTRA_TEST_ARGS[*]}" + + docker build \ + --platform "$platform" \ + --build-arg BASE_IMAGE="$base_image" \ + --build-arg RID="$rid" \ + --build-arg DOTNET_TEST_ARGS="$test_args_string" \ + --target "$target" \ + -t "libfs-${target}:${rid}" \ + -f Dockerfile.linux \ + . + + # Create a temporary container to extract the built library + local container_id=$(docker create --platform "$platform" "libfs-${target}:${rid}") + + # Extract the built library + mkdir -p "$SCRIPT_DIR/runtimes/$rid/native" + docker cp "$container_id:/build/runtimes/$rid/native/libfs.so" "$SCRIPT_DIR/runtimes/$rid/native/libfs.so" + + # Clean up + docker rm "$container_id" > /dev/null + + if [ "$RUN_TESTS" = true ] && [ "$arch" = "x64" ]; then + echo -e "${GREEN}✓ Built and tested $rid${NC}" + else + echo -e "${GREEN}✓ Built $rid${NC}" + fi + echo + } + + # Build all combinations + build_and_test_variant "x64" "glibc" "mcr.microsoft.com/dotnet/sdk:10.0" + build_and_test_variant "x64" "musl" "mcr.microsoft.com/dotnet/sdk:10.0-alpine" + build_and_test_variant "arm64" "glibc" "mcr.microsoft.com/dotnet/sdk:10.0" + build_and_test_variant "arm64" "musl" "mcr.microsoft.com/dotnet/sdk:10.0-alpine" + + echo "" + echo -e "${GREEN}All Linux builds complete!${NC}" + echo "Outputs in $SCRIPT_DIR/runtimes/" + echo "" + tree "$SCRIPT_DIR/runtimes/" +fi diff --git a/src/StatsdClient.Native/CMakeLists.txt b/src/StatsdClient.Native/CMakeLists.txt new file mode 100644 index 0000000..7137662 --- /dev/null +++ b/src/StatsdClient.Native/CMakeLists.txt @@ -0,0 +1,6 @@ +cmake_minimum_required(VERSION 3.15) +project(fs C) + +add_library(fs SHARED fs.c fs.h) + +install(TARGETS fs DESTINATION lib) diff --git a/src/StatsdClient.Native/README.md b/src/StatsdClient.Native/README.md new file mode 100644 index 0000000..93671e3 --- /dev/null +++ b/src/StatsdClient.Native/README.md @@ -0,0 +1,116 @@ +# libfs - Native Library for Linux File System Operations + +This directory contains a small C library (`libfs`) that provides cross-platform access to file inode numbers on Linux. + +## Purpose + +The library wraps the `stat()` system call to retrieve inode numbers, avoiding cross-platform P/Invoke compatibility issues when calling libc's `stat()` directly from C#. + +## Building + +### Using Docker (Recommended) + +Build all 4 Linux variants using Docker from the repository root: + +```bash +# Build all Linux variants +./build-and-test.sh --platform linux + +# Build and test all Linux variants +./build-and-test.sh --test --platform linux +``` + +**Supported RIDs** (per [.NET RID catalog](https://learn.microsoft.com/en-us/dotnet/core/rid-catalog#linux-rids)): +- `linux-x64` - Standard x64 Linux with glibc +- `linux-musl-x64` - Alpine x64 Linux with musl libc +- `linux-arm64` - Standard ARM64 Linux with glibc +- `linux-musl-arm64` - Alpine ARM64 Linux with musl libc + +**Output:** `runtimes/{rid}/native/libfs.so` + +### Local Build (Linux/WSL only) + +Build without Docker: + +```bash +# Build for specific RID (outputs to runtimes/{rid}/native/) +./src/StatsdClient.Native/build.sh linux-x64 +./src/StatsdClient.Native/build.sh linux-musl-x64 +./src/StatsdClient.Native/build.sh linux-arm64 +./src/StatsdClient.Native/build.sh linux-musl-arm64 + +# Build for local development (outputs to src/StatsdClient.Native/build/) +./src/StatsdClient.Native/build.sh +``` + +**Note:** Local builds compile for the current system only. Use Docker to cross-compile for different architectures or libc variants. + +### Testing + +**Using build-and-test.sh:** +```bash +# Test all Linux variants in Docker +./build-and-test.sh --test --platform linux + +# Test specific framework on native platform +./build-and-test.sh --test --platform native --framework net8.0 +``` + +**Using dotnet test directly:** + +After building, run tests using standard .NET tooling. **Always specify `--framework`** to avoid parallel test execution conflicts: + +```bash +# Run native library tests for a specific framework +dotnet test tests/StatsdClient.Tests/StatsdClient.Tests.csproj \ + --framework net8.0 \ + --filter 'FullyQualifiedName~NativeLibraryTests' + +# Run all tests for a specific framework +dotnet test tests/StatsdClient.Tests/StatsdClient.Tests.csproj \ + --framework net8.0 +``` + +The .NET runtime automatically selects the correct native library based on the current platform: +- On glibc systems: uses `runtimes/linux-{arch}/native/libfs.so` +- On musl systems (Alpine): uses `runtimes/linux-musl-{arch}/native/libfs.so` + +## API + +### C API + +```c +int get_inode(const char* path, unsigned long long* ino); +``` + +Returns: +- `0` on success, with `ino` populated with the inode number +- `-1` on failure (e.g., file not found, permission denied) + +### .NET API + +```csharp +using StatsdClient.Native; + +if (NativeInode.TryGetInode("/path/to/file", out ulong inode)) +{ + Console.WriteLine($"Inode: {inode}"); +} +else +{ + Console.WriteLine("Failed to get inode (not on Linux or file not found)"); +} +``` + +The .NET wrapper automatically detects the platform and returns `false` on Windows/macOS. + +## CI/CD + +The GitHub Actions workflow (`.github/workflows/build-and-test.yml`) automatically builds all variants using Docker containers and includes them in the test runs and NuGet package. + +## Files + +- `fs.h` - Header file +- `fs.c` - Implementation +- `CMakeLists.txt` - CMake build configuration +- `README.md` - This file diff --git a/src/StatsdClient.Native/build.sh b/src/StatsdClient.Native/build.sh new file mode 100755 index 0000000..d260bf7 --- /dev/null +++ b/src/StatsdClient.Native/build.sh @@ -0,0 +1,46 @@ +#!/bin/bash +set -e + +# Simple build script for libfs +# Usage: ./build.sh [rid] +# Example: ./build.sh linux-glibc-x64 +# If no RID is provided, builds to src/StatsdClient.Native/build/ (for local development) + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$SCRIPT_DIR/../.." +RID="$1" + +echo "Building libfs..." + +if [ -n "$RID" ]; then + # Build to runtimes/{rid}/native/ for consistency with Docker builds + OUTPUT_DIR="$REPO_ROOT/runtimes/$RID/native" + BUILD_DIR="$OUTPUT_DIR/build" + mkdir -p "$BUILD_DIR" + cd "$BUILD_DIR" + + # Configure and build + cmake "$SCRIPT_DIR" + cmake --build . --config Release + + # Move the library to the native directory (one level up from build) + mv libfs.so "$OUTPUT_DIR/libfs.so" + + # Clean up build artifacts + cd "$OUTPUT_DIR" + rm -rf build + + echo "Build complete. Library at: $OUTPUT_DIR/libfs.so" +else + # No RID specified - build to local build directory for development + OUTPUT_DIR="$SCRIPT_DIR" + mkdir -p "$OUTPUT_DIR/build" + cd "$OUTPUT_DIR/build" + + # Configure and build + cmake "$SCRIPT_DIR" + cmake --build . --config Release + + echo "Build complete. Library at: $OUTPUT_DIR/build/libfs.so" + echo "Note: To build for testing, specify a RID: ./build.sh linux-glibc-x64" +fi diff --git a/src/StatsdClient.Native/fs.c b/src/StatsdClient.Native/fs.c new file mode 100644 index 0000000..429babc --- /dev/null +++ b/src/StatsdClient.Native/fs.c @@ -0,0 +1,10 @@ +#include "fs.h" +#include + +int get_inode(const char* path, unsigned long long* ino) +{ + struct stat s; + if (stat(path, &s) != 0) return -1; + *ino = (unsigned long long)s.st_ino; + return 0; +} diff --git a/src/StatsdClient.Native/fs.h b/src/StatsdClient.Native/fs.h new file mode 100644 index 0000000..4a9bffa --- /dev/null +++ b/src/StatsdClient.Native/fs.h @@ -0,0 +1,6 @@ +#ifndef FS_H +#define FS_H + +int get_inode(const char* path, unsigned long long* ino); + +#endif diff --git a/src/StatsdClient/IFileSystem.cs b/src/StatsdClient/IFileSystem.cs index bde4519..d168819 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,13 +81,14 @@ 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 !NETFRAMEWORK // Unix Domain Sockets not supported on .NET Framework (always runs on Windows). + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { - inode = stat.st_ino; - return true; + return NativeMethods.TryGetInode(path, out inode); } +#endif + // Unsupported .NET runtime or OS inode = 0; return false; } diff --git a/src/StatsdClient/NativeMethods.cs b/src/StatsdClient/NativeMethods.cs new file mode 100644 index 0000000..b872335 --- /dev/null +++ b/src/StatsdClient/NativeMethods.cs @@ -0,0 +1,64 @@ +#if !NETFRAMEWORK + +using System; +using System.Runtime.InteropServices; + +namespace StatsdClient +{ + /// + /// P/Invoke wrapper for libfs native library to retrieve file inodes on Linux. + /// + internal static class NativeMethods + { + private const string LibraryName = "fs"; + + /// + /// Gets a value indicating whether the native library is supported on the current platform (Linux only). + /// + public static bool IsSupported => RuntimeInformation.IsOSPlatform(OSPlatform.Linux); + + /// + /// Attempts to get the inode number for the specified file path. + /// + /// The file path. + /// The inode number if successful, 0 otherwise. + /// true if the inode was successfully retrieved, false otherwise. + public static bool TryGetInode(string path, out ulong inode) + { + if (!IsSupported) + { + inode = 0; + return false; + } + + try + { + return GetInode(path, out inode) == 0; + } + catch (DllNotFoundException) + { + // Native library not available + inode = 0; + return false; + } + catch (EntryPointNotFoundException) + { + // Function not found in library + inode = 0; + return false; + } + } + + /// + /// Gets the inode number for a file path using the native libfs library. + /// + /// The file path. + /// The inode number if successful. + /// 0 on success, -1 on failure. + [DllImport(LibraryName, EntryPoint = "get_inode", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] + private static extern int GetInode( + string path, + out ulong inode); + } +} +#endif diff --git a/src/StatsdClient/StatsdClient.csproj b/src/StatsdClient/StatsdClient.csproj index c013ff1..f4f23a9 100644 --- a/src/StatsdClient/StatsdClient.csproj +++ b/src/StatsdClient/StatsdClient.csproj @@ -38,7 +38,6 @@ - @@ -47,4 +46,41 @@ + + + + + + + + + + + + + + + + + diff --git a/src/StatsdClient/UnixEndPoint.cs b/src/StatsdClient/UnixEndPoint.cs new file mode 100644 index 0000000..3ee6b52 --- /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); + } + } +} diff --git a/tests/StatsdClient.Tests/NativeLibraryTests.cs b/tests/StatsdClient.Tests/NativeLibraryTests.cs new file mode 100644 index 0000000..9a50449 --- /dev/null +++ b/tests/StatsdClient.Tests/NativeLibraryTests.cs @@ -0,0 +1,233 @@ +#if !NETFRAMEWORK + +using System; +using System.IO; +using System.Runtime.InteropServices; +using NUnit.Framework; +using StatsdClient; + +namespace Tests +{ + /// + /// Integration tests for the native libfs library on Linux. + /// These tests interact directly with the native library rather than mocking. + /// + [TestFixture] + public class NativeLibraryTests + { + private FileSystem _fileSystem; + + [SetUp] + public void SetUp() + { + _fileSystem = new FileSystem(); + } + + [Test] + public void TryStat_WithValidFile_ReturnsTrue() + { + // Only run on Linux where the native library is available + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + Assert.Ignore("Test only runs on Linux"); + } + + // Use a file that should always exist on Linux + var testPath = "/proc/self/exe"; + + var result = _fileSystem.TryStat(testPath, out ulong inode); + + Assert.IsTrue(result, "TryStat should succeed for valid file"); + Assert.Greater(inode, 0UL, "Inode should be greater than 0"); + } + + [Test] + public void TryStat_WithNonExistentFile_ReturnsFalse() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + Assert.Ignore("Test only runs on Linux"); + } + + var testPath = "/this/path/does/not/exist/file.txt"; + + var result = _fileSystem.TryStat(testPath, out ulong inode); + + Assert.IsFalse(result, "TryStat should fail for non-existent file"); + Assert.AreEqual(0UL, inode, "Inode should be 0 on failure"); + } + + [Test] + public void TryStat_WithSameFileTwice_ReturnsSameInode() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + Assert.Ignore("Test only runs on Linux"); + } + + var testPath = "/proc/self/exe"; + + var result1 = _fileSystem.TryStat(testPath, out ulong inode1); + var result2 = _fileSystem.TryStat(testPath, out ulong inode2); + + Assert.IsTrue(result1, "First TryStat should succeed"); + Assert.IsTrue(result2, "Second TryStat should succeed"); + Assert.AreEqual(inode1, inode2, "Same file should have same inode"); + } + + [Test] + public void TryStat_WithDifferentFiles_ReturnsDifferentInodes() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + Assert.Ignore("Test only runs on Linux"); + } + + var path1 = "/proc/self/exe"; + var path2 = "/proc/self/cmdline"; + + var result1 = _fileSystem.TryStat(path1, out ulong inode1); + var result2 = _fileSystem.TryStat(path2, out ulong inode2); + + Assert.IsTrue(result1, "First TryStat should succeed"); + Assert.IsTrue(result2, "Second TryStat should succeed"); + Assert.AreNotEqual(inode1, inode2, "Different files should have different inodes"); + } + + [Test] + public void TryStat_WithTemporaryFile_ReturnsValidInode() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + Assert.Ignore("Test only runs on Linux"); + } + + // Create a temporary file + var tempFile = Path.GetTempFileName(); + + try + { + File.WriteAllText(tempFile, "test content"); + + var result = _fileSystem.TryStat(tempFile, out ulong inode); + + Assert.IsTrue(result, "TryStat should succeed for temp file"); + Assert.Greater(inode, 0UL, "Inode should be greater than 0"); + } + finally + { + if (File.Exists(tempFile)) + { + File.Delete(tempFile); + } + } + } + + [Test] + public void TryStat_WithDirectory_ReturnsValidInode() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + Assert.Ignore("Test only runs on Linux"); + } + + var testPath = "/proc/self"; + + var result = _fileSystem.TryStat(testPath, out ulong inode); + + Assert.IsTrue(result, "TryStat should succeed for directory"); + Assert.Greater(inode, 0UL, "Inode should be greater than 0 for directory"); + } + + [Test] + public void TryStat_OnNonLinux_ReturnsFalse() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + Assert.Ignore("Test only runs on non-Linux platforms"); + } + + var testPath = "/some/path"; + + var result = _fileSystem.TryStat(testPath, out ulong inode); + + Assert.IsFalse(result, "TryStat should return false on non-Linux platforms"); + Assert.AreEqual(0UL, inode, "Inode should be 0 on non-Linux platforms"); + } + + [Test] + public void TryStat_VerifyHostCgroupNamespaceInode() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + Assert.Ignore("Test only runs on Linux"); + } + + var cgroupNsPath = "/proc/self/ns/cgroup"; + + // This test verifies we can read the cgroup namespace inode + // It will be 0xEFFFFFFB (4026531835) if running in the host namespace + var result = _fileSystem.TryStat(cgroupNsPath, out ulong inode); + + Assert.IsTrue(result, "TryStat should succeed for cgroup namespace"); + Assert.Greater(inode, 0UL, "Inode should be greater than 0"); + + // Note: We don't assert the specific value because it depends on whether + // we're running in a container or on the host + Console.WriteLine($"Cgroup namespace inode: {inode} (0x{inode:X})"); + } + + [Test] + public void TryStat_WithSymlink_ReturnsTargetInode() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + Assert.Ignore("Test only runs on Linux"); + } + + // /proc/self/exe is typically a symlink to the actual executable + var symlinkPath = "/proc/self/exe"; + + var result = _fileSystem.TryStat(symlinkPath, out ulong inode); + + Assert.IsTrue(result, "TryStat should succeed for symlink"); + Assert.Greater(inode, 0UL, "Inode should be greater than 0"); + + // stat() follows symlinks by default, so we should get the target's inode + // We can't easily verify this without lstat support, but at least verify it works + } + + [Test] + public void NativeMethods_DirectCall_WithValidFile() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + Assert.Ignore("Test only runs on Linux"); + } + + var testPath = "/proc/self/exe"; + + // Test the NativeMethods wrapper directly + var result = NativeMethods.TryGetInode(testPath, out ulong inode); + + Assert.IsTrue(result, "NativeMethods.TryGetInode should succeed"); + Assert.Greater(inode, 0UL, "Inode should be greater than 0"); + } + + [Test] + public void NativeMethods_IsSupported_ReflectsPlatform() + { + var isSupported = NativeMethods.IsSupported; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + Assert.IsTrue(isSupported, "IsSupported should be true on Linux"); + } + else + { + Assert.IsFalse(isSupported, "IsSupported should be false on non-Linux"); + } + } + } +} +#endif