From 2b9703f98fedcf97fd5e511f1e30bcc8fd94b5cc Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 11 May 2020 01:40:46 -0400 Subject: fix Harmony issue when assembly is loaded from memory (#711) --- src/SMAPI.Installer/InteractiveInstaller.cs | 40 ++---------------------- src/SMAPI/Constants.cs | 3 ++ src/SMAPI/Framework/ModLoading/AssemblyLoader.cs | 27 ++++++++++++++-- src/SMAPI/Framework/SCore.cs | 16 +++++++++- 4 files changed, 44 insertions(+), 42 deletions(-) diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index 5b0c6e1f..1457848b 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; -using System.Threading; using Microsoft.Win32; using StardewModdingApi.Installer.Enums; using StardewModdingAPI.Installer.Framework; @@ -624,7 +623,7 @@ namespace StardewModdingApi.Installer { try { - this.ForceDelete(Directory.Exists(path) ? new DirectoryInfo(path) : (FileSystemInfo)new FileInfo(path)); + FileUtilities.ForceDelete(Directory.Exists(path) ? new DirectoryInfo(path) : (FileSystemInfo)new FileInfo(path)); break; } catch (Exception ex) @@ -665,41 +664,6 @@ namespace StardewModdingApi.Installer } } - /// Delete a file or folder regardless of file permissions, and block until deletion completes. - /// The file or folder to reset. - /// This method is mirrored from FileUtilities.ForceDelete in the toolkit. - private void ForceDelete(FileSystemInfo entry) - { - // ignore if already deleted - entry.Refresh(); - if (!entry.Exists) - return; - - // delete children - if (entry is DirectoryInfo folder) - { - foreach (FileSystemInfo child in folder.GetFileSystemInfos()) - this.ForceDelete(child); - } - - // reset permissions & delete - entry.Attributes = FileAttributes.Normal; - entry.Delete(); - - // wait for deletion to finish - for (int i = 0; i < 10; i++) - { - entry.Refresh(); - if (entry.Exists) - Thread.Sleep(500); - } - - // throw exception if deletion didn't happen before timeout - entry.Refresh(); - if (entry.Exists) - throw new IOException($"Timed out trying to delete {entry.FullName}"); - } - /// Interactively ask the user to choose a value. /// A callback which prints a message to the console. /// The message to print. @@ -707,7 +671,7 @@ namespace StardewModdingApi.Installer /// The indentation to prefix to output. private string InteractivelyChoose(string message, string[] options, string indent = "", Action print = null) { - print = print ?? this.PrintInfo; + print ??= this.PrintInfo; while (true) { diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index a898fccd..907a93b2 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -61,6 +61,9 @@ namespace StardewModdingAPI /// The absolute path to the folder containing SMAPI's internal files. internal static readonly string InternalFilesPath = Program.DllSearchPath; + /// The folder containing temporary files that are only valid for the current session. + internal static string InternalTempFilesPath => Path.Combine(Program.DllSearchPath, ".temp"); + /// The file path for the SMAPI configuration file. internal static string ApiConfigPath => Path.Combine(Constants.InternalFilesPath, "config.json"); diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index 570686fe..78e717e9 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -36,6 +36,9 @@ namespace StardewModdingAPI.Framework.ModLoading /// The objects to dispose as part of this instance. private readonly HashSet Disposables = new HashSet(); + /// The full path to the folder in which to save rewritten assemblies. + private readonly string TempFolderPath; + /********* ** Public methods @@ -44,11 +47,15 @@ namespace StardewModdingAPI.Framework.ModLoading /// The current game platform. /// Encapsulates monitoring and logging. /// Whether to detect paranoid mode issues. - public AssemblyLoader(Platform targetPlatform, IMonitor monitor, bool paranoidMode) + /// The full path to the folder in which to save rewritten assemblies. + public AssemblyLoader(Platform targetPlatform, IMonitor monitor, bool paranoidMode, string tempFolderPath) { this.Monitor = monitor; this.ParanoidMode = paranoidMode; this.AssemblyMap = this.TrackForDisposal(Constants.GetAssemblyMap(targetPlatform)); + this.TempFolderPath = tempFolderPath; + + // init resolver this.AssemblyDefinitionResolver = this.TrackForDisposal(new AssemblyDefinitionResolver()); this.AssemblyDefinitionResolver.AddSearchDirectory(Constants.ExecutionPath); this.AssemblyDefinitionResolver.AddSearchDirectory(Constants.InternalFilesPath); @@ -124,9 +131,23 @@ namespace StardewModdingAPI.Framework.ModLoading if (changed) { if (!oneAssembly) - this.Monitor.Log($" Loading {assembly.File.Name} (rewritten in memory)...", LogLevel.Trace); - using (MemoryStream outStream = new MemoryStream()) + this.Monitor.Log($" Loading {assembly.File.Name} (rewritten)...", LogLevel.Trace); + + if (assembly.Definition.MainModule.AssemblyReferences.Any(p => p.Name == "0Harmony")) + { + // Note: the assembly must be loaded from disk for Harmony compatibility. + // Loading it from memory sets the assembly module's FullyQualifiedName to + // "", so Harmony incorrectly identifies the module in its + // Patch.PatchMethod when handling multiple patches for the same method, + // leading to "Token 0x... is not valid in the scope of module HarmonySharedState" + // errors (e.g. https://smapi.io/log/A0gAsc3M). + string tempPath = Path.Combine(this.TempFolderPath, $"{Path.GetFileNameWithoutExtension(assemblyPath)}.{Guid.NewGuid()}.dll"); + assembly.Definition.Write(tempPath); + lastAssembly = Assembly.LoadFile(tempPath); + } + else { + using MemoryStream outStream = new MemoryStream(); assembly.Definition.Write(outStream); byte[] bytes = outStream.ToArray(); lastAssembly = Assembly.Load(bytes); diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index de9c955d..12dc9c3d 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -213,6 +213,20 @@ namespace StardewModdingAPI.Framework return; } #endif + + // reset temp folder + if (Directory.Exists(Constants.InternalTempFilesPath)) + { + try + { + FileUtilities.ForceDelete(new DirectoryInfo(Constants.InternalTempFilesPath)); + } + catch (Exception ex) + { + this.Monitor.Log($"Couldn't delete temporary files at {Constants.InternalTempFilesPath}: {ex}", LogLevel.Trace); + } + } + Directory.CreateDirectory(Constants.InternalTempFilesPath); } /// Launch SMAPI. @@ -748,7 +762,7 @@ namespace StardewModdingAPI.Framework // load mods IDictionary> skippedMods = new Dictionary>(); - using (AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.Platform, this.Monitor, this.Settings.ParanoidWarnings)) + using (AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.Platform, this.Monitor, this.Settings.ParanoidWarnings, Constants.InternalTempFilesPath)) { // init HashSet suppressUpdateChecks = new HashSet(this.Settings.SuppressUpdateChecks, StringComparer.InvariantCultureIgnoreCase); -- cgit