using System.Security.Cryptography; using System.Text; namespace WiiCompiled.Setup; /// /// A fail-fast, cross-process lock covering install, repair and launch operations for one install /// root. The lock file lives beside the installation, so replacing the installation cannot replace /// or release the lock that protects it. /// internal sealed class InstallOperationLock : IDisposable { private readonly FileStream _stream; private InstallOperationLock(FileStream stream) => _stream = stream; public string LockPath => _stream.Name; public static InstallOperationLock Acquire(string installDirectory, IInstallReporter? reporter = null) { var root = InstallOperationPaths.NormalizeRoot(installDirectory); var parent = InstallOperationPaths.GetParent(root); Directory.CreateDirectory(parent); var path = InstallOperationPaths.GetLockPath(root); FileStream stream; try { stream = new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None, bufferSize: 1, FileOptions.WriteThrough); } catch (IOException ex) { throw new InvalidOperationException( "Another WiiCompiled operation is already using this installation. " + "Wait for the current game, install, or repair operation to finish.", ex); } try { InstallTransaction.Recover(root, reporter); InstallScratchSpace.Recover(root, reporter); return new InstallOperationLock(stream); } catch { stream.Dispose(); throw; } } public void Dispose() => _stream.Dispose(); } /// Deterministic paths shared by the operation lock and its recovery journal. internal static class InstallOperationPaths { public static string NormalizeRoot(string installDirectory) { if (string.IsNullOrWhiteSpace(installDirectory)) throw new ArgumentException("The installation directory is required.", nameof(installDirectory)); return FileSystemUtilities.NormalizePath(installDirectory); } public static string GetParent(string normalizedRoot) => Directory.GetParent(normalizedRoot)?.FullName ?? throw new InvalidOperationException("The installation directory must have a parent directory."); public static string GetScopeId(string normalizedRoot) => HashId(16, normalizedRoot.ToUpperInvariant()); public static string HashId(int hexLength, params string[] canonicalParts) => Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(string.Join('\n', canonicalParts)))) .ToLowerInvariant()[..hexLength]; public static string GetLockPath(string normalizedRoot) => Path.Combine(GetParent(normalizedRoot), $".mkwc-operation-{GetScopeId(normalizedRoot)}.lock"); public static string GetJournalPath(string normalizedRoot) => Path.Combine(GetParent(normalizedRoot), $".mkwc-transaction-{GetScopeId(normalizedRoot)}.json"); public static string GetWorkRoot(string normalizedRoot, Guid transactionId) => Path.Combine(GetParent(normalizedRoot), $".mkwc-transaction-{GetScopeId(normalizedRoot)}-{transactionId:N}"); }