mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
0.7.8
This commit is contained in:
@@ -1,13 +1,16 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using LanMountainDesktop.Models;
|
using LanMountainDesktop.Models;
|
||||||
using LanMountainDesktop.PluginSdk;
|
using LanMountainDesktop.PluginSdk;
|
||||||
using LanMountainDesktop.Services;
|
using LanMountainDesktop.Services;
|
||||||
|
using LanMountainDesktop.Services.PluginMarket;
|
||||||
using LanMountainDesktop.Settings.Core;
|
using LanMountainDesktop.Settings.Core;
|
||||||
|
|
||||||
namespace LanMountainDesktop.Services.Settings;
|
namespace LanMountainDesktop.Services.Settings
|
||||||
|
{
|
||||||
|
|
||||||
public enum WallpaperMediaType
|
public enum WallpaperMediaType
|
||||||
{
|
{
|
||||||
@@ -66,10 +69,272 @@ public sealed record UpdateSettingsState(
|
|||||||
long? PendingUpdatePublishedAtUtcMs,
|
long? PendingUpdatePublishedAtUtcMs,
|
||||||
long? LastUpdateCheckUtcMs);
|
long? LastUpdateCheckUtcMs);
|
||||||
public sealed record PluginManagementSettingsState(IReadOnlyList<string> DisabledPluginIds);
|
public sealed record PluginManagementSettingsState(IReadOnlyList<string> DisabledPluginIds);
|
||||||
|
public enum PluginPackageSourceKind
|
||||||
|
{
|
||||||
|
ReleaseAsset = 0,
|
||||||
|
RawFallback = 1,
|
||||||
|
WorkspaceLocal = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record PluginCatalogSourceInfo(
|
||||||
|
string Id,
|
||||||
|
string Name,
|
||||||
|
string? Description,
|
||||||
|
string? SourceUrl,
|
||||||
|
string? CachePath,
|
||||||
|
bool IsOfficial,
|
||||||
|
int Priority);
|
||||||
|
|
||||||
|
public sealed record PluginCatalogSharedContractInfo(
|
||||||
|
string Id,
|
||||||
|
string Version,
|
||||||
|
string AssemblyName);
|
||||||
|
|
||||||
|
public sealed record PluginCapabilityInfo(
|
||||||
|
string Id,
|
||||||
|
string? Version,
|
||||||
|
string? AssemblyName);
|
||||||
|
|
||||||
|
public sealed record PluginPackageSourceInfo(
|
||||||
|
PluginPackageSourceKind Kind,
|
||||||
|
string Url,
|
||||||
|
string Sha256,
|
||||||
|
long PackageSizeBytes);
|
||||||
|
|
||||||
|
public sealed record PluginCatalogManifestInfo(
|
||||||
|
string Id,
|
||||||
|
string Name,
|
||||||
|
string Description,
|
||||||
|
string Author,
|
||||||
|
string Version,
|
||||||
|
string ApiVersion,
|
||||||
|
string EntranceAssembly,
|
||||||
|
IReadOnlyList<PluginCatalogSharedContractInfo> SharedContracts);
|
||||||
|
|
||||||
|
public sealed record PluginCatalogCompatibilityInfo(
|
||||||
|
string MinHostVersion,
|
||||||
|
string ApiVersion);
|
||||||
|
|
||||||
|
public sealed record PluginCatalogRepositoryInfo(
|
||||||
|
string IconUrl,
|
||||||
|
string ProjectUrl,
|
||||||
|
string ReadmeUrl,
|
||||||
|
string HomepageUrl,
|
||||||
|
string RepositoryUrl,
|
||||||
|
IReadOnlyList<string> Tags,
|
||||||
|
string ReleaseNotes);
|
||||||
|
|
||||||
|
public sealed record PluginCatalogPublicationInfo(
|
||||||
|
string ReleaseTag,
|
||||||
|
string ReleaseAssetName,
|
||||||
|
DateTimeOffset PublishedAt,
|
||||||
|
DateTimeOffset UpdatedAt,
|
||||||
|
long PackageSizeBytes,
|
||||||
|
string Sha256,
|
||||||
|
string? Md5);
|
||||||
|
|
||||||
|
public sealed record PluginCatalogItemInfo(
|
||||||
|
PluginCatalogManifestInfo Manifest,
|
||||||
|
PluginCatalogCompatibilityInfo Compatibility,
|
||||||
|
PluginCatalogRepositoryInfo Repository,
|
||||||
|
PluginCatalogPublicationInfo Publication,
|
||||||
|
IReadOnlyList<PluginPackageSourceInfo> PackageSources,
|
||||||
|
IReadOnlyList<PluginCapabilityInfo> Capabilities)
|
||||||
|
{
|
||||||
|
public string Id => Manifest.Id;
|
||||||
|
|
||||||
|
public string Name => Manifest.Name;
|
||||||
|
|
||||||
|
public string Description => Manifest.Description;
|
||||||
|
|
||||||
|
public string Author => Manifest.Author;
|
||||||
|
|
||||||
|
public string Version => Manifest.Version;
|
||||||
|
|
||||||
|
public string ApiVersion => Manifest.ApiVersion;
|
||||||
|
|
||||||
|
public string MinHostVersion => Compatibility.MinHostVersion;
|
||||||
|
|
||||||
|
public string DownloadUrl => PackageSources.FirstOrDefault()?.Url ?? string.Empty;
|
||||||
|
|
||||||
|
public string Sha256 => Publication.Sha256;
|
||||||
|
|
||||||
|
public long PackageSizeBytes => Publication.PackageSizeBytes;
|
||||||
|
|
||||||
|
public string IconUrl => Repository.IconUrl;
|
||||||
|
|
||||||
|
public string ProjectUrl => Repository.ProjectUrl;
|
||||||
|
|
||||||
|
public string ReadmeUrl => Repository.ReadmeUrl;
|
||||||
|
|
||||||
|
public string HomepageUrl => Repository.HomepageUrl;
|
||||||
|
|
||||||
|
public string RepositoryUrl => Repository.RepositoryUrl;
|
||||||
|
|
||||||
|
public IReadOnlyList<string> Tags => Repository.Tags;
|
||||||
|
|
||||||
|
public IReadOnlyList<PluginCatalogSharedContractInfo> SharedContracts => Manifest.SharedContracts;
|
||||||
|
|
||||||
|
public IReadOnlyList<PluginCatalogDependencyInfo> Dependencies =>
|
||||||
|
Manifest.SharedContracts
|
||||||
|
.Select(contract => new PluginCatalogDependencyInfo(
|
||||||
|
contract.Id,
|
||||||
|
contract.Version,
|
||||||
|
contract.AssemblyName))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
public DateTimeOffset PublishedAt => Publication.PublishedAt;
|
||||||
|
|
||||||
|
public DateTimeOffset UpdatedAt => Publication.UpdatedAt;
|
||||||
|
|
||||||
|
public string ReleaseTag => Publication.ReleaseTag;
|
||||||
|
|
||||||
|
public string ReleaseAssetName => Publication.ReleaseAssetName;
|
||||||
|
|
||||||
|
public string ReleaseNotes => Repository.ReleaseNotes;
|
||||||
|
|
||||||
|
public static implicit operator PluginMarketPluginInfo(PluginCatalogItemInfo item)
|
||||||
|
{
|
||||||
|
return new PluginMarketPluginInfo(
|
||||||
|
item.Id,
|
||||||
|
item.Name,
|
||||||
|
item.Description,
|
||||||
|
item.Author,
|
||||||
|
item.Version,
|
||||||
|
item.ApiVersion,
|
||||||
|
item.MinHostVersion,
|
||||||
|
item.DownloadUrl,
|
||||||
|
item.ReleaseTag,
|
||||||
|
item.ReleaseAssetName,
|
||||||
|
item.IconUrl,
|
||||||
|
item.ReadmeUrl,
|
||||||
|
item.HomepageUrl,
|
||||||
|
item.RepositoryUrl,
|
||||||
|
item.Tags.ToArray(),
|
||||||
|
item.Dependencies.Select(dependency => new PluginMarketDependencyInfo(
|
||||||
|
dependency.Id,
|
||||||
|
dependency.Version,
|
||||||
|
dependency.AssemblyName)).ToArray(),
|
||||||
|
item.PublishedAt,
|
||||||
|
item.UpdatedAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static implicit operator PluginCatalogItemInfo(PluginMarketPluginInfo plugin)
|
||||||
|
{
|
||||||
|
return new PluginCatalogItemInfo(
|
||||||
|
new PluginCatalogManifestInfo(
|
||||||
|
plugin.Id,
|
||||||
|
plugin.Name,
|
||||||
|
plugin.Description,
|
||||||
|
plugin.Author,
|
||||||
|
plugin.Version,
|
||||||
|
plugin.ApiVersion,
|
||||||
|
string.Empty,
|
||||||
|
plugin.Dependencies
|
||||||
|
.Select(dependency => new PluginCatalogSharedContractInfo(
|
||||||
|
dependency.Id,
|
||||||
|
dependency.Version,
|
||||||
|
dependency.AssemblyName))
|
||||||
|
.ToArray()),
|
||||||
|
new PluginCatalogCompatibilityInfo(
|
||||||
|
plugin.MinHostVersion,
|
||||||
|
plugin.ApiVersion),
|
||||||
|
new PluginCatalogRepositoryInfo(
|
||||||
|
plugin.IconUrl,
|
||||||
|
plugin.RepositoryUrl,
|
||||||
|
plugin.ReadmeUrl,
|
||||||
|
plugin.HomepageUrl,
|
||||||
|
plugin.RepositoryUrl,
|
||||||
|
plugin.Tags,
|
||||||
|
string.Empty),
|
||||||
|
new PluginCatalogPublicationInfo(
|
||||||
|
plugin.ReleaseTag,
|
||||||
|
plugin.ReleaseAssetName,
|
||||||
|
plugin.PublishedAt,
|
||||||
|
plugin.UpdatedAt,
|
||||||
|
0,
|
||||||
|
string.Empty,
|
||||||
|
null),
|
||||||
|
string.IsNullOrWhiteSpace(plugin.DownloadUrl)
|
||||||
|
? []
|
||||||
|
: [
|
||||||
|
new PluginPackageSourceInfo(
|
||||||
|
string.IsNullOrWhiteSpace(plugin.ReleaseTag)
|
||||||
|
? PluginPackageSourceKind.RawFallback
|
||||||
|
: PluginPackageSourceKind.ReleaseAsset,
|
||||||
|
plugin.DownloadUrl,
|
||||||
|
string.Empty,
|
||||||
|
0)
|
||||||
|
],
|
||||||
|
[]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record PluginCatalogIndexResult(
|
||||||
|
bool Success,
|
||||||
|
IReadOnlyList<PluginCatalogItemInfo> Plugins,
|
||||||
|
IReadOnlyList<PluginCatalogSourceInfo> Sources,
|
||||||
|
string? Source,
|
||||||
|
string? SourceLocation,
|
||||||
|
string? WarningMessage,
|
||||||
|
string? ErrorMessage)
|
||||||
|
{
|
||||||
|
public static implicit operator PluginMarketIndexResult(PluginCatalogIndexResult result)
|
||||||
|
{
|
||||||
|
return new PluginMarketIndexResult(
|
||||||
|
result.Success,
|
||||||
|
result.Plugins.Select(plugin => (PluginMarketPluginInfo)plugin).ToArray(),
|
||||||
|
result.Source,
|
||||||
|
result.SourceLocation,
|
||||||
|
result.WarningMessage,
|
||||||
|
result.ErrorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record PluginInstallDiagnostic(
|
||||||
|
string Code,
|
||||||
|
string Message,
|
||||||
|
string? Details = null);
|
||||||
|
|
||||||
|
public sealed record PluginCatalogInstallResult(
|
||||||
|
bool Success,
|
||||||
|
string? PluginId,
|
||||||
|
string? PluginName,
|
||||||
|
PluginManifest? InstalledManifest,
|
||||||
|
IReadOnlyList<PluginInstallDiagnostic> Diagnostics,
|
||||||
|
string? ErrorMessage)
|
||||||
|
{
|
||||||
|
public static implicit operator PluginMarketInstallResult(PluginCatalogInstallResult result)
|
||||||
|
{
|
||||||
|
return new PluginMarketInstallResult(
|
||||||
|
result.Success,
|
||||||
|
result.PluginId,
|
||||||
|
result.PluginName,
|
||||||
|
result.ErrorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record PluginCatalogDependencyInfo(
|
||||||
|
string Id,
|
||||||
|
string Version,
|
||||||
|
string AssemblyName)
|
||||||
|
{
|
||||||
|
public static implicit operator PluginMarketDependencyInfo(PluginCatalogDependencyInfo dependency)
|
||||||
|
{
|
||||||
|
return new PluginMarketDependencyInfo(
|
||||||
|
dependency.Id,
|
||||||
|
dependency.Version,
|
||||||
|
dependency.AssemblyName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Obsolete("Use PluginCatalogSharedContractInfo and PluginCatalogItemInfo instead.")]
|
||||||
public sealed record PluginMarketDependencyInfo(
|
public sealed record PluginMarketDependencyInfo(
|
||||||
string Id,
|
string Id,
|
||||||
string Version,
|
string Version,
|
||||||
string AssemblyName);
|
string AssemblyName);
|
||||||
|
|
||||||
|
[Obsolete("Use PluginCatalogItemInfo instead.")]
|
||||||
public sealed record PluginMarketPluginInfo(
|
public sealed record PluginMarketPluginInfo(
|
||||||
string Id,
|
string Id,
|
||||||
string Name,
|
string Name,
|
||||||
@@ -89,6 +354,8 @@ public sealed record PluginMarketPluginInfo(
|
|||||||
IReadOnlyList<PluginMarketDependencyInfo> Dependencies,
|
IReadOnlyList<PluginMarketDependencyInfo> Dependencies,
|
||||||
DateTimeOffset PublishedAt,
|
DateTimeOffset PublishedAt,
|
||||||
DateTimeOffset UpdatedAt);
|
DateTimeOffset UpdatedAt);
|
||||||
|
|
||||||
|
[Obsolete("Use PluginCatalogIndexResult instead.")]
|
||||||
public sealed record PluginMarketIndexResult(
|
public sealed record PluginMarketIndexResult(
|
||||||
bool Success,
|
bool Success,
|
||||||
IReadOnlyList<PluginMarketPluginInfo> Plugins,
|
IReadOnlyList<PluginMarketPluginInfo> Plugins,
|
||||||
@@ -96,12 +363,39 @@ public sealed record PluginMarketIndexResult(
|
|||||||
string? SourceLocation,
|
string? SourceLocation,
|
||||||
string? WarningMessage,
|
string? WarningMessage,
|
||||||
string? ErrorMessage);
|
string? ErrorMessage);
|
||||||
|
|
||||||
|
[Obsolete("Use PluginCatalogInstallResult instead.")]
|
||||||
public sealed record PluginMarketInstallResult(
|
public sealed record PluginMarketInstallResult(
|
||||||
bool Success,
|
bool Success,
|
||||||
string? PluginId,
|
string? PluginId,
|
||||||
string? PluginName,
|
string? PluginName,
|
||||||
string? ErrorMessage);
|
string? ErrorMessage);
|
||||||
|
|
||||||
|
public interface IPluginCatalogSourceProvider
|
||||||
|
{
|
||||||
|
Task<PluginCatalogIndexResult> LoadCatalogAsync(CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IPluginCatalogService : IPluginCatalogSourceProvider
|
||||||
|
{
|
||||||
|
Task<PluginCatalogInstallResult> InstallAsync(string pluginId, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IPackageSourceResolver
|
||||||
|
{
|
||||||
|
IReadOnlyList<PluginPackageSourceInfo> ResolveSources(PluginCatalogItemInfo item);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IPluginCompatibilityEvaluator
|
||||||
|
{
|
||||||
|
PluginInstallDiagnostic? Evaluate(PluginCatalogItemInfo item, Version? hostVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IPluginInstallOrchestrator
|
||||||
|
{
|
||||||
|
Task<PluginCatalogInstallResult> InstallAsync(PluginCatalogItemInfo item, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
|
|
||||||
public interface IGridSettingsService
|
public interface IGridSettingsService
|
||||||
{
|
{
|
||||||
GridSettingsState Get();
|
GridSettingsState Get();
|
||||||
@@ -223,10 +517,17 @@ public interface IPluginManagementSettingsService
|
|||||||
bool DeleteInstalledPlugin(string pluginId);
|
bool DeleteInstalledPlugin(string pluginId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IPluginMarketSettingsService
|
public interface IPluginCatalogSettingsService : IPluginCatalogSourceProvider
|
||||||
|
{
|
||||||
|
new Task<PluginCatalogIndexResult> LoadCatalogAsync(CancellationToken cancellationToken = default);
|
||||||
|
Task<PluginCatalogInstallResult> InstallAsync(string pluginId, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Obsolete("Use IPluginCatalogSettingsService instead.")]
|
||||||
|
public interface IPluginMarketSettingsService : IPluginCatalogSettingsService
|
||||||
{
|
{
|
||||||
Task<PluginMarketIndexResult> LoadIndexAsync(CancellationToken cancellationToken = default);
|
Task<PluginMarketIndexResult> LoadIndexAsync(CancellationToken cancellationToken = default);
|
||||||
Task<PluginMarketInstallResult> InstallAsync(string pluginId, CancellationToken cancellationToken = default);
|
new Task<PluginMarketInstallResult> InstallAsync(string pluginId, CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IApplicationInfoService
|
public interface IApplicationInfoService
|
||||||
@@ -252,6 +553,20 @@ public interface ISettingsFacadeService
|
|||||||
ILauncherCatalogService LauncherCatalog { get; }
|
ILauncherCatalogService LauncherCatalog { get; }
|
||||||
ILauncherPolicyService LauncherPolicy { get; }
|
ILauncherPolicyService LauncherPolicy { get; }
|
||||||
IPluginManagementSettingsService PluginManagement { get; }
|
IPluginManagementSettingsService PluginManagement { get; }
|
||||||
|
IPluginCatalogSettingsService PluginCatalog { get; }
|
||||||
|
[Obsolete("Use PluginCatalog instead.")]
|
||||||
IPluginMarketSettingsService PluginMarket { get; }
|
IPluginMarketSettingsService PluginMarket { get; }
|
||||||
IApplicationInfoService ApplicationInfo { get; }
|
IApplicationInfoService ApplicationInfo { get; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.Services.PluginMarket
|
||||||
|
{
|
||||||
|
internal enum PluginPackageSourceKind
|
||||||
|
{
|
||||||
|
ReleaseAsset = 0,
|
||||||
|
RawFallback = 1,
|
||||||
|
WorkspaceLocal = 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -870,14 +870,41 @@ internal sealed class PluginMarketSettingsService : IPluginMarketSettingsService
|
|||||||
_installService = new AirAppMarketInstallService(_pluginRuntimeService, dataRoot);
|
_installService = new AirAppMarketInstallService(_pluginRuntimeService, dataRoot);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<PluginMarketIndexResult> LoadIndexAsync(CancellationToken cancellationToken = default)
|
public Task<PluginCatalogIndexResult> LoadCatalogAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var result = await _indexService.LoadAsync(cancellationToken);
|
return LoadCatalogCoreAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task<PluginMarketIndexResult> IPluginMarketSettingsService.LoadIndexAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return await LoadCatalogCoreAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<PluginCatalogInstallResult> InstallAsync(
|
||||||
|
string pluginId,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return InstallCatalogCoreAsync(pluginId, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task<PluginMarketInstallResult> IPluginMarketSettingsService.InstallAsync(
|
||||||
|
string pluginId,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return await InstallCatalogCoreAsync(pluginId, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<PluginCatalogIndexResult> LoadCatalogCoreAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var result = await _indexService.LoadAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
var sources = BuildCatalogSources(result.Source?.ToString(), result.SourceLocation, result.WarningMessage);
|
||||||
if (!result.Success || result.Document is null)
|
if (!result.Success || result.Document is null)
|
||||||
{
|
{
|
||||||
return new PluginMarketIndexResult(
|
_cachedPlugins.Clear();
|
||||||
|
return new PluginCatalogIndexResult(
|
||||||
false,
|
false,
|
||||||
[],
|
[],
|
||||||
|
sources,
|
||||||
result.Source?.ToString(),
|
result.Source?.ToString(),
|
||||||
result.SourceLocation,
|
result.SourceLocation,
|
||||||
result.WarningMessage,
|
result.WarningMessage,
|
||||||
@@ -889,81 +916,189 @@ internal sealed class PluginMarketSettingsService : IPluginMarketSettingsService
|
|||||||
.Select(entry =>
|
.Select(entry =>
|
||||||
{
|
{
|
||||||
_cachedPlugins[entry.Id] = entry;
|
_cachedPlugins[entry.Id] = entry;
|
||||||
return new PluginMarketPluginInfo(
|
return MapCatalogItem(entry);
|
||||||
entry.Id,
|
|
||||||
entry.Name,
|
|
||||||
entry.Description,
|
|
||||||
entry.Author,
|
|
||||||
entry.Version,
|
|
||||||
entry.ApiVersion,
|
|
||||||
entry.MinHostVersion,
|
|
||||||
entry.DownloadUrl,
|
|
||||||
entry.ReleaseTag,
|
|
||||||
entry.ReleaseAssetName,
|
|
||||||
entry.IconUrl,
|
|
||||||
entry.ReadmeUrl,
|
|
||||||
entry.HomepageUrl,
|
|
||||||
entry.RepositoryUrl,
|
|
||||||
entry.Tags,
|
|
||||||
entry.SharedContracts
|
|
||||||
.Select(contract => new PluginMarketDependencyInfo(
|
|
||||||
contract.Id,
|
|
||||||
contract.Version,
|
|
||||||
contract.AssemblyName))
|
|
||||||
.ToArray(),
|
|
||||||
entry.PublishedAt,
|
|
||||||
entry.UpdatedAt);
|
|
||||||
})
|
})
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
return new PluginMarketIndexResult(
|
return new PluginCatalogIndexResult(
|
||||||
true,
|
true,
|
||||||
plugins,
|
plugins,
|
||||||
|
sources,
|
||||||
result.Source?.ToString(),
|
result.Source?.ToString(),
|
||||||
result.SourceLocation,
|
result.SourceLocation,
|
||||||
result.WarningMessage,
|
result.WarningMessage,
|
||||||
null);
|
null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<PluginMarketInstallResult> InstallAsync(
|
private async Task<PluginCatalogInstallResult> InstallCatalogCoreAsync(
|
||||||
string pluginId,
|
string pluginId,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(pluginId))
|
if (string.IsNullOrWhiteSpace(pluginId))
|
||||||
{
|
{
|
||||||
return new PluginMarketInstallResult(false, null, null, "Plugin id is required.");
|
return new PluginCatalogInstallResult(
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
[new PluginInstallDiagnostic("invalid_request", "Plugin id is required.")],
|
||||||
|
"Plugin id is required.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_installService is null || _pluginRuntimeService is null)
|
if (_installService is null || _pluginRuntimeService is null)
|
||||||
{
|
{
|
||||||
return new PluginMarketInstallResult(
|
return new PluginCatalogInstallResult(
|
||||||
false,
|
false,
|
||||||
pluginId,
|
pluginId,
|
||||||
null,
|
null,
|
||||||
|
null,
|
||||||
|
[new PluginInstallDiagnostic("runtime_unavailable", "Plugin runtime is unavailable.")],
|
||||||
"Plugin runtime is unavailable.");
|
"Plugin runtime is unavailable.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_cachedPlugins.TryGetValue(pluginId, out var entry))
|
if (!_cachedPlugins.TryGetValue(pluginId, out var entry))
|
||||||
{
|
{
|
||||||
var load = await LoadIndexAsync(cancellationToken);
|
var load = await LoadCatalogCoreAsync(cancellationToken).ConfigureAwait(false);
|
||||||
if (!load.Success)
|
if (!load.Success)
|
||||||
{
|
{
|
||||||
return new PluginMarketInstallResult(false, pluginId, null, load.ErrorMessage);
|
return new PluginCatalogInstallResult(
|
||||||
|
false,
|
||||||
|
pluginId,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
[new PluginInstallDiagnostic("catalog_load_failed", load.ErrorMessage ?? "Failed to load the plugin catalog.")],
|
||||||
|
load.ErrorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_cachedPlugins.TryGetValue(pluginId, out entry))
|
if (!_cachedPlugins.TryGetValue(pluginId, out entry))
|
||||||
{
|
{
|
||||||
return new PluginMarketInstallResult(false, pluginId, null, "Plugin was not found in market index.");
|
return new PluginCatalogInstallResult(
|
||||||
|
false,
|
||||||
|
pluginId,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
[new PluginInstallDiagnostic("not_found", "Plugin was not found in the official catalog.")],
|
||||||
|
"Plugin was not found in the official catalog.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = await _installService.InstallAsync(entry, cancellationToken);
|
var result = await _installService.InstallAsync(entry, cancellationToken).ConfigureAwait(false);
|
||||||
if (!result.Success)
|
if (!result.Success)
|
||||||
{
|
{
|
||||||
return new PluginMarketInstallResult(false, entry.Id, entry.Name, result.ErrorMessage);
|
return new PluginCatalogInstallResult(
|
||||||
|
false,
|
||||||
|
entry.Id,
|
||||||
|
entry.Name,
|
||||||
|
null,
|
||||||
|
[new PluginInstallDiagnostic("install_failed", result.ErrorMessage ?? "Plugin install failed.")],
|
||||||
|
result.ErrorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new PluginMarketInstallResult(true, result.Manifest?.Id ?? entry.Id, result.Manifest?.Name ?? entry.Name, null);
|
return new PluginCatalogInstallResult(
|
||||||
|
true,
|
||||||
|
result.Manifest?.Id ?? entry.Id,
|
||||||
|
result.Manifest?.Name ?? entry.Name,
|
||||||
|
result.Manifest,
|
||||||
|
[],
|
||||||
|
null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PluginCatalogItemInfo MapCatalogItem(AirAppMarketPluginEntry entry)
|
||||||
|
{
|
||||||
|
var manifest = new PluginCatalogManifestInfo(
|
||||||
|
entry.Id,
|
||||||
|
entry.Name,
|
||||||
|
entry.Description,
|
||||||
|
entry.Author,
|
||||||
|
entry.Version,
|
||||||
|
entry.ApiVersion,
|
||||||
|
string.Empty,
|
||||||
|
entry.SharedContracts
|
||||||
|
.Select(contract => new PluginCatalogSharedContractInfo(
|
||||||
|
contract.Id,
|
||||||
|
contract.Version,
|
||||||
|
contract.AssemblyName))
|
||||||
|
.ToArray());
|
||||||
|
|
||||||
|
var compatibility = new PluginCatalogCompatibilityInfo(
|
||||||
|
entry.MinHostVersion,
|
||||||
|
entry.ApiVersion);
|
||||||
|
|
||||||
|
var repository = new PluginCatalogRepositoryInfo(
|
||||||
|
entry.IconUrl,
|
||||||
|
entry.ProjectUrl,
|
||||||
|
entry.ReadmeUrl,
|
||||||
|
entry.HomepageUrl,
|
||||||
|
entry.RepositoryUrl,
|
||||||
|
entry.Tags.ToArray(),
|
||||||
|
entry.ReleaseNotes);
|
||||||
|
|
||||||
|
var publication = new PluginCatalogPublicationInfo(
|
||||||
|
entry.ReleaseTag,
|
||||||
|
entry.ReleaseAssetName,
|
||||||
|
entry.PublishedAt,
|
||||||
|
entry.UpdatedAt,
|
||||||
|
entry.PackageSizeBytes,
|
||||||
|
entry.Sha256,
|
||||||
|
null);
|
||||||
|
|
||||||
|
var sources = BuildPackageSources(entry);
|
||||||
|
|
||||||
|
return new PluginCatalogItemInfo(
|
||||||
|
manifest,
|
||||||
|
compatibility,
|
||||||
|
repository,
|
||||||
|
publication,
|
||||||
|
sources,
|
||||||
|
[]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyList<PluginPackageSourceInfo> BuildPackageSources(AirAppMarketPluginEntry entry)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(entry.DownloadUrl))
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
var sourceKind = entry.HasReleaseDownloadMetadata
|
||||||
|
? PluginPackageSourceKind.ReleaseAsset
|
||||||
|
: PluginPackageSourceKind.RawFallback;
|
||||||
|
|
||||||
|
return
|
||||||
|
[
|
||||||
|
new PluginPackageSourceInfo(
|
||||||
|
sourceKind,
|
||||||
|
entry.DownloadUrl,
|
||||||
|
entry.Sha256,
|
||||||
|
entry.PackageSizeBytes)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyList<PluginCatalogSourceInfo> BuildCatalogSources(
|
||||||
|
string? sourceId,
|
||||||
|
string? sourceLocation,
|
||||||
|
string? warningMessage)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(sourceId) && string.IsNullOrWhiteSpace(sourceLocation))
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalizedSourceId = string.IsNullOrWhiteSpace(sourceId)
|
||||||
|
? "plugin-catalog"
|
||||||
|
: sourceId.Trim();
|
||||||
|
|
||||||
|
return
|
||||||
|
[
|
||||||
|
new PluginCatalogSourceInfo(
|
||||||
|
normalizedSourceId,
|
||||||
|
normalizedSourceId,
|
||||||
|
string.IsNullOrWhiteSpace(warningMessage) ? null : warningMessage.Trim(),
|
||||||
|
string.IsNullOrWhiteSpace(sourceLocation) ? null : sourceLocation.Trim(),
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
0)
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
@@ -1054,6 +1189,7 @@ internal sealed class SettingsFacadeService : ISettingsFacadeService, IDisposabl
|
|||||||
_pluginManagementSettingsService = new PluginManagementSettingsService(Settings, pluginRuntimeService);
|
_pluginManagementSettingsService = new PluginManagementSettingsService(Settings, pluginRuntimeService);
|
||||||
PluginManagement = _pluginManagementSettingsService;
|
PluginManagement = _pluginManagementSettingsService;
|
||||||
_pluginMarketSettingsService = new PluginMarketSettingsService(pluginRuntimeService);
|
_pluginMarketSettingsService = new PluginMarketSettingsService(pluginRuntimeService);
|
||||||
|
PluginCatalog = _pluginMarketSettingsService;
|
||||||
PluginMarket = _pluginMarketSettingsService;
|
PluginMarket = _pluginMarketSettingsService;
|
||||||
ApplicationInfo = new ApplicationInfoService();
|
ApplicationInfo = new ApplicationInfoService();
|
||||||
}
|
}
|
||||||
@@ -1086,6 +1222,8 @@ internal sealed class SettingsFacadeService : ISettingsFacadeService, IDisposabl
|
|||||||
|
|
||||||
public IPluginManagementSettingsService PluginManagement { get; }
|
public IPluginManagementSettingsService PluginManagement { get; }
|
||||||
|
|
||||||
|
public IPluginCatalogSettingsService PluginCatalog { get; }
|
||||||
|
|
||||||
public IPluginMarketSettingsService PluginMarket { get; }
|
public IPluginMarketSettingsService PluginMarket { get; }
|
||||||
|
|
||||||
public IApplicationInfoService ApplicationInfo { get; }
|
public IApplicationInfoService ApplicationInfo { get; }
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
@@ -31,7 +31,7 @@ public sealed partial class PluginMarketItemViewModel : ViewModelBase
|
|||||||
private bool _isLoadingIcon;
|
private bool _isLoadingIcon;
|
||||||
|
|
||||||
public PluginMarketItemViewModel(
|
public PluginMarketItemViewModel(
|
||||||
PluginMarketPluginInfo plugin,
|
PluginCatalogItemInfo plugin,
|
||||||
LocalizationService localizationService,
|
LocalizationService localizationService,
|
||||||
string languageCode)
|
string languageCode)
|
||||||
{
|
{
|
||||||
@@ -46,7 +46,7 @@ public sealed partial class PluginMarketItemViewModel : ViewModelBase
|
|||||||
ActionTooltip = L("market.button.install", "Install");
|
ActionTooltip = L("market.button.install", "Install");
|
||||||
}
|
}
|
||||||
|
|
||||||
public PluginMarketPluginInfo Info { get; }
|
public PluginCatalogItemInfo Info { get; }
|
||||||
|
|
||||||
public string PluginId => Info.Id;
|
public string PluginId => Info.Id;
|
||||||
|
|
||||||
@@ -64,7 +64,11 @@ public sealed partial class PluginMarketItemViewModel : ViewModelBase
|
|||||||
|
|
||||||
public string ReadmeUrl => Info.ReadmeUrl;
|
public string ReadmeUrl => Info.ReadmeUrl;
|
||||||
|
|
||||||
public IReadOnlyList<PluginMarketDependencyInfo> Dependencies => Info.Dependencies;
|
public IReadOnlyList<PluginCatalogSharedContractInfo> Dependencies => Info.SharedContracts;
|
||||||
|
|
||||||
|
public IReadOnlyList<PluginPackageSourceInfo> PackageSources => Info.PackageSources;
|
||||||
|
|
||||||
|
public IReadOnlyList<PluginCapabilityInfo> Capabilities => Info.Capabilities;
|
||||||
|
|
||||||
public string IconFallbackText { get; }
|
public string IconFallbackText { get; }
|
||||||
|
|
||||||
@@ -259,7 +263,7 @@ public sealed partial class PluginMarketDetailViewModel : ViewModelBase
|
|||||||
_readmeService = readmeService;
|
_readmeService = readmeService;
|
||||||
_primaryActionAsync = primaryActionAsync;
|
_primaryActionAsync = primaryActionAsync;
|
||||||
|
|
||||||
Dependencies = new ObservableCollection<PluginMarketDependencyInfo>(item.Dependencies);
|
Dependencies = new ObservableCollection<PluginCatalogSharedContractInfo>(item.Dependencies);
|
||||||
VersionLabel = L("market.detail.version", "Version");
|
VersionLabel = L("market.detail.version", "Version");
|
||||||
PublisherLabel = L("market.detail.author", "Author");
|
PublisherLabel = L("market.detail.author", "Author");
|
||||||
ApiVersionLabel = L("market.detail.api_version", "API Version");
|
ApiVersionLabel = L("market.detail.api_version", "API Version");
|
||||||
@@ -271,7 +275,7 @@ public sealed partial class PluginMarketDetailViewModel : ViewModelBase
|
|||||||
|
|
||||||
public PluginMarketItemViewModel Item { get; }
|
public PluginMarketItemViewModel Item { get; }
|
||||||
|
|
||||||
public ObservableCollection<PluginMarketDependencyInfo> Dependencies { get; }
|
public ObservableCollection<PluginCatalogSharedContractInfo> Dependencies { get; }
|
||||||
|
|
||||||
public string DrawerTitle => Item.Name;
|
public string DrawerTitle => Item.Name;
|
||||||
|
|
||||||
@@ -306,6 +310,10 @@ public sealed partial class PluginMarketDetailViewModel : ViewModelBase
|
|||||||
|
|
||||||
public bool HasReadmeContent => !IsReadmeLoading && !HasReadmeError && !string.IsNullOrWhiteSpace(ReadmeMarkdown);
|
public bool HasReadmeContent => !IsReadmeLoading && !HasReadmeError && !string.IsNullOrWhiteSpace(ReadmeMarkdown);
|
||||||
|
|
||||||
|
public IReadOnlyList<PluginPackageSourceInfo> PackageSources => Item.PackageSources;
|
||||||
|
|
||||||
|
public IReadOnlyList<PluginCapabilityInfo> Capabilities => Item.Capabilities;
|
||||||
|
|
||||||
public async Task InitializeAsync()
|
public async Task InitializeAsync()
|
||||||
{
|
{
|
||||||
if (_isInitialized)
|
if (_isInitialized)
|
||||||
@@ -370,6 +378,7 @@ public sealed partial class PluginMarketDetailViewModel : ViewModelBase
|
|||||||
public sealed partial class PluginMarketSettingsPageViewModel : ViewModelBase
|
public sealed partial class PluginMarketSettingsPageViewModel : ViewModelBase
|
||||||
{
|
{
|
||||||
private readonly ISettingsFacadeService _settingsFacade;
|
private readonly ISettingsFacadeService _settingsFacade;
|
||||||
|
private readonly IPluginCatalogSettingsService _pluginCatalog;
|
||||||
private readonly LocalizationService _localizationService;
|
private readonly LocalizationService _localizationService;
|
||||||
private readonly AirAppMarketIconService _iconService;
|
private readonly AirAppMarketIconService _iconService;
|
||||||
private readonly AirAppMarketReadmeService _readmeService;
|
private readonly AirAppMarketReadmeService _readmeService;
|
||||||
@@ -386,6 +395,7 @@ public sealed partial class PluginMarketSettingsPageViewModel : ViewModelBase
|
|||||||
AirAppMarketReadmeService readmeService)
|
AirAppMarketReadmeService readmeService)
|
||||||
{
|
{
|
||||||
_settingsFacade = settingsFacade;
|
_settingsFacade = settingsFacade;
|
||||||
|
_pluginCatalog = _settingsFacade.PluginCatalog;
|
||||||
_localizationService = localizationService;
|
_localizationService = localizationService;
|
||||||
_iconService = iconService;
|
_iconService = iconService;
|
||||||
_readmeService = readmeService;
|
_readmeService = readmeService;
|
||||||
@@ -468,7 +478,7 @@ public sealed partial class PluginMarketSettingsPageViewModel : ViewModelBase
|
|||||||
StatusMessage = L("market.status.loading", "Loading the official plugin market...");
|
StatusMessage = L("market.status.loading", "Loading the official plugin market...");
|
||||||
RefreshInstalledSnapshot();
|
RefreshInstalledSnapshot();
|
||||||
|
|
||||||
var result = await _settingsFacade.PluginMarket.LoadIndexAsync();
|
var result = await _pluginCatalog.LoadCatalogAsync();
|
||||||
if (!result.Success)
|
if (!result.Success)
|
||||||
{
|
{
|
||||||
_hasLoadedMarket = false;
|
_hasLoadedMarket = false;
|
||||||
@@ -559,7 +569,7 @@ public sealed partial class PluginMarketSettingsPageViewModel : ViewModelBase
|
|||||||
L("market.status.installing_format", "Downloading and staging plugin '{0}'..."),
|
L("market.status.installing_format", "Downloading and staging plugin '{0}'..."),
|
||||||
item.Name);
|
item.Name);
|
||||||
|
|
||||||
var result = await _settingsFacade.PluginMarket.InstallAsync(item.PluginId);
|
var result = await _pluginCatalog.InstallAsync(item.PluginId);
|
||||||
if (result.Success)
|
if (result.Success)
|
||||||
{
|
{
|
||||||
RefreshInstalledSnapshot();
|
RefreshInstalledSnapshot();
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ public partial class IfengNewsWidget : UserControl, IDesktopComponentWidget, IRe
|
|||||||
|
|
||||||
foreach (var visual in _itemVisuals)
|
foreach (var visual in _itemVisuals)
|
||||||
{
|
{
|
||||||
visual.Host.Background = new SolidColorBrush(_isNightVisual ? Color.Parse("#2D3440") : Color.Parse("#F7F8FA"));
|
visual.Host.Background = Brushes.Transparent;
|
||||||
visual.TitleTextBlock.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#E8EAED") : Color.Parse("#202327"));
|
visual.TitleTextBlock.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#E8EAED") : Color.Parse("#202327"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
@@ -12,6 +13,8 @@ namespace LanMountainDesktop.Services.PluginMarket;
|
|||||||
|
|
||||||
internal sealed class AirAppMarketInstallService : IDisposable
|
internal sealed class AirAppMarketInstallService : IDisposable
|
||||||
{
|
{
|
||||||
|
private const string HelperExecutableName = "LanMountainDesktop.PluginsInstallHelper.exe";
|
||||||
|
|
||||||
private readonly PluginRuntimeService _runtime;
|
private readonly PluginRuntimeService _runtime;
|
||||||
private readonly PluginsInstallHelperClient _helperClient = new();
|
private readonly PluginsInstallHelperClient _helperClient = new();
|
||||||
private readonly HttpClient _httpClient;
|
private readonly HttpClient _httpClient;
|
||||||
@@ -38,107 +41,226 @@ internal sealed class AirAppMarketInstallService : IDisposable
|
|||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(plugin);
|
ArgumentNullException.ThrowIfNull(plugin);
|
||||||
|
|
||||||
Directory.CreateDirectory(_downloadsDirectory);
|
if (OperatingSystem.IsWindows())
|
||||||
var downloadPath = Path.Combine(
|
|
||||||
_downloadsDirectory,
|
|
||||||
$"{SanitizeFileName(plugin.Id)}-{SanitizeFileName(plugin.Version)}.laapp");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
AppLogger.Info(
|
var helperPath = ResolveHelperPath();
|
||||||
"PluginMarket",
|
if (!File.Exists(helperPath))
|
||||||
$"Starting install. PluginId='{plugin.Id}'; Version='{plugin.Version}'; DownloadPath='{downloadPath}'.");
|
|
||||||
var resolvedDownloadUrl = await _releaseResolverService.ResolveDownloadUrlAsync(plugin, cancellationToken);
|
|
||||||
AppLogger.Info(
|
|
||||||
"PluginMarket",
|
|
||||||
$"Resolved download url for '{plugin.Id}' to '{resolvedDownloadUrl}'.");
|
|
||||||
|
|
||||||
if (AirAppMarketDefaults.TryResolveWorkspaceFile(resolvedDownloadUrl, out var localPackagePath))
|
|
||||||
{
|
{
|
||||||
var localCopyResult = await _downloadService.DownloadAsync(
|
|
||||||
localPackagePath,
|
|
||||||
downloadPath,
|
|
||||||
new DownloadOptions(ExpectedSizeBytes: plugin.PackageSizeBytes),
|
|
||||||
cancellationToken: cancellationToken);
|
|
||||||
if (!localCopyResult.Success)
|
|
||||||
{
|
|
||||||
return new AirAppMarketInstallResult(false, null, localCopyResult.ErrorMessage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var downloadResult = await _downloadService.DownloadAsync(
|
|
||||||
resolvedDownloadUrl,
|
|
||||||
downloadPath,
|
|
||||||
new DownloadOptions(ExpectedSizeBytes: plugin.PackageSizeBytes),
|
|
||||||
cancellationToken: cancellationToken);
|
|
||||||
if (!downloadResult.Success)
|
|
||||||
{
|
|
||||||
return new AirAppMarketInstallResult(false, null, downloadResult.ErrorMessage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var actualSize = new FileInfo(downloadPath).Length;
|
|
||||||
string actualHash;
|
|
||||||
await using (var hashStream = File.OpenRead(downloadPath))
|
|
||||||
{
|
|
||||||
var hashBytes = await SHA256.HashDataAsync(hashStream, cancellationToken);
|
|
||||||
actualHash = Convert.ToHexString(hashBytes).ToLowerInvariant();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.Equals(actualHash, plugin.Sha256, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
AppLogger.Error(
|
|
||||||
"PluginMarket",
|
|
||||||
$"SHA-256 verification failed. PluginId='{plugin.Id}'; Version='{plugin.Version}'; DownloadUrl='{resolvedDownloadUrl}'; DownloadPath='{downloadPath}'; ExpectedHash='{plugin.Sha256}'; ActualHash='{actualHash}'; ExpectedSize='{plugin.PackageSizeBytes}'; ActualSize='{actualSize}'.");
|
|
||||||
File.Delete(downloadPath);
|
|
||||||
return new AirAppMarketInstallResult(
|
return new AirAppMarketInstallResult(
|
||||||
false,
|
false,
|
||||||
null,
|
null,
|
||||||
$"SHA-256 mismatch. Expected {plugin.Sha256}, actual {actualHash}. Expected size {plugin.PackageSizeBytes}, actual size {actualSize}. Source {resolvedDownloadUrl}.");
|
$"Plugins install helper was not found at '{helperPath}'.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Directory.CreateDirectory(_downloadsDirectory);
|
||||||
|
var sources = plugin.GetPackageSourcesInInstallOrder();
|
||||||
|
if (sources.Count == 0)
|
||||||
|
{
|
||||||
|
return new AirAppMarketInstallResult(
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
"Plugin does not declare any package sources.");
|
||||||
|
}
|
||||||
|
|
||||||
|
AppLogger.Info(
|
||||||
|
"PluginMarket",
|
||||||
|
$"Starting install. PluginId='{plugin.Id}'; Version='{plugin.Version}'; Sources='{string.Join(", ", sources.Select(source => source.SourceKind.ToString()))}'.");
|
||||||
|
|
||||||
|
var sourceErrors = new List<string>();
|
||||||
|
foreach (var source in sources)
|
||||||
|
{
|
||||||
|
var attemptResult = await TryInstallFromSourceAsync(plugin, source, cancellationToken).ConfigureAwait(false);
|
||||||
|
if (attemptResult.Success)
|
||||||
|
{
|
||||||
|
return new AirAppMarketInstallResult(true, attemptResult.Manifest, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attemptResult.Fatal)
|
||||||
|
{
|
||||||
|
return new AirAppMarketInstallResult(false, null, attemptResult.ErrorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(attemptResult.ErrorMessage))
|
||||||
|
{
|
||||||
|
sourceErrors.Add($"{source.SourceKind}: {attemptResult.ErrorMessage}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var combinedMessage = sourceErrors.Count == 0
|
||||||
|
? $"Failed to install plugin '{plugin.Id}' from all available package sources."
|
||||||
|
: $"Failed to install plugin '{plugin.Id}' from all available package sources. {string.Join(" ", sourceErrors)}";
|
||||||
|
return new AirAppMarketInstallResult(false, null, combinedMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<AirAppMarketInstallAttemptResult> TryInstallFromSourceAsync(
|
||||||
|
AirAppMarketPluginEntry plugin,
|
||||||
|
AirAppMarketPluginPackageSourceEntry source,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var attemptPath = 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.Warn(
|
||||||
|
"PluginMarket",
|
||||||
|
$"Resolved package source for '{plugin.Id}' to '{resolvedDownloadUrl}' using '{source.SourceKind}'.");
|
||||||
|
|
||||||
|
var acquireResult = await AcquirePackageAsync(plugin, source, resolvedDownloadUrl, attemptPath, cancellationToken).ConfigureAwait(false);
|
||||||
|
if (!acquireResult.Success)
|
||||||
|
{
|
||||||
|
TryDeleteFile(attemptPath);
|
||||||
|
return new AirAppMarketInstallAttemptResult(false, false, null, acquireResult.ErrorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
var verificationResult = await VerifyPackageAsync(plugin, attemptPath, cancellationToken).ConfigureAwait(false);
|
||||||
|
if (!verificationResult.Success)
|
||||||
|
{
|
||||||
|
TryDeleteFile(attemptPath);
|
||||||
|
return new AirAppMarketInstallAttemptResult(false, false, null, verificationResult.ErrorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
PluginManifest manifest;
|
PluginManifest manifest;
|
||||||
if (OperatingSystem.IsWindows())
|
if (OperatingSystem.IsWindows())
|
||||||
{
|
{
|
||||||
var helperResult = await _helperClient.InstallPackageAsync(
|
var helperResult = await _helperClient.InstallPackageAsync(
|
||||||
downloadPath,
|
attemptPath,
|
||||||
_runtime.PluginsDirectory,
|
_runtime.PluginsDirectory,
|
||||||
cancellationToken);
|
cancellationToken).ConfigureAwait(false);
|
||||||
if (!helperResult.Success || string.IsNullOrWhiteSpace(helperResult.InstalledPackagePath))
|
if (!helperResult.Success || string.IsNullOrWhiteSpace(helperResult.InstalledPackagePath))
|
||||||
{
|
{
|
||||||
return new AirAppMarketInstallResult(
|
var helperMessage = helperResult.ErrorMessage ?? "Plugins install helper failed.";
|
||||||
false,
|
AppLogger.Error(
|
||||||
null,
|
"PluginMarket",
|
||||||
helperResult.ErrorMessage ?? "Plugins install helper failed.");
|
$"Windows install helper failed for plugin '{plugin.Id}' from source '{source.SourceKind}'. Message='{helperMessage}'.");
|
||||||
|
return new AirAppMarketInstallAttemptResult(false, true, null, helperMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
manifest = _runtime.RegisterInstalledPluginPackage(helperResult.InstalledPackagePath);
|
manifest = _runtime.RegisterInstalledPluginPackage(helperResult.InstalledPackagePath);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
manifest = _runtime.InstallPluginPackage(downloadPath);
|
manifest = _runtime.InstallPluginPackage(attemptPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
AppLogger.Info(
|
AppLogger.Info(
|
||||||
"PluginMarket",
|
"PluginMarket",
|
||||||
$"Install staged successfully. PluginId='{manifest.Id}'; InstalledName='{manifest.Name}'; PackagePath='{downloadPath}'.");
|
$"Install staged successfully. PluginId='{manifest.Id}'; InstalledName='{manifest.Name}'; PackagePath='{attemptPath}'; SourceKind='{source.SourceKind}'.");
|
||||||
return new AirAppMarketInstallResult(true, manifest, null);
|
return new AirAppMarketInstallAttemptResult(true, true, manifest, null);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
AppLogger.Warn(
|
AppLogger.Warn(
|
||||||
"PluginMarket",
|
"PluginMarket",
|
||||||
$"Install canceled. PluginId='{plugin.Id}'; Version='{plugin.Version}'; DownloadPath='{downloadPath}'.");
|
$"Install canceled. PluginId='{plugin.Id}'; Version='{plugin.Version}'; SourceKind='{source.SourceKind}'; DownloadPath='{attemptPath}'.");
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
AppLogger.Error(
|
AppLogger.Error(
|
||||||
"PluginMarket",
|
"PluginMarket",
|
||||||
$"Install failed. PluginId='{plugin.Id}'; Version='{plugin.Version}'; DownloadPath='{downloadPath}'.",
|
$"Install attempt failed. PluginId='{plugin.Id}'; Version='{plugin.Version}'; SourceKind='{source.SourceKind}'; DownloadPath='{attemptPath}'.",
|
||||||
ex);
|
ex);
|
||||||
return new AirAppMarketInstallResult(false, null, ex.Message);
|
TryDeleteFile(attemptPath);
|
||||||
|
return new AirAppMarketInstallAttemptResult(false, false, null, ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<AirAppMarketAcquisitionResult> AcquirePackageAsync(
|
||||||
|
AirAppMarketPluginEntry plugin,
|
||||||
|
AirAppMarketPluginPackageSourceEntry source,
|
||||||
|
string resolvedDownloadUrl,
|
||||||
|
string attemptPath,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (AirAppMarketDefaults.TryResolveWorkspaceFile(resolvedDownloadUrl, out var localPackagePath))
|
||||||
|
{
|
||||||
|
if (source.SourceKind == PluginPackageSourceKind.WorkspaceLocal)
|
||||||
|
{
|
||||||
|
AppLogger.Info(
|
||||||
|
"PluginMarket",
|
||||||
|
$"Copying workspace package for '{plugin.Id}' from '{localPackagePath}' to '{attemptPath}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var localCopyResult = await _downloadService.DownloadAsync(
|
||||||
|
localPackagePath,
|
||||||
|
attemptPath,
|
||||||
|
new DownloadOptions(ExpectedSizeBytes: plugin.PackageSizeBytes),
|
||||||
|
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||||
|
if (!localCopyResult.Success)
|
||||||
|
{
|
||||||
|
return new AirAppMarketAcquisitionResult(false, localCopyResult.ErrorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new AirAppMarketAcquisitionResult(true, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (source.SourceKind == PluginPackageSourceKind.WorkspaceLocal)
|
||||||
|
{
|
||||||
|
return new AirAppMarketAcquisitionResult(
|
||||||
|
false,
|
||||||
|
$"Workspace package source '{source.Url}' could not be resolved to a local file.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var downloadResult = await _downloadService.DownloadAsync(
|
||||||
|
resolvedDownloadUrl,
|
||||||
|
attemptPath,
|
||||||
|
new DownloadOptions(ExpectedSizeBytes: plugin.PackageSizeBytes),
|
||||||
|
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||||
|
if (!downloadResult.Success)
|
||||||
|
{
|
||||||
|
return new AirAppMarketAcquisitionResult(false, downloadResult.ErrorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new AirAppMarketAcquisitionResult(true, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<AirAppMarketVerificationResult> VerifyPackageAsync(
|
||||||
|
AirAppMarketPluginEntry plugin,
|
||||||
|
string attemptPath,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var actualSize = new FileInfo(attemptPath).Length;
|
||||||
|
string actualHash;
|
||||||
|
await using (var hashStream = File.OpenRead(attemptPath))
|
||||||
|
{
|
||||||
|
var hashBytes = await SHA256.HashDataAsync(hashStream, cancellationToken).ConfigureAwait(false);
|
||||||
|
actualHash = Convert.ToHexString(hashBytes).ToLowerInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actualSize != plugin.PackageSizeBytes || !string.Equals(actualHash, plugin.Sha256, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
AppLogger.Error(
|
||||||
|
"PluginMarket",
|
||||||
|
$"Package verification failed. PluginId='{plugin.Id}'; Version='{plugin.Version}'; DownloadPath='{attemptPath}'; ExpectedHash='{plugin.Sha256}'; ActualHash='{actualHash}'; ExpectedSize='{plugin.PackageSizeBytes}'; ActualSize='{actualSize}'.");
|
||||||
|
return new AirAppMarketVerificationResult(
|
||||||
|
false,
|
||||||
|
$"Package verification failed. Expected SHA-256 {plugin.Sha256}, actual {actualHash}. Expected size {plugin.PackageSizeBytes}, actual size {actualSize}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new AirAppMarketVerificationResult(true, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolveHelperPath()
|
||||||
|
{
|
||||||
|
return Path.Combine(AppContext.BaseDirectory, "PluginsInstallHelper", HelperExecutableName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TryDeleteFile(string path)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (File.Exists(path))
|
||||||
|
{
|
||||||
|
File.Delete(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Ignore cleanup failures for temporary install artifacts.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,4 +274,18 @@ internal sealed class AirAppMarketInstallService : IDisposable
|
|||||||
var invalidChars = Path.GetInvalidFileNameChars();
|
var invalidChars = Path.GetInvalidFileNameChars();
|
||||||
return new string(value.Select(ch => invalidChars.Contains(ch) ? '_' : ch).ToArray());
|
return new string(value.Select(ch => invalidChars.Contains(ch) ? '_' : ch).ToArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed record AirAppMarketInstallAttemptResult(
|
||||||
|
bool Success,
|
||||||
|
bool Fatal,
|
||||||
|
PluginManifest? Manifest,
|
||||||
|
string? ErrorMessage);
|
||||||
|
|
||||||
|
private sealed record AirAppMarketAcquisitionResult(
|
||||||
|
bool Success,
|
||||||
|
string? ErrorMessage);
|
||||||
|
|
||||||
|
private sealed record AirAppMarketVerificationResult(
|
||||||
|
bool Success,
|
||||||
|
string? ErrorMessage);
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -22,14 +22,46 @@ internal sealed class AirAppMarketReleaseResolverService
|
|||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(plugin);
|
ArgumentNullException.ThrowIfNull(plugin);
|
||||||
|
|
||||||
if (!plugin.HasReleaseDownloadMetadata)
|
var firstSource = plugin.GetPackageSourcesInInstallOrder().FirstOrDefault();
|
||||||
|
if (firstSource is null)
|
||||||
{
|
{
|
||||||
return plugin.DownloadUrl;
|
return plugin.DownloadUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return await ResolveDownloadUrlAsync(plugin, firstSource, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> ResolveDownloadUrlAsync(
|
||||||
|
AirAppMarketPluginEntry plugin,
|
||||||
|
AirAppMarketPluginPackageSourceEntry source,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(plugin);
|
||||||
|
ArgumentNullException.ThrowIfNull(source);
|
||||||
|
|
||||||
|
return source.SourceKind switch
|
||||||
|
{
|
||||||
|
PluginPackageSourceKind.ReleaseAsset => await ResolveReleaseAssetDownloadUrlAsync(plugin, source, cancellationToken).ConfigureAwait(false),
|
||||||
|
PluginPackageSourceKind.RawFallback => source.Url,
|
||||||
|
PluginPackageSourceKind.WorkspaceLocal => source.Url,
|
||||||
|
_ => source.Url
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string> ResolveReleaseAssetDownloadUrlAsync(
|
||||||
|
AirAppMarketPluginEntry plugin,
|
||||||
|
AirAppMarketPluginPackageSourceEntry source,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var sourceUrl = source.Url;
|
||||||
|
if (!plugin.HasReleaseDownloadMetadata)
|
||||||
|
{
|
||||||
|
return sourceUrl;
|
||||||
|
}
|
||||||
|
|
||||||
if (!TryGetRepositoryIdentity(plugin, out var owner, out var repositoryName))
|
if (!TryGetRepositoryIdentity(plugin, out var owner, out var repositoryName))
|
||||||
{
|
{
|
||||||
return plugin.DownloadUrl;
|
return sourceUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
var releaseDownloadUrl = AirAppMarketDefaults.BuildGitHubReleaseDownloadUrl(
|
var releaseDownloadUrl = AirAppMarketDefaults.BuildGitHubReleaseDownloadUrl(
|
||||||
@@ -46,15 +78,15 @@ internal sealed class AirAppMarketReleaseResolverService
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var updateService = new GitHubReleaseUpdateService(owner, repositoryName, _httpClient);
|
using var updateService = new GitHubReleaseUpdateService(owner, repositoryName, _httpClient);
|
||||||
var release = await updateService.GetReleaseByTagAsync(plugin.ReleaseTag, cancellationToken);
|
var release = await updateService.GetReleaseByTagAsync(plugin.ReleaseTag, cancellationToken).ConfigureAwait(false);
|
||||||
var asset = release?.Assets.FirstOrDefault(candidate =>
|
var asset = release?.Assets.FirstOrDefault(candidate =>
|
||||||
string.Equals(candidate.Name, plugin.ReleaseAssetName, StringComparison.OrdinalIgnoreCase));
|
string.Equals(candidate.Name, plugin.ReleaseAssetName, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
return asset?.BrowserDownloadUrl ?? plugin.DownloadUrl;
|
return asset?.BrowserDownloadUrl ?? releaseDownloadUrl;
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
return plugin.DownloadUrl;
|
return releaseDownloadUrl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user