mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-23 01:44:26 +08:00
Harden OOBE, launch-source and elevation flow
Introduce a per-user OOBE state model and hardened launch/elevation handling. Adds OobeStateFile/OobeLaunchDecision models, OobeStateService (persisting %LOCALAPPDATA%/.launcher/state/oobe-state.json), and LauncherExecutionContext to capture elevation and user SID. CommandContext now normalizes/infers launch-source values (normal, postinstall, apply-update, plugin-install, debug-preview) and exposes maintenance checks. LauncherFlowCoordinator propagates richer launcher context details for diagnostics and suppresses OOBE for elevated/maintenance contexts. PluginInstallerService avoids requesting elevation for user-scoped installs and returns a clear error when installation target is outside the current user's LocalAppData. LauncherClient maps and surfaces result codes, UpdateWorkflow and installer invocation now pass explicit --launch-source values, and WelcomeOobeStep persists OOBE completion via the new service. Adds unit tests (CommandContext, OobeStateService, PluginInstallerService), docs/specs/checklists for the contract, and makes internals visible to tests.
This commit is contained in:
@@ -5,6 +5,7 @@ using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@@ -28,7 +29,8 @@ internal sealed class LauncherClient
|
||||
return new LauncherInstallResult(
|
||||
false,
|
||||
null,
|
||||
"Elevated helper install is only supported on Windows.");
|
||||
"Elevated helper install is only supported on Windows.",
|
||||
"failed");
|
||||
}
|
||||
|
||||
var launcherPath = ResolveLauncherPath();
|
||||
@@ -37,7 +39,8 @@ internal sealed class LauncherClient
|
||||
return new LauncherInstallResult(
|
||||
false,
|
||||
null,
|
||||
$"Launcher executable was not found at '{launcherPath}'.");
|
||||
$"Launcher executable was not found at '{launcherPath}'.",
|
||||
"failed");
|
||||
}
|
||||
|
||||
var resultPath = Path.Combine(
|
||||
@@ -53,14 +56,18 @@ internal sealed class LauncherClient
|
||||
using var process = StartLauncherProcess(launcherPath, packagePath, pluginsDirectory, resultPath);
|
||||
if (process is null)
|
||||
{
|
||||
return new LauncherInstallResult(false, null, "Failed to start launcher process.");
|
||||
return new LauncherInstallResult(false, null, "Failed to start launcher process.", "failed");
|
||||
}
|
||||
|
||||
await process.WaitForExitAsync(cancellationToken);
|
||||
var result = await ReadResultAsync(resultPath, cancellationToken);
|
||||
if (result is not null)
|
||||
{
|
||||
return new LauncherInstallResult(result.Success, result.InstalledPackagePath, result.ErrorMessage);
|
||||
return new LauncherInstallResult(
|
||||
result.Success,
|
||||
result.InstalledPackagePath,
|
||||
result.ErrorMessage ?? result.Message,
|
||||
MapResultCode(result.Code));
|
||||
}
|
||||
|
||||
if (process.ExitCode == 0)
|
||||
@@ -68,7 +75,8 @@ internal sealed class LauncherClient
|
||||
return new LauncherInstallResult(
|
||||
false,
|
||||
null,
|
||||
"Launcher exited without producing a result file.");
|
||||
"Launcher exited without producing a result file.",
|
||||
"failed");
|
||||
}
|
||||
|
||||
return new LauncherInstallResult(
|
||||
@@ -77,11 +85,12 @@ internal sealed class LauncherClient
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"Launcher exited with code {0}.",
|
||||
process.ExitCode));
|
||||
process.ExitCode),
|
||||
"failed");
|
||||
}
|
||||
catch (Win32Exception ex) when (ex.NativeErrorCode == UserCanceledUacErrorCode)
|
||||
{
|
||||
return new LauncherInstallResult(false, null, "Administrator permission request was canceled.");
|
||||
return new LauncherInstallResult(false, null, "Administrator permission request was canceled.", "elevation_cancelled");
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -98,12 +107,11 @@ internal sealed class LauncherClient
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = launcherPath,
|
||||
Verb = "runas",
|
||||
UseShellExecute = true,
|
||||
WorkingDirectory = Path.GetDirectoryName(launcherPath) ?? AppContext.BaseDirectory,
|
||||
Arguments = string.Create(
|
||||
CultureInfo.InvariantCulture,
|
||||
$"--source {QuoteArgument(Path.GetFullPath(packagePath))} --plugins-dir {QuoteArgument(Path.GetFullPath(pluginsDirectory))} --result {QuoteArgument(Path.GetFullPath(resultPath))}")
|
||||
$"--source {QuoteArgument(Path.GetFullPath(packagePath))} --plugins-dir {QuoteArgument(Path.GetFullPath(pluginsDirectory))} --result {QuoteArgument(Path.GetFullPath(resultPath))} --launch-source plugin-install")
|
||||
};
|
||||
|
||||
return Process.Start(startInfo);
|
||||
@@ -170,12 +178,32 @@ internal sealed class LauncherClient
|
||||
}
|
||||
}
|
||||
|
||||
private static string MapResultCode(string? launcherCode)
|
||||
{
|
||||
return launcherCode switch
|
||||
{
|
||||
"plugin_elevation_required" => "requires_elevation",
|
||||
"elevation_cancelled" => "elevation_cancelled",
|
||||
"ok" => "ok",
|
||||
_ => "failed"
|
||||
};
|
||||
}
|
||||
|
||||
private sealed class HelperResultFile
|
||||
{
|
||||
[JsonPropertyName("success")]
|
||||
public bool Success { get; init; }
|
||||
|
||||
[JsonPropertyName("code")]
|
||||
public string? Code { get; init; }
|
||||
|
||||
[JsonPropertyName("message")]
|
||||
public string? Message { get; init; }
|
||||
|
||||
[JsonPropertyName("installedPackagePath")]
|
||||
public string? InstalledPackagePath { get; init; }
|
||||
|
||||
[JsonPropertyName("errorMessage")]
|
||||
public string? ErrorMessage { get; init; }
|
||||
}
|
||||
}
|
||||
@@ -183,4 +211,5 @@ internal sealed class LauncherClient
|
||||
internal sealed record LauncherInstallResult(
|
||||
bool Success,
|
||||
string? InstalledPackagePath,
|
||||
string? ErrorMessage);
|
||||
string? ErrorMessage,
|
||||
string Code);
|
||||
|
||||
@@ -1454,7 +1454,7 @@ public sealed class UpdateWorkflowService
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = launcherPath,
|
||||
Arguments = $"apply-update --app-root \"{launcherRoot}\"",
|
||||
Arguments = $"apply-update --app-root \"{launcherRoot}\" --launch-source apply-update",
|
||||
UseShellExecute = false,
|
||||
WorkingDirectory = launcherRoot
|
||||
};
|
||||
@@ -1493,6 +1493,7 @@ public sealed class UpdateWorkflowService
|
||||
|
||||
try
|
||||
{
|
||||
AppLogger.Info("UpdateWorkflow", "Launching pending full installer with elevation reason 'full_update_apply'.");
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = pending.InstallerPath,
|
||||
|
||||
Reference in New Issue
Block a user