Files
YKanBan/YKanBan/Storage/Workspace/WorkspaceLock.cs
T

92 lines
3.4 KiB
C#

using System.Text;
namespace YKanBan.Storage.Workspace;
/// <summary>
/// Exclusive, OS-enforced lock over a workspace's ykanban.lock. The handle is
/// held until disposal, so crashes and power loss release it automatically.
/// The lock file also carries PID / machine name / time for human diagnostics.
/// </summary>
public sealed class WorkspaceLock : IDisposable
{
private readonly FileStream _stream;
/// <summary>
/// Initializes the lock around an already-acquired handle.
/// </summary>
/// <param name="stream">The exclusively opened lock file stream.</param>
/// <param name="lockFilePath">Path of the lock file being held.</param>
private WorkspaceLock(FileStream stream, string lockFilePath)
{
_stream = stream;
LockFilePath = lockFilePath;
}
/// <summary>
/// Gets the path of the lock file being held.
/// </summary>
public string LockFilePath { get; }
/// <summary>
/// Acquires the workspace lock, writing this process's diagnostics into it.
/// </summary>
/// <param name="folderPath">The workspace folder to lock.</param>
/// <returns>The held lock; dispose it to release.</returns>
/// <exception cref="WorkspaceNotInitializedException">The folder has no .ykanban structure.</exception>
/// <exception cref="WorkspaceLockException">Another instance already holds the lock.</exception>
public static WorkspaceLock Acquire(string folderPath)
{
string lockFilePath = WorkspacePaths.LockFile(folderPath);
FileStream stream;
try
{
// FileShare.None gives the exclusive semantics; FileMode.Create truncates stale content.
stream = new FileStream(lockFilePath, FileMode.Create, FileAccess.Write, FileShare.None);
}
catch (DirectoryNotFoundException)
{
throw new WorkspaceNotInitializedException(folderPath);
}
catch (IOException ex)
{
throw new WorkspaceLockException(lockFilePath, ReadDiagnosticsBestEffort(lockFilePath), ex);
}
// Write holder diagnostics so humans can identify the owning instance from the file alone.
string diagnostics = $"pid={Environment.ProcessId};machine={Environment.MachineName};time={DateTimeOffset.UtcNow:O}";
byte[] payload = Encoding.UTF8.GetBytes(diagnostics);
stream.Write(payload, 0, payload.Length);
stream.Flush();
return new WorkspaceLock(stream, lockFilePath);
}
/// <summary>
/// Attempts to read holder diagnostics from a lock file we failed to
/// acquire. The read is best-effort: a live FileShare.None holder makes the
/// file unreadable, in which case <see langword="null"/> is returned.
/// </summary>
/// <param name="lockFilePath">The lock file to read.</param>
/// <returns>The diagnostics text, or <see langword="null"/> when unreadable.</returns>
private static string? ReadDiagnosticsBestEffort(string lockFilePath)
{
try
{
return File.ReadAllText(lockFilePath);
}
catch (IOException)
{
return null;
}
catch (UnauthorizedAccessException)
{
return null;
}
}
/// <summary>
/// Releases the exclusive handle. The lock file itself is intentionally
/// left on disk; the OS releases the handle even on a crash.
/// </summary>
public void Dispose() => _stream.Dispose();
}