diff --git a/AsarSharp/AsarFileSystem/Disk.cs b/AsarSharp/AsarFileSystem/Disk.cs index 42a2809..e088b3a 100644 --- a/AsarSharp/AsarFileSystem/Disk.cs +++ b/AsarSharp/AsarFileSystem/Disk.cs @@ -152,6 +152,8 @@ namespace AsarSharp.AsarFileSystem return; } + // A read-only or hidden archive would fail the swap the same way an overwrite does. + Extensions.ClearAttributes(dest); // File.Replace swaps in one step, so dest is never observed missing or half-written. File.Replace(tempPath, dest, null, true); } diff --git a/AsarSharp/Utils/Extensions.cs b/AsarSharp/Utils/Extensions.cs index 34dad7f..0b95e09 100644 --- a/AsarSharp/Utils/Extensions.cs +++ b/AsarSharp/Utils/Extensions.cs @@ -153,6 +153,28 @@ namespace AsarSharp.Utils return result; } + /// + /// Overwrites , clearing attributes on both ends. CopyFile + /// carries the source's ReadOnly flag onto the copy and then refuses to overwrite what it + /// produced, failing with "Access to the path is denied" - so one read-only source (an exe + /// run straight out of a .zip, say) poisons the destination for every later run. + /// + public static void CopyOver(string source, string destination) + { + ClearAttributes(destination); + File.Copy(source, destination, true); + ClearAttributes(destination); + } + + /// Resets a file to Normal: ReadOnly, Hidden and System all block an overwrite. + public static void ClearAttributes(string path) + { + if (File.Exists(path)) + { + File.SetAttributes(path, FileAttributes.Normal); + } + } + public static void CopyDirectory(string sourceDir, string destinationDir) { Directory.CreateDirectory(destinationDir); @@ -160,7 +182,7 @@ namespace AsarSharp.Utils foreach (var file in Directory.GetFiles(sourceDir)) { var destFile = Path.Combine(destinationDir, Path.GetFileName(file)); - File.Copy(file, destFile, true); + CopyOver(file, destFile); } foreach (var dir in Directory.GetDirectories(sourceDir)) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6629a49..2149ae6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ The newest entry must match the version in `WandEnhancer/Properties/AssemblyInfo - Fixed the "Buy Pro" banner still showing after a successful patch, and Pro not activating on newer clients. - Fixed the Enhancer closing itself when any button was pressed. #184 - A failed patch now puts your original Wand files back instead of leaving a half-patched install behind. Packing also builds the archive beside the old one and swaps it in at the end, so a failure can no longer destroy `app.asar`. #221 +- Fixed patching and *Restore* both failing with "Access to the path is denied" after the first successful patch. Copying carried the read-only flag from the patcher onto the launcher it installs, and then refused to overwrite what it had written - so running WandEnhancer straight out of the downloaded `.zip`, which Windows marks read-only, broke every later run. #214 - Fixed a half-written backup reporting the installation as patched, which blocked patching and restore at the same time. - Fixed invalid ASAR integrity metadata produced from short reads, which could yield an archive the client rejects. #170 - Fixed the packer silently dropping files it could not read, for example while Wand was still running. diff --git a/WandEnhancer/Core/Enhancer.cs b/WandEnhancer/Core/Enhancer.cs index 72e1b95..4171d04 100644 --- a/WandEnhancer/Core/Enhancer.cs +++ b/WandEnhancer/Core/Enhancer.cs @@ -277,7 +277,7 @@ namespace WandEnhancer.Core int copied = 0; foreach (var file in files.Where(WeModInstalls.IsJavaScriptFile).Distinct(StringComparer.OrdinalIgnoreCase)) { - File.Copy(file, GetAvailableScriptPath(destinationDir, Path.GetFileName(file))); + AsarSharp.Utils.Extensions.CopyOver(file, GetAvailableScriptPath(destinationDir, Path.GetFileName(file))); copied++; } @@ -354,10 +354,10 @@ namespace WandEnhancer.Core if (File.Exists(stubPath) && !File.Exists(stubBackup)) { - File.Copy(stubPath, stubBackup); + AsarSharp.Utils.Extensions.CopyOver(stubPath, stubBackup); } - File.Copy(self, stubPath, true); + AsarSharp.Utils.Extensions.CopyOver(self, stubPath); _logger("[ENHANCER] Launcher deployed to root directory", ELogType.Info); } @@ -401,12 +401,12 @@ namespace WandEnhancer.Core if (!File.Exists(_backupPath)) { _logger("[ENHANCER] Creating backup...", ELogType.Info); - File.Copy(_asarPath, _backupPath); + AsarSharp.Utils.Extensions.CopyOver(_asarPath, _backupPath); } else { _logger("[ENHANCER] Backup found, restoring pristine app.asar before patching...", ELogType.Info); - File.Copy(_backupPath, _asarPath, true); + AsarSharp.Utils.Extensions.CopyOver(_backupPath, _asarPath); } if (!Directory.Exists(_unpackedBackupPath) && Directory.Exists(_unpackedPath)) @@ -503,7 +503,7 @@ namespace WandEnhancer.Core { if (File.Exists(_backupPath)) { - File.Copy(_backupPath, _asarPath, true); + AsarSharp.Utils.Extensions.CopyOver(_backupPath, _asarPath); } if (Directory.Exists(_unpackedBackupPath)) @@ -533,7 +533,7 @@ namespace WandEnhancer.Core } ProcessTerminator.TryKillProcess(_weModConfig.BrandName); - File.Copy(_backupPath, _asarPath, true); + AsarSharp.Utils.Extensions.CopyOver(_backupPath, _asarPath); if (Directory.Exists(_unpackedPath)) { @@ -555,14 +555,17 @@ namespace WandEnhancer.Core string stubBackup = stubPath + StubBackupSuffix; if (File.Exists(stubBackup)) { - File.Copy(stubBackup, stubPath, true); + AsarSharp.Utils.Extensions.CopyOver(stubBackup, stubPath); File.Delete(stubBackup); } - string autoPatchConfig = Path.Combine(squirrelRoot, Constants.AutoPatchConfigFileName); - if (File.Exists(autoPatchConfig)) + foreach (var leftover in new[] { Constants.AutoPatchConfigFileName, LauncherLog.FileName }) { - File.Delete(autoPatchConfig); + string path = Path.Combine(squirrelRoot, leftover); + if (File.Exists(path)) + { + File.Delete(path); + } } File.Delete(_backupPath); diff --git a/WandEnhancer/Core/FuseLauncher.cs b/WandEnhancer/Core/FuseLauncher.cs index 24c3f1b..822c719 100644 --- a/WandEnhancer/Core/FuseLauncher.cs +++ b/WandEnhancer/Core/FuseLauncher.cs @@ -58,8 +58,7 @@ namespace WandEnhancer.Core // Electron dies a second or two after a renderer fails, which is past the detach. // Watching that window is the only way the exit code reaches the log. private const int PostDetachWatchMs = 5000; - - /// Electron's exit code when the ASAR integrity fuse rejects the archive. + private const int AsarIntegrityExitCode = -36861; private static readonly byte[] Sentinel = @@ -226,7 +225,7 @@ namespace WandEnhancer.Core log?.Invoke($"Wand exited right after detach with code {DescribeCode(exitCode)}.", ELogType.Error); } - /// Names the exit and exception codes that actually turn up when Wand will not start. + private static string DescribeCode(int code) { switch (code)