diff --git a/Daybreak/Configuration/Options/LauncherOptions.cs b/Daybreak/Configuration/Options/LauncherOptions.cs
index 40b4850f..f7706573 100644
--- a/Daybreak/Configuration/Options/LauncherOptions.cs
+++ b/Daybreak/Configuration/Options/LauncherOptions.cs
@@ -1,9 +1,7 @@
using Daybreak.Attributes;
-using Daybreak.Models;
using Daybreak.Views;
using Newtonsoft.Json;
using System;
-using System.Collections.Generic;
namespace Daybreak.Configuration.Options;
@@ -42,4 +40,8 @@ public sealed class LauncherOptions
[JsonProperty(nameof(DownloadIcons))]
[OptionName(Name = "Download Icons", Description = "If true, the launcher will download icons that are not found in the local cache")]
public bool DownloadIcons { get; set; } = true;
+
+ [JsonProperty(nameof(ModStartupTimeout))]
+ [OptionName(Name = "Mod Startup Timeout", Description = "Amount of seconds that Daybreak will wait for each mod to start-up before cancelling the tasks")]
+ public int ModStartupTimeout { get; set; } = 30;
}
diff --git a/Daybreak/Daybreak.csproj b/Daybreak/Daybreak.csproj
index a4a0d6db..a81d2ee6 100644
--- a/Daybreak/Daybreak.csproj
+++ b/Daybreak/Daybreak.csproj
@@ -13,7 +13,7 @@
preview
Daybreak.ico
true
- 0.9.8.125
+ 0.9.8.126
true
cfb2a489-db80-448d-a969-80270f314c46
True
diff --git a/Daybreak/Services/ApplicationLauncher/ApplicationLauncher.cs b/Daybreak/Services/ApplicationLauncher/ApplicationLauncher.cs
index 3f7dadbf..f54405c1 100644
--- a/Daybreak/Services/ApplicationLauncher/ApplicationLauncher.cs
+++ b/Daybreak/Services/ApplicationLauncher/ApplicationLauncher.cs
@@ -20,6 +20,7 @@
using System.Runtime.InteropServices;
using System.Security;
using System.Text;
+using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using static Daybreak.Utils.NativeMethods;
@@ -198,14 +199,57 @@ public void RestartDaybreakAsNormalUser()
}
};
- var preLaunchActions = this.modsManager.GetMods().Where(m => m.IsEnabled).Select(m => m.OnGuildwarsStarting(process));
- await Task.WhenAll(preLaunchActions);
+ foreach(var mod in mods)
+ {
+ using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(this.launcherOptions.Value.ModStartupTimeout));
+ try
+ {
+ await mod.OnGuildwarsStarting(process, cts.Token);
+ }
+ catch (TaskCanceledException)
+ {
+ this.logger.LogError($"{mod.Name} timeout");
+ this.notificationService.NotifyError(
+ title: $"{mod.Name} timeout",
+ description: $"Mod timed out while processing {nameof(mod.OnGuildwarsStarting)}");
+ }
+ catch (Exception e)
+ {
+ this.KillGuildWarsProcess(process);
+ this.logger.LogError(e, $"{mod.Name} unhandled exception");
+ this.notificationService.NotifyError(
+ title: $"{mod.Name} exception",
+ description: $"Mod encountered exception of type {e.GetType().Name} while processing {nameof(mod.OnGuildwarsStarting)}");
+ return default;
+ }
+ }
+
var pId = LaunchClient(executable, string.Join(" ", args), this.privilegeManager.AdminPrivileges, out var clientHandle);
process = Process.GetProcessById(pId);
- foreach(var mod in mods)
+ foreach (var mod in mods)
{
- await mod.OnGuildWarsCreated(process);
+ using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(this.launcherOptions.Value.ModStartupTimeout));
+ try
+ {
+ await mod.OnGuildWarsCreated(process, cts.Token);
+ }
+ catch (TaskCanceledException)
+ {
+ this.logger.LogError($"{mod.Name} timeout");
+ this.notificationService.NotifyError(
+ title: $"{mod.Name} timeout",
+ description: $"Mod timed out while processing {nameof(mod.OnGuildWarsCreated)}");
+ }
+ catch (Exception e)
+ {
+ this.KillGuildWarsProcess(process);
+ this.logger.LogError(e, $"{mod.Name} unhandled exception");
+ this.notificationService.NotifyError(
+ title: $"{mod.Name} exception",
+ description: $"Mod encountered exception of type {e.GetType().Name} while processing {nameof(mod.OnGuildWarsCreated)}");
+ return default;
+ }
}
if (clientHandle != IntPtr.Zero)
@@ -219,7 +263,27 @@ public void RestartDaybreakAsNormalUser()
*/
foreach (var mod in mods)
{
- await mod.OnGuildwarsStarted(process);
+ using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(this.launcherOptions.Value.ModStartupTimeout));
+ try
+ {
+ await mod.OnGuildwarsStarted(process, cts.Token);
+ }
+ catch (TaskCanceledException)
+ {
+ this.logger.LogError($"{mod.Name} timeout");
+ this.notificationService.NotifyError(
+ title: $"{mod.Name} timeout",
+ description: $"Mod timed out while processing {nameof(mod.OnGuildwarsStarted)}");
+ }
+ catch (Exception e)
+ {
+ this.KillGuildWarsProcess(process);
+ this.logger.LogError(e, $"{mod.Name} unhandled exception");
+ this.notificationService.NotifyError(
+ title: $"{mod.Name} exception",
+ description: $"Mod encountered exception of type {e.GetType().Name} while processing {nameof(mod.OnGuildwarsStarted)}");
+ return default;
+ }
}
var retries = 0;
@@ -284,6 +348,34 @@ public IEnumerable GetGuildwarsProcesses()
return Process.GetProcessesByName(ProcessName);
}
+ public void KillGuildWarsProcess(Process process)
+ {
+ process.ThrowIfNull();
+ try
+ {
+ if (process.StartInfo is not null &&
+ process.StartInfo.FileName.Contains("Gw.exe", StringComparison.OrdinalIgnoreCase))
+ {
+ process.Kill(true);
+ return;
+ }
+
+ if (process.MainModule?.FileName is not null &&
+ process.MainModule.FileName.Contains("Gw.exe", StringComparison.OrdinalIgnoreCase))
+ {
+ process.Kill(true);
+ return;
+ }
+ }
+ catch(Exception e)
+ {
+ this.logger.LogError(e, $"Failed to kill GuildWars process with id {process?.Id}");
+ this.notificationService.NotifyError(
+ title: "Failed to kill GuildWars process",
+ description: $"Encountered exception while trying to kill GuildWars process with id {process?.Id}. Check logs for details");
+ }
+ }
+
private void ClearGwLocks(string path)
{
this.SetRegistryGuildwarsPath(path);
diff --git a/Daybreak/Services/ApplicationLauncher/IApplicationLauncher.cs b/Daybreak/Services/ApplicationLauncher/IApplicationLauncher.cs
index c35b5c81..347565a3 100644
--- a/Daybreak/Services/ApplicationLauncher/IApplicationLauncher.cs
+++ b/Daybreak/Services/ApplicationLauncher/IApplicationLauncher.cs
@@ -10,6 +10,7 @@ public interface IApplicationLauncher
GuildWarsApplicationLaunchContext? GetGuildwarsProcess(LaunchConfigurationWithCredentials launchConfigurationWithCredentials);
IEnumerable GetGuildwarsProcesses(params LaunchConfigurationWithCredentials[] launchConfigurationWithCredentials);
IEnumerable GetGuildwarsProcesses();
+ void KillGuildWarsProcess(Process process);
Task LaunchGuildwars(LaunchConfigurationWithCredentials launchConfigurationWithCredentials);
void RestartDaybreak();
void RestartDaybreakAsAdmin();
diff --git a/Daybreak/Services/DSOAL/DSOALService.cs b/Daybreak/Services/DSOAL/DSOALService.cs
index c0ee022a..69c27c5c 100644
--- a/Daybreak/Services/DSOAL/DSOALService.cs
+++ b/Daybreak/Services/DSOAL/DSOALService.cs
@@ -14,6 +14,7 @@
using System.IO;
using System.IO.Compression;
using System.Linq;
+using System.Threading;
using System.Threading.Tasks;
namespace Daybreak.Services.DSOAL;
@@ -41,6 +42,7 @@ public sealed class DSOALService : IDSOALService
private readonly ILiveUpdateableOptions options;
private readonly ILogger logger;
+ public string Name => "DSOAL";
public bool IsEnabled
{
get => this.options.Value.Enabled;
@@ -118,17 +120,17 @@ public IEnumerable GetCustomArguments()
}
}
- public Task OnGuildWarsCreated(Process process)
+ public Task OnGuildWarsCreated(Process process, CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
- public Task OnGuildwarsStarted(Process process)
+ public Task OnGuildwarsStarted(Process process, CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
- public Task OnGuildwarsStarting(Process process)
+ public Task OnGuildwarsStarting(Process process, CancellationToken cancellationToken)
{
var guildwarsDirectory = new FileInfo(process.StartInfo.FileName).Directory!.FullName;
if (this.options.Value.Enabled)
diff --git a/Daybreak/Services/Mods/IModService.cs b/Daybreak/Services/Mods/IModService.cs
index 40674a53..24394ad0 100644
--- a/Daybreak/Services/Mods/IModService.cs
+++ b/Daybreak/Services/Mods/IModService.cs
@@ -1,11 +1,13 @@
using System.Collections.Generic;
using System.Diagnostics;
+using System.Threading;
using System.Threading.Tasks;
namespace Daybreak.Services.Mods;
public interface IModService
{
+ string Name { get; }
bool IsEnabled { get; set; }
bool IsInstalled { get; }
IEnumerable GetCustomArguments();
@@ -13,15 +15,15 @@ public interface IModService
/// Called before starting the guild wars process.
/// Do mod preparation here.
///
- Task OnGuildwarsStarting(Process process);
+ Task OnGuildwarsStarting(Process process, CancellationToken cancellationToken);
///
/// Called when the process is created in suspended state.
/// Do dll injection here.
///
- Task OnGuildWarsCreated(Process process);
+ Task OnGuildWarsCreated(Process process, CancellationToken cancellationToken);
///
/// Called after the process has been resumed.
/// Do clean-up/integration with the guild wars process here.
///
- Task OnGuildwarsStarted(Process process);
+ Task OnGuildwarsStarted(Process process, CancellationToken cancellationToken);
}
diff --git a/Daybreak/Services/ReShade/ReShadeService.cs b/Daybreak/Services/ReShade/ReShadeService.cs
index d0e6006b..7458be29 100644
--- a/Daybreak/Services/ReShade/ReShadeService.cs
+++ b/Daybreak/Services/ReShade/ReShadeService.cs
@@ -54,6 +54,7 @@ public sealed class ReShadeService : IReShadeService, IApplicationLifetimeServic
private readonly IDownloadService downloadService;
private readonly ILogger logger;
+ public string Name => "ReShade";
public bool IsEnabled
{
get => this.liveUpdateableOptions.Value.Enabled;
@@ -118,30 +119,31 @@ public void OnClosing()
public IEnumerable GetCustomArguments() => Enumerable.Empty();
- public Task OnGuildWarsCreated(Process process)
+ public Task OnGuildWarsCreated(Process process, CancellationToken cancellationToken)
{
- var scopedLogger = this.logger.CreateScopedLogger(nameof(this.OnGuildWarsCreated), process?.MainModule?.FileName ?? string.Empty);
- if (this.processInjector.Inject(process!, ReShadeDllPath))
+ return Task.Run(() =>
{
- scopedLogger.LogInformation("Injected ReShade dll");
- this.notificationService.NotifyInformation(
- title: "ReShade started",
- description: "ReShade has been injected");
- }
- else
- {
- scopedLogger.LogError("Failed to inject ReShade dll");
- this.notificationService.NotifyError(
- title: "ReShade failed to start",
- description: "Failed to inject ReShade");
- }
-
- return Task.CompletedTask;
+ var scopedLogger = this.logger.CreateScopedLogger(nameof(this.OnGuildWarsCreated), process?.MainModule?.FileName ?? string.Empty);
+ if (this.processInjector.Inject(process!, ReShadeDllPath))
+ {
+ scopedLogger.LogInformation("Injected ReShade dll");
+ this.notificationService.NotifyInformation(
+ title: "ReShade started",
+ description: "ReShade has been injected");
+ }
+ else
+ {
+ scopedLogger.LogError("Failed to inject ReShade dll");
+ this.notificationService.NotifyError(
+ title: "ReShade failed to start",
+ description: "Failed to inject ReShade");
+ }
+ }, cancellationToken);
}
- public Task OnGuildwarsStarted(Process process) => Task.CompletedTask;
+ public Task OnGuildwarsStarted(Process process, CancellationToken cancellationToken) => Task.CompletedTask;
- public Task OnGuildwarsStarting(Process process)
+ public Task OnGuildwarsStarting(Process process, CancellationToken cancellationToken)
{
var destinationDirectory = Path.GetFullPath(new FileInfo(process.StartInfo.FileName).DirectoryName!);
EnsureFileExistsInGuildwarsDirectory(ReShadeLog, destinationDirectory);
diff --git a/Daybreak/Services/Screens/GuildwarsScreenPlacer.cs b/Daybreak/Services/Screens/GuildwarsScreenPlacer.cs
index 94746172..88de743d 100644
--- a/Daybreak/Services/Screens/GuildwarsScreenPlacer.cs
+++ b/Daybreak/Services/Screens/GuildwarsScreenPlacer.cs
@@ -32,6 +32,7 @@ public GuildwarsScreenPlacer(
this.logger = logger.ThrowIfNull();
}
+ public string Name => "Screen placer";
public bool IsEnabled
{
get => this.liveOptions.Value.SetGuildwarsWindowSizeOnLaunch;
@@ -44,7 +45,7 @@ public bool IsEnabled
public bool IsInstalled => true;
- public async Task OnGuildwarsStarted(Process process)
+ public async Task OnGuildwarsStarted(Process process, CancellationToken cancellationToken)
{
var screen = this.screenManager.Screens.Skip(this.liveOptions.Value.DesiredGuildwarsScreen).FirstOrDefault();
if (screen is null)
@@ -55,7 +56,7 @@ public async Task OnGuildwarsStarted(Process process)
var tries = 0;
while (await this.guildwarsMemoryCache.ReadLoginData(CancellationToken.None) is null)
{
- await Task.Delay(1000);
+ await Task.Delay(1000, cancellationToken);
tries++;
if (tries > MaxTries)
{
@@ -70,7 +71,7 @@ public async Task OnGuildwarsStarted(Process process)
public IEnumerable GetCustomArguments() => Enumerable.Empty();
- public Task OnGuildwarsStarting(Process process) => Task.CompletedTask;
+ public Task OnGuildwarsStarting(Process process, CancellationToken cancellationToken) => Task.CompletedTask;
- public Task OnGuildWarsCreated(Process process) => Task.CompletedTask;
+ public Task OnGuildWarsCreated(Process process, CancellationToken cancellationToken) => Task.CompletedTask;
}
diff --git a/Daybreak/Services/Toolbox/ToolboxService.cs b/Daybreak/Services/Toolbox/ToolboxService.cs
index 8ffeda35..4993c6b5 100644
--- a/Daybreak/Services/Toolbox/ToolboxService.cs
+++ b/Daybreak/Services/Toolbox/ToolboxService.cs
@@ -35,6 +35,7 @@ internal sealed class ToolboxService : IToolboxService
private readonly ILiveUpdateableOptions toolboxOptions;
private readonly ILogger logger;
+ public string Name => "GWToolbox";
public bool IsEnabled
{
get => this.toolboxOptions.Value.Enabled;
@@ -62,14 +63,14 @@ public ToolboxService(
this.logger = logger.ThrowIfNull();
}
- public Task OnGuildwarsStarting(Process process) => Task.CompletedTask;
+ public Task OnGuildwarsStarting(Process process, CancellationToken cancellationToken) => Task.CompletedTask;
- public async Task OnGuildWarsCreated(Process process)
+ public Task OnGuildWarsCreated(Process process, CancellationToken cancellationToken)
{
- await this.LaunchToolbox(process);
+ return Task.Run(() => this.LaunchToolbox(process), cancellationToken);
}
- public Task OnGuildwarsStarted(Process process) => Task.CompletedTask;
+ public Task OnGuildwarsStarted(Process process, CancellationToken cancellationToken) => Task.CompletedTask;
public IEnumerable GetCustomArguments()
{
@@ -149,7 +150,7 @@ private async Task SetupToolboxDll(ToolboxInstallationStatus toolboxInstal
return true;
}
- private async Task LaunchToolbox(Process process)
+ private void LaunchToolbox(Process process)
{
var scopedLogger = this.logger.CreateScopedLogger(nameof(this.LaunchToolbox), string.Empty);
if (this.toolboxOptions.Value.Enabled is false)
diff --git a/Daybreak/Services/UMod/UModService.cs b/Daybreak/Services/UMod/UModService.cs
index 94ea9da6..04b56da9 100644
--- a/Daybreak/Services/UMod/UModService.cs
+++ b/Daybreak/Services/UMod/UModService.cs
@@ -39,6 +39,8 @@ public sealed class UModService : IUModService
private readonly ILiveUpdateableOptions uModOptions;
private readonly ILogger logger;
+ public string Name => "uMod";
+
public bool IsEnabled
{
get => this.uModOptions.Value.Enabled;
@@ -74,25 +76,48 @@ public IEnumerable GetCustomArguments()
return Enumerable.Empty();
}
- public async Task OnGuildwarsStarting(Process process)
+ public Task OnGuildwarsStarting(Process process, CancellationToken cancellationToken)
{
- this.uModClient.Initialize(CancellationToken.None);
+ this.uModClient.Initialize(cancellationToken);
+ return Task.CompletedTask;
}
- public Task OnGuildWarsCreated(Process process)
+ public Task OnGuildWarsCreated(Process process, CancellationToken cancellationToken)
{
- this.processInjector.Inject(process, Path.Combine(Path.GetFullPath(UModDirectory), D3D9Dll));
- return Task.CompletedTask;
+ return Task.Run(() =>
+ {
+ this.processInjector.Inject(process, Path.Combine(Path.GetFullPath(UModDirectory), D3D9Dll));
+ }, cancellationToken);
}
- public async Task OnGuildwarsStarted(Process process)
+ public async Task OnGuildwarsStarted(Process process, CancellationToken cancellationToken)
{
foreach(var entry in this.uModOptions.Value.Mods.Where(e => e.Enabled && e.PathToFile is not null))
{
- await this.uModClient.AddFile(entry.PathToFile!, CancellationToken.None);
+ await this.uModClient.AddFile(entry.PathToFile!, cancellationToken);
+ }
+
+ try
+ {
+ await this.uModClient.Send(cancellationToken);
+ }
+ catch(IOException e)
+ {
+ if (!e.Message.Contains("Pipe is broken", StringComparison.OrdinalIgnoreCase))
+ {
+ throw;
+ }
+
+ /*
+ * Known issue where Guild Wars updater breaks the executable, which in turn breaks the integration with uMod.
+ * Prompt the user to manually reinstall Guild Wars.
+ */
+ this.notificationService.NotifyInformation(
+ title: "uMod failed to start",
+ description: "uMod failed to start due to a known issue with Guild Wars updating process. Please manually re-install Guild Wars in order to restore uMod functionality");
+ return;
}
- await this.uModClient.Send(CancellationToken.None);
this.uModClient.CloseConnection();
this.notificationService.NotifyInformation(
title: "uMod started",