revamp 'exit immediately' to abort ongoing SMAPI tasks
This commit is contained in:
parent
971bfd32d2
commit
0cf15d36d9
|
@ -19,6 +19,7 @@ For players:
|
|||
|
||||
For mod developers:
|
||||
* `Console.Out` messages are now written to the log file.
|
||||
* `Monitor.ExitGameImmediately` now aborts SMAPI initialisation and events more quickly.
|
||||
|
||||
## 1.10
|
||||
See [log](https://github.com/Pathoschild/SMAPI/compare/1.9...1.10).
|
||||
|
|
|
@ -41,6 +41,14 @@ namespace StardewModdingAPI.Framework
|
|||
|
||||
foreach (EventHandler handler in handlers.Cast<EventHandler>())
|
||||
{
|
||||
// handle SMAPI exiting
|
||||
if (monitor.IsExiting)
|
||||
{
|
||||
monitor.Log($"SMAPI shutting down: aborting {name} event.", LogLevel.Warn);
|
||||
return;
|
||||
}
|
||||
|
||||
// raise event
|
||||
try
|
||||
{
|
||||
handler.Invoke(sender, args ?? EventArgs.Empty);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using StardewModdingAPI.Framework.Logging;
|
||||
|
||||
namespace StardewModdingAPI.Framework
|
||||
|
@ -34,13 +35,16 @@ namespace StardewModdingAPI.Framework
|
|||
[LogLevel.Alert] = ConsoleColor.Magenta
|
||||
};
|
||||
|
||||
/// <summary>A delegate which requests that SMAPI immediately exit the game. This should only be invoked when an irrecoverable fatal error happens that risks save corruption or game-breaking bugs.</summary>
|
||||
private readonly RequestExitDelegate RequestExit;
|
||||
/// <summary>Propagates notification that SMAPI should exit.</summary>
|
||||
private readonly CancellationTokenSource ExitTokenSource;
|
||||
|
||||
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>Whether SMAPI is aborting. Mods don't need to worry about this unless they have background tasks.</summary>
|
||||
public bool IsExiting => this.ExitTokenSource.IsCancellationRequested;
|
||||
|
||||
/// <summary>Whether to show trace messages in the console.</summary>
|
||||
internal bool ShowTraceInConsole { get; set; }
|
||||
|
||||
|
@ -58,8 +62,8 @@ namespace StardewModdingAPI.Framework
|
|||
/// <param name="source">The name of the module which logs messages using this instance.</param>
|
||||
/// <param name="consoleManager">Manages access to the console output.</param>
|
||||
/// <param name="logFile">The log file to which to write messages.</param>
|
||||
/// <param name="requestExitDelegate">A delegate which requests that SMAPI immediately exit the game.</param>
|
||||
public Monitor(string source, ConsoleInterceptionManager consoleManager, LogFileManager logFile, RequestExitDelegate requestExitDelegate)
|
||||
/// <param name="exitTokenSource">Propagates notification that SMAPI should exit.</param>
|
||||
public Monitor(string source, ConsoleInterceptionManager consoleManager, LogFileManager logFile, CancellationTokenSource exitTokenSource)
|
||||
{
|
||||
// validate
|
||||
if (string.IsNullOrWhiteSpace(source))
|
||||
|
@ -69,7 +73,7 @@ namespace StardewModdingAPI.Framework
|
|||
this.Source = source;
|
||||
this.LogFile = logFile ?? throw new ArgumentNullException(nameof(logFile), "The log file manager cannot be null.");
|
||||
this.ConsoleManager = consoleManager;
|
||||
this.RequestExit = requestExitDelegate;
|
||||
this.ExitTokenSource = exitTokenSource;
|
||||
}
|
||||
|
||||
/// <summary>Log a message for the player or developer.</summary>
|
||||
|
@ -84,14 +88,8 @@ namespace StardewModdingAPI.Framework
|
|||
/// <param name="reason">The reason for the shutdown.</param>
|
||||
public void ExitGameImmediately(string reason)
|
||||
{
|
||||
this.RequestExit(this.Source, reason);
|
||||
}
|
||||
|
||||
/// <summary>Log a fatal error message.</summary>
|
||||
/// <param name="message">The message to log.</param>
|
||||
internal void LogFatal(string message)
|
||||
{
|
||||
this.LogImpl(this.Source, message, LogLevel.Error, ConsoleColor.White, background: ConsoleColor.Red);
|
||||
this.LogFatal($"{this.Source} requested an immediate game shutdown: {reason}");
|
||||
this.ExitTokenSource.Cancel();
|
||||
}
|
||||
|
||||
/// <summary>Log a message for the player or developer, using the specified console color.</summary>
|
||||
|
@ -109,6 +107,13 @@ namespace StardewModdingAPI.Framework
|
|||
/*********
|
||||
** Private methods
|
||||
*********/
|
||||
/// <summary>Log a fatal error message.</summary>
|
||||
/// <param name="message">The message to log.</param>
|
||||
private void LogFatal(string message)
|
||||
{
|
||||
this.LogImpl(this.Source, message, LogLevel.Error, ConsoleColor.White, background: ConsoleColor.Red);
|
||||
}
|
||||
|
||||
/// <summary>Write a message line to the log.</summary>
|
||||
/// <param name="source">The name of the mod logging the message.</param>
|
||||
/// <param name="message">The message to log.</param>
|
||||
|
|
|
@ -200,6 +200,13 @@ namespace StardewModdingAPI.Framework
|
|||
/// <param name="gameTime">A snapshot of the game timing state.</param>
|
||||
protected override void Update(GameTime gameTime)
|
||||
{
|
||||
// SMAPI exiting, stop processing game updates
|
||||
if (this.Monitor.IsExiting)
|
||||
{
|
||||
this.Monitor.Log("SMAPI shutting down: aborting update.", LogLevel.Trace);
|
||||
return;
|
||||
}
|
||||
|
||||
// While a background new-day task is in progress, the game skips its own update logic
|
||||
// and defers to the XNA Update method. Running mod code in parallel to the background
|
||||
// update is risky, because data changes can conflict (e.g. collection changed during
|
||||
|
|
|
@ -3,6 +3,13 @@
|
|||
/// <summary>Encapsulates monitoring and logging for a given module.</summary>
|
||||
public interface IMonitor
|
||||
{
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>Whether SMAPI is aborting. Mods don't need to worry about this unless they have background tasks.</summary>
|
||||
bool IsExiting { get; }
|
||||
|
||||
|
||||
/*********
|
||||
** Methods
|
||||
*********/
|
||||
|
|
|
@ -32,7 +32,7 @@ namespace StardewModdingAPI
|
|||
/// <summary>Manages console output interception.</summary>
|
||||
private readonly ConsoleInterceptionManager ConsoleManager = new ConsoleInterceptionManager();
|
||||
|
||||
/// <summary>The core logger for SMAPI.</summary>
|
||||
/// <summary>The core logger and monitor for SMAPI.</summary>
|
||||
private readonly Monitor Monitor;
|
||||
|
||||
/// <summary>Tracks whether the game should exit immediately and any pending initialisation should be cancelled.</summary>
|
||||
|
@ -99,7 +99,7 @@ namespace StardewModdingAPI
|
|||
public Program(bool writeToConsole, string logPath)
|
||||
{
|
||||
this.LogFile = new LogFileManager(logPath);
|
||||
this.Monitor = new Monitor("SMAPI", this.ConsoleManager, this.LogFile, this.ExitGameImmediately) { WriteToConsole = writeToConsole };
|
||||
this.Monitor = new Monitor("SMAPI", this.ConsoleManager, this.LogFile, this.CancellationTokenSource) { WriteToConsole = writeToConsole };
|
||||
}
|
||||
|
||||
/// <summary>Launch SMAPI.</summary>
|
||||
|
@ -142,6 +142,17 @@ namespace StardewModdingAPI
|
|||
this.GameInstance = new SGame(this.Monitor);
|
||||
StardewValley.Program.gamePtr = this.GameInstance;
|
||||
|
||||
// add exit handler
|
||||
new Thread(() =>
|
||||
{
|
||||
this.CancellationTokenSource.Token.WaitHandle.WaitOne();
|
||||
if (this.IsGameRunning)
|
||||
{
|
||||
this.GameInstance.Exiting += (sender, e) => this.PressAnyKeyToExit();
|
||||
this.GameInstance.Exit();
|
||||
}
|
||||
}).Start();
|
||||
|
||||
// hook into game events
|
||||
#if SMAPI_FOR_WINDOWS
|
||||
((Form)Control.FromHandle(this.GameInstance.Window.Handle)).FormClosing += (sender, args) => this.Dispose();
|
||||
|
@ -180,20 +191,6 @@ namespace StardewModdingAPI
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>Immediately exit the game without saving. This should only be invoked when an irrecoverable fatal error happens that risks save corruption or game-breaking bugs.</summary>
|
||||
/// <param name="module">The module which requested an immediate exit.</param>
|
||||
/// <param name="reason">The reason provided for the shutdown.</param>
|
||||
public void ExitGameImmediately(string module, string reason)
|
||||
{
|
||||
this.Monitor.LogFatal($"{module} requested an immediate game shutdown: {reason}");
|
||||
this.CancellationTokenSource.Cancel();
|
||||
if (this.IsGameRunning)
|
||||
{
|
||||
this.GameInstance.Exiting += (sender, e) => this.PressAnyKeyToExit();
|
||||
this.GameInstance.Exit();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Get a monitor for legacy code which doesn't have one passed in.</summary>
|
||||
[Obsolete("This method should only be used when needed for backwards compatibility.")]
|
||||
internal IMonitor GetLegacyMonitorForMod()
|
||||
|
@ -264,9 +261,9 @@ namespace StardewModdingAPI
|
|||
|
||||
// load mods
|
||||
int modsLoaded = this.LoadMods();
|
||||
if (this.CancellationTokenSource.IsCancellationRequested)
|
||||
if (this.Monitor.IsExiting)
|
||||
{
|
||||
this.Monitor.Log("Shutdown requested; interrupting initialisation.", LogLevel.Error);
|
||||
this.Monitor.Log("SMAPI shutting down: aborting initialisation.", LogLevel.Warn);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -307,7 +304,7 @@ namespace StardewModdingAPI
|
|||
inputThread.Start();
|
||||
|
||||
// keep console thread alive while the game is running
|
||||
while (this.IsGameRunning)
|
||||
while (this.IsGameRunning && !this.Monitor.IsExiting)
|
||||
Thread.Sleep(1000 / 10);
|
||||
if (inputThread.ThreadState == ThreadState.Running)
|
||||
inputThread.Abort();
|
||||
|
@ -368,18 +365,17 @@ namespace StardewModdingAPI
|
|||
List<Action> deprecationWarnings = new List<Action>(); // queue up deprecation warnings to show after mod list
|
||||
foreach (string directoryPath in Directory.GetDirectories(Constants.ModPath))
|
||||
{
|
||||
if (this.Monitor.IsExiting)
|
||||
{
|
||||
this.Monitor.Log("SMAPI shutting down: aborting mod scan.", LogLevel.Warn);
|
||||
return modsLoaded;
|
||||
}
|
||||
|
||||
// passthrough empty directories
|
||||
DirectoryInfo directory = new DirectoryInfo(directoryPath);
|
||||
while (!directory.GetFiles().Any() && directory.GetDirectories().Length == 1)
|
||||
directory = directory.GetDirectories().First();
|
||||
|
||||
// check for cancellation
|
||||
if (this.CancellationTokenSource.IsCancellationRequested)
|
||||
{
|
||||
this.Monitor.Log("Shutdown requested; interrupting mod loading.", LogLevel.Error);
|
||||
return modsLoaded;
|
||||
}
|
||||
|
||||
// get manifest path
|
||||
string manifestPath = Path.Combine(directory.FullName, "manifest.json");
|
||||
if (!File.Exists(manifestPath))
|
||||
|
@ -625,7 +621,7 @@ namespace StardewModdingAPI
|
|||
/// <param name="name">The name of the module which will log messages with this instance.</param>
|
||||
private Monitor GetSecondaryMonitor(string name)
|
||||
{
|
||||
return new Monitor(name, this.ConsoleManager, this.LogFile, this.ExitGameImmediately) { WriteToConsole = this.Monitor.WriteToConsole, ShowTraceInConsole = this.Settings.DeveloperMode };
|
||||
return new Monitor(name, this.ConsoleManager, this.LogFile, this.CancellationTokenSource) { WriteToConsole = this.Monitor.WriteToConsole, ShowTraceInConsole = this.Settings.DeveloperMode };
|
||||
}
|
||||
|
||||
/// <summary>Get a human-readable name for the current platform.</summary>
|
||||
|
|
Loading…
Reference in New Issue