diff --git a/tracer/src/Datadog.Trace.Trimming/build/Datadog.Trace.Trimming.xml b/tracer/src/Datadog.Trace.Trimming/build/Datadog.Trace.Trimming.xml
index 863c8336830e..7a79a7909450 100644
--- a/tracer/src/Datadog.Trace.Trimming/build/Datadog.Trace.Trimming.xml
+++ b/tracer/src/Datadog.Trace.Trimming/build/Datadog.Trace.Trimming.xml
@@ -803,6 +803,7 @@
+
diff --git a/tracer/src/Datadog.Trace/Ci/Configuration/TestOptimizationSettings.cs b/tracer/src/Datadog.Trace/Ci/Configuration/TestOptimizationSettings.cs
index 210c91b4b177..34be034d5a36 100644
--- a/tracer/src/Datadog.Trace/Ci/Configuration/TestOptimizationSettings.cs
+++ b/tracer/src/Datadog.Trace/Ci/Configuration/TestOptimizationSettings.cs
@@ -370,5 +370,41 @@ internal TracerSettings InitializeTracerSettings(CompositeConfigurationSource so
var newSource = new CompositeConfigurationSource([new DictionaryObjectConfigurationSource(additionalSource), source]);
return new TracerSettings(newSource, telemetry, new OverrideErrorLog());
}
+
+ public override string ToString()
+ {
+ var sb = StringBuilderCache.Acquire(StringBuilderCache.MaxBuilderSize);
+ sb.AppendFormat(
+ "{{ Enabled={0}, Agentless={1}, Site={2}, ApiKey={3}, AgentlessUrl={4}, MaximumAgentlessPayloadSize={5}, ProxyHttps={6}, ProxyNoProxy={7}, Logs={8}, CodeCoverageEnabled={9}, CodeCoverageSnkFilePath={10}, CodeCoveragePath={11}, CodeCoverageEnableJitOptimizations={12}, CodeCoverageMode={13}, GitUploadEnabled={14}, TestsSkippingEnabled={15}, IntelligentTestRunnerEnabled={16}, ForceAgentsEvpProxy={17}, InstallDatadogTraceInGac={18}, EarlyFlakeDetectionEnabled={19}, KnownTestsEnabled={20}, RumFlushWaitMillis={21}, TestSessionName='{22}', FlakyRetryEnabled={23}, FlakyRetryCount={24}, TotalFlakyRetryCount={25}, ImpactedTestsDetectionEnabled={26}, TestManagementEnabled={27} }}",
+ Enabled,
+ Agentless,
+ Site,
+ !string.IsNullOrEmpty(ApiKey) ? "" : "null",
+ AgentlessUrl,
+ MaximumAgentlessPayloadSize,
+ ProxyHttps,
+ string.Join(", ", ProxyNoProxy ?? []),
+ Logs,
+ CodeCoverageEnabled,
+ CodeCoverageSnkFilePath,
+ CodeCoveragePath,
+ CodeCoverageEnableJitOptimizations,
+ CodeCoverageMode,
+ GitUploadEnabled,
+ TestsSkippingEnabled,
+ IntelligentTestRunnerEnabled,
+ ForceAgentsEvpProxy,
+ InstallDatadogTraceInGac,
+ EarlyFlakeDetectionEnabled,
+ KnownTestsEnabled,
+ RumFlushWaitMillis,
+ TestSessionName,
+ FlakyRetryEnabled,
+ FlakyRetryCount,
+ TotalFlakyRetryCount,
+ ImpactedTestsDetectionEnabled,
+ TestManagementEnabled);
+ return StringBuilderCache.GetStringAndRelease(sb);
+ }
}
}
diff --git a/tracer/src/Datadog.Trace/Ci/Ipc/CircularChannel.cs b/tracer/src/Datadog.Trace/Ci/Ipc/CircularChannel.cs
index b766db605782..3cc073ee0f8b 100644
--- a/tracer/src/Datadog.Trace/Ci/Ipc/CircularChannel.cs
+++ b/tracer/src/Datadog.Trace/Ci/Ipc/CircularChannel.cs
@@ -19,7 +19,7 @@ internal partial class CircularChannel : IChannel
private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor(typeof(CircularChannel));
private readonly MemoryMappedFile _mmf;
- private readonly Mutex _mutex;
+ private readonly CrossPlatformLock _mutex;
private readonly CircularChannelSettings _settings;
private long _disposed;
@@ -72,9 +72,16 @@ public CircularChannel(string fileName, CircularChannelSettings settings)
}
_disposed = 0;
- _mutex = new Mutex(
- initiallyOwned: false,
- FrameworkDescription.Instance.IsWindows() ? @$"Global\{Path.GetFileNameWithoutExtension(fileName)}" : $"{Path.GetFileNameWithoutExtension(fileName)}");
+ var lockName = Path.GetFileNameWithoutExtension(fileName);
+ Log.Debug("CircularChannel: Initializing with file {FileName} and lock name {LockName}", fileName, lockName);
+ if (CrossPlatformLock.TryOpenExisting(lockName, out var existingLock) && existingLock != null)
+ {
+ _mutex = existingLock;
+ }
+ else
+ {
+ _mutex = new CrossPlatformLock(lockName);
+ }
var hasHandle = _mutex.WaitOne(_settings.MutexTimeout);
if (!hasHandle)
diff --git a/tracer/src/Datadog.Trace/Ci/Ipc/CrossPlatformLock.cs b/tracer/src/Datadog.Trace/Ci/Ipc/CrossPlatformLock.cs
new file mode 100644
index 000000000000..ad9de7cbee74
--- /dev/null
+++ b/tracer/src/Datadog.Trace/Ci/Ipc/CrossPlatformLock.cs
@@ -0,0 +1,437 @@
+//
+// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
+// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
+//
+#nullable enable
+
+using System;
+using System.IO;
+using System.Runtime.Versioning;
+using System.Threading;
+using Datadog.Trace.Logging;
+
+namespace Datadog.Trace.Ci.Ipc;
+
+// Potential infinite loop
+#pragma warning disable DD0001
+
+///
+/// Cross-platform locking mechanism that uses named mutexes on Windows
+/// and file-based locking on Unix-like systems (including macOS)
+///
+internal sealed class CrossPlatformLock : IDisposable
+{
+ private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor(typeof(CrossPlatformLock));
+
+ private readonly ILockImplementation _lockImpl;
+ private volatile bool _disposed;
+
+ public CrossPlatformLock(string name)
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ throw new ArgumentException("Lock name cannot be null or empty", nameof(name));
+ }
+
+ // Use platform-specific locking mechanism
+ if (FrameworkDescription.Instance.IsWindows())
+ {
+ _lockImpl = new WindowsMutexLock(name);
+ }
+ else if (FrameworkDescription.Instance.OSPlatform == OSPlatformName.Linux)
+ {
+// This call site is reachable on all platforms.
+#pragma warning disable CA1416
+ _lockImpl = new LinuxFileLock(name);
+#pragma warning restore CA1416
+ }
+ else
+ {
+ // macOS and other Unix systems - use exclusive file access
+ _lockImpl = new MacOSFileLock(name);
+ }
+ }
+
+ private CrossPlatformLock(ILockImplementation lockImpl)
+ {
+ _lockImpl = lockImpl;
+ }
+
+ public static bool TryOpenExisting(string name, out CrossPlatformLock? existingLock)
+ {
+ existingLock = null;
+
+ try
+ {
+ if (FrameworkDescription.Instance.IsWindows())
+ {
+ var mutexName = $@"Global\{name}";
+ if (Mutex.TryOpenExisting(mutexName, out var existingMutex))
+ {
+ existingLock = new CrossPlatformLock(new WindowsMutexLock(existingMutex));
+ return true;
+ }
+ }
+ else
+ {
+ // For Unix systems, we can't really "open existing" in the same way
+ // So we just create a new lock pointing to the same file
+ existingLock = new CrossPlatformLock(name);
+ return true;
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.Debug(ex, "Failed to open existing lock {Name}", name);
+ }
+
+ return false;
+ }
+
+ public bool WaitOne(int timeoutMs)
+ {
+ ThrowIfDisposed();
+ return _lockImpl.WaitOne(timeoutMs);
+ }
+
+ public void ReleaseMutex()
+ {
+ ThrowIfDisposed();
+ _lockImpl.Release();
+ }
+
+ public void Dispose()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ _disposed = true;
+ _lockImpl?.Dispose();
+ }
+
+ private void ThrowIfDisposed()
+ {
+ if (_disposed)
+ {
+ throw new ObjectDisposedException(nameof(CrossPlatformLock));
+ }
+ }
+
+#pragma warning disable SA1201
+ private interface ILockImplementation : IDisposable
+#pragma warning restore SA1201
+ {
+ bool WaitOne(int timeoutMs);
+
+ void Release();
+ }
+
+ private sealed class WindowsMutexLock : ILockImplementation
+ {
+ private readonly Mutex _mutex;
+ private volatile bool _disposed;
+
+ public WindowsMutexLock(string name)
+ {
+ var mutexName = $@"Global\{name}";
+ _mutex = new Mutex(initiallyOwned: false, name: mutexName);
+ }
+
+ public WindowsMutexLock(Mutex existingMutex)
+ {
+ _mutex = existingMutex ?? throw new ArgumentNullException(nameof(existingMutex));
+ }
+
+ public bool WaitOne(int timeoutMs)
+ {
+ return _mutex.WaitOne(timeoutMs);
+ }
+
+ public void Release()
+ {
+ try
+ {
+ _mutex.ReleaseMutex();
+ }
+ catch (ApplicationException)
+ {
+ // Mutex was not owned by current thread, ignore
+ }
+ }
+
+ public void Dispose()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ _disposed = true;
+ _mutex?.Dispose();
+ }
+ }
+
+#if NET6_0_OR_GREATER
+ [UnsupportedOSPlatform("ios")]
+ [UnsupportedOSPlatform("macos")]
+ [UnsupportedOSPlatform("tvos")]
+#endif
+ private sealed class LinuxFileLock : ILockImplementation
+ {
+ private readonly string _lockFilePath;
+ private readonly FileStream _lockFile;
+ private volatile bool _disposed;
+ private volatile bool _isLocked;
+
+ public LinuxFileLock(string name)
+ {
+ // Create lock file in a system temp directory
+ var tempDir = Path.GetTempPath();
+ var lockDir = Path.Combine(tempDir, "dd-trace-locks");
+
+ try
+ {
+ Directory.CreateDirectory(lockDir);
+ }
+ catch (Exception ex)
+ {
+ Log.Warning(ex, "Failed to create lock directory {LockDir}, using temp directory", lockDir);
+ lockDir = tempDir;
+ }
+
+ // Sanitize the name to be safe for filesystem
+ var safeName = string.Join("_", name.Split(Path.GetInvalidFileNameChars()));
+ _lockFilePath = Path.Combine(lockDir, $"{safeName}.lock");
+
+ Log.Debug("Using Linux lock file: {LockFilePath}", _lockFilePath);
+
+ // Open the file once and keep it open for the lifetime of this lock
+ _lockFile = new FileStream(
+ _lockFilePath,
+ FileMode.OpenOrCreate,
+ FileAccess.ReadWrite,
+ FileShare.ReadWrite, // Allow other processes to open the file
+ bufferSize: 1024);
+
+ // Write current process ID to the lock file for debugging
+ try
+ {
+ var processInfo = $"PID:{System.Diagnostics.Process.GetCurrentProcess().Id} Created:{DateTime.UtcNow:yyyy-MM-dd HH:mm:ss}";
+ var bytes = System.Text.Encoding.UTF8.GetBytes(processInfo);
+ _lockFile.Write(bytes, 0, bytes.Length);
+ _lockFile.Flush();
+ }
+ catch (Exception ex)
+ {
+ Log.Debug(ex, "Failed to write process info to lock file");
+ }
+ }
+
+ public bool WaitOne(int timeoutMs)
+ {
+ if (_disposed)
+ {
+ return false;
+ }
+
+ var endTime = DateTime.UtcNow.AddMilliseconds(timeoutMs);
+
+ while (DateTime.UtcNow < endTime && !_disposed)
+ {
+ try
+ {
+ // Try to acquire exclusive lock on the entire file (Linux supports FileStream.Lock)
+ _lockFile.Lock(0, _lockFile.Length > 0 ? _lockFile.Length : 1);
+ _isLocked = true;
+ return true;
+ }
+ catch (IOException)
+ {
+ // File region is locked by another process, wait a bit and retry
+ Thread.Sleep(10); // Small delay before retry
+ }
+ catch (Exception ex)
+ {
+ Log.Debug(ex, "Unexpected error acquiring file lock");
+ return false;
+ }
+ }
+
+ return false; // Timeout reached
+ }
+
+ public void Release()
+ {
+ if (_isLocked && !_disposed)
+ {
+ try
+ {
+ _lockFile.Unlock(0, _lockFile.Length > 0 ? _lockFile.Length : 1);
+ _isLocked = false;
+ }
+ catch (Exception ex)
+ {
+ Log.Debug(ex, "Failed to unlock file {LockFilePath}", _lockFilePath);
+ }
+ }
+ }
+
+ public void Dispose()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ _disposed = true;
+
+ // Release the lock if we still have it
+ Release();
+
+ // Close the file stream
+ try
+ {
+ _lockFile?.Dispose();
+ }
+ catch (Exception ex)
+ {
+ Log.Debug(ex, "Failed to dispose lock file stream");
+ }
+
+ // Clean up the lock file
+ try
+ {
+ if (File.Exists(_lockFilePath))
+ {
+ File.Delete(_lockFilePath);
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.Debug(ex, "Failed to delete lock file {LockFilePath}", _lockFilePath);
+ }
+ }
+ }
+
+ private sealed class MacOSFileLock : ILockImplementation
+ {
+ private readonly string _lockFilePath;
+ private FileStream? _lockFile;
+ private volatile bool _disposed;
+
+ public MacOSFileLock(string name)
+ {
+ // Create lock file in a system temp directory
+ var tempDir = Path.GetTempPath();
+ var lockDir = Path.Combine(tempDir, "dd-trace-locks");
+
+ try
+ {
+ Directory.CreateDirectory(lockDir);
+ }
+ catch (Exception ex)
+ {
+ Log.Warning(ex, "Failed to create lock directory {LockDir}, using temp directory", lockDir);
+ lockDir = tempDir;
+ }
+
+ // Sanitize the name to be safe for filesystem
+ var safeName = string.Join("_", name.Split(Path.GetInvalidFileNameChars()));
+ _lockFilePath = Path.Combine(lockDir, $"{safeName}.lock");
+
+ Log.Debug("Using macOS lock file: {LockFilePath}", _lockFilePath);
+ }
+
+ public bool WaitOne(int timeoutMs)
+ {
+ if (_disposed)
+ {
+ return false;
+ }
+
+ var endTime = DateTime.UtcNow.AddMilliseconds(timeoutMs);
+
+ while (DateTime.UtcNow < endTime && !_disposed)
+ {
+ try
+ {
+ // On macOS, use exclusive file access since FileStream.Lock is not supported
+ _lockFile = new FileStream(
+ _lockFilePath,
+ FileMode.OpenOrCreate,
+ FileAccess.ReadWrite,
+ FileShare.None, // Exclusive access - this is the locking mechanism
+ bufferSize: 1);
+
+ // Write current process ID to the lock file for debugging
+ var processInfo = $"PID:{System.Diagnostics.Process.GetCurrentProcess().Id} Created:{DateTime.UtcNow:yyyy-MM-dd HH:mm:ss}";
+ var bytes = System.Text.Encoding.UTF8.GetBytes(processInfo);
+ _lockFile.Write(bytes, 0, bytes.Length);
+ _lockFile.Flush();
+
+ return true;
+ }
+ catch (IOException)
+ {
+ // File is locked by another process, wait a bit and retry
+ _lockFile?.Dispose();
+ _lockFile = null;
+
+ Thread.Sleep(10); // Small delay before retry
+ }
+ catch (UnauthorizedAccessException)
+ {
+ // Permission issue, wait a bit and retry
+ _lockFile?.Dispose();
+ _lockFile = null;
+
+ Thread.Sleep(10);
+ }
+ catch (Exception ex)
+ {
+ Log.Debug(ex, "Unexpected error acquiring file lock");
+ _lockFile?.Dispose();
+ _lockFile = null;
+ return false;
+ }
+ }
+
+ return false; // Timeout reached
+ }
+
+ public void Release()
+ {
+ if (!_disposed)
+ {
+ _lockFile?.Dispose();
+ _lockFile = null;
+
+ // Clean up the lock file
+ try
+ {
+ if (File.Exists(_lockFilePath))
+ {
+ File.Delete(_lockFilePath);
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.Debug(ex, "Failed to delete lock file {LockFilePath}", _lockFilePath);
+ }
+ }
+ }
+
+ public void Dispose()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ _disposed = true;
+ Release();
+ }
+ }
+}
diff --git a/tracer/src/Datadog.Trace/Ci/Ipc/IpcClient.cs b/tracer/src/Datadog.Trace/Ci/Ipc/IpcClient.cs
index 3700cf32f07c..c9a337b931a1 100644
--- a/tracer/src/Datadog.Trace/Ci/Ipc/IpcClient.cs
+++ b/tracer/src/Datadog.Trace/Ci/Ipc/IpcClient.cs
@@ -11,7 +11,7 @@ namespace Datadog.Trace.Ci.Ipc;
internal class IpcClient : IpcDualChannel
{
public IpcClient(string name)
- : base($"{name}.send", $"{name}.recv")
+ : base($"{name}_send.mtx", $"{name}_recv.mtx")
{
}
}
diff --git a/tracer/src/Datadog.Trace/Ci/Ipc/IpcServer.cs b/tracer/src/Datadog.Trace/Ci/Ipc/IpcServer.cs
index 119f7e6448c6..62b4fa3a9657 100644
--- a/tracer/src/Datadog.Trace/Ci/Ipc/IpcServer.cs
+++ b/tracer/src/Datadog.Trace/Ci/Ipc/IpcServer.cs
@@ -12,7 +12,7 @@ namespace Datadog.Trace.Ci.Ipc;
internal class IpcServer : IpcDualChannel
{
public IpcServer(string name)
- : base($"{name}.recv", $"{name}.send")
+ : base($"{name}_recv.mtx", $"{name}_send.mtx")
{
}
}
diff --git a/tracer/src/Datadog.Trace/Ci/TestOptimization.cs b/tracer/src/Datadog.Trace/Ci/TestOptimization.cs
index 47acc9ad427c..163833c8ac17 100644
--- a/tracer/src/Datadog.Trace/Ci/TestOptimization.cs
+++ b/tracer/src/Datadog.Trace/Ci/TestOptimization.cs
@@ -221,6 +221,8 @@ public void Initialize()
Log.Information("TestOptimization: Initializing CI Visibility");
var settings = Settings;
+ Log.Information("TestOptimizationSettings: {Settings}", settings);
+
// In case we are running using the agent, check if the event platform proxy is supported.
TracerManagement = new TestOptimizationTracerManagement(
settings: Settings,
@@ -288,6 +290,7 @@ public void InitializeFromRunner(TestOptimizationSettings settings, IDiscoverySe
}
Log.Information("TestOptimization: Initializing CI Visibility from dd-trace / runner");
+ Log.Information("TestOptimizationSettings: {Settings}", settings);
Settings = settings;
LifetimeManager.Instance.AddAsyncShutdownTask(ShutdownAsync);