fix.在线安装器,启动器

This commit is contained in:
lincube
2026-06-05 11:08:11 +08:00
parent bb4e90ea8d
commit 8c88e305ee
42 changed files with 1507 additions and 393 deletions

View File

@@ -1,67 +0,0 @@
using Avalonia.Threading;
using LanMountainDesktop.Launcher.Models;
using LanMountainDesktop.Launcher.Views;
namespace LanMountainDesktop.Launcher.Oobe;
internal sealed class DataLocationOobeStep : IOobeStep
{
private readonly DataLocationResolver _resolver;
public DataLocationOobeStep(DataLocationResolver resolver)
{
_resolver = resolver;
}
public async Task RunAsync(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var existingConfig = _resolver.LoadConfig();
if (existingConfig is not null)
{
Logger.Info("DataLocation OOBE step skipped: config already exists.");
return;
}
DataLocationPromptWindow? window = null;
await Dispatcher.UIThread.InvokeAsync(() =>
{
window = new DataLocationPromptWindow(_resolver);
window.Show();
});
if (window is null)
{
Logger.Warn("DataLocation OOBE step failed: window could not be created.");
return;
}
try
{
var result = await window.WaitForChoiceAsync().ConfigureAwait(false);
if (result is null)
{
Logger.Info("DataLocation OOBE step: user cancelled or closed window. Using default system location.");
_resolver.ApplyLocationChoice(DataLocationMode.System, null, false);
}
else
{
var success = _resolver.ApplyLocationChoice(result.SelectedMode, null, result.MigrateExistingData);
Logger.Info(
$"DataLocation OOBE step: user selected '{result.SelectedMode}'. " +
$"Migrate={result.MigrateExistingData}; Success={success}.");
}
}
finally
{
await Dispatcher.UIThread.InvokeAsync(() =>
{
if (window.IsVisible)
{
window.Close();
}
});
}
}
}

View File

