refactor: keep the workspace lock file empty
This commit is contained in:
1 parent
d8a4dd6190
commit
8a7c6fe8d6
3 files changed
+11
-67
No files matched your search
@@ -5,7 +5,7 @@ using YKanBan.Tests.TestUtilities;
|
||||
namespace YKanBan.Tests.Storage.Workspace;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for exclusive lock acquisition, diagnostics and release.
|
||||
/// Tests for exclusive lock acquisition and release.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class WorkspaceLockTests
|
||||
@@ -42,7 +42,7 @@ public class WorkspaceLockTests
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void LockFileContainsHolderDiagnosticsAfterRelease()
|
||||
public void LockFileIsEmptyAfterRelease()
|
||||
{
|
||||
using var directory = new TempDirectory();
|
||||
WorkspaceInitializer.Initialize(directory.FullPath);
|
||||
@@ -51,9 +51,7 @@ public class WorkspaceLockTests
|
||||
{
|
||||
}
|
||||
|
||||
// The file survives release and carries PID / machine name / time for humans.
|
||||
string content = File.ReadAllText(WorkspacePaths.LockFile(directory.FullPath));
|
||||
StringAssert.Contains(content, "pid=");
|
||||
StringAssert.Contains(content, $"machine={Environment.MachineName}");
|
||||
// The lock file survives release but carries no content; only the OS handle matters.
|
||||
Assert.AreEqual(0L, new FileInfo(WorkspacePaths.LockFile(directory.FullPath)).Length);
|
||||
}
|
||||
}
|
||||
@@ -72,39 +72,15 @@ public sealed class WorkspaceLockException : YKanBanException
|
||||
/// </summary>
|
||||
public string LockFilePath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the best-effort holder diagnostics read from the lock file, or
|
||||
/// <see langword="null"/> when they could not be read.
|
||||
/// </summary>
|
||||
public string? HolderDiagnostics { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the exception for a failed lock acquisition.
|
||||
/// </summary>
|
||||
/// <param name="lockFilePath">The lock file that is already held.</param>
|
||||
/// <param name="holderDiagnostics">Diagnostics of the current holder, when readable.</param>
|
||||
/// <param name="innerException">The underlying I/O error.</param>
|
||||
public WorkspaceLockException(string lockFilePath, string? holderDiagnostics, Exception innerException)
|
||||
: base(BuildMessage(lockFilePath, holderDiagnostics), innerException)
|
||||
public WorkspaceLockException(string lockFilePath, Exception innerException)
|
||||
: base($"The workspace is locked by another instance: {lockFilePath}", innerException)
|
||||
{
|
||||
LockFilePath = lockFilePath;
|
||||
HolderDiagnostics = holderDiagnostics;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Composes the diagnostic message, appending holder details when they were readable.
|
||||
/// </summary>
|
||||
/// <param name="lockFilePath">The lock file that is already held.</param>
|
||||
/// <param name="holderDiagnostics">Diagnostics of the current holder, when readable.</param>
|
||||
/// <returns>The English diagnostic message.</returns>
|
||||
private static string BuildMessage(string lockFilePath, string? holderDiagnostics)
|
||||
{
|
||||
string message = $"The workspace is locked by another instance: {lockFilePath}";
|
||||
if (!string.IsNullOrEmpty(holderDiagnostics))
|
||||
{
|
||||
message += $" Lock holder: {holderDiagnostics}";
|
||||
}
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
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.
|
||||
/// held until disposal, so crashes and power loss release it automatically. The
|
||||
/// lock file is kept empty; only its exclusive OS handle carries meaning.
|
||||
/// </summary>
|
||||
public sealed class WorkspaceLock : IDisposable
|
||||
{
|
||||
@@ -28,7 +26,7 @@ public sealed class WorkspaceLock : IDisposable
|
||||
public string LockFilePath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Acquires the workspace lock, writing this process's diagnostics into it.
|
||||
/// Acquires the workspace lock, leaving the lock file empty.
|
||||
/// </summary>
|
||||
/// <param name="folderPath">The workspace folder to lock.</param>
|
||||
/// <returns>The held lock; dispose it to release.</returns>
|
||||
@@ -40,7 +38,7 @@ public sealed class WorkspaceLock : IDisposable
|
||||
FileStream stream;
|
||||
try
|
||||
{
|
||||
// FileShare.None gives the exclusive semantics; FileMode.Create truncates stale content.
|
||||
// FileShare.None gives the exclusive semantics; FileMode.Create empties any stale file.
|
||||
stream = new FileStream(lockFilePath, FileMode.Create, FileAccess.Write, FileShare.None);
|
||||
}
|
||||
catch (DirectoryNotFoundException)
|
||||
@@ -49,40 +47,12 @@ public sealed class WorkspaceLock : IDisposable
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
throw new WorkspaceLockException(lockFilePath, ReadDiagnosticsBestEffort(lockFilePath), ex);
|
||||
throw new WorkspaceLockException(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.
|
||||
|
||||
Reference in new issue
Block a user