Files
2026-07-13 12:31:29 +08:00

154 lines
4.5 KiB
C#

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<LockHandle> 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<int, Task>? delayAsync = null,
Func<string, int>? 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;
}
}