@@ -1,6 +1,15 @@
using LanMountainDesktop.Launcher.Models;
namespace LanMountainDesktop.Launcher.Oobe;
internal sealed record OobeStepResult(bool ContinueLaunch, LauncherResult? Result = null)
{
public static OobeStepResult Continue { get; } = new(true);
public static OobeStepResult Complete(LauncherResult result) => new(false, result);
}
internal interface IOobeStep
{
Task RunAsync(CancellationToken cancellationToken);
Task<OobeStepResult> RunAsync(CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,92 @@
using System.Text.Json;
using LanMountainDesktop.Launcher.Models;
namespace LanMountainDesktop.Launcher.Oobe;
internal sealed class OobeSessionCommitService
{
private readonly DataLocationResolver _dataLocationResolver;
private readonly OobeStateService _oobeStateService;
private readonly CommandContext _context;
private readonly Func<bool, bool>? _setWindowsStartup;
public OobeSessionCommitService(
DataLocationResolver dataLocationResolver,
OobeStateService oobeStateService,
CommandContext context,
Func<bool, bool>? setWindowsStartup = null)
{
_dataLocationResolver = dataLocationResolver;
_oobeStateService = oobeStateService;
_context = context;
_setWindowsStartup = setWindowsStartup;
}
public OobeCompletionResult Commit(OobeSessionDraft draft)
{
ArgumentNullException.ThrowIfNull(draft);
if (!_dataLocationResolver.ApplyLocationChoice(
draft.DataLocationMode,
customPath: null,
draft.MigrateExistingData))
{
return Failure("data_location_save_failed", "Failed to save the selected data location.");
}
var dataRoot = _dataLocationResolver.ResolveDataRoot();
try
{
var settingsPath = HostAppSettingsOobeMerger.GetSettingsFilePath(dataRoot);
HostAppSettingsOobeMerger.MergeStartupPresentation(settingsPath, draft.StartupChoices);
}
catch (Exception ex)
{
return Failure("startup_settings_save_failed", ex.Message);
}
var setWindowsStartup = _setWindowsStartup ?? new LauncherWindowsStartupService().SetEnabled;
if (OperatingSystem.IsWindows() &&
!setWindowsStartup(draft.StartupChoices.AutoStartWithWindows))
{
return Failure("windows_startup_save_failed", "Failed to save Windows startup preference.");
}
try
{
var launcherDataPath = _dataLocationResolver.ResolveLauncherDataPath();
Directory.CreateDirectory(launcherDataPath);
var privacyConfigPath = Path.Combine(launcherDataPath, "privacy-config.json");
var privacyJson = JsonSerializer.Serialize(draft.PrivacyConfig, AppJsonContext.Default.PrivacyConfig);
File.WriteAllText(privacyConfigPath, privacyJson);
var agreementService = new PrivacyAgreementService(launcherDataPath);
if (!agreementService.SaveAgreement(
draft.PrivacyAgreementAccepted,
draft.PrivacyUserId,
draft.PrivacyDeviceId))
{
return Failure("privacy_agreement_save_failed", "Failed to save privacy agreement state.");
}
}
catch (Exception ex)
{
return Failure("privacy_settings_save_failed", ex.Message);
}
var completion = _oobeStateService.MarkCompleted(_context, dataRoot);
return completion.Success
? completion
: Failure(completion.ResultCode, completion.ErrorMessage);
}
private static OobeCompletionResult Failure(string code, string message) =>
new()
{
Success = false,
ResultCode = code,
ErrorMessage = message
};
}

View File

@@ -7,10 +7,12 @@ internal sealed class OobeStateService
{
private const int CurrentSchemaVersion = 1;
private readonly string _appRoot;
private readonly string? _stateRootOverride;
private readonly string _stateDirectory;
private readonly string _statePath;
private readonly string _legacyStatePath;
private readonly string _legacyMarkerPath;
private readonly IReadOnlyList<string> _legacyStatePaths;
private readonly IReadOnlyList<string> _legacyMarkerPaths;
private readonly LauncherExecutionSnapshot _executionSnapshot;
public OobeStateService(
@@ -18,21 +20,17 @@ internal sealed class OobeStateService
string? stateRootOverride = null,
LauncherExecutionSnapshot? executionSnapshot = null)
{
_ = Path.GetFullPath(appRoot);
_appRoot = Path.GetFullPath(appRoot);
_stateRootOverride = string.IsNullOrWhiteSpace(stateRootOverride)
? null
: Path.GetFullPath(stateRootOverride);
_executionSnapshot = executionSnapshot ?? LauncherExecutionContext.Capture();
var stateRoot = string.IsNullOrWhiteSpace(stateRootOverride)
? ResolveStateRoot(appRoot)
: Path.GetFullPath(stateRootOverride);
_stateDirectory = Path.Combine(stateRoot, "Launcher", "state");
_statePath = Path.Combine(_stateDirectory, "oobe-state.json");
var stateRoot = ResolveCurrentStateRoot();
(_stateDirectory, _statePath) = BuildStatePaths(stateRoot);
var legacyRoot = string.IsNullOrWhiteSpace(stateRootOverride)
? Path.GetFullPath(appRoot)
: Path.GetFullPath(stateRootOverride);
var legacyStateDirectory = Path.Combine(legacyRoot, ".launcher", "state");
_legacyStatePath = Path.Combine(legacyStateDirectory, "oobe-state.json");
_legacyMarkerPath = Path.Combine(legacyStateDirectory, "first_run_completed");
_legacyStatePaths = BuildLegacyPaths("oobe-state.json");
_legacyMarkerPaths = BuildLegacyPaths("first_run_completed");
}
public OobeLaunchDecision Evaluate(CommandContext context)
@@ -47,10 +45,17 @@ internal sealed class OobeStateService
}
public OobeCompletionResult MarkCompleted(CommandContext context)
{
return MarkCompleted(context, null);
}
public OobeCompletionResult MarkCompleted(CommandContext context, string? stateRoot)
{
try
{
Directory.CreateDirectory(_stateDirectory);
var (stateDirectory, statePath) = BuildStatePaths(
string.IsNullOrWhiteSpace(stateRoot) ? ResolveCurrentStateRoot() : Path.GetFullPath(stateRoot));
Directory.CreateDirectory(stateDirectory);
var payload = new OobeStateFile
{
SchemaVersion = CurrentSchemaVersion,
@@ -60,14 +65,14 @@ internal sealed class OobeStateService
LaunchSource = context.LaunchSource
};
var tempPath = Path.Combine(_stateDirectory, $"oobe-state.{Guid.NewGuid():N}.tmp");
var tempPath = Path.Combine(stateDirectory, $"oobe-state.{Guid.NewGuid():N}.tmp");
var json = JsonSerializer.Serialize(payload, AppJsonContext.Default.OobeStateFile);
File.WriteAllText(tempPath, json);
File.Move(tempPath, _statePath, overwrite: true);
File.Move(tempPath, statePath, overwrite: true);
TryDeleteLegacyMarker();
Logger.Info(
$"OOBE completion persisted. LaunchSource='{context.LaunchSource}'; StatePath='{_statePath}'; " +
$"OOBE completion persisted. LaunchSource='{context.LaunchSource}'; StatePath='{statePath}'; " +
$"UserSid='{_executionSnapshot.UserSid ?? string.Empty}'.");
return new OobeCompletionResult
@@ -110,20 +115,27 @@ internal sealed class OobeStateService
return EvaluateStateFile(context, _statePath, migratedLegacyState: false);
}
if (File.Exists(_legacyStatePath))
foreach (var legacyStatePath in _legacyStatePaths)
{
return EvaluateStateFile(context, _legacyStatePath, migratedLegacyState: false);
if (File.Exists(legacyStatePath))
{
var decision = EvaluateStateFile(context, legacyStatePath, migratedLegacyState: true);
if (decision.Status == OobeStateStatus.Completed)
{
_ = MarkCompleted(context);
}
return decision;
}
}
if (File.Exists(_legacyMarkerPath))
foreach (var legacyMarkerPath in _legacyMarkerPaths)
{
migratedLegacyMarker = TryMigrateLegacyMarker(context);
return BuildDecision(context, OobeStateStatus.Completed, shouldShowOobe: false, usedLegacyMarker: true, migratedLegacyMarker: migratedLegacyMarker);
}
if (_executionSnapshot.IsElevated)
{
return BuildSuppressedDecision(context, "elevated", "oobe_suppressed_elevated");
if (File.Exists(legacyMarkerPath))
{
migratedLegacyMarker = TryMigrateLegacyMarker(context);
return BuildDecision(context, OobeStateStatus.Completed, shouldShowOobe: false, usedLegacyMarker: true, migratedLegacyMarker: migratedLegacyMarker);
}
}
if (string.Equals(context.LaunchSource, "postinstall", StringComparison.OrdinalIgnoreCase))
@@ -159,15 +171,18 @@ internal sealed class OobeStateService
private void TryDeleteLegacyMarker()
{
try
foreach (var legacyMarkerPath in _legacyMarkerPaths)
{
if (File.Exists(_legacyMarkerPath))
try
{
if (File.Exists(legacyMarkerPath))
{
File.Delete(legacyMarkerPath);
}
}
catch
{
File.Delete(_legacyMarkerPath);
}
}
catch
{
}
}
@@ -225,6 +240,44 @@ internal sealed class OobeStateService
};
}
private string ResolveCurrentStateRoot()
{
return _stateRootOverride ?? ResolveStateRoot(_appRoot);
}
private static (string StateDirectory, string StatePath) BuildStatePaths(string stateRoot)
{
var stateDirectory = Path.Combine(Path.GetFullPath(stateRoot), "Launcher", "state");
return (stateDirectory, Path.Combine(stateDirectory, "oobe-state.json"));
}
private IReadOnlyList<string> BuildLegacyPaths(string fileName)
{
var roots = new List<string>();
if (_stateRootOverride is not null)
{
roots.Add(_stateRootOverride);
}
else
{
roots.Add(ResolveDefaultSystemStateRoot());
roots.Add(_appRoot);
try
{
roots.Add(ResolveCurrentStateRoot());
}
catch
{
}
}
return roots
.Where(root => !string.IsNullOrWhiteSpace(root))
.Select(root => Path.Combine(Path.GetFullPath(root), ".launcher", "state", fileName))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();
}
private static string ResolveStateRoot(string appRoot)
{
try
@@ -243,4 +296,15 @@ internal sealed class OobeStateService
return Path.Combine(appData, "LanMountainDesktop");
}
}
private static string ResolveDefaultSystemStateRoot()
{
var appData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
if (string.IsNullOrWhiteSpace(appData))
{
return string.Empty;
}
return Path.Combine(appData, "LanMountainDesktop");
}
}

View File

@@ -7,14 +7,19 @@ internal sealed class WelcomeOobeStep : IOobeStep
{
private readonly CommandContext _context;
private readonly OobeStateService _oobeStateService;
private readonly DataLocationResolver _dataLocationResolver;
public WelcomeOobeStep(OobeStateService oobeStateService, CommandContext context)
public WelcomeOobeStep(
OobeStateService oobeStateService,
CommandContext context,
DataLocationResolver dataLocationResolver)
{
_oobeStateService = oobeStateService;
_context = context;
_dataLocationResolver = dataLocationResolver;
}
public async Task RunAsync(CancellationToken cancellationToken)
public async Task<OobeStepResult> RunAsync(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
@@ -27,16 +32,32 @@ internal sealed class WelcomeOobeStep : IOobeStep
if (window is null)
{
return;
return BuildCancelledResult("OOBE window could not be created.");
}
await window.WaitForEnterAsync().ConfigureAwait(false);
var completion = _oobeStateService.MarkCompleted(_context);
var draft = await window.WaitForCompletionAsync().ConfigureAwait(false);
if (draft is null)
{
Logger.Info("OOBE was cancelled before completion; Host launch will be skipped.");
return BuildCancelledResult("OOBE was cancelled before completion.");
}
var completion = new OobeSessionCommitService(
_dataLocationResolver,
_oobeStateService,
_context)
.Commit(draft);
if (!completion.Success)
{
Logger.Warn(
$"OOBE completion state was not persisted. ResultCode='{completion.ResultCode}'; " +
$"OOBE session was not persisted. ResultCode='{completion.ResultCode}'; " +
$"Error='{completion.ErrorMessage}'.");
return OobeStepResult.Complete(LaunchResultBuilder.Build(
false,
"oobe",
completion.ResultCode,
"OOBE settings could not be saved.",
errorMessage: completion.ErrorMessage));
}
await Dispatcher.UIThread.InvokeAsync(() =>
@@ -46,5 +67,16 @@ internal sealed class WelcomeOobeStep : IOobeStep
window.Close();
}
});
return OobeStepResult.Continue;
}
private static OobeStepResult BuildCancelledResult(string message)
{
return OobeStepResult.Complete(LaunchResultBuilder.Build(
false,
"oobe",
"oobe_cancelled",
message));
}
}