Fix updater and installer (Closes #1407) (#1412)

This commit is contained in:
2026-02-10 05:15:44 -08:00
parent 20d0bade79
commit 067da8a091
7 changed files with 284 additions and 64 deletions
@@ -28,9 +28,10 @@ internal sealed class ApplicationLauncher(
INotificationService notificationService,
IOptionsMonitor<LauncherOptions> launcherOptions,
IModsManager modsManager,
IPrivilegeManager privilegeManager,
IGuildWarsReadyChecker guildWarsReadyChecker,
IGuildWarsProcessFinder guildWarsProcessFinder,
IDaybreakRestartingService daybreakRestartingService,
IPrivilegeManager privilegeManager,
ILogger<ApplicationLauncher> logger
) : IApplicationLauncher
{
@@ -48,11 +49,15 @@ internal sealed class ApplicationLauncher(
launcherOptions.ThrowIfNull();
private readonly IModsManager modsManager = modsManager.ThrowIfNull();
private readonly ILogger<ApplicationLauncher> logger = logger.ThrowIfNull();
private readonly IPrivilegeManager privilegeManager = privilegeManager.ThrowIfNull();
private readonly IGuildWarsReadyChecker guildWarsReadyChecker =
guildWarsReadyChecker.ThrowIfNull();
private readonly IGuildWarsProcessFinder guildWarsProcessFinder =
guildWarsProcessFinder.ThrowIfNull();
private readonly IDaybreakRestartingService daybreakRestartingService =
daybreakRestartingService.ThrowIfNull();
private readonly IPrivilegeManager privilegeManager =
privilegeManager.ThrowIfNull();
public async Task<GuildWarsApplicationLaunchContext?> LaunchGuildwars(
LaunchConfigurationWithCredentials launchConfigurationWithCredentials,
@@ -83,76 +88,19 @@ internal sealed class ApplicationLauncher(
};
}
// TODO: Needs to be reworked to be cross-platform
public void RestartDaybreak()
{
if (this.privilegeManager.AdminPrivileges)
{
this.RestartDaybreakAsAdmin();
}
else
{
this.RestartDaybreakAsNormalUser();
}
this.daybreakRestartingService.RestartDaybreak();
}
// TODO: Needs to be reworked to be cross-platform
public void RestartDaybreakAsAdmin()
{
this.logger.LogInformation("Restarting daybreak with admin rights");
var processName = Process.GetCurrentProcess()?.MainModule?.FileName;
if (processName!.IsNullOrWhiteSpace() || File.Exists(processName) is false)
{
throw new InvalidOperationException("Unable to find executable. Aborting restart");
}
var process = new Process()
{
StartInfo = new()
{
Verb = "runas",
WorkingDirectory = Environment.CurrentDirectory,
UseShellExecute = true,
CreateNoWindow = true,
FileName = "cmd.exe",
Arguments =
$"/c cd /d \"{Environment.CurrentDirectory}\" && timeout /t 1 /nobreak && start \"\" \"{processName}\" && exit",
},
};
if (process.Start() is false)
{
throw new InvalidOperationException($"Unable to start {processName} as admin");
}
this.photinoWindow.Close();
this.daybreakRestartingService.RestartDaybreakAsAdmin();
}
// TODO: Needs to be reworked to be cross-platform
public void RestartDaybreakAsNormalUser()
{
this.logger.LogInformation("Restarting daybreak as normal user");
var processName = Process.GetCurrentProcess()?.MainModule?.FileName;
if (processName!.IsNullOrWhiteSpace() || File.Exists(processName) is false)
{
throw new InvalidOperationException("Unable to find executable. Aborting restart");
}
var process = new Process()
{
StartInfo = new()
{
WorkingDirectory = Environment.CurrentDirectory,
FileName = processName,
UseShellExecute = true,
CreateNoWindow = true,
},
};
if (process.Start() is false)
{
throw new InvalidOperationException($"Unable to start {processName} as normal user");
}
this.photinoWindow.Close();
this.daybreakRestartingService.RestartDaybreakAsNormalUser();
}
private async Task<Process?> LaunchGuildwarsProcess(
+2 -2
View File
@@ -4,8 +4,8 @@ using System.Net;
using System.Text;
const string LatestUrl = "https://github.com/gwdevhub/Daybreak/releases/latest";
var daybreakExecutable = OperatingSystem.IsWindows() ? "Daybreak.exe" : "Daybreak.Linux";
var daybreakProcessName = OperatingSystem.IsWindows() ? "Daybreak" : "Daybreak.Linux";
var daybreakExecutable = OperatingSystem.IsWindows() ? "Daybreak.exe" : "Daybreak";
var daybreakProcessName = "Daybreak";
var releaseAssetSuffix = OperatingSystem.IsWindows() ? "" : "-linux";
static void RenderProgressBar(int currentStep, int totalSteps, int barSize)
@@ -61,6 +61,7 @@ public sealed class LinuxPlatformConfiguration : PluginConfigurationBase
services.AddHostedSingleton<IMDomainRegistrar, PortScanningDomainRegistrar>();
services.AddSingleton<IWindowManipulationService, WindowManipulationService>();
services.AddSingleton<ICrashDumpService, CrashDumpService>();
services.AddSingleton<IDaybreakRestartingService, DaybreakRestartingService>();
}
public override void RegisterStartupActions(IStartupActionProducer startupActionProducer)
@@ -0,0 +1,154 @@
using Daybreak.Shared.Services.ApplicationLauncher;
using Daybreak.Shared.Services.Privilege;
using Microsoft.Extensions.Logging;
using Photino.NET;
using System.Core.Extensions;
using System.Diagnostics;
using System.Extensions;
using System.Extensions.Core;
namespace Daybreak.Linux.Services.ApplicationLauncher;
/// <summary>
/// Linux-specific implementation for restarting the Daybreak application.
/// Uses bash for process spawning and pkexec/sudo for privilege elevation.
/// </summary>
internal sealed class DaybreakRestartingService(
PhotinoWindow photinoWindow,
IPrivilegeManager privilegeManager,
ILogger<DaybreakRestartingService> logger
) : IDaybreakRestartingService
{
private readonly PhotinoWindow photinoWindow = photinoWindow.ThrowIfNull();
private readonly IPrivilegeManager privilegeManager = privilegeManager.ThrowIfNull();
private readonly ILogger<DaybreakRestartingService> logger = logger.ThrowIfNull();
public void RestartDaybreak()
{
if (this.privilegeManager.AdminPrivileges)
{
this.RestartDaybreakAsAdmin();
}
else
{
this.RestartDaybreakAsNormalUser();
}
}
public void RestartDaybreakAsAdmin()
{
this.logger.LogInformation("Restarting daybreak with elevated privileges");
var processName = Process.GetCurrentProcess()?.MainModule?.FileName;
if (processName!.IsNullOrWhiteSpace() || File.Exists(processName) is false)
{
throw new InvalidOperationException("Unable to find executable. Aborting restart");
}
// On Linux, use pkexec for graphical privilege elevation
// Fall back to sudo if pkexec is not available
var elevationCommand = GetElevationCommand();
if (elevationCommand is null)
{
this.logger.LogWarning("No elevation command (pkexec/sudo) found. Restarting without elevation");
this.RestartDaybreakAsNormalUser();
return;
}
var process = new Process()
{
StartInfo = new()
{
WorkingDirectory = Environment.CurrentDirectory,
UseShellExecute = false,
CreateNoWindow = true,
FileName = "/bin/bash",
Arguments = $"-c \"sleep 1 && {elevationCommand} \\\"{processName}\\\" &\"",
},
};
if (process.Start() is false)
{
throw new InvalidOperationException($"Unable to start {processName} with elevated privileges");
}
this.photinoWindow.Close();
}
public void RestartDaybreakAsNormalUser()
{
this.logger.LogInformation("Restarting daybreak as normal user");
var processName = Process.GetCurrentProcess()?.MainModule?.FileName;
if (processName!.IsNullOrWhiteSpace() || File.Exists(processName) is false)
{
throw new InvalidOperationException("Unable to find executable. Aborting restart");
}
// Use nohup to ensure the process survives parent exit, and disown to detach from terminal
var process = new Process()
{
StartInfo = new()
{
WorkingDirectory = Environment.CurrentDirectory,
UseShellExecute = false,
CreateNoWindow = true,
FileName = "/bin/bash",
Arguments = $"-c \"sleep 1 && nohup \\\"{processName}\\\" > /dev/null 2>&1 &\"",
},
};
if (process.Start() is false)
{
throw new InvalidOperationException($"Unable to start {processName} as normal user");
}
this.photinoWindow.Close();
}
/// <summary>
/// Gets the appropriate elevation command for the system.
/// Prefers pkexec (graphical) over sudo (terminal-based).
/// </summary>
private static string? GetElevationCommand()
{
// Check for pkexec first (graphical polkit elevation)
if (CommandExists("pkexec"))
{
return "pkexec";
}
// Fall back to sudo
if (CommandExists("sudo"))
{
return "sudo";
}
return null;
}
/// <summary>
/// Checks if a command exists in the system PATH.
/// </summary>
private static bool CommandExists(string command)
{
try
{
var process = new Process()
{
StartInfo = new()
{
FileName = "/bin/bash",
Arguments = $"-c \"command -v {command}\"",
UseShellExecute = false,
RedirectStandardOutput = true,
CreateNoWindow = true,
},
};
process.Start();
var output = process.StandardOutput.ReadToEnd();
process.WaitForExit();
return process.ExitCode == 0 && !string.IsNullOrWhiteSpace(output);
}
catch
{
return false;
}
}
}
@@ -0,0 +1,22 @@
namespace Daybreak.Shared.Services.ApplicationLauncher;
/// <summary>
/// Platform-specific service for restarting the Daybreak application.
/// </summary>
public interface IDaybreakRestartingService
{
/// <summary>
/// Restarts Daybreak, choosing the appropriate privilege level based on current state.
/// </summary>
void RestartDaybreak();
/// <summary>
/// Restarts Daybreak with elevated privileges.
/// </summary>
void RestartDaybreakAsAdmin();
/// <summary>
/// Restarts Daybreak as a normal user (without elevation).
/// </summary>
void RestartDaybreakAsNormalUser();
}
@@ -80,6 +80,7 @@ public sealed class WindowsPlatformConfiguration : PluginConfigurationBase
services.AddHostedSingleton<IMDomainRegistrar, MDomainRegistrar>();
services.AddSingleton<IWindowManipulationService, WindowManipulationService>();
services.AddSingleton<ICrashDumpService, CrashDumpService>();
services.AddSingleton<IDaybreakRestartingService, DaybreakRestartingService>();
}
public override void RegisterMods(IModsProducer modsProducer)
@@ -0,0 +1,94 @@
using Daybreak.Shared.Services.ApplicationLauncher;
using Daybreak.Shared.Services.Privilege;
using Microsoft.Extensions.Logging;
using Photino.NET;
using System.Core.Extensions;
using System.Diagnostics;
using System.Extensions;
using System.Extensions.Core;
namespace Daybreak.Windows.Services.ApplicationLauncher;
/// <summary>
/// Windows-specific implementation for restarting the Daybreak application.
/// Uses cmd.exe with 'runas' verb for admin elevation.
/// </summary>
internal sealed class DaybreakRestartingService(
PhotinoWindow photinoWindow,
IPrivilegeManager privilegeManager,
ILogger<DaybreakRestartingService> logger
) : IDaybreakRestartingService
{
private readonly PhotinoWindow photinoWindow = photinoWindow.ThrowIfNull();
private readonly IPrivilegeManager privilegeManager = privilegeManager.ThrowIfNull();
private readonly ILogger<DaybreakRestartingService> logger = logger.ThrowIfNull();
public void RestartDaybreak()
{
if (this.privilegeManager.AdminPrivileges)
{
this.RestartDaybreakAsAdmin();
}
else
{
this.RestartDaybreakAsNormalUser();
}
}
public void RestartDaybreakAsAdmin()
{
this.logger.LogInformation("Restarting daybreak with admin rights");
var processName = Process.GetCurrentProcess()?.MainModule?.FileName;
if (processName!.IsNullOrWhiteSpace() || File.Exists(processName) is false)
{
throw new InvalidOperationException("Unable to find executable. Aborting restart");
}
var process = new Process()
{
StartInfo = new()
{
Verb = "runas",
WorkingDirectory = Environment.CurrentDirectory,
UseShellExecute = true,
CreateNoWindow = true,
FileName = "cmd.exe",
Arguments =
$"/c cd /d \"{Environment.CurrentDirectory}\" && timeout /t 1 /nobreak && start \"\" \"{processName}\" && exit",
},
};
if (process.Start() is false)
{
throw new InvalidOperationException($"Unable to start {processName} as admin");
}
this.photinoWindow.Close();
}
public void RestartDaybreakAsNormalUser()
{
this.logger.LogInformation("Restarting daybreak as normal user");
var processName = Process.GetCurrentProcess()?.MainModule?.FileName;
if (processName!.IsNullOrWhiteSpace() || File.Exists(processName) is false)
{
throw new InvalidOperationException("Unable to find executable. Aborting restart");
}
var process = new Process()
{
StartInfo = new()
{
WorkingDirectory = Environment.CurrentDirectory,
FileName = processName,
UseShellExecute = true,
CreateNoWindow = true,
},
};
if (process.Start() is false)
{
throw new InvalidOperationException($"Unable to start {processName} as normal user");
}
this.photinoWindow.Close();
}
}