2026-04-21 20:59:52 +08:00
|
|
|
using System.IO.Compression;
|
|
|
|
|
using System.Text.Json;
|
|
|
|
|
using LanMountainDesktop.Launcher.Models;
|
|
|
|
|
|
|
|
|
|
namespace LanMountainDesktop.Launcher.Services;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 插件安装服务 - 简化版,不依赖 PluginSdk
|
|
|
|
|
/// </summary>
|
|
|
|
|
internal sealed class PluginInstallerService
|
|
|
|
|
{
|
Support .laapp/plugin.json and improve market models
Add support for the new plugin package contract (.laapp + plugin.json) while keeping backward compatibility with legacy .lmdp/manifest.json, and improve market metadata resolution and launcher handling.
Key changes:
- LanMountainDesktop.Launcher: PluginInstallerService now recognizes plugin.json and .laapp, preserves legacy manifest/package names, searches for manifests with a helper, and removes existing packages matching either extension.
- LanMountainDesktop.PluginTemplate: README updated to document .laapp, plugin.json, runtime contract and packaging expectations.
- Tests: New and extended tests for PluginInstallerService and a PluginMarketIndexDocumentTests covering nested index parsing and metadata enrichment.
- LauncherClient & PluginMarketInstallService: ResolveLauncherPath now probes multiple candidate locations (useful for dev and packaged layouts); LauncherClient also adjusted launcher arguments to use the updated CLI form.
- SettingsDomainServices: Added BuildCapabilities to safely build capability lists from entries (null checks, projection, de-dup via DistinctBy).
- AirAppMarketMetadataResolverService & PluginMarketModels: Prefer existing manifest/publication/compatibility values when enriching entries, add ApiVersion/Path fields, normalize compatibility logic and package source URL/path handling; handle Sha256/size/publication dates more robustly.
- Misc: Added localization spec/checklist/tasks under .trae for a localization fix initiative.
These changes enable the new plugin packaging format, improve robustness of market data enrichment, make launcher discovery more flexible for different environments, and add tests and docs to cover the new behaviors.
2026-04-30 00:02:52 +08:00
|
|
|
private const string ManifestFileName = "plugin.json";
|
|
|
|
|
private const string LegacyManifestFileName = "manifest.json";
|
|
|
|
|
private const string PackageFileExtension = ".laapp";
|
|
|
|
|
private const string LegacyPackageFileExtension = ".lmdp";
|
2026-04-21 20:59:52 +08:00
|
|
|
private const string RuntimeDirectoryName = "runtime";
|
|
|
|
|
|
|
|
|
|
private static readonly TimeSpan[] RetryDelays =
|
|
|
|
|
[
|
|
|
|
|
TimeSpan.FromMilliseconds(120),
|
|
|
|
|
TimeSpan.FromMilliseconds(250),
|
|
|
|
|
TimeSpan.FromMilliseconds(500)
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
public LauncherResult InstallPackage(string sourcePath, string pluginsDirectory)
|
|
|
|
|
{
|
|
|
|
|
var fullSourcePath = Path.GetFullPath(sourcePath);
|
|
|
|
|
var fullPluginsDirectory = Path.GetFullPath(pluginsDirectory);
|
|
|
|
|
|
|
|
|
|
if (!File.Exists(fullSourcePath))
|
|
|
|
|
{
|
|
|
|
|
throw new FileNotFoundException($"Plugin package '{fullSourcePath}' was not found.", fullSourcePath);
|
|
|
|
|
}
|
|
|
|
|
|
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.
2026-04-22 09:25:22 +08:00
|
|
|
if (TryBuildElevationRequiredResult(fullPluginsDirectory) is { } elevationRequiredResult)
|
|
|
|
|
{
|
|
|
|
|
return elevationRequiredResult;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-21 20:59:52 +08:00
|
|
|
var manifest = ReadManifestFromPackage(fullSourcePath);
|
|
|
|
|
Directory.CreateDirectory(fullPluginsDirectory);
|
|
|
|
|
var destinationPath = Path.Combine(fullPluginsDirectory, BuildInstalledPackageFileName(manifest.Id));
|
|
|
|
|
var stagingPath = destinationPath + ".incoming";
|
|
|
|
|
DeleteFileWithRetry(stagingPath);
|
|
|
|
|
CopyWithRetry(fullSourcePath, stagingPath, overwrite: true);
|
|
|
|
|
RemoveExistingPluginPackages(fullPluginsDirectory, manifest.Id, destinationPath, stagingPath);
|
|
|
|
|
MoveWithOverwriteRetry(stagingPath, destinationPath);
|
|
|
|
|
|
|
|
|
|
return new LauncherResult
|
|
|
|
|
{
|
|
|
|
|
Success = true,
|
|
|
|
|
Stage = "plugin.install",
|
|
|
|
|
Code = "ok",
|
|
|
|
|
Message = "Plugin installed.",
|
|
|
|
|
InstalledPackagePath = destinationPath,
|
|
|
|
|
ManifestId = manifest.Id,
|
|
|
|
|
ManifestName = manifest.Name
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
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.
2026-04-22 09:25:22 +08:00
|
|
|
private static LauncherResult? TryBuildElevationRequiredResult(string pluginsDirectory)
|
|
|
|
|
{
|
|
|
|
|
if (!OperatingSystem.IsWindows())
|
|
|
|
|
{
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
Launcher fix (#6)
* fix.hy3试图修复中
* Resolve dev paths and fix splash UI thread
Compute a solutionRoot and expand development search paths (LanMountainDesktop and dev-test) in DeploymentLocator, add logging when scanning/finding hosts, and return distinct full paths. Ensure backward-compatible path checks. Fix cross-thread UI calls: invoke splashWindow.DismissAsync on the UI thread in LauncherFlowCoordinator, and make SplashWindow.DismissAsync ensure it runs on the UI thread before closing (simplified Close call). These changes improve development host discovery and prevent UI-thread access issues during shutdown.
* Add configurable data location (portable/system)
Introduce support for choosing and resolving the application's data root (system user dir vs. portable app folder). Adds DataLocationConfig model, DataLocationResolver (load/save/resolve/migrate), a UI prompt (DataLocationPromptWindow) and an OOBE step (DataLocationOobeStep) to let users pick and optionally migrate existing data. Wire the chosen data root into the launcher flow and host launch plan (forwarded via --data-root and LMD_DATA_ROOT), and add AppDataPathProvider to let runtime services read the effective data root (initialized in Program.Main). Update various services (logging, settings, DB, plugin/market, startup registry, etc.) to use the new provider/resolver and register the config type in the JSON context. This enables portable installs, safe migration, and runtime overrides via CLI or environment variable.
* Add dev/debug startup flow and launch profiles
Handle design-time initialization and add a developer debug startup path: App now skips normal startup when in design mode and shows a DevDebugWindow when running in debug (unless a preview or apply-update command). CommandContext.IsDebugMode is extended to include DOTNET_ENVIRONMENT=Development via a new IsDevelopmentEnvironment helper. Program.Main and BuildAvaloniaApp are made public to aid tooling. Added multiple launchSettings profiles for debug and preview commands that set DOTNET_ENVIRONMENT=Development to simplify IDE debugging and UI previewing.
* Simplify splash to fade; add themed about banners
Simplify splash startup visuals by removing the multi-mode/slide behavior and always using a fade animation. Update App to create SplashWindow without a StartupVisualMode parameter and remove related fields, layout configuration, slide animation, and easing helpers from SplashWindow. Clean up unused using. Replace the single about_banner asset with theme-aware variants (about_banner_dark.png and about_banner_light.png), delete the old about_banner.png, and update AboutSettingsPage to use a DynamicResource ImageBrush (AboutBannerBrush) that selects the appropriate banner per theme.
* Use AppJsonContext for startup state serialization
Switch serialization to the source-generated System.Text.Json context: add JsonSerializable(typeof(StartupAttemptRecord)) to AppJsonContext and replace the previous JsonSerializerOptions-based Serialize/Deserialize calls with AppJsonContext.Default.StartupAttemptRecord. Also remove the now-unused SerializerOptions field. Additionally, update .gitignore to exclude /test-aot-publish.
* Add OOBE redesign, theme & data location support
Introduce a redesigned OOBE flow and data-location/theme support across the launcher. Adds a new ThemeService for applying light/dark and accent colors; integrates FluentIcons.Avalonia package for icons. Overhauls OobeWindow (UX animations, typing effect, multi-step theme and data-location pages, Monet options, and final welcome step) and its code-behind to handle step navigation, accent selection, and data-location resolution. Adds DataLocation UI and handlers (DataLocationPromptWindow changes, DataLocation resolver usage) and wires a DevDebug UI for toggling/opening the data-location page. UpdateEngineService now resolves the launcher root via DataLocationResolver. Misc: update various view models, localization entries and remove TrimmerRoots.xml.
* Refactor data location paths and add background service
Refactor DataLocationResolver to centralize data path resolution (ResolveLauncherDataPath, ResolveDesktopDataPath, ResolveConfigPath, ResolveLauncherLogsPath, ResolveLauncherStatePath) and replace usages of the previous ".launcher" layout with a "Launcher" folder. Update API: LoadConfig/SaveConfig reorganized and ApplyLocationChoice now accepts an optional custom path and migration flag; migration logic updated accordingly. Update dependent services and views (Logger, DeploymentLocator, UpdateEngineService, OobeStateService, StartupAttemptRegistry, LauncherDebugSettingsStore, OobeWindow) to use the new resolver APIs and paths. Add LauncherBackgroundService to load/validate/cache a custom splash background image and wire it into SplashWindow (AXAML/Axaml.cs) with UI placeholders and overlay. Misc: minor cleanup of Oobe/Splash XAML and related code adjustments and logging improvements.
2026-04-25 18:41:26 +08:00
|
|
|
string? allowedRoot = null;
|
|
|
|
|
try
|
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.
2026-04-22 09:25:22 +08:00
|
|
|
{
|
Launcher fix (#6)
* fix.hy3试图修复中
* Resolve dev paths and fix splash UI thread
Compute a solutionRoot and expand development search paths (LanMountainDesktop and dev-test) in DeploymentLocator, add logging when scanning/finding hosts, and return distinct full paths. Ensure backward-compatible path checks. Fix cross-thread UI calls: invoke splashWindow.DismissAsync on the UI thread in LauncherFlowCoordinator, and make SplashWindow.DismissAsync ensure it runs on the UI thread before closing (simplified Close call). These changes improve development host discovery and prevent UI-thread access issues during shutdown.
* Add configurable data location (portable/system)
Introduce support for choosing and resolving the application's data root (system user dir vs. portable app folder). Adds DataLocationConfig model, DataLocationResolver (load/save/resolve/migrate), a UI prompt (DataLocationPromptWindow) and an OOBE step (DataLocationOobeStep) to let users pick and optionally migrate existing data. Wire the chosen data root into the launcher flow and host launch plan (forwarded via --data-root and LMD_DATA_ROOT), and add AppDataPathProvider to let runtime services read the effective data root (initialized in Program.Main). Update various services (logging, settings, DB, plugin/market, startup registry, etc.) to use the new provider/resolver and register the config type in the JSON context. This enables portable installs, safe migration, and runtime overrides via CLI or environment variable.
* Add dev/debug startup flow and launch profiles
Handle design-time initialization and add a developer debug startup path: App now skips normal startup when in design mode and shows a DevDebugWindow when running in debug (unless a preview or apply-update command). CommandContext.IsDebugMode is extended to include DOTNET_ENVIRONMENT=Development via a new IsDevelopmentEnvironment helper. Program.Main and BuildAvaloniaApp are made public to aid tooling. Added multiple launchSettings profiles for debug and preview commands that set DOTNET_ENVIRONMENT=Development to simplify IDE debugging and UI previewing.
* Simplify splash to fade; add themed about banners
Simplify splash startup visuals by removing the multi-mode/slide behavior and always using a fade animation. Update App to create SplashWindow without a StartupVisualMode parameter and remove related fields, layout configuration, slide animation, and easing helpers from SplashWindow. Clean up unused using. Replace the single about_banner asset with theme-aware variants (about_banner_dark.png and about_banner_light.png), delete the old about_banner.png, and update AboutSettingsPage to use a DynamicResource ImageBrush (AboutBannerBrush) that selects the appropriate banner per theme.
* Use AppJsonContext for startup state serialization
Switch serialization to the source-generated System.Text.Json context: add JsonSerializable(typeof(StartupAttemptRecord)) to AppJsonContext and replace the previous JsonSerializerOptions-based Serialize/Deserialize calls with AppJsonContext.Default.StartupAttemptRecord. Also remove the now-unused SerializerOptions field. Additionally, update .gitignore to exclude /test-aot-publish.
* Add OOBE redesign, theme & data location support
Introduce a redesigned OOBE flow and data-location/theme support across the launcher. Adds a new ThemeService for applying light/dark and accent colors; integrates FluentIcons.Avalonia package for icons. Overhauls OobeWindow (UX animations, typing effect, multi-step theme and data-location pages, Monet options, and final welcome step) and its code-behind to handle step navigation, accent selection, and data-location resolution. Adds DataLocation UI and handlers (DataLocationPromptWindow changes, DataLocation resolver usage) and wires a DevDebug UI for toggling/opening the data-location page. UpdateEngineService now resolves the launcher root via DataLocationResolver. Misc: update various view models, localization entries and remove TrimmerRoots.xml.
* Refactor data location paths and add background service
Refactor DataLocationResolver to centralize data path resolution (ResolveLauncherDataPath, ResolveDesktopDataPath, ResolveConfigPath, ResolveLauncherLogsPath, ResolveLauncherStatePath) and replace usages of the previous ".launcher" layout with a "Launcher" folder. Update API: LoadConfig/SaveConfig reorganized and ApplyLocationChoice now accepts an optional custom path and migration flag; migration logic updated accordingly. Update dependent services and views (Logger, DeploymentLocator, UpdateEngineService, OobeStateService, StartupAttemptRegistry, LauncherDebugSettingsStore, OobeWindow) to use the new resolver APIs and paths. Add LauncherBackgroundService to load/validate/cache a custom splash background image and wire it into SplashWindow (AXAML/Axaml.cs) with UI placeholders and overlay. Misc: minor cleanup of Oobe/Splash XAML and related code adjustments and logging improvements.
2026-04-25 18:41:26 +08:00
|
|
|
var appRoot = Commands.ResolveAppRoot(CommandContext.FromArgs([]));
|
|
|
|
|
var resolver = new DataLocationResolver(appRoot);
|
|
|
|
|
allowedRoot = EnsureTrailingSeparator(resolver.ResolveDataRoot());
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(allowedRoot))
|
|
|
|
|
{
|
|
|
|
|
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
|
|
|
|
if (string.IsNullOrWhiteSpace(localAppData))
|
|
|
|
|
{
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
allowedRoot = EnsureTrailingSeparator(Path.Combine(Path.GetFullPath(localAppData), "LanMountainDesktop"));
|
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.
2026-04-22 09:25:22 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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"
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-21 20:59:52 +08:00
|
|
|
public PluginManifest ReadManifestFromPackage(string packagePath)
|
|
|
|
|
{
|
|
|
|
|
using var archive = ZipFile.OpenRead(packagePath);
|
Support .laapp/plugin.json and improve market models
Add support for the new plugin package contract (.laapp + plugin.json) while keeping backward compatibility with legacy .lmdp/manifest.json, and improve market metadata resolution and launcher handling.
Key changes:
- LanMountainDesktop.Launcher: PluginInstallerService now recognizes plugin.json and .laapp, preserves legacy manifest/package names, searches for manifests with a helper, and removes existing packages matching either extension.
- LanMountainDesktop.PluginTemplate: README updated to document .laapp, plugin.json, runtime contract and packaging expectations.
- Tests: New and extended tests for PluginInstallerService and a PluginMarketIndexDocumentTests covering nested index parsing and metadata enrichment.
- LauncherClient & PluginMarketInstallService: ResolveLauncherPath now probes multiple candidate locations (useful for dev and packaged layouts); LauncherClient also adjusted launcher arguments to use the updated CLI form.
- SettingsDomainServices: Added BuildCapabilities to safely build capability lists from entries (null checks, projection, de-dup via DistinctBy).
- AirAppMarketMetadataResolverService & PluginMarketModels: Prefer existing manifest/publication/compatibility values when enriching entries, add ApiVersion/Path fields, normalize compatibility logic and package source URL/path handling; handle Sha256/size/publication dates more robustly.
- Misc: Added localization spec/checklist/tasks under .trae for a localization fix initiative.
These changes enable the new plugin packaging format, improve robustness of market data enrichment, make launcher discovery more flexible for different environments, and add tests and docs to cover the new behaviors.
2026-04-30 00:02:52 +08:00
|
|
|
var entries = FindManifestEntries(archive, ManifestFileName);
|
|
|
|
|
if (entries.Length == 0)
|
|
|
|
|
{
|
|
|
|
|
entries = FindManifestEntries(archive, LegacyManifestFileName);
|
|
|
|
|
}
|
2026-04-21 20:59:52 +08:00
|
|
|
|
|
|
|
|
if (entries.Length == 0)
|
|
|
|
|
{
|
|
|
|
|
throw new InvalidOperationException(
|
Support .laapp/plugin.json and improve market models
Add support for the new plugin package contract (.laapp + plugin.json) while keeping backward compatibility with legacy .lmdp/manifest.json, and improve market metadata resolution and launcher handling.
Key changes:
- LanMountainDesktop.Launcher: PluginInstallerService now recognizes plugin.json and .laapp, preserves legacy manifest/package names, searches for manifests with a helper, and removes existing packages matching either extension.
- LanMountainDesktop.PluginTemplate: README updated to document .laapp, plugin.json, runtime contract and packaging expectations.
- Tests: New and extended tests for PluginInstallerService and a PluginMarketIndexDocumentTests covering nested index parsing and metadata enrichment.
- LauncherClient & PluginMarketInstallService: ResolveLauncherPath now probes multiple candidate locations (useful for dev and packaged layouts); LauncherClient also adjusted launcher arguments to use the updated CLI form.
- SettingsDomainServices: Added BuildCapabilities to safely build capability lists from entries (null checks, projection, de-dup via DistinctBy).
- AirAppMarketMetadataResolverService & PluginMarketModels: Prefer existing manifest/publication/compatibility values when enriching entries, add ApiVersion/Path fields, normalize compatibility logic and package source URL/path handling; handle Sha256/size/publication dates more robustly.
- Misc: Added localization spec/checklist/tasks under .trae for a localization fix initiative.
These changes enable the new plugin packaging format, improve robustness of market data enrichment, make launcher discovery more flexible for different environments, and add tests and docs to cover the new behaviors.
2026-04-30 00:02:52 +08:00
|
|
|
$"Plugin package '{packagePath}' does not contain '{ManifestFileName}' or '{LegacyManifestFileName}'.");
|
2026-04-21 20:59:52 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (entries.Length > 1)
|
|
|
|
|
{
|
|
|
|
|
throw new InvalidOperationException(
|
|
|
|
|
$"Plugin package '{packagePath}' contains multiple '{ManifestFileName}' files.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
using var stream = entries[0].Open();
|
|
|
|
|
using var reader = new StreamReader(stream);
|
|
|
|
|
var json = reader.ReadToEnd();
|
|
|
|
|
var manifest = JsonSerializer.Deserialize(json, AppJsonContext.Default.PluginManifest);
|
|
|
|
|
if (manifest == null)
|
|
|
|
|
{
|
|
|
|
|
throw new InvalidOperationException($"Failed to deserialize manifest from '{packagePath}'.");
|
|
|
|
|
}
|
|
|
|
|
return manifest;
|
|
|
|
|
}
|
|
|
|
|
|
Support .laapp/plugin.json and improve market models
Add support for the new plugin package contract (.laapp + plugin.json) while keeping backward compatibility with legacy .lmdp/manifest.json, and improve market metadata resolution and launcher handling.
Key changes:
- LanMountainDesktop.Launcher: PluginInstallerService now recognizes plugin.json and .laapp, preserves legacy manifest/package names, searches for manifests with a helper, and removes existing packages matching either extension.
- LanMountainDesktop.PluginTemplate: README updated to document .laapp, plugin.json, runtime contract and packaging expectations.
- Tests: New and extended tests for PluginInstallerService and a PluginMarketIndexDocumentTests covering nested index parsing and metadata enrichment.
- LauncherClient & PluginMarketInstallService: ResolveLauncherPath now probes multiple candidate locations (useful for dev and packaged layouts); LauncherClient also adjusted launcher arguments to use the updated CLI form.
- SettingsDomainServices: Added BuildCapabilities to safely build capability lists from entries (null checks, projection, de-dup via DistinctBy).
- AirAppMarketMetadataResolverService & PluginMarketModels: Prefer existing manifest/publication/compatibility values when enriching entries, add ApiVersion/Path fields, normalize compatibility logic and package source URL/path handling; handle Sha256/size/publication dates more robustly.
- Misc: Added localization spec/checklist/tasks under .trae for a localization fix initiative.
These changes enable the new plugin packaging format, improve robustness of market data enrichment, make launcher discovery more flexible for different environments, and add tests and docs to cover the new behaviors.
2026-04-30 00:02:52 +08:00
|
|
|
private static ZipArchiveEntry[] FindManifestEntries(ZipArchive archive, string manifestFileName)
|
|
|
|
|
{
|
|
|
|
|
return archive.Entries
|
|
|
|
|
.Where(entry => string.Equals(entry.Name, manifestFileName, StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
.ToArray();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-21 20:59:52 +08:00
|
|
|
private void RemoveExistingPluginPackages(string pluginsDirectory, string pluginId, string destinationPath, string stagingPath)
|
|
|
|
|
{
|
|
|
|
|
var runtimeRootDirectory = EnsureTrailingSeparator(Path.Combine(Path.GetFullPath(pluginsDirectory), RuntimeDirectoryName));
|
|
|
|
|
var pendingDeletionDir = Path.Combine(pluginsDirectory, ".pending-deletions");
|
|
|
|
|
Directory.CreateDirectory(pendingDeletionDir);
|
|
|
|
|
|
|
|
|
|
foreach (var existingPackagePath in Directory
|
Support .laapp/plugin.json and improve market models
Add support for the new plugin package contract (.laapp + plugin.json) while keeping backward compatibility with legacy .lmdp/manifest.json, and improve market metadata resolution and launcher handling.
Key changes:
- LanMountainDesktop.Launcher: PluginInstallerService now recognizes plugin.json and .laapp, preserves legacy manifest/package names, searches for manifests with a helper, and removes existing packages matching either extension.
- LanMountainDesktop.PluginTemplate: README updated to document .laapp, plugin.json, runtime contract and packaging expectations.
- Tests: New and extended tests for PluginInstallerService and a PluginMarketIndexDocumentTests covering nested index parsing and metadata enrichment.
- LauncherClient & PluginMarketInstallService: ResolveLauncherPath now probes multiple candidate locations (useful for dev and packaged layouts); LauncherClient also adjusted launcher arguments to use the updated CLI form.
- SettingsDomainServices: Added BuildCapabilities to safely build capability lists from entries (null checks, projection, de-dup via DistinctBy).
- AirAppMarketMetadataResolverService & PluginMarketModels: Prefer existing manifest/publication/compatibility values when enriching entries, add ApiVersion/Path fields, normalize compatibility logic and package source URL/path handling; handle Sha256/size/publication dates more robustly.
- Misc: Added localization spec/checklist/tasks under .trae for a localization fix initiative.
These changes enable the new plugin packaging format, improve robustness of market data enrichment, make launcher discovery more flexible for different environments, and add tests and docs to cover the new behaviors.
2026-04-30 00:02:52 +08:00
|
|
|
.EnumerateFiles(pluginsDirectory, "*", SearchOption.AllDirectories)
|
2026-04-21 20:59:52 +08:00
|
|
|
.Select(Path.GetFullPath)
|
Support .laapp/plugin.json and improve market models
Add support for the new plugin package contract (.laapp + plugin.json) while keeping backward compatibility with legacy .lmdp/manifest.json, and improve market metadata resolution and launcher handling.
Key changes:
- LanMountainDesktop.Launcher: PluginInstallerService now recognizes plugin.json and .laapp, preserves legacy manifest/package names, searches for manifests with a helper, and removes existing packages matching either extension.
- LanMountainDesktop.PluginTemplate: README updated to document .laapp, plugin.json, runtime contract and packaging expectations.
- Tests: New and extended tests for PluginInstallerService and a PluginMarketIndexDocumentTests covering nested index parsing and metadata enrichment.
- LauncherClient & PluginMarketInstallService: ResolveLauncherPath now probes multiple candidate locations (useful for dev and packaged layouts); LauncherClient also adjusted launcher arguments to use the updated CLI form.
- SettingsDomainServices: Added BuildCapabilities to safely build capability lists from entries (null checks, projection, de-dup via DistinctBy).
- AirAppMarketMetadataResolverService & PluginMarketModels: Prefer existing manifest/publication/compatibility values when enriching entries, add ApiVersion/Path fields, normalize compatibility logic and package source URL/path handling; handle Sha256/size/publication dates more robustly.
- Misc: Added localization spec/checklist/tasks under .trae for a localization fix initiative.
These changes enable the new plugin packaging format, improve robustness of market data enrichment, make launcher discovery more flexible for different environments, and add tests and docs to cover the new behaviors.
2026-04-30 00:02:52 +08:00
|
|
|
.Where(path =>
|
|
|
|
|
path.EndsWith(PackageFileExtension, StringComparison.OrdinalIgnoreCase) ||
|
|
|
|
|
path.EndsWith(LegacyPackageFileExtension, StringComparison.OrdinalIgnoreCase))
|
2026-04-21 20:59:52 +08:00
|
|
|
.Where(path => !path.StartsWith(runtimeRootDirectory, StringComparison.OrdinalIgnoreCase)))
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
if (string.Equals(existingPackagePath, Path.GetFullPath(destinationPath), StringComparison.OrdinalIgnoreCase) ||
|
|
|
|
|
string.Equals(existingPackagePath, Path.GetFullPath(stagingPath), StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var existingManifest = ReadManifestFromPackage(existingPackagePath);
|
|
|
|
|
if (!string.Equals(existingManifest.Id, pluginId, StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TryRemoveExistingPackage(existingPackagePath, pendingDeletionDir);
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
CleanupPendingDeletions(pendingDeletionDir);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void TryRemoveExistingPackage(string existingPackagePath, string pendingDeletionDir)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
DeleteFileWithRetry(existingPackagePath);
|
|
|
|
|
}
|
|
|
|
|
catch (IOException)
|
|
|
|
|
{
|
|
|
|
|
var fileName = Path.GetFileName(existingPackagePath);
|
|
|
|
|
var pendingPath = Path.Combine(pendingDeletionDir, $"{fileName}.{Guid.NewGuid():N}.pending");
|
|
|
|
|
File.Move(existingPackagePath, pendingPath);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static void CleanupPendingDeletions(string pendingDeletionDir)
|
|
|
|
|
{
|
|
|
|
|
if (!Directory.Exists(pendingDeletionDir))
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach (var pendingFile in Directory.EnumerateFiles(pendingDeletionDir, "*.pending"))
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
File.Delete(pendingFile);
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static void CopyWithRetry(string sourcePath, string destinationPath, bool overwrite)
|
|
|
|
|
{
|
|
|
|
|
Retry(() => File.Copy(sourcePath, destinationPath, overwrite));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static void MoveWithOverwriteRetry(string sourcePath, string destinationPath)
|
|
|
|
|
{
|
|
|
|
|
Retry(() => File.Move(sourcePath, destinationPath, overwrite: true));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static void DeleteFileWithRetry(string filePath)
|
|
|
|
|
{
|
|
|
|
|
Retry(() =>
|
|
|
|
|
{
|
|
|
|
|
if (File.Exists(filePath))
|
|
|
|
|
{
|
|
|
|
|
File.Delete(filePath);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static void Retry(Action action)
|
|
|
|
|
{
|
|
|
|
|
Exception? lastException = null;
|
|
|
|
|
for (var attempt = 0; attempt <= RetryDelays.Length; attempt++)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
action();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
|
|
|
|
{
|
|
|
|
|
lastException = ex;
|
|
|
|
|
if (attempt >= RetryDelays.Length)
|
|
|
|
|
{
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Thread.Sleep(RetryDelays[attempt]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (lastException is not null)
|
|
|
|
|
{
|
|
|
|
|
throw lastException;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string BuildInstalledPackageFileName(string pluginId)
|
|
|
|
|
{
|
|
|
|
|
var invalidChars = Path.GetInvalidFileNameChars();
|
|
|
|
|
var fileName = new string(pluginId.Select(ch => invalidChars.Contains(ch) ? '_' : ch).ToArray());
|
|
|
|
|
return fileName + PackageFileExtension;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string EnsureTrailingSeparator(string path)
|
|
|
|
|
{
|
|
|
|
|
return path.EndsWith(Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal)
|
|
|
|
|
? path
|
|
|
|
|
: path + Path.DirectorySeparatorChar;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 简化的插件清单模型
|
|
|
|
|
/// </summary>
|
|
|
|
|
public class PluginManifest
|
|
|
|
|
{
|
|
|
|
|
public string Id { get; set; } = "";
|
|
|
|
|
public string Name { get; set; } = "";
|
|
|
|
|
public string Version { get; set; } = "";
|
|
|
|
|
public string? Description { get; set; }
|
|
|
|
|
public string? Author { get; set; }
|
|
|
|
|
}
|