154 lines
4.5 KiB
C#
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;
|
|
}
|
|
}
|