Skip to content

Commit

Permalink
Improve Guild Wars launcher process (#423)
Browse files Browse the repository at this point in the history
Added timeouts for mod startup
Added detection logic for uMod breaking after GuildWars update

Closes #415
  • Loading branch information
AlexMacocian authored Oct 13, 2023
1 parent 8fdc9a6 commit f3b8221
Show file tree
Hide file tree
Showing 10 changed files with 178 additions and 50 deletions.
6 changes: 4 additions & 2 deletions Daybreak/Configuration/Options/LauncherOptions.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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;
}
2 changes: 1 addition & 1 deletion Daybreak/Daybreak.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<LangVersion>preview</LangVersion>
<ApplicationIcon>Daybreak.ico</ApplicationIcon>
<IncludePackageReferencesDuringMarkupCompilation>true</IncludePackageReferencesDuringMarkupCompilation>
<Version>0.9.8.125</Version>
<Version>0.9.8.126</Version>
<EnableWindowsTargeting>true</EnableWindowsTargeting>
<UserSecretsId>cfb2a489-db80-448d-a969-80270f314c46</UserSecretsId>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
Expand Down
102 changes: 97 additions & 5 deletions Daybreak/Services/ApplicationLauncher/ApplicationLauncher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand All @@ -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;
Expand Down Expand Up @@ -284,6 +348,34 @@ public IEnumerable<Process> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public interface IApplicationLauncher
GuildWarsApplicationLaunchContext? GetGuildwarsProcess(LaunchConfigurationWithCredentials launchConfigurationWithCredentials);
IEnumerable<GuildWarsApplicationLaunchContext?> GetGuildwarsProcesses(params LaunchConfigurationWithCredentials[] launchConfigurationWithCredentials);
IEnumerable<Process> GetGuildwarsProcesses();
void KillGuildWarsProcess(Process process);
Task<GuildWarsApplicationLaunchContext?> LaunchGuildwars(LaunchConfigurationWithCredentials launchConfigurationWithCredentials);
void RestartDaybreak();
void RestartDaybreakAsAdmin();
Expand Down
8 changes: 5 additions & 3 deletions Daybreak/Services/DSOAL/DSOALService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -41,6 +42,7 @@ public sealed class DSOALService : IDSOALService
private readonly ILiveUpdateableOptions<DSOALOptions> options;
private readonly ILogger<DSOALService> logger;

public string Name => "DSOAL";
public bool IsEnabled
{
get => this.options.Value.Enabled;
Expand Down Expand Up @@ -118,17 +120,17 @@ public IEnumerable<string> 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)
Expand Down
8 changes: 5 additions & 3 deletions Daybreak/Services/Mods/IModService.cs
Original file line number Diff line number Diff line change
@@ -1,27 +1,29 @@
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<string> GetCustomArguments();
/// <summary>
/// Called before starting the guild wars process.
/// Do mod preparation here.
/// </summary>
Task OnGuildwarsStarting(Process process);
Task OnGuildwarsStarting(Process process, CancellationToken cancellationToken);
/// <summary>
/// Called when the process is created in suspended state.
/// Do dll injection here.
/// </summary>
Task OnGuildWarsCreated(Process process);
Task OnGuildWarsCreated(Process process, CancellationToken cancellationToken);
/// <summary>
/// Called after the process has been resumed.
/// Do clean-up/integration with the guild wars process here.
/// </summary>
Task OnGuildwarsStarted(Process process);
Task OnGuildwarsStarted(Process process, CancellationToken cancellationToken);
}
40 changes: 21 additions & 19 deletions Daybreak/Services/ReShade/ReShadeService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ public sealed class ReShadeService : IReShadeService, IApplicationLifetimeServic
private readonly IDownloadService downloadService;
private readonly ILogger<ReShadeService> logger;

public string Name => "ReShade";
public bool IsEnabled
{
get => this.liveUpdateableOptions.Value.Enabled;
Expand Down Expand Up @@ -118,30 +119,31 @@ public void OnClosing()

public IEnumerable<string> GetCustomArguments() => Enumerable.Empty<string>();

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);
Expand Down
9 changes: 5 additions & 4 deletions Daybreak/Services/Screens/GuildwarsScreenPlacer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public GuildwarsScreenPlacer(
this.logger = logger.ThrowIfNull();
}

public string Name => "Screen placer";
public bool IsEnabled
{
get => this.liveOptions.Value.SetGuildwarsWindowSizeOnLaunch;
Expand All @@ -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)
Expand All @@ -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)
{
Expand All @@ -70,7 +71,7 @@ public async Task OnGuildwarsStarted(Process process)

public IEnumerable<string> GetCustomArguments() => Enumerable.Empty<string>();

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;
}
11 changes: 6 additions & 5 deletions Daybreak/Services/Toolbox/ToolboxService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ internal sealed class ToolboxService : IToolboxService
private readonly ILiveUpdateableOptions<ToolboxOptions> toolboxOptions;
private readonly ILogger<ToolboxService> logger;

public string Name => "GWToolbox";
public bool IsEnabled
{
get => this.toolboxOptions.Value.Enabled;
Expand Down Expand Up @@ -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<string> GetCustomArguments()
{
Expand Down Expand Up @@ -149,7 +150,7 @@ private async Task<bool> 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)
Expand Down
Loading

0 comments on commit f3b8221

Please sign in to comment.