mirror of
https://github.com/patchzyy/wiicompiled
synced 2026-09-11 01:23:15 -04:00
fix shortcut
This commit is contained in:
@@ -304,13 +304,36 @@ internal sealed class InstallTransaction : IDisposable
|
||||
private static bool ExistsAsOtherKind(string path, InstallTransactionEntryKind kind) =>
|
||||
kind == InstallTransactionEntryKind.Directory ? File.Exists(path) : Directory.Exists(path);
|
||||
|
||||
// Antivirus and indexers briefly hold handles inside freshly written trees; ride those out
|
||||
// before treating a locked path as fatal.
|
||||
private const int MoveAttempts = 10;
|
||||
private static readonly TimeSpan MoveRetryDelay = TimeSpan.FromMilliseconds(500);
|
||||
|
||||
private static void Move(string source, string destination, InstallTransactionEntryKind kind)
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(destination)!);
|
||||
if (kind == InstallTransactionEntryKind.Directory) Directory.Move(source, destination);
|
||||
else File.Move(source, destination);
|
||||
for (var attempt = 1; ; attempt++)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (kind == InstallTransactionEntryKind.Directory) Directory.Move(source, destination);
|
||||
else File.Move(source, destination);
|
||||
return;
|
||||
}
|
||||
catch (Exception ex) when (IsTransientLock(ex))
|
||||
{
|
||||
if (attempt == MoveAttempts)
|
||||
throw new IOException($"Could not replace \"{destination}\": {ex.Message} " +
|
||||
"Close any program using this folder and retry the update.", ex);
|
||||
Thread.Sleep(MoveRetryDelay);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool IsTransientLock(Exception ex) =>
|
||||
ex is IOException or UnauthorizedAccessException &&
|
||||
(ex.HResult & 0xFFFF) is 5 or 32 or 33; // ACCESS_DENIED, SHARING_VIOLATION, LOCK_VIOLATION
|
||||
|
||||
private static void Delete(string path, InstallTransactionEntryKind kind)
|
||||
{
|
||||
if (kind == InstallTransactionEntryKind.Directory) FileSystemUtilities.DeleteDirectoryIfExists(path);
|
||||
|
||||
@@ -291,6 +291,7 @@ internal sealed class InstallerEngine
|
||||
Path.Combine(installDirectory, InstalledLayout.InstallStateFileName)));
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
RunningProductGuard.EnsureProductsNotRunning(installDirectory);
|
||||
_reporter.Progress(InstallStages.Publish, "Publishing the completed installation...", progressPercent);
|
||||
var configPath = RuntimeConfiguration.ResolveConfigPath(installDirectory);
|
||||
var configSnapshot = RuntimeConfiguration.Capture(configPath);
|
||||
@@ -318,11 +319,11 @@ internal sealed class InstallerEngine
|
||||
try
|
||||
{
|
||||
ShellIntegration.RegisterUninstaller(installDirectory, state.RetroRewindInstalled);
|
||||
ShellIntegration.RemoveAllShortcuts();
|
||||
ShellIntegration.CreateShortcuts(installDirectory);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_reporter.Diagnostic("The installation succeeded, but Windows uninstall registration failed: " +
|
||||
_reporter.Diagnostic("The installation succeeded, but Windows shell integration failed: " +
|
||||
ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,6 +80,7 @@ internal sealed class ProductRepairService
|
||||
if (!_installation.HasToolkit)
|
||||
throw new InvalidOperationException(
|
||||
"The installed recompilation toolkit is missing. Apply the current setup release before repairing products.");
|
||||
RunningProductGuard.EnsureProductsNotRunning(_installation.Root);
|
||||
var expectedDolSha256 = options.ExpectedDolSha256;
|
||||
var expectedRelSha256 = options.ExpectedRelSha256;
|
||||
if (string.IsNullOrWhiteSpace(expectedDolSha256) != string.IsNullOrWhiteSpace(expectedRelSha256))
|
||||
@@ -450,6 +451,8 @@ internal sealed class ProductRepairService
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
// The game may have been started during a long compile; publishing renames its directory.
|
||||
RunningProductGuard.EnsureProductsNotRunning(_installation.Root);
|
||||
var configPath = RuntimeConfiguration.ResolveConfigPath(_installation.Root);
|
||||
var configSnapshot = RuntimeConfiguration.Capture(configPath);
|
||||
using var transaction = InstallTransaction.Begin(_installation.Root, _reporter, entries.ToArray());
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace WiiCompiled.Setup;
|
||||
|
||||
/// <summary>
|
||||
/// Refuses to replace installed products while one of them is running: publishing renames the
|
||||
/// product directories, which Windows rejects while a process runs from them.
|
||||
/// </summary>
|
||||
internal static class RunningProductGuard
|
||||
{
|
||||
public static void EnsureProductsNotRunning(string installDirectory)
|
||||
{
|
||||
var installation = new Installation(installDirectory);
|
||||
var running = FindProcessesUnder(installation.BaseDirectory, installation.RetroDirectory);
|
||||
if (running.Count == 0) return;
|
||||
|
||||
throw new InvalidOperationException(
|
||||
$"Mario Kart Wii is still running ({string.Join(", ", running)}). " +
|
||||
"Close the game, then retry the update.");
|
||||
}
|
||||
|
||||
private static List<string> FindProcessesUnder(params string[] roots)
|
||||
{
|
||||
var names = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var process in Process.GetProcesses())
|
||||
{
|
||||
try
|
||||
{
|
||||
if (process.Id == Environment.ProcessId) continue;
|
||||
var path = process.MainModule?.FileName;
|
||||
if (path is null) continue;
|
||||
if (roots.Any(root => FileSystemUtilities.PathContains(root, Path.GetFullPath(path))))
|
||||
names.Add(Path.GetFileName(path));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Inaccessible processes (elevated, exited, protected) cannot run our products' exes
|
||||
// from a user-writable install directory in any case that matters here.
|
||||
}
|
||||
finally
|
||||
{
|
||||
process.Dispose();
|
||||
}
|
||||
}
|
||||
return names.ToList();
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,7 @@ internal static class SelfTests
|
||||
Test("Portable relative path settings", TestPortableRelativePathSettings, failures);
|
||||
Test("Portable move healing", TestPortableMoveHealing, failures);
|
||||
Test("Install transaction rollback", TestInstallTransactionRollback, failures);
|
||||
Test("Install move transient-lock policy", TestInstallMoveTransientLockPolicy, failures);
|
||||
Test("Install scratch recovery", TestInstallScratchRecovery, failures);
|
||||
Test("Shortcut cleanup is best effort", TestShortcutCleanupIsBestEffort, failures);
|
||||
Test("Canonical Retro Rewind resolution", TestCanonicalRetroRewindResolution, failures);
|
||||
@@ -434,30 +435,25 @@ internal static class SelfTests
|
||||
private static void TestShortcutCleanupIsBestEffort()
|
||||
{
|
||||
var root = Path.Combine(Path.GetTempPath(), "mkwc-shortcuts-" + Guid.NewGuid().ToString("N"));
|
||||
var desktop = Path.Combine(root, "Desktop");
|
||||
var startMenu = Path.Combine(root, "Start Menu", ProductInfo.Name);
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(desktop);
|
||||
Directory.CreateDirectory(root);
|
||||
// File.Delete cannot delete a directory. This deterministically forces the first cleanup
|
||||
// to fail while the remaining independently guarded targets must still be attempted.
|
||||
Directory.CreateDirectory(Path.Combine(desktop, "WiiCompiled.lnk"));
|
||||
var secondShortcut = Path.Combine(desktop, "Retro Rewind.lnk");
|
||||
var firstShortcut = Path.Combine(root, "first.lnk");
|
||||
Directory.CreateDirectory(firstShortcut);
|
||||
var secondShortcut = Path.Combine(root, "second.lnk");
|
||||
File.WriteAllText(secondShortcut, "shortcut");
|
||||
Directory.CreateDirectory(startMenu);
|
||||
File.WriteAllText(Path.Combine(startMenu, "WiiCompiled.lnk"), "shortcut");
|
||||
|
||||
try
|
||||
{
|
||||
ShellIntegration.RemoveAllShortcuts(desktop, startMenu);
|
||||
ShellIntegration.RemoveShortcuts([firstShortcut, secondShortcut]);
|
||||
throw new Exception("A shortcut deletion failure was not reported.");
|
||||
}
|
||||
catch (AggregateException) { }
|
||||
|
||||
if (File.Exists(secondShortcut))
|
||||
throw new Exception("One failed shortcut deletion prevented the next file deletion.");
|
||||
if (Directory.Exists(startMenu))
|
||||
throw new Exception("One failed shortcut deletion prevented Start Menu cleanup.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -727,6 +723,20 @@ internal static class SelfTests
|
||||
}
|
||||
}
|
||||
|
||||
private static void TestInstallMoveTransientLockPolicy()
|
||||
{
|
||||
if (!InstallTransaction.IsTransientLock(new IOException("in use", unchecked((int)0x80070020))))
|
||||
throw new Exception("A sharing violation would not be retried.");
|
||||
if (!InstallTransaction.IsTransientLock(new IOException("locked", unchecked((int)0x80070021))))
|
||||
throw new Exception("A lock violation would not be retried.");
|
||||
if (!InstallTransaction.IsTransientLock(new UnauthorizedAccessException("denied")))
|
||||
throw new Exception("A transient access-denied would not be retried.");
|
||||
if (InstallTransaction.IsTransientLock(new IOException("disk full", unchecked((int)0x80070070))))
|
||||
throw new Exception("A permanent I/O failure would be retried.");
|
||||
if (InstallTransaction.IsTransientLock(new InvalidDataException("bad")))
|
||||
throw new Exception("A non-I/O failure would be retried.");
|
||||
}
|
||||
|
||||
private static void TestRetroWfcTransientRetryPolicy()
|
||||
{
|
||||
if (!InputValidation.IsTransientRetroWfcDownloadFailure(
|
||||
|
||||
@@ -4,6 +4,8 @@ namespace WiiCompiled.Setup;
|
||||
|
||||
internal static class ShellIntegration
|
||||
{
|
||||
private const string ShortcutFileName = "wiicompiled (base) (beta).lnk";
|
||||
|
||||
public static void RegisterUninstaller(string installDirectory, bool retroInstalled)
|
||||
{
|
||||
using var key = Registry.CurrentUser.CreateSubKey(ProductInfo.UninstallKey, writable: true)
|
||||
@@ -25,26 +27,43 @@ internal static class ShellIntegration
|
||||
public static void UnregisterUninstaller() =>
|
||||
Registry.CurrentUser.DeleteSubKeyTree(ProductInfo.UninstallKey, throwOnMissingSubKey: false);
|
||||
|
||||
/// <summary>Removes shortcuts left by GUI-capable releases from before Wheel Wizard owned the UI.</summary>
|
||||
public static void RemoveAllShortcuts()
|
||||
/// <summary>Creates the desktop and Start Menu shortcuts that launch the base game.</summary>
|
||||
public static void CreateShortcuts(string installDirectory)
|
||||
{
|
||||
var desktop = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory);
|
||||
var startMenuFolder = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.StartMenu), "Programs", ProductInfo.Name);
|
||||
RemoveAllShortcuts(desktop, startMenuFolder);
|
||||
var cli = Path.Combine(installDirectory, ProductInfo.SetupCopyName);
|
||||
var shellType = Type.GetTypeFromProgID("WScript.Shell")
|
||||
?? throw new InvalidOperationException("The Windows Script Host shell is unavailable.");
|
||||
dynamic shell = Activator.CreateInstance(shellType)!;
|
||||
foreach (var path in ShortcutPaths())
|
||||
{
|
||||
dynamic shortcut = shell.CreateShortcut(path);
|
||||
shortcut.TargetPath = cli;
|
||||
shortcut.Arguments = "--launch-base";
|
||||
shortcut.WorkingDirectory = installDirectory;
|
||||
shortcut.IconLocation = cli + ",0";
|
||||
shortcut.Description = "Play Mario Kart Wii (base game)";
|
||||
shortcut.Save();
|
||||
}
|
||||
}
|
||||
|
||||
internal static void RemoveAllShortcuts(string desktop, string startMenuFolder)
|
||||
public static void RemoveShortcuts() => RemoveShortcuts(ShortcutPaths());
|
||||
|
||||
internal static void RemoveShortcuts(IEnumerable<string> shortcutPaths)
|
||||
{
|
||||
var failures = new List<Exception>();
|
||||
DeleteFileBestEffort(Path.Combine(desktop, "WiiCompiled.lnk"), failures);
|
||||
DeleteFileBestEffort(Path.Combine(desktop, "Retro Rewind.lnk"), failures);
|
||||
DeleteDirectoryBestEffort(startMenuFolder, failures);
|
||||
foreach (var path in shortcutPaths)
|
||||
DeleteFileBestEffort(path, failures);
|
||||
|
||||
if (failures.Count != 0)
|
||||
throw new AggregateException("One or more WiiCompiled shortcuts could not be removed.", failures);
|
||||
}
|
||||
|
||||
private static string[] ShortcutPaths() =>
|
||||
[
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), ShortcutFileName),
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.StartMenu), "Programs", ShortcutFileName),
|
||||
];
|
||||
|
||||
private static void DeleteFileBestEffort(string path, List<Exception> failures)
|
||||
{
|
||||
try
|
||||
@@ -57,18 +76,6 @@ internal static class ShellIntegration
|
||||
}
|
||||
}
|
||||
|
||||
private static void DeleteDirectoryBestEffort(string path, List<Exception> failures)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(path)) Directory.Delete(path, recursive: true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
failures.Add(new IOException($"Could not delete shortcut folder {path}: {ex.Message}", ex));
|
||||
}
|
||||
}
|
||||
|
||||
private static int EstimateSizeKb(string directory)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -28,7 +28,7 @@ internal static class UninstallService
|
||||
{
|
||||
installDirectory = Path.GetFullPath(installDirectory);
|
||||
Thread.Sleep(750);
|
||||
TryCleanup("remove shortcuts", quiet, ShellIntegration.RemoveAllShortcuts);
|
||||
TryCleanup("remove shortcuts", quiet, ShellIntegration.RemoveShortcuts);
|
||||
|
||||
// A portable installation never registered an uninstall entry, and the single machine-wide
|
||||
// key may belong to a normal installation on the same account. Removing it here would
|
||||
|
||||
Reference in New Issue
Block a user