fix(launcher): close LA4 review findings

This commit is contained in:
Erik 2026-08-14 19:02:20 +02:00
parent d0a9c65d85
commit 10a712d66b
19 changed files with 1631 additions and 134 deletions

View file

@ -99,6 +99,12 @@ public sealed class LauncherOrchestrator : ILauncherOrchestrator
return platformCapability;
}
LauncherCapability executableCapability = _executables.GetAvailability(mode);
if (!executableCapability.IsAvailable)
{
return executableCapability;
}
lock (_gate)
{
ThrowIfDisposed();
@ -108,6 +114,33 @@ public sealed class LauncherOrchestrator : ILauncherOrchestrator
}
}
public LauncherCapability GetAccountLaunchCapability(
string serverName,
string accountName,
LaunchMode mode)
{
ArgumentException.ThrowIfNullOrWhiteSpace(serverName);
ArgumentException.ThrowIfNullOrWhiteSpace(accountName);
LauncherCapability capability = GetLaunchCapability(mode);
if (!capability.IsAvailable)
{
return capability;
}
lock (_gate)
{
ThrowIfDisposed();
_ = FindAccountLocked(serverName, accountName);
ManagedActivity? active = FindActiveActivityLocked(serverName, accountName);
return active is null
? LauncherCapability.Available
: LauncherCapability.Unavailable(
$"Stop the running {active.Kind.ToString().ToLowerInvariant()} "
+ "for this account before starting another activity.");
}
}
public LauncherCapability GetProbeCapability(string serverName, string accountName)
{
ArgumentException.ThrowIfNullOrWhiteSpace(serverName);
@ -120,6 +153,13 @@ public sealed class LauncherOrchestrator : ILauncherOrchestrator
return platformCapability;
}
LauncherCapability executableCapability =
_executables.GetAvailability(LaunchMode.Headless);
if (!executableCapability.IsAvailable)
{
return executableCapability;
}
lock (_gate)
{
ThrowIfDisposed();
@ -254,7 +294,7 @@ public sealed class LauncherOrchestrator : ILauncherOrchestrator
public Task<LauncherSessionSnapshot> LaunchAsync(
string serverName,
string accountName,
string characterName,
string? characterName,
LaunchMode mode,
CancellationToken cancellationToken = default)
{
@ -268,12 +308,38 @@ public sealed class LauncherOrchestrator : ILauncherOrchestrator
lock (_gate)
{
ThrowIfDisposed();
if (FindActiveActivityLocked(serverName, accountName) is not null)
{
throw new LauncherOperationException(
"A session or character refresh is already running for this account.");
}
ServerProfile server = FindServerLocked(serverName);
AccountProfile account = FindAccountLocked(serverName, accountName);
CharacterProfile character = FindCharacterLocked(
serverName,
accountName,
characterName);
CharacterProfile character;
if (string.IsNullOrWhiteSpace(characterName))
{
if (mode != LaunchMode.GuiSelect)
{
throw new LauncherOperationException(
"Select a cached character for GUI or headless launch.");
}
character = new CharacterProfile
{
Name = string.Empty,
LaunchMode = LaunchMode.GuiSelect,
Plugins = [],
LoginCommands = [],
};
}
else
{
character = FindCharacterLocked(
serverName,
accountName,
characterName);
}
LauncherInstallRecord install = _installRecord
?? throw new LauncherOperationException(FirstRunRequired);
@ -283,7 +349,7 @@ public sealed class LauncherOrchestrator : ILauncherOrchestrator
LauncherActivityKind.Play,
server.Name,
account.Account,
character.Name,
string.IsNullOrWhiteSpace(characterName) ? null : character.Name,
mode,
"Preparing session configuration…");
_activities.Add(activity);
@ -620,9 +686,12 @@ public sealed class LauncherOrchestrator : ILauncherOrchestrator
lock (_gate)
{
request.Activity.State = LauncherActivityState.Cancelled;
request.Activity.Status = "Operation cancelled.";
request.Activity.Error = null;
if (!request.Activity.IsTerminal)
{
request.Activity.State = LauncherActivityState.Cancelled;
request.Activity.Status = "Operation cancelled.";
request.Activity.Error = null;
}
}
RaiseStateChanged();
@ -638,9 +707,12 @@ public sealed class LauncherOrchestrator : ILauncherOrchestrator
password);
lock (_gate)
{
request.Activity.State = LauncherActivityState.Failed;
request.Activity.Status = message;
request.Activity.Error = message;
if (!request.Activity.IsTerminal)
{
request.Activity.State = LauncherActivityState.Failed;
request.Activity.Status = message;
request.Activity.Error = message;
}
}
RaiseStateChanged();
@ -681,15 +753,19 @@ public sealed class LauncherOrchestrator : ILauncherOrchestrator
}
break;
case LauncherSessionState.Exited:
activity.ExitCode = activity.Supervisor?.ExitCode;
if (activity.State is not (
LauncherActivityState.Failed
or LauncherActivityState.Cancelled))
activity.ExitCode ??= activity.Supervisor?.ExitCode;
if (!activity.IsTerminal)
{
activity.State = LauncherActivityState.Exited;
activity.Status = activity.ExitCode is int code
? $"Host process exited with code {code}."
: "Host process exited.";
activity.Status = activity.HostTerminalStatus
?? (activity.ExitCode is int code
? $"Host process exited with code {code}."
: "Host process exited.");
}
else if (activity.State == LauncherActivityState.Exited
&& activity.HostTerminalStatus is not null)
{
activity.Status = activity.HostTerminalStatus;
}
break;
}
@ -723,6 +799,27 @@ public sealed class LauncherOrchestrator : ILauncherOrchestrator
return;
}
if (activity.IsTerminal)
{
switch (statusEvent)
{
case ExitedStatusEvent exited:
activity.ExitCode ??= exited.Code;
activity.HostTerminalStatus ??=
$"Exited: {exited.Reason} (code {exited.Code}).";
if (activity.State == LauncherActivityState.Exited)
{
activity.Status = activity.HostTerminalStatus;
}
break;
case CharacterListStatusEvent roster:
ApplyRosterLocked(activity, roster, updateStatus: false);
break;
}
return;
}
switch (statusEvent)
{
case StartedStatusEvent:
@ -762,7 +859,9 @@ public sealed class LauncherOrchestrator : ILauncherOrchestrator
case ExitedStatusEvent exited:
activity.State = LauncherActivityState.Exited;
activity.ExitCode = exited.Code;
activity.Status = $"Exited: {exited.Reason} (code {exited.Code}).";
activity.HostTerminalStatus =
$"Exited: {exited.Reason} (code {exited.Code}).";
activity.Status = activity.HostTerminalStatus;
break;
case MalformedStatusEvent malformed:
activity.Error = $"Malformed host status event: {malformed.Error}";
@ -778,7 +877,8 @@ public sealed class LauncherOrchestrator : ILauncherOrchestrator
private void ApplyRosterLocked(
ManagedActivity activity,
CharacterListStatusEvent roster)
CharacterListStatusEvent roster,
bool updateStatus = true)
{
if (!string.Equals(
roster.AccountName,
@ -792,19 +892,22 @@ public sealed class LauncherOrchestrator : ILauncherOrchestrator
try
{
_profileStore.MergeRoster(
activity.ServerName,
activity.AccountName,
roster.Characters
.Select(character => new CharacterRosterEntry(
character.Id,
character.Name,
character.SecondsGreyedOut))
.ToArray());
_profileStore.Save();
activity.Status = roster.Characters.Count == 1
? "Character roster refreshed: 1 character."
: $"Character roster refreshed: {roster.Characters.Count} characters.";
_profileStore.ExecuteTransaction(() =>
_profileStore.MergeRoster(
activity.ServerName,
activity.AccountName,
roster.Characters
.Select(character => new CharacterRosterEntry(
character.Id,
character.Name,
character.SecondsGreyedOut))
.ToArray()));
if (updateStatus)
{
activity.Status = roster.Characters.Count == 1
? "Character roster refreshed: 1 character."
: $"Character roster refreshed: {roster.Characters.Count} characters.";
}
}
catch (Exception ex)
{
@ -812,7 +915,10 @@ public sealed class LauncherOrchestrator : ILauncherOrchestrator
"Could not save the refreshed character roster",
ex,
secret: null);
activity.Status = activity.Error;
if (updateStatus)
{
activity.Status = activity.Error;
}
}
}
@ -879,8 +985,7 @@ public sealed class LauncherOrchestrator : ILauncherOrchestrator
lock (_gate)
{
ThrowIfDisposed();
mutation();
_profileStore.Save();
_profileStore.ExecuteTransaction(mutation);
}
RaiseStateChanged();
@ -1125,6 +1230,8 @@ public sealed class LauncherOrchestrator : ILauncherOrchestrator
public string? Error { get; set; }
public string? HostTerminalStatus { get; set; }
public ILauncherProcessSupervisor? Supervisor { get; set; }
public EventHandler<LauncherSessionState>? SupervisorStateHandler { get; set; }
@ -1140,6 +1247,8 @@ public sealed class LauncherOrchestrator : ILauncherOrchestrator
or LauncherActivityState.Failed
or LauncherActivityState.Cancelled);
public bool IsTerminal => !IsActive;
public LauncherSessionSnapshot ToSnapshot() =>
new(
SessionId,