using System.Runtime.InteropServices; using System.Text.Json; namespace CodexProviderSync.Core; public sealed class LockService { private const int Win32ErrorAlreadyExists = 183; private const int Win32ErrorAccessDenied = 5; private const int DefaultLockCreateRetryCount = 3; private const int DefaultLockCreateRetryDelayMs = 75; public async Task AcquireLockAsync(string codexHome, string label = "codex-provider-sync") { string lockPath = AppConstants.LockPath(codexHome); Directory.CreateDirectory(Path.GetDirectoryName(lockPath)!); await CreateLockDirectoryAsync(lockPath); try { LockOwner owner = new() { ProcessId = Environment.ProcessId, StartedAt = DateTimeOffset.UtcNow, Label = label, CurrentDirectory = Environment.CurrentDirectory }; await File.WriteAllTextAsync( Path.Combine(lockPath, "owner.json"), JsonSerializer.Serialize(owner, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, WriteIndented = true })); return new LockHandle(lockPath); } catch { Directory.Delete(lockPath, recursive: true); throw; } } internal static async Task CreateLockDirectoryAsync( string lockPath, int retryCount = DefaultLockCreateRetryCount, int retryDelayMs = DefaultLockCreateRetryDelayMs, Func? delayAsync = null, Func? tryCreateDirectory = null) { delayAsync ??= static delay => Task.Delay(delay); tryCreateDirectory ??= TryCreateDirectory; int attempts = 0; while (true) { int errorCode = tryCreateDirectory(lockPath); if (errorCode == 0) { return; } if (errorCode == Win32ErrorAlreadyExists) { throw new InvalidOperationException( $"Lock already exists at {lockPath}. Close Codex/App and retry, or remove the stale lock if you are sure no sync is running."); } if (!IsTransientLockCreateError(errorCode) || attempts >= retryCount) { throw new IOException($"Unable to create lock directory at {lockPath}. Win32 error: {errorCode}"); } attempts += 1; await delayAsync(retryDelayMs); } } private static bool IsTransientLockCreateError(int errorCode) { return errorCode == Win32ErrorAccessDenied; } private static int TryCreateDirectory(string lockPath) { return OperatingSystem.IsWindows() ? TryCreateDirectoryWindows(lockPath) : TryCreateDirectoryUnix(lockPath); } private static int TryCreateDirectoryWindows(string lockPath) { return CreateDirectory(lockPath, IntPtr.Zero) ? 0 : Marshal.GetLastWin32Error(); } private static int TryCreateDirectoryUnix(string lockPath) { if (Mkdir(lockPath, 448) == 0) { return 0; } int errorCode = Marshal.GetLastWin32Error(); return errorCode switch { 17 => Win32ErrorAlreadyExists, 1 or 13 => Win32ErrorAccessDenied, _ => errorCode }; } [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] private static extern bool CreateDirectory(string lpPathName, IntPtr lpSecurityAttributes); [DllImport("libc", SetLastError = true, EntryPoint = "mkdir")] private static extern int Mkdir(string pathname, uint mode); private sealed class LockOwner { public required int ProcessId { get; init; } public required DateTimeOffset StartedAt { get; init; } public required string Label { get; init; } public required string CurrentDirectory { get; init; } } } public sealed class LockHandle : IAsyncDisposable { private readonly string _lockPath; private bool _released; public LockHandle(string lockPath) { _lockPath = lockPath; } public ValueTask DisposeAsync() { if (_released) { return ValueTask.CompletedTask; } _released = true; if (Directory.Exists(_lockPath)) { Directory.Delete(_lockPath, recursive: true); } return ValueTask.CompletedTask; } }