mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-22 17:24:27 +08:00
fix.开发者调试工具设置无法正常持久化的问题。修复了插件无法进行更新的问题。
This commit is contained in:
@@ -784,12 +784,28 @@ internal sealed class PluginMarketEmbeddedView : UserControl, IDisposable
|
||||
}
|
||||
|
||||
RefreshInstalledSnapshot();
|
||||
SetStatus(
|
||||
F(
|
||||
"market.status.install_success_format",
|
||||
"Plugin '{0}' has been staged. Restart the app to apply it.",
|
||||
result.Manifest.Name),
|
||||
SuccessBrush);
|
||||
|
||||
if (result.RestartRequired)
|
||||
{
|
||||
SetStatus(
|
||||
F(
|
||||
"market.status.upgrade_staged_format",
|
||||
"Plugin '{0}' v{1} has been downloaded. Restart to complete the upgrade.",
|
||||
result.Manifest.Name,
|
||||
result.Manifest.Version),
|
||||
WarningBrush);
|
||||
PendingRestartStateService.SetPending(PendingRestartStateService.PluginCatalogReason, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
SetStatus(
|
||||
F(
|
||||
"market.status.install_success_format",
|
||||
"Plugin '{0}' has been installed successfully.",
|
||||
result.Manifest.Name),
|
||||
SuccessBrush);
|
||||
}
|
||||
|
||||
RebuildSurface();
|
||||
}
|
||||
catch (OperationCanceledException) when (_lifetimeCts.IsCancellationRequested)
|
||||
@@ -1015,14 +1031,22 @@ internal sealed class PluginMarketEmbeddedView : UserControl, IDisposable
|
||||
|
||||
private static int CompareVersions(string? left, string? right)
|
||||
{
|
||||
if (!AirAppMarketIndexDocument.TryParseVersion(left, out var leftVersion))
|
||||
var leftParsed = AirAppMarketIndexDocument.TryParseVersion(left, out var leftVersion);
|
||||
var rightParsed = AirAppMarketIndexDocument.TryParseVersion(right, out var rightVersion);
|
||||
|
||||
if (!leftParsed && !rightParsed)
|
||||
{
|
||||
leftVersion = new Version(0, 0, 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!AirAppMarketIndexDocument.TryParseVersion(right, out var rightVersion))
|
||||
if (!leftParsed)
|
||||
{
|
||||
rightVersion = new Version(0, 0, 0);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!rightParsed)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
return (leftVersion ?? new Version(0, 0, 0)).CompareTo(rightVersion ?? new Version(0, 0, 0));
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Security.Cryptography;
|
||||
@@ -20,7 +21,9 @@ internal sealed class AirAppMarketInstallService : IDisposable
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly ResumableDownloadService _downloadService;
|
||||
private readonly AirAppMarketReleaseResolverService _releaseResolverService;
|
||||
private readonly PendingPluginUpgradeService _pendingUpgradeService;
|
||||
private readonly string _downloadsDirectory;
|
||||
private readonly Version? _hostVersion;
|
||||
|
||||
public AirAppMarketInstallService(PluginRuntimeService runtime, string dataDirectory)
|
||||
{
|
||||
@@ -33,6 +36,8 @@ internal sealed class AirAppMarketInstallService : IDisposable
|
||||
_httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("LanMountainDesktop-PluginMarketplace/1.0");
|
||||
_downloadService = new ResumableDownloadService(_httpClient);
|
||||
_releaseResolverService = new AirAppMarketReleaseResolverService(_httpClient);
|
||||
_pendingUpgradeService = new PendingPluginUpgradeService(runtime.PluginsDirectory);
|
||||
_hostVersion = typeof(App).Assembly.GetName().Version;
|
||||
}
|
||||
|
||||
public async Task<AirAppMarketInstallResult> InstallAsync(
|
||||
@@ -41,18 +46,6 @@ internal sealed class AirAppMarketInstallService : IDisposable
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(plugin);
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
var helperPath = ResolveHelperPath();
|
||||
if (!File.Exists(helperPath))
|
||||
{
|
||||
return new AirAppMarketInstallResult(
|
||||
false,
|
||||
null,
|
||||
$"Plugins install helper was not found at '{helperPath}'.");
|
||||
}
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(_downloadsDirectory);
|
||||
var sources = plugin.GetPackageSourcesInInstallOrder();
|
||||
if (sources.Count == 0)
|
||||
@@ -67,6 +60,39 @@ internal sealed class AirAppMarketInstallService : IDisposable
|
||||
"PluginMarket",
|
||||
$"Starting install. PluginId='{plugin.Id}'; Version='{plugin.Version}'; Sources='{string.Join(", ", sources.Select(source => source.SourceKind.ToString()))}'.");
|
||||
|
||||
var compatibilityError = ValidateCompatibility(plugin);
|
||||
if (!string.IsNullOrWhiteSpace(compatibilityError))
|
||||
{
|
||||
AppLogger.Warn("PluginMarket", $"Compatibility check failed. PluginId='{plugin.Id}'; Error='{compatibilityError}'.");
|
||||
return new AirAppMarketInstallResult(false, null, compatibilityError);
|
||||
}
|
||||
|
||||
var isUpgrade = IsPluginInstalled(plugin.Id);
|
||||
if (isUpgrade)
|
||||
{
|
||||
return await InstallUpgradeAsync(plugin, sources, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return await InstallNewAsync(plugin, sources, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<AirAppMarketInstallResult> InstallNewAsync(
|
||||
AirAppMarketPluginEntry plugin,
|
||||
IReadOnlyList<AirAppMarketPluginPackageSourceEntry> sources,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
var helperPath = ResolveHelperPath();
|
||||
if (!File.Exists(helperPath))
|
||||
{
|
||||
return new AirAppMarketInstallResult(
|
||||
false,
|
||||
null,
|
||||
$"Plugins install helper was not found at '{helperPath}'.");
|
||||
}
|
||||
}
|
||||
|
||||
var sourceErrors = new List<string>();
|
||||
foreach (var source in sources)
|
||||
{
|
||||
@@ -93,6 +119,88 @@ internal sealed class AirAppMarketInstallService : IDisposable
|
||||
return new AirAppMarketInstallResult(false, null, combinedMessage);
|
||||
}
|
||||
|
||||
private async Task<AirAppMarketInstallResult> InstallUpgradeAsync(
|
||||
AirAppMarketPluginEntry plugin,
|
||||
IReadOnlyList<AirAppMarketPluginPackageSourceEntry> sources,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
AppLogger.Info("PluginMarket", $"Detected upgrade scenario. Downloading package for deferred upgrade. PluginId='{plugin.Id}'.");
|
||||
|
||||
foreach (var source in sources)
|
||||
{
|
||||
var downloadResult = await DownloadPackageAsync(plugin, source, cancellationToken).ConfigureAwait(false);
|
||||
if (downloadResult.Success && !string.IsNullOrWhiteSpace(downloadResult.PackagePath))
|
||||
{
|
||||
_pendingUpgradeService.AddPendingUpgrade(plugin.Id, downloadResult.PackagePath, plugin.Version);
|
||||
|
||||
AppLogger.Info(
|
||||
"PluginMarket",
|
||||
$"Upgrade staged for next restart. PluginId='{plugin.Id}'; Version='{plugin.Version}'; PackagePath='{downloadResult.PackagePath}'.");
|
||||
|
||||
var manifest = ReadManifestFromPackage(downloadResult.PackagePath);
|
||||
return new AirAppMarketInstallResult(true, manifest, null, RestartRequired: true);
|
||||
}
|
||||
}
|
||||
|
||||
return new AirAppMarketInstallResult(
|
||||
false,
|
||||
null,
|
||||
$"Failed to download upgrade package for plugin '{plugin.Id}' from all available sources.");
|
||||
}
|
||||
|
||||
private bool IsPluginInstalled(string pluginId)
|
||||
{
|
||||
return _runtime.Catalog.Any(entry =>
|
||||
string.Equals(entry.Manifest.Id, pluginId, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private string? ValidateCompatibility(AirAppMarketPluginEntry plugin)
|
||||
{
|
||||
if (_hostVersion is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(plugin.MinHostVersion))
|
||||
{
|
||||
if (!AirAppMarketIndexDocument.TryParseVersion(plugin.MinHostVersion, out var minHostVersion) ||
|
||||
minHostVersion is null)
|
||||
{
|
||||
return $"Plugin '{plugin.Id}' declares invalid minimum host version '{plugin.MinHostVersion}'.";
|
||||
}
|
||||
|
||||
if (_hostVersion < minHostVersion)
|
||||
{
|
||||
return $"Plugin '{plugin.Id}' requires host version {plugin.MinHostVersion} or newer. Current host version is {_hostVersion}.";
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(plugin.ApiVersion))
|
||||
{
|
||||
if (!AirAppMarketIndexDocument.TryParseVersion(plugin.ApiVersion, out var pluginApiVersion) ||
|
||||
pluginApiVersion is null)
|
||||
{
|
||||
return $"Plugin '{plugin.Id}' declares invalid API version '{plugin.ApiVersion}'.";
|
||||
}
|
||||
|
||||
var hostApiVersion = PluginSdkInfo.ApiVersion;
|
||||
if (hostApiVersion is not null)
|
||||
{
|
||||
if (!AirAppMarketIndexDocument.TryParseVersion(hostApiVersion, out var hostApiVersionParsed) ||
|
||||
hostApiVersionParsed is null)
|
||||
{
|
||||
AppLogger.Warn("PluginMarket", $"Host API version '{hostApiVersion}' could not be parsed. Skipping API version check.");
|
||||
}
|
||||
else if (pluginApiVersion.Major != hostApiVersionParsed.Major)
|
||||
{
|
||||
return $"Plugin '{plugin.Id}' uses incompatible API version {plugin.ApiVersion}. Host API version is {hostApiVersion}. Major version must match.";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<AirAppMarketInstallAttemptResult> TryInstallFromSourceAsync(
|
||||
AirAppMarketPluginEntry plugin,
|
||||
AirAppMarketPluginPackageSourceEntry source,
|
||||
@@ -275,6 +383,71 @@ internal sealed class AirAppMarketInstallService : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<DownloadPackageResult> DownloadPackageAsync(
|
||||
AirAppMarketPluginEntry plugin,
|
||||
AirAppMarketPluginPackageSourceEntry source,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var packagePath = Path.Combine(
|
||||
_downloadsDirectory,
|
||||
$"{SanitizeFileName(plugin.Id)}-{SanitizeFileName(plugin.Version)}-{SanitizeFileName(source.SourceKind.ToString())}-{Guid.NewGuid():N}.laapp");
|
||||
|
||||
try
|
||||
{
|
||||
var resolvedDownloadUrl = await _releaseResolverService.ResolveDownloadUrlAsync(plugin, source, cancellationToken).ConfigureAwait(false);
|
||||
AppLogger.Info(
|
||||
"PluginMarket",
|
||||
$"Downloading upgrade package for '{plugin.Id}' from '{resolvedDownloadUrl}'.");
|
||||
|
||||
var acquireResult = await AcquirePackageAsync(plugin, source, resolvedDownloadUrl, packagePath, cancellationToken).ConfigureAwait(false);
|
||||
if (!acquireResult.Success)
|
||||
{
|
||||
TryDeleteFile(packagePath);
|
||||
return new DownloadPackageResult(false, null, acquireResult.ErrorMessage);
|
||||
}
|
||||
|
||||
var verificationResult = await VerifyPackageAsync(plugin, packagePath, cancellationToken).ConfigureAwait(false);
|
||||
if (!verificationResult.Success)
|
||||
{
|
||||
TryDeleteFile(packagePath);
|
||||
return new DownloadPackageResult(false, null, verificationResult.ErrorMessage);
|
||||
}
|
||||
|
||||
return new DownloadPackageResult(true, packagePath, null);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
TryDeleteFile(packagePath);
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
TryDeleteFile(packagePath);
|
||||
return new DownloadPackageResult(false, null, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private static PluginManifest ReadManifestFromPackage(string packagePath)
|
||||
{
|
||||
using var archive = System.IO.Compression.ZipFile.OpenRead(packagePath);
|
||||
var entries = archive.Entries
|
||||
.Where(entry => string.Equals(entry.Name, "plugin.json", StringComparison.OrdinalIgnoreCase))
|
||||
.ToArray();
|
||||
|
||||
if (entries.Length == 0)
|
||||
{
|
||||
throw new InvalidOperationException($"Plugin package '{packagePath}' does not contain 'plugin.json'.");
|
||||
}
|
||||
|
||||
if (entries.Length > 1)
|
||||
{
|
||||
throw new InvalidOperationException($"Plugin package '{packagePath}' contains multiple 'plugin.json' files.");
|
||||
}
|
||||
|
||||
using var stream = entries[0].Open();
|
||||
return PluginManifest.Load(stream, $"{packagePath}!/{entries[0].FullName}");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_httpClient.Dispose();
|
||||
@@ -299,4 +472,9 @@ internal sealed class AirAppMarketInstallService : IDisposable
|
||||
private sealed record AirAppMarketVerificationResult(
|
||||
bool Success,
|
||||
string? ErrorMessage);
|
||||
|
||||
private sealed record DownloadPackageResult(
|
||||
bool Success,
|
||||
string? PackagePath,
|
||||
string? ErrorMessage);
|
||||
}
|
||||
|
||||
@@ -305,7 +305,8 @@ internal sealed record AirAppMarketLoadResult(
|
||||
internal sealed record AirAppMarketInstallResult(
|
||||
bool Success,
|
||||
PluginManifest? Manifest,
|
||||
string? ErrorMessage);
|
||||
string? ErrorMessage,
|
||||
bool RestartRequired = false);
|
||||
|
||||
internal sealed class AirAppMarketIndexDocument
|
||||
{
|
||||
|
||||
@@ -773,11 +773,6 @@ public sealed class PluginRuntimeService : IDisposable
|
||||
private void ApplyPendingPluginDeletions()
|
||||
{
|
||||
var pendingPaths = ReadPendingPluginDeletions();
|
||||
if (pendingPaths.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var remainingPaths = new List<string>();
|
||||
foreach (var path in pendingPaths)
|
||||
{
|
||||
@@ -788,6 +783,41 @@ public sealed class PluginRuntimeService : IDisposable
|
||||
}
|
||||
|
||||
SavePendingPluginDeletions(remainingPaths);
|
||||
CleanupPendingDeletionDirectory();
|
||||
}
|
||||
|
||||
private void CleanupPendingDeletionDirectory()
|
||||
{
|
||||
var pendingDeletionDir = Path.Combine(PluginsDirectory, ".pending-deletions");
|
||||
if (!Directory.Exists(pendingDeletionDir))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var pendingFile in Directory.EnumerateFiles(pendingDeletionDir, "*.pending"))
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(pendingFile);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore cleanup failures for pending deletions.
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (Directory.GetFiles(pendingDeletionDir).Length == 0 &&
|
||||
Directory.GetDirectories(pendingDeletionDir).Length == 0)
|
||||
{
|
||||
Directory.Delete(pendingDeletionDir);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore directory cleanup failures.
|
||||
}
|
||||
}
|
||||
|
||||
private string ResolvePluginRemovalTargetPath(PluginCatalogEntry entry)
|
||||
|
||||
Reference in New Issue
Block a user