using System;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
namespace LibationFileManager;
///
/// Process-wide guard that prevents more than one Libation instance from running against the same
/// LibationFiles folder at the same time. Concurrent instances race on the SQLite database, the
/// Lucene search index, and the log file, which can corrupt state (see issue #1931).
///
/// The lock is keyed on the LibationFiles location, so separate portable installs that use different
/// folders may still run simultaneously. The returned instance must be kept alive for the lifetime of
/// the process and disposed at exit. The guard fails open: any error acquiring it is treated as "first
/// instance" so it can never block startup on its own.
///
public sealed class SingleInstance : IDisposable
{
private readonly Mutex? _mutex;
private readonly bool _hasHandle;
/// True if this process acquired the lock (no other instance is running for this folder).
public bool IsFirstInstance => _hasHandle;
private SingleInstance(Mutex? mutex, bool hasHandle)
{
_mutex = mutex;
_hasHandle = hasHandle;
}
///
/// Attempts to acquire the single-instance lock for .
///
public static SingleInstance TryAcquire(string libationFilesLocation)
{
Mutex? mutex = null;
try
{
mutex = new Mutex(initiallyOwned: false, buildMutexName(libationFilesLocation));
bool hasHandle;
try
{
hasHandle = mutex.WaitOne(TimeSpan.Zero, exitContext: false);
}
catch (AbandonedMutexException)
{
// The previous owner exited without releasing (e.g. it crashed). Ownership transfers to us.
hasHandle = true;
}
return new SingleInstance(mutex, hasHandle);
}
catch (Exception ex)
{
// Never let the guard itself prevent startup. Fail open by treating this as the first instance.
StartupLog.Warning(ex, "Could not evaluate the single-instance lock; continuing without it.");
mutex?.Dispose();
return new SingleInstance(null, true);
}
}
private static string buildMutexName(string libationFilesLocation)
{
var normalized = (libationFilesLocation ?? string.Empty).Trim().TrimEnd('/', '\\').ToUpperInvariant();
var hash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(normalized)));
// Local (per-session) scope is sufficient: two OS users cannot safely share one files folder,
// and each user gets their own default folder. The name must be filesystem-safe and unique per folder.
return $"Libation-SingleInstance-{hash}";
}
public void Dispose()
{
try
{
if (_hasHandle)
_mutex?.ReleaseMutex();
}
catch
{
// best effort; the OS releases the handle on process exit regardless
}
_mutex?.Dispose();
}
}