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:
lincube
2026-04-22 09:25:22 +08:00
parent 703ed7b48a
commit 9224c9a33a
28 changed files with 843 additions and 109 deletions

View File

@@ -30,6 +30,11 @@ internal sealed class PluginInstallerService
throw new FileNotFoundException($"Plugin package '{fullSourcePath}' was not found.", fullSourcePath);
}
if (TryBuildElevationRequiredResult(fullPluginsDirectory) is { } elevationRequiredResult)
{
return elevationRequiredResult;
}
var manifest = ReadManifestFromPackage(fullSourcePath);
Directory.CreateDirectory(fullPluginsDirectory);
var destinationPath = Path.Combine(fullPluginsDirectory, BuildInstalledPackageFileName(manifest.Id));
@@ -51,6 +56,46 @@ internal sealed class PluginInstallerService
};
}
private static LauncherResult? TryBuildElevationRequiredResult(string pluginsDirectory)
{
if (!OperatingSystem.IsWindows())
{
return null;
}
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
if (string.IsNullOrWhiteSpace(localAppData))
{
return null;
}
var allowedRoot = EnsureTrailingSeparator(Path.Combine(Path.GetFullPath(localAppData), "LanMountainDesktop"));
var normalizedPluginsDirectory = EnsureTrailingSeparator(Path.GetFullPath(pluginsDirectory));
if (normalizedPluginsDirectory.StartsWith(allowedRoot, StringComparison.OrdinalIgnoreCase))
{
return null;
}
Logger.Warn(
$"Plugin installation requires explicit elevation. Reason='plugin_requires_elevation'; " +
$"PluginsDirectory='{pluginsDirectory}'; AllowedRoot='{allowedRoot}'.");
return new LauncherResult
{
Success = false,
Stage = "plugin.install",
Code = "plugin_elevation_required",
Message = "Plugin installation outside the current user's LanMountainDesktop data directory requires explicit elevation.",
ErrorMessage = "Plugin installation target is outside the current user's LanMountainDesktop data directory.",
Details = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
["pluginsDirectory"] = pluginsDirectory,
["allowedRoot"] = allowedRoot,
["elevationReason"] = "outside_user_scope"
}
};
}
public PluginManifest ReadManifestFromPackage(string packagePath)
{
using var archive = ZipFile.OpenRead(packagePath);