LANCommander/LANCommander.Launcher/Program.cs
Pat Hartl 0ed8a80fbb Only allow one instance of the launcher to run at a time
Restores launcher from the system tray on subsequent runs
2026-06-19 19:37:31 -05:00

225 lines
8.7 KiB
C#

using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using System;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Avalonia.Controls;
using LANCommander.Launcher.Services;
using LANCommander.Launcher.ViewModels;
using LANCommander.Launcher.Views;
using LANCommander.Launcher.Data;
using LANCommander.Launcher.Services;
using LANCommander.Launcher.Services.Extensions;
using LANCommander.SDK;
using LANCommander.SDK.Extensions;
using LANCommander.SDK.Services;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace LANCommander.Launcher;
class Program
{
// Used to pass the service provider and args into ScriptDebugApp
internal static IServiceProvider? HeadlessServiceProvider;
internal static string[]? HeadlessArgs;
internal static bool BigScreenMode;
private static readonly string[] CliVerbs =
[
"RunScript", "Install", "Uninstall", "Run", "Sync",
"Import", "Export", "Upload", "Login", "Logout", "ChangeAlias"
];
[STAThread]
public static void Main(string[] args)
{
if (args.Any(a => CliVerbs.Any(v => v.Equals(a, StringComparison.OrdinalIgnoreCase)))
|| args.Any(a => a is "--help" or "--version"))
{
RunHeadlessAsync(args).GetAwaiter().GetResult();
return;
}
// If a protocol arg like "lancommander://game/{guid}" is present, this
// is a secondary instance spawned by a notification click. Forward the
// navigation request to the already-running instance, then exit.
foreach (var arg in args)
{
var gameId = SingleInstanceService.ParseProtocolArg(arg);
if (gameId.HasValue)
{
SingleInstanceService.TrySendToServer($"navigate-game:{gameId}");
return;
}
}
// Only one GUI instance should run at a time. The launcher hides to the tray on
// close, so a user may relaunch it without realizing it's still running. If another
// instance already holds the lock, ask it to surface its window, then exit.
if (!SingleInstanceService.TryAcquireInstanceLock())
{
SingleInstanceService.TrySendToServer("restore");
return;
}
BigScreenMode = args.Any(a => a.Equals("--big-screen", StringComparison.OrdinalIgnoreCase));
BuildAvaloniaApp()
#if DEBUG
.WithDeveloperTools()
#endif
.StartWithClassicDesktopLifetime(args);
}
// Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.WithInterFont()
.LogToTrace();
static async Task RunHeadlessAsync(string[] args)
{
IConfiguration configuration = new ConfigurationBuilder().ReadFromFile<Settings.Settings>();
var settings = new Settings.Settings();
configuration.Bind(settings);
var services = new ServiceCollection();
services.AddLogging(builder =>
{
builder.AddConsole();
builder.SetMinimumLevel(LogLevel.Information);
});
services.Configure<Settings.Settings>(configuration);
services.AddLANCommanderClient<Settings.Settings>();
services.AddLANCommanderLauncher();
services.AddSingleton<InstallService>();
services.AddSingleton<SingleInstanceService>();
var serviceProvider = services.BuildServiceProvider();
if (settings.Debug.EnableScriptDebugging)
{
HeadlessServiceProvider = serviceProvider;
HeadlessArgs = args;
AppBuilder.Configure<ScriptDebugApp>()
.UsePlatformDetect()
.WithInterFont()
.LogToTrace()
.StartWithClassicDesktopLifetime(args);
}
else
{
using var scope = serviceProvider.CreateScope();
var connectionClient = scope.ServiceProvider.GetRequiredService<IConnectionClient>();
var commandLineService = scope.ServiceProvider.GetRequiredService<CommandLineService>();
var settingsProvider = scope.ServiceProvider.GetRequiredService<SettingsProvider<Settings.Settings>>();
var databaseContext = scope.ServiceProvider.GetRequiredService<DatabaseContext>();
await connectionClient.ConnectAsync().ConfigureAwait(false);
if (!await connectionClient.PingAsync().ConfigureAwait(false))
await connectionClient.EnableOfflineModeAsync().ConfigureAwait(false);
if (settingsProvider.CurrentValue.Games.InstallDirectories.Length == 0)
{
settingsProvider.Update(static s => s.Games.InstallDirectories = GetOSPlatform() switch
{
var platform when platform == OSPlatform.Windows => [Path.Combine(Path.GetPathRoot(AppContext.BaseDirectory) ?? "C:", "Games")],
var platform when platform == OSPlatform.Linux => [Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Games")],
var platform when platform == OSPlatform.OSX => [Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Games")],
_ => throw new NotSupportedException("Unsupported OS platform")
});
}
await databaseContext.Database.MigrateAsync().ConfigureAwait(false);
await databaseContext.EnableWalModeAsync().ConfigureAwait(false);
await commandLineService.ParseCommandLineAsync(args);
}
}
private static OSPlatform GetOSPlatform()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
return OSPlatform.Windows;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
return OSPlatform.Linux;
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
return OSPlatform.OSX;
throw new NotSupportedException("Unsupported OS platform");
}
}
/// <summary>
/// Minimal Avalonia application used when EnableScriptDebugging is true.
/// Shows only a PowerShellConsoleWindow and runs the requested script inside it.
/// </summary>
internal class ScriptDebugApp : Application
{
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.ShutdownMode = ShutdownMode.OnMainWindowClose;
var vm = new PowerShellConsoleViewModel("Script Debugger", string.Empty);
var window = new PowerShellConsoleWindow { DataContext = vm };
vm.CloseAction = () => window.Close();
desktop.MainWindow = window;
window.Show();
_ = RunScriptAsync(window, Program.HeadlessServiceProvider!, Program.HeadlessArgs!);
}
base.OnFrameworkInitializationCompleted();
}
private static async Task RunScriptAsync(
PowerShellConsoleWindow window,
IServiceProvider serviceProvider,
string[] args)
{
try
{
using var scope = serviceProvider.CreateScope();
var connectionClient = scope.ServiceProvider.GetRequiredService<IConnectionClient>();
var commandLineService = scope.ServiceProvider.GetRequiredService<CommandLineService>();
var settingsProvider = scope.ServiceProvider.GetRequiredService<SettingsProvider<Settings.Settings>>();
var databaseContext = scope.ServiceProvider.GetRequiredService<DatabaseContext>();
await connectionClient.ConnectAsync().ConfigureAwait(false);
if (!await connectionClient.PingAsync().ConfigureAwait(false))
await connectionClient.EnableOfflineModeAsync().ConfigureAwait(false);
if (settingsProvider.CurrentValue.Games.InstallDirectories.Length == 0)
{
settingsProvider.Update(static s => s.Games.InstallDirectories = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? [Path.Combine(Path.GetPathRoot(AppContext.BaseDirectory) ?? "C:", "Games")]
: [Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Games")]);
}
await databaseContext.Database.MigrateAsync().ConfigureAwait(false);
await databaseContext.EnableWalModeAsync().ConfigureAwait(false);
await commandLineService.ParseCommandLineAsync(args).ConfigureAwait(false);
}
catch (Exception ex)
{
window.ConsoleControl.OnOutput(LogLevel.Error, $"Fatal error: {ex.Message}");
}
}
}