mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
fix.开发者调试工具设置无法正常持久化的问题。修复了插件无法进行更新的问题。
This commit is contained in:
16
CHANGELOG.md
16
CHANGELOG.md
@@ -1,13 +1,10 @@
|
|||||||
# 更新日志 / Changelog
|
# 更新日志 / Changelog
|
||||||
|
|
||||||
## [0.8.3.4](https://github.com/yourorg/LanMountainDesktop/releases/tag/v0.8.3.4) - 2026-04-12
|
## [0.8.3.5](https://github.com/yourorg/LanMountainDesktop/releases/tag/v0.8.3.5) - 2026-04-12
|
||||||
|
|
||||||
### 新增 (Added)
|
### 新增 (Added)
|
||||||
|
|
||||||
- ✨ **开发者调试工具**: 新增开发者调试工具,优化插件开发体验
|
- 无
|
||||||
- 提供便捷的调试功能,帮助开发者快速定位和解决问题
|
|
||||||
- 支持插件运行时状态监控和日志查看
|
|
||||||
- 提升插件开发效率和调试体验
|
|
||||||
|
|
||||||
### 变更 (Changed)
|
### 变更 (Changed)
|
||||||
|
|
||||||
@@ -15,12 +12,21 @@
|
|||||||
- 插件开发者可以通过 View 自定义设置页面的 UI 和交互
|
- 插件开发者可以通过 View 自定义设置页面的 UI 和交互
|
||||||
- 提供更灵活的设置页面展示方式,提升插件用户体验
|
- 提供更灵活的设置页面展示方式,提升插件用户体验
|
||||||
- 兼容原有的设置方式,平滑过渡
|
- 兼容原有的设置方式,平滑过渡
|
||||||
|
- 🔧 **三指滑动与融合桌面功能开关位置调整**: 将三指滑动与融合桌面功能开关移动到了开发者设置界面
|
||||||
|
- 优化设置页面结构,将高级功能集中管理
|
||||||
|
- 普通用户界面更加简洁,开发者可在已有的开发者设置界面中访问相关设置
|
||||||
|
|
||||||
### 修复 (Fixed)
|
### 修复 (Fixed)
|
||||||
|
|
||||||
- 🐛 **快捷方式组件透明问题**: 修复了快捷方式组件无法正常透明的问题
|
- 🐛 **快捷方式组件透明问题**: 修复了快捷方式组件无法正常透明的问题
|
||||||
- 问题原因: 组件背景透明属性设置异常或渲染层级问题
|
- 问题原因: 组件背景透明属性设置异常或渲染层级问题
|
||||||
- 修复方案: 修正透明属性配置,确保快捷方式组件背景透明效果正常显示
|
- 修复方案: 修正透明属性配置,确保快捷方式组件背景透明效果正常显示
|
||||||
|
- 🐛 **插件无法正常升级问题**: 修复了插件无法正常升级的问题
|
||||||
|
- 问题原因: 插件升级流程中存在异常,导致升级操作失败或中断
|
||||||
|
- 修复方案: 修复插件升级逻辑,确保插件可以正常检测、下载和安装更新
|
||||||
|
- 🐛 **开发者设置项持久化问题**: 修复了开发者设置项不能正确持久化的问题
|
||||||
|
- 问题原因: 开发者设置项的保存或读取逻辑存在缺陷,导致设置无法正确保存或恢复
|
||||||
|
- 修复方案: 修复设置持久化逻辑,确保开发者设置项能够正确保存并在重启后恢复
|
||||||
|
|
||||||
### 移除 (Removed)
|
### 移除 (Removed)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>disable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<AssemblyName>LanMountainDesktop.PluginUpgradeHelper</AssemblyName>
|
||||||
|
<RootNamespace>LanMountainDesktop.PluginUpgradeHelper</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\LanMountainDesktop.PluginSdk\LanMountainDesktop.PluginSdk.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
372
LanMountainDesktop.PluginUpgradeHelper/Program.cs
Normal file
372
LanMountainDesktop.PluginUpgradeHelper/Program.cs
Normal file
@@ -0,0 +1,372 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.IO.Compression;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading;
|
||||||
|
using LanMountainDesktop.PluginSdk;
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.PluginUpgradeHelper;
|
||||||
|
|
||||||
|
internal static class Program
|
||||||
|
{
|
||||||
|
private const string PendingUpgradesFileName = ".pending-plugin-upgrades.json";
|
||||||
|
private const string LogFileName = "plugin-upgrade-helper.log";
|
||||||
|
|
||||||
|
private static int Main(string[] args)
|
||||||
|
{
|
||||||
|
var logPath = Path.Combine(Path.GetTempPath(), "LanMountainDesktop", LogFileName);
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(logPath)!);
|
||||||
|
File.AppendAllText(logPath, $"\n[{DateTime.Now:O}] PluginUpgradeHelper started. Args: {string.Join(" ", args)}\n");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var parsedArgs = ParseArgs(args);
|
||||||
|
|
||||||
|
if (!parsedArgs.TryGetValue("plugins-dir", out var pluginsDirectory) ||
|
||||||
|
string.IsNullOrWhiteSpace(pluginsDirectory))
|
||||||
|
{
|
||||||
|
LogError(logPath, "Missing required argument: --plugins-dir");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parsedArgs.TryGetValue("parent-pid", out var parentPidStr) ||
|
||||||
|
!int.TryParse(parentPidStr, out var parentPid))
|
||||||
|
{
|
||||||
|
LogError(logPath, "Missing or invalid argument: --parent-pid");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedArgs.TryGetValue("launch", out var launchCommand);
|
||||||
|
|
||||||
|
LogInfo(logPath, $"Waiting for parent process {parentPid} to exit...");
|
||||||
|
WaitForParentProcess(parentPid);
|
||||||
|
|
||||||
|
LogInfo(logPath, $"Processing pending upgrades in '{pluginsDirectory}'...");
|
||||||
|
var upgradeResults = ProcessPendingUpgrades(pluginsDirectory, logPath);
|
||||||
|
|
||||||
|
LogInfo(logPath, $"Upgrades completed. Success: {upgradeResults.SuccessCount}, Failed: {upgradeResults.FailureCount}");
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(launchCommand))
|
||||||
|
{
|
||||||
|
LogInfo(logPath, $"Launching application: {launchCommand}");
|
||||||
|
LaunchApplication(launchCommand, parsedArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
return upgradeResults.FailureCount > 0 ? 2 : 0;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LogError(logPath, $"Unexpected error: {ex}");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WaitForParentProcess(int parentPid)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var parentProcess = Process.GetProcessById(parentPid);
|
||||||
|
parentProcess.WaitForExit(TimeSpan.FromSeconds(30));
|
||||||
|
}
|
||||||
|
catch (ArgumentException)
|
||||||
|
{
|
||||||
|
// Process already exited
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// Ignore errors, continue anyway
|
||||||
|
}
|
||||||
|
|
||||||
|
Thread.Sleep(500);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static UpgradeResults ProcessPendingUpgrades(string pluginsDirectory, string logPath)
|
||||||
|
{
|
||||||
|
var pendingUpgradesPath = Path.Combine(pluginsDirectory, PendingUpgradesFileName);
|
||||||
|
var successCount = 0;
|
||||||
|
var failureCount = 0;
|
||||||
|
|
||||||
|
if (!File.Exists(pendingUpgradesPath))
|
||||||
|
{
|
||||||
|
LogInfo(logPath, "No pending upgrades found.");
|
||||||
|
return new UpgradeResults(0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<PendingUpgrade>? pendingUpgrades;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var json = File.ReadAllText(pendingUpgradesPath);
|
||||||
|
pendingUpgrades = JsonSerializer.Deserialize<List<PendingUpgrade>>(json);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LogError(logPath, $"Failed to read pending upgrades: {ex.Message}");
|
||||||
|
return new UpgradeResults(0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pendingUpgrades is null || pendingUpgrades.Count == 0)
|
||||||
|
{
|
||||||
|
LogInfo(logPath, "No pending upgrades to process.");
|
||||||
|
return new UpgradeResults(0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
Directory.CreateDirectory(pluginsDirectory);
|
||||||
|
var pendingDeletionDir = Path.Combine(pluginsDirectory, ".pending-deletions");
|
||||||
|
Directory.CreateDirectory(pendingDeletionDir);
|
||||||
|
|
||||||
|
foreach (var upgrade in pendingUpgrades)
|
||||||
|
{
|
||||||
|
if (!upgrade.IsValid())
|
||||||
|
{
|
||||||
|
LogWarn(logPath, $"Skipping invalid upgrade entry for plugin '{upgrade.PluginId}'.");
|
||||||
|
failureCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
LogInfo(logPath, $"Processing upgrade for plugin '{upgrade.PluginId}' to version '{upgrade.TargetVersion}'...");
|
||||||
|
|
||||||
|
var manifest = ReadManifestFromPackage(upgrade.SourcePackagePath);
|
||||||
|
var destinationPath = Path.Combine(pluginsDirectory, BuildInstalledPackageFileName(manifest.Id));
|
||||||
|
|
||||||
|
RemoveExistingPluginPackages(pluginsDirectory, manifest.Id, destinationPath, pendingDeletionDir, logPath);
|
||||||
|
|
||||||
|
File.Copy(upgrade.SourcePackagePath, destinationPath, overwrite: true);
|
||||||
|
|
||||||
|
LogInfo(logPath, $"Successfully upgraded plugin '{upgrade.PluginId}' to '{upgrade.TargetVersion}'.");
|
||||||
|
successCount++;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LogError(logPath, $"Failed to upgrade plugin '{upgrade.PluginId}': {ex.Message}");
|
||||||
|
failureCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Delete(pendingUpgradesPath);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LogWarn(logPath, $"Failed to delete pending upgrades file: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
CleanupPendingDeletions(pendingDeletionDir, logPath);
|
||||||
|
|
||||||
|
return new UpgradeResults(successCount, failureCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RemoveExistingPluginPackages(
|
||||||
|
string pluginsDirectory,
|
||||||
|
string pluginId,
|
||||||
|
string destinationPath,
|
||||||
|
string pendingDeletionDir,
|
||||||
|
string logPath)
|
||||||
|
{
|
||||||
|
var runtimeRootDirectory = EnsureTrailingSeparator(Path.Combine(Path.GetFullPath(pluginsDirectory), ".runtime"));
|
||||||
|
|
||||||
|
foreach (var existingPackagePath in Directory
|
||||||
|
.EnumerateFiles(pluginsDirectory, "*.laapp", SearchOption.AllDirectories)
|
||||||
|
.Select(Path.GetFullPath)
|
||||||
|
.Where(path => !path.StartsWith(runtimeRootDirectory, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (string.Equals(existingPackagePath, Path.GetFullPath(destinationPath), StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var existingManifest = ReadManifestFromPackage(existingPackagePath);
|
||||||
|
if (!string.Equals(existingManifest.Id, pluginId, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
TryDeleteOrMoveFile(existingPackagePath, pendingDeletionDir, logPath);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Ignore unrelated or malformed packages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TryDeleteOrMoveFile(string filePath, string pendingDeletionDir, string logPath)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Delete(filePath);
|
||||||
|
LogInfo(logPath, $"Deleted existing package: {filePath}");
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
var fileName = Path.GetFileName(filePath);
|
||||||
|
var pendingPath = Path.Combine(pendingDeletionDir, $"{fileName}.{Guid.NewGuid():N}.pending");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Move(filePath, pendingPath);
|
||||||
|
LogInfo(logPath, $"Moved existing package to pending deletion: {filePath} -> {pendingPath}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LogWarn(logPath, $"Failed to move existing package '{filePath}': {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CleanupPendingDeletions(string pendingDeletionDir, string logPath)
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(pendingDeletionDir))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var pendingFile in Directory.EnumerateFiles(pendingDeletionDir, "*.pending"))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Delete(pendingFile);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LogWarn(logPath, $"Failed to delete pending file '{pendingFile}': {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Directory.GetFiles(pendingDeletionDir).Length == 0 &&
|
||||||
|
Directory.GetDirectories(pendingDeletionDir).Length == 0)
|
||||||
|
{
|
||||||
|
Directory.Delete(pendingDeletionDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void LaunchApplication(string launchCommand, Dictionary<string, string> args)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var startInfo = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = launchCommand,
|
||||||
|
UseShellExecute = true,
|
||||||
|
WorkingDirectory = args.TryGetValue("working-dir", out var workingDir)
|
||||||
|
? workingDir
|
||||||
|
: AppContext.BaseDirectory
|
||||||
|
};
|
||||||
|
|
||||||
|
if (args.TryGetValue("launch-args", out var launchArgs) && !string.IsNullOrWhiteSpace(launchArgs))
|
||||||
|
{
|
||||||
|
startInfo.Arguments = launchArgs;
|
||||||
|
}
|
||||||
|
|
||||||
|
Process.Start(startInfo);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"[PluginUpgradeHelper] Failed to launch application: {ex}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PluginManifest ReadManifestFromPackage(string packagePath)
|
||||||
|
{
|
||||||
|
using var archive = 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}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildInstalledPackageFileName(string pluginId)
|
||||||
|
{
|
||||||
|
var invalidChars = Path.GetInvalidFileNameChars();
|
||||||
|
var fileName = new string(pluginId.Select(ch => invalidChars.Contains(ch) ? '_' : ch).ToArray());
|
||||||
|
return fileName + ".laapp";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string EnsureTrailingSeparator(string path)
|
||||||
|
{
|
||||||
|
return path.EndsWith(Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal)
|
||||||
|
? path
|
||||||
|
: path + Path.DirectorySeparatorChar;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Dictionary<string, string> ParseArgs(string[] args)
|
||||||
|
{
|
||||||
|
var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
for (var i = 0; i < args.Length; i++)
|
||||||
|
{
|
||||||
|
var current = args[i];
|
||||||
|
if (!current.StartsWith("--", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var key = current[2..];
|
||||||
|
if (string.IsNullOrWhiteSpace(key) || i + 1 >= args.Length)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
values[key] = args[++i];
|
||||||
|
}
|
||||||
|
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void LogInfo(string logPath, string message)
|
||||||
|
{
|
||||||
|
File.AppendAllText(logPath, $"[{DateTime.Now:O}] [INFO] {message}\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void LogWarn(string logPath, string message)
|
||||||
|
{
|
||||||
|
File.AppendAllText(logPath, $"[{DateTime.Now:O}] [WARN] {message}\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void LogError(string logPath, string message)
|
||||||
|
{
|
||||||
|
File.AppendAllText(logPath, $"[{DateTime.Now:O}] [ERROR] {message}\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed record PendingUpgrade(
|
||||||
|
string PluginId,
|
||||||
|
string SourcePackagePath,
|
||||||
|
string TargetVersion,
|
||||||
|
DateTimeOffset CreatedAt)
|
||||||
|
{
|
||||||
|
public bool IsValid()
|
||||||
|
{
|
||||||
|
return !string.IsNullOrWhiteSpace(PluginId) &&
|
||||||
|
!string.IsNullOrWhiteSpace(SourcePackagePath) &&
|
||||||
|
!string.IsNullOrWhiteSpace(TargetVersion) &&
|
||||||
|
File.Exists(SourcePackagePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed record UpgradeResults(int SuccessCount, int FailureCount);
|
||||||
|
}
|
||||||
@@ -135,6 +135,9 @@ internal static class Program
|
|||||||
private static void RemoveExistingPluginPackages(string pluginsDirectory, string pluginId, string destinationPath, string stagingPath)
|
private static void RemoveExistingPluginPackages(string pluginsDirectory, string pluginId, string destinationPath, string stagingPath)
|
||||||
{
|
{
|
||||||
var runtimeRootDirectory = EnsureTrailingSeparator(Path.Combine(Path.GetFullPath(pluginsDirectory), PluginSdkInfo.RuntimeDirectoryName));
|
var runtimeRootDirectory = EnsureTrailingSeparator(Path.Combine(Path.GetFullPath(pluginsDirectory), PluginSdkInfo.RuntimeDirectoryName));
|
||||||
|
var pendingDeletionDir = Path.Combine(pluginsDirectory, ".pending-deletions");
|
||||||
|
Directory.CreateDirectory(pendingDeletionDir);
|
||||||
|
|
||||||
foreach (var existingPackagePath in Directory
|
foreach (var existingPackagePath in Directory
|
||||||
.EnumerateFiles(pluginsDirectory, "*" + PluginSdkInfo.PackageFileExtension, SearchOption.AllDirectories)
|
.EnumerateFiles(pluginsDirectory, "*" + PluginSdkInfo.PackageFileExtension, SearchOption.AllDirectories)
|
||||||
.Select(Path.GetFullPath)
|
.Select(Path.GetFullPath)
|
||||||
@@ -154,13 +157,58 @@ internal static class Program
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
DeleteFileWithRetry(existingPackagePath);
|
TryRemoveExistingPackage(existingPackagePath, pendingDeletionDir);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
// Ignore unrelated or malformed packages while replacing an install target.
|
// Ignore unrelated or malformed packages while replacing an install target.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CleanupPendingDeletions(pendingDeletionDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static 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");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Move(existingPackagePath, pendingPath);
|
||||||
|
}
|
||||||
|
catch (IOException moveEx)
|
||||||
|
{
|
||||||
|
throw new IOException(
|
||||||
|
$"Cannot delete or move existing plugin package '{existingPackagePath}'. " +
|
||||||
|
$"The file may be in use by another process. Error: {moveEx.Message}", moveEx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CleanupPendingDeletions(string pendingDeletionDir)
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(pendingDeletionDir))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var pendingFile in Directory.EnumerateFiles(pendingDeletionDir, "*.pending"))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Delete(pendingFile);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Ignore cleanup failures for pending deletions.
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void CopyWithRetry(string sourcePath, string destinationPath, bool overwrite)
|
private static void CopyWithRetry(string sourcePath, string destinationPath, bool overwrite)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
<Project Path="LanMountainDesktop.PluginSdk/LanMountainDesktop.PluginSdk.csproj" />
|
<Project Path="LanMountainDesktop.PluginSdk/LanMountainDesktop.PluginSdk.csproj" />
|
||||||
<Project Path="LanMountainDesktop.PluginTemplate/LanMountainDesktop.PluginTemplate.csproj" />
|
<Project Path="LanMountainDesktop.PluginTemplate/LanMountainDesktop.PluginTemplate.csproj" />
|
||||||
<Project Path="LanMountainDesktop.PluginsInstallHelper/LanMountainDesktop.PluginsInstallHelper.csproj" />
|
<Project Path="LanMountainDesktop.PluginsInstallHelper/LanMountainDesktop.PluginsInstallHelper.csproj" />
|
||||||
|
<Project Path="LanMountainDesktop.PluginUpgradeHelper/LanMountainDesktop.PluginUpgradeHelper.csproj" />
|
||||||
<Project Path="LanMountainDesktop/LanMountainDesktop.csproj" />
|
<Project Path="LanMountainDesktop/LanMountainDesktop.csproj" />
|
||||||
<Project Path="LanMountainDesktop.Tests/LanMountainDesktop.Tests.csproj" />
|
<Project Path="LanMountainDesktop.Tests/LanMountainDesktop.Tests.csproj" />
|
||||||
</Solution>
|
</Solution>
|
||||||
|
|||||||
@@ -404,10 +404,7 @@ public partial class App : Application
|
|||||||
_traySettingsMenuItem.Header = L("tray.menu.settings", "Settings");
|
_traySettingsMenuItem.Header = L("tray.menu.settings", "Settings");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_trayComponentLibraryMenuItem is not null)
|
RefreshFusedDesktopMenuItemVisibility();
|
||||||
{
|
|
||||||
_trayComponentLibraryMenuItem.Header = L("tray.menu.component_library", "Component Library");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_trayRestartMenuItem is not null)
|
if (_trayRestartMenuItem is not null)
|
||||||
{
|
{
|
||||||
@@ -420,6 +417,30 @@ public partial class App : Application
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void RefreshFusedDesktopMenuItemVisibility()
|
||||||
|
{
|
||||||
|
if (_trayComponentLibraryMenuItem is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 仅在 Windows 上支持融合桌面功能
|
||||||
|
if (!OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
_trayComponentLibraryMenuItem.IsVisible = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查融合桌面功能是否启用
|
||||||
|
var appSnapshot = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
|
||||||
|
_trayComponentLibraryMenuItem.IsVisible = appSnapshot.EnableFusedDesktop;
|
||||||
|
|
||||||
|
if (_trayComponentLibraryMenuItem.IsVisible)
|
||||||
|
{
|
||||||
|
_trayComponentLibraryMenuItem.Header = L("tray.menu.component_library", "Component Library");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void DisposeTrayIcon()
|
private void DisposeTrayIcon()
|
||||||
{
|
{
|
||||||
if (_trayIcon is null)
|
if (_trayIcon is null)
|
||||||
@@ -687,6 +708,16 @@ public partial class App : Application
|
|||||||
ApplyCurrentCultureFromSettings();
|
ApplyCurrentCultureFromSettings();
|
||||||
RefreshTrayIconContent();
|
RefreshTrayIconContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查融合桌面设置是否变更
|
||||||
|
var fusedDesktopChanged =
|
||||||
|
refreshAll ||
|
||||||
|
changedKeys.Contains(nameof(AppSettingsSnapshot.EnableFusedDesktop), StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
if (fusedDesktopChanged)
|
||||||
|
{
|
||||||
|
RefreshFusedDesktopMenuItemVisibility();
|
||||||
|
}
|
||||||
}, DispatcherPriority.Background);
|
}, DispatcherPriority.Background);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -152,6 +152,8 @@ public sealed class AppSettingsSnapshot
|
|||||||
|
|
||||||
public bool EnableThreeFingerSwipe { get; set; } = false;
|
public bool EnableThreeFingerSwipe { get; set; } = false;
|
||||||
|
|
||||||
|
public bool EnableFusedDesktop { get; set; } = false;
|
||||||
|
|
||||||
public List<string> DisabledPluginIds { get; set; } = [];
|
public List<string> DisabledPluginIds { get; set; } = [];
|
||||||
|
|
||||||
public bool IsDevModeEnabled { get; set; }
|
public bool IsDevModeEnabled { get; set; }
|
||||||
|
|||||||
@@ -58,6 +58,14 @@ internal sealed class FusedDesktopManagerService : IFusedDesktopManagerService
|
|||||||
{
|
{
|
||||||
if (!OperatingSystem.IsWindows()) return;
|
if (!OperatingSystem.IsWindows()) return;
|
||||||
|
|
||||||
|
// 检查融合桌面功能是否启用
|
||||||
|
var appSnapshot = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
|
||||||
|
if (!appSnapshot.EnableFusedDesktop)
|
||||||
|
{
|
||||||
|
AppLogger.Info("FusedDesktop", "Fused desktop is disabled. Skipping initialization.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
EnsureRegistries();
|
EnsureRegistries();
|
||||||
ReloadWidgets();
|
ReloadWidgets();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls.ApplicationLifetimes;
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
@@ -9,6 +10,8 @@ namespace LanMountainDesktop.Services;
|
|||||||
|
|
||||||
public sealed class HostApplicationLifecycleService : IHostApplicationLifecycle
|
public sealed class HostApplicationLifecycleService : IHostApplicationLifecycle
|
||||||
{
|
{
|
||||||
|
private const string UpgradeHelperExecutableName = "LanMountainDesktop.PluginUpgradeHelper.exe";
|
||||||
|
|
||||||
public bool TryExit(HostApplicationLifecycleRequest? request = null)
|
public bool TryExit(HostApplicationLifecycleRequest? request = null)
|
||||||
{
|
{
|
||||||
App? app = null;
|
App? app = null;
|
||||||
@@ -50,28 +53,14 @@ public sealed class HostApplicationLifecycleService : IHostApplicationLifecycle
|
|||||||
App? app = null;
|
App? app = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var startInfo = AppRestartService.CreateRestartStartInfo();
|
app = Application.Current as App;
|
||||||
if (startInfo is null)
|
|
||||||
|
if (HasPendingPluginUpgrades())
|
||||||
{
|
{
|
||||||
AppLogger.Warn(
|
return TryRestartWithUpgradeHelper(request);
|
||||||
"HostLifecycle",
|
|
||||||
$"Restart request rejected because restart start info could not be resolved. Source='{request?.Source ?? "Unknown"}'.");
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Process.Start(startInfo);
|
return TryRestartDirectly(request);
|
||||||
app = Application.Current as App;
|
|
||||||
app?.PrepareForShutdown(isRestart: true, request?.Source ?? "Unknown");
|
|
||||||
var exitRequest = request is null
|
|
||||||
? new HostApplicationLifecycleRequest(Reason: "Restart accepted.")
|
|
||||||
: request with
|
|
||||||
{
|
|
||||||
Reason = string.IsNullOrWhiteSpace(request.Reason)
|
|
||||||
? "Restart accepted."
|
|
||||||
: request.Reason
|
|
||||||
};
|
|
||||||
|
|
||||||
return TryExit(exitRequest);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -80,4 +69,92 @@ public sealed class HostApplicationLifecycleService : IHostApplicationLifecycle
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool HasPendingPluginUpgrades()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var pluginsDirectory = Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||||
|
"LanMountainDesktop",
|
||||||
|
"Extensions",
|
||||||
|
"Plugins");
|
||||||
|
var pendingUpgradesPath = Path.Combine(pluginsDirectory, ".pending-plugin-upgrades.json");
|
||||||
|
return File.Exists(pendingUpgradesPath);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryRestartWithUpgradeHelper(HostApplicationLifecycleRequest? request)
|
||||||
|
{
|
||||||
|
AppLogger.Info("HostLifecycle", "Detected pending plugin upgrades. Using upgrade helper for restart.");
|
||||||
|
|
||||||
|
var helperPath = ResolveUpgradeHelperPath();
|
||||||
|
if (!File.Exists(helperPath))
|
||||||
|
{
|
||||||
|
AppLogger.Warn("HostLifecycle", $"Upgrade helper not found at '{helperPath}'. Falling back to direct restart.");
|
||||||
|
return TryRestartDirectly(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
var pluginsDirectory = Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||||
|
"LanMountainDesktop",
|
||||||
|
"Extensions",
|
||||||
|
"Plugins");
|
||||||
|
|
||||||
|
var startInfo = AppRestartService.CreateRestartStartInfo();
|
||||||
|
var launchCommand = startInfo?.FileName ?? Process.GetCurrentProcess().MainModule?.FileName ?? AppContext.BaseDirectory;
|
||||||
|
var launchArgs = startInfo?.Arguments ?? "";
|
||||||
|
|
||||||
|
var helperStartInfo = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = helperPath,
|
||||||
|
Arguments = $"--plugins-dir \"{pluginsDirectory}\" --parent-pid {Environment.ProcessId} --launch \"{launchCommand}\" --launch-args \"{launchArgs}\" --working-dir \"{AppContext.BaseDirectory}\"",
|
||||||
|
UseShellExecute = true,
|
||||||
|
WorkingDirectory = AppContext.BaseDirectory
|
||||||
|
};
|
||||||
|
|
||||||
|
AppLogger.Info("HostLifecycle", $"Starting upgrade helper: {helperStartInfo.FileName} {helperStartInfo.Arguments}");
|
||||||
|
|
||||||
|
Process.Start(helperStartInfo);
|
||||||
|
|
||||||
|
var app = Application.Current as App;
|
||||||
|
app?.PrepareForShutdown(isRestart: true, request?.Source ?? "Unknown");
|
||||||
|
|
||||||
|
return TryExit(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryRestartDirectly(HostApplicationLifecycleRequest? request)
|
||||||
|
{
|
||||||
|
var startInfo = AppRestartService.CreateRestartStartInfo();
|
||||||
|
if (startInfo is null)
|
||||||
|
{
|
||||||
|
AppLogger.Warn(
|
||||||
|
"HostLifecycle",
|
||||||
|
$"Restart request rejected because restart start info could not be resolved. Source='{request?.Source ?? "Unknown"}'.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Process.Start(startInfo);
|
||||||
|
var app = Application.Current as App;
|
||||||
|
app?.PrepareForShutdown(isRestart: true, request?.Source ?? "Unknown");
|
||||||
|
var exitRequest = request is null
|
||||||
|
? new HostApplicationLifecycleRequest(Reason: "Restart accepted.")
|
||||||
|
: request with
|
||||||
|
{
|
||||||
|
Reason = string.IsNullOrWhiteSpace(request.Reason)
|
||||||
|
? "Restart accepted."
|
||||||
|
: request.Reason
|
||||||
|
};
|
||||||
|
|
||||||
|
return TryExit(exitRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolveUpgradeHelperPath()
|
||||||
|
{
|
||||||
|
return Path.Combine(AppContext.BaseDirectory, "PluginUpgradeHelper", UpgradeHelperExecutableName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
160
LanMountainDesktop/Services/PendingPluginUpgradeService.cs
Normal file
160
LanMountainDesktop/Services/PendingPluginUpgradeService.cs
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.Services;
|
||||||
|
|
||||||
|
public sealed class PendingPluginUpgradeService
|
||||||
|
{
|
||||||
|
private const string PendingUpgradesFileName = ".pending-plugin-upgrades.json";
|
||||||
|
private static readonly Lock Gate = new();
|
||||||
|
private readonly string _pendingUpgradesFilePath;
|
||||||
|
|
||||||
|
public PendingPluginUpgradeService(string pluginsDirectory)
|
||||||
|
{
|
||||||
|
_pendingUpgradesFilePath = Path.Combine(pluginsDirectory, PendingUpgradesFileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<PendingPluginUpgrade> GetPendingUpgrades()
|
||||||
|
{
|
||||||
|
lock (Gate)
|
||||||
|
{
|
||||||
|
return ReadPendingUpgradesCore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddPendingUpgrade(string pluginId, string sourcePackagePath, string targetVersion)
|
||||||
|
{
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(pluginId);
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(sourcePackagePath);
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(targetVersion);
|
||||||
|
|
||||||
|
lock (Gate)
|
||||||
|
{
|
||||||
|
var upgrades = ReadPendingUpgradesCore().ToList();
|
||||||
|
|
||||||
|
upgrades.RemoveAll(u =>
|
||||||
|
string.Equals(u.PluginId, pluginId, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
upgrades.Add(new PendingPluginUpgrade(
|
||||||
|
pluginId,
|
||||||
|
Path.GetFullPath(sourcePackagePath),
|
||||||
|
targetVersion,
|
||||||
|
DateTimeOffset.UtcNow));
|
||||||
|
|
||||||
|
SavePendingUpgradesCore(upgrades);
|
||||||
|
|
||||||
|
AppLogger.Info(
|
||||||
|
"PendingPluginUpgrade",
|
||||||
|
$"Added pending upgrade. PluginId='{pluginId}'; TargetVersion='{targetVersion}'; SourcePath='{sourcePackagePath}'.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemovePendingUpgrade(string pluginId)
|
||||||
|
{
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(pluginId);
|
||||||
|
|
||||||
|
lock (Gate)
|
||||||
|
{
|
||||||
|
var upgrades = ReadPendingUpgradesCore().ToList();
|
||||||
|
var removed = upgrades.RemoveAll(u =>
|
||||||
|
string.Equals(u.PluginId, pluginId, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
if (removed > 0)
|
||||||
|
{
|
||||||
|
SavePendingUpgradesCore(upgrades);
|
||||||
|
AppLogger.Info(
|
||||||
|
"PendingPluginUpgrade",
|
||||||
|
$"Removed pending upgrade. PluginId='{pluginId}'.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearPendingUpgrades()
|
||||||
|
{
|
||||||
|
lock (Gate)
|
||||||
|
{
|
||||||
|
if (File.Exists(_pendingUpgradesFilePath))
|
||||||
|
{
|
||||||
|
File.Delete(_pendingUpgradesFilePath);
|
||||||
|
AppLogger.Info("PendingPluginUpgrade", "Cleared all pending upgrades.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasPendingUpgrades()
|
||||||
|
{
|
||||||
|
lock (Gate)
|
||||||
|
{
|
||||||
|
return ReadPendingUpgradesCore().Count > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<PendingPluginUpgrade> ReadPendingUpgradesCore()
|
||||||
|
{
|
||||||
|
if (!File.Exists(_pendingUpgradesFilePath))
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var json = File.ReadAllText(_pendingUpgradesFilePath);
|
||||||
|
var upgrades = JsonSerializer.Deserialize<List<PendingPluginUpgrade>>(json);
|
||||||
|
return upgrades?.Where(u => u.IsValid()).ToList() ?? [];
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AppLogger.Warn(
|
||||||
|
"PendingPluginUpgrade",
|
||||||
|
$"Failed to read pending upgrades from '{_pendingUpgradesFilePath}'.",
|
||||||
|
ex);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SavePendingUpgradesCore(List<PendingPluginUpgrade> upgrades)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var directory = Path.GetDirectoryName(_pendingUpgradesFilePath);
|
||||||
|
if (!string.IsNullOrWhiteSpace(directory))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(directory);
|
||||||
|
}
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(upgrades, new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
WriteIndented = true
|
||||||
|
});
|
||||||
|
|
||||||
|
File.WriteAllText(_pendingUpgradesFilePath, json);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AppLogger.Error(
|
||||||
|
"PendingPluginUpgrade",
|
||||||
|
$"Failed to save pending upgrades to '{_pendingUpgradesFilePath}'.",
|
||||||
|
ex);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record PendingPluginUpgrade(
|
||||||
|
string PluginId,
|
||||||
|
string SourcePackagePath,
|
||||||
|
string TargetVersion,
|
||||||
|
DateTimeOffset CreatedAt)
|
||||||
|
{
|
||||||
|
public bool IsValid()
|
||||||
|
{
|
||||||
|
return !string.IsNullOrWhiteSpace(PluginId) &&
|
||||||
|
!string.IsNullOrWhiteSpace(SourcePackagePath) &&
|
||||||
|
!string.IsNullOrWhiteSpace(TargetVersion) &&
|
||||||
|
File.Exists(SourcePackagePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -174,9 +174,6 @@ public sealed partial class GeneralSettingsPageViewModel : ViewModelBase, IDispo
|
|||||||
private bool _isInitializing;
|
private bool _isInitializing;
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
private bool _enableThreeFingerSwipe;
|
|
||||||
|
|
||||||
public GeneralSettingsPageViewModel(ISettingsFacadeService settingsFacade)
|
public GeneralSettingsPageViewModel(ISettingsFacadeService settingsFacade)
|
||||||
{
|
{
|
||||||
_settingsFacade = settingsFacade;
|
_settingsFacade = settingsFacade;
|
||||||
@@ -204,7 +201,6 @@ public sealed partial class GeneralSettingsPageViewModel : ViewModelBase, IDispo
|
|||||||
SelectedRenderMode = RenderModes.FirstOrDefault(option =>
|
SelectedRenderMode = RenderModes.FirstOrDefault(option =>
|
||||||
string.Equals(option.Value, normalizedRenderMode, StringComparison.OrdinalIgnoreCase))
|
string.Equals(option.Value, normalizedRenderMode, StringComparison.OrdinalIgnoreCase))
|
||||||
?? RenderModes[0];
|
?? RenderModes[0];
|
||||||
EnableThreeFingerSwipe = appSnapshot.EnableThreeFingerSwipe;
|
|
||||||
_isInitializing = false;
|
_isInitializing = false;
|
||||||
|
|
||||||
RefreshPreview();
|
RefreshPreview();
|
||||||
@@ -236,33 +232,6 @@ public sealed partial class GeneralSettingsPageViewModel : ViewModelBase, IDispo
|
|||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果是其他设置变更,重新加载我们的设置
|
|
||||||
_isInitializing = true;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var appSnapshot = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
|
|
||||||
EnableThreeFingerSwipe = appSnapshot.EnableThreeFingerSwipe;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_isInitializing = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
partial void OnEnableThreeFingerSwipeChanged(bool value)
|
|
||||||
{
|
|
||||||
if (_isInitializing)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var appSnapshot = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
|
|
||||||
appSnapshot.EnableThreeFingerSwipe = value;
|
|
||||||
_settingsFacade.Settings.SaveSnapshot(
|
|
||||||
SettingsScope.App,
|
|
||||||
appSnapshot,
|
|
||||||
changedKeys: [nameof(AppSettingsSnapshot.EnableThreeFingerSwipe)]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public event Action? RestartRequested;
|
public event Action? RestartRequested;
|
||||||
@@ -3100,6 +3069,9 @@ public sealed partial class DevSettingsPageViewModel : ViewModelBase
|
|||||||
_isInitializing = true;
|
_isInitializing = true;
|
||||||
LoadSettings();
|
LoadSettings();
|
||||||
_isInitializing = false;
|
_isInitializing = false;
|
||||||
|
|
||||||
|
// 监听设置变更,防止被意外重置
|
||||||
|
_settingsFacade.Settings.Changed += OnSettingsChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
@@ -3108,6 +3080,12 @@ public sealed partial class DevSettingsPageViewModel : ViewModelBase
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _devPluginPath = string.Empty;
|
private string _devPluginPath = string.Empty;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _enableThreeFingerSwipe;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _enableFusedDesktop;
|
||||||
|
|
||||||
partial void OnIsDevModeEnabledChanged(bool value)
|
partial void OnIsDevModeEnabledChanged(bool value)
|
||||||
{
|
{
|
||||||
if (_isInitializing) return;
|
if (_isInitializing) return;
|
||||||
@@ -3120,11 +3098,52 @@ public sealed partial class DevSettingsPageViewModel : ViewModelBase
|
|||||||
SaveField(nameof(AppSettingsSnapshot.DevPluginPath), value);
|
SaveField(nameof(AppSettingsSnapshot.DevPluginPath), value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
partial void OnEnableThreeFingerSwipeChanged(bool value)
|
||||||
|
{
|
||||||
|
if (_isInitializing) return;
|
||||||
|
SaveField(nameof(AppSettingsSnapshot.EnableThreeFingerSwipe), value);
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnEnableFusedDesktopChanged(bool value)
|
||||||
|
{
|
||||||
|
if (_isInitializing) return;
|
||||||
|
SaveField(nameof(AppSettingsSnapshot.EnableFusedDesktop), value);
|
||||||
|
}
|
||||||
|
|
||||||
private void LoadSettings()
|
private void LoadSettings()
|
||||||
{
|
{
|
||||||
var snapshot = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
|
var snapshot = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
|
||||||
IsDevModeEnabled = snapshot.IsDevModeEnabled;
|
IsDevModeEnabled = snapshot.IsDevModeEnabled;
|
||||||
DevPluginPath = snapshot.DevPluginPath ?? string.Empty;
|
DevPluginPath = snapshot.DevPluginPath ?? string.Empty;
|
||||||
|
EnableThreeFingerSwipe = snapshot.EnableThreeFingerSwipe;
|
||||||
|
EnableFusedDesktop = snapshot.EnableFusedDesktop;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSettingsChanged(object? sender, SettingsChangedEvent e)
|
||||||
|
{
|
||||||
|
if (e.Scope != SettingsScope.App)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var changedKeys = e.ChangedKeys?.ToArray();
|
||||||
|
if (changedKeys is null || changedKeys.Length == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是其他设置变更,重新加载我们的设置
|
||||||
|
_isInitializing = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var snapshot = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
|
||||||
|
EnableThreeFingerSwipe = snapshot.EnableThreeFingerSwipe;
|
||||||
|
EnableFusedDesktop = snapshot.EnableFusedDesktop;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_isInitializing = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SaveField<T>(string key, T value)
|
private void SaveField<T>(string key, T value)
|
||||||
|
|||||||
@@ -25,6 +25,26 @@
|
|||||||
</ui:SettingsExpander.Footer>
|
</ui:SettingsExpander.Footer>
|
||||||
</ui:SettingsExpander>
|
</ui:SettingsExpander>
|
||||||
|
|
||||||
|
<ui:SettingsExpander Header="启用三指滑动"
|
||||||
|
Description="使用三根手指或鼠标右键拖动自由滑动页面,在第一页向右滑动可回到 Windows 桌面(实验性功能)">
|
||||||
|
<ui:SettingsExpander.IconSource>
|
||||||
|
<fi:SymbolIconSource Symbol="Gesture" />
|
||||||
|
</ui:SettingsExpander.IconSource>
|
||||||
|
<ui:SettingsExpander.Footer>
|
||||||
|
<ToggleSwitch IsChecked="{Binding EnableThreeFingerSwipe}" />
|
||||||
|
</ui:SettingsExpander.Footer>
|
||||||
|
</ui:SettingsExpander>
|
||||||
|
|
||||||
|
<ui:SettingsExpander Header="启用融合桌面"
|
||||||
|
Description="允许将组件放置在 Windows 系统桌面上(实验性功能,重启后生效)">
|
||||||
|
<ui:SettingsExpander.IconSource>
|
||||||
|
<fi:SymbolIconSource Symbol="Apps" />
|
||||||
|
</ui:SettingsExpander.IconSource>
|
||||||
|
<ui:SettingsExpander.Footer>
|
||||||
|
<ToggleSwitch IsChecked="{Binding EnableFusedDesktop}" />
|
||||||
|
</ui:SettingsExpander.Footer>
|
||||||
|
</ui:SettingsExpander>
|
||||||
|
|
||||||
<Separator Classes="settings-separator" />
|
<Separator Classes="settings-separator" />
|
||||||
|
|
||||||
<ui:SettingsExpander Header="开发者插件路径"
|
<ui:SettingsExpander Header="开发者插件路径"
|
||||||
|
|||||||
@@ -14,13 +14,6 @@
|
|||||||
Text="{Binding BasicHeader}"
|
Text="{Binding BasicHeader}"
|
||||||
Margin="0,0,0,4" />
|
Margin="0,0,0,4" />
|
||||||
|
|
||||||
<ui:SettingsExpander Header="启用三指滑动"
|
|
||||||
Description="使用三根手指或鼠标右键拖动自由滑动页面,在第一页向右滑动可回到 Windows 桌面">
|
|
||||||
<ui:SettingsExpander.Footer>
|
|
||||||
<ToggleSwitch IsChecked="{Binding EnableThreeFingerSwipe}" />
|
|
||||||
</ui:SettingsExpander.Footer>
|
|
||||||
</ui:SettingsExpander>
|
|
||||||
|
|
||||||
<ui:SettingsExpander Header="{Binding LanguageHeader}">
|
<ui:SettingsExpander Header="{Binding LanguageHeader}">
|
||||||
<ui:SettingsExpander.IconSource>
|
<ui:SettingsExpander.IconSource>
|
||||||
<fi:SymbolIconSource Symbol="Settings" />
|
<fi:SymbolIconSource Symbol="Settings" />
|
||||||
|
|||||||
@@ -784,12 +784,28 @@ internal sealed class PluginMarketEmbeddedView : UserControl, IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
RefreshInstalledSnapshot();
|
RefreshInstalledSnapshot();
|
||||||
SetStatus(
|
|
||||||
F(
|
if (result.RestartRequired)
|
||||||
"market.status.install_success_format",
|
{
|
||||||
"Plugin '{0}' has been staged. Restart the app to apply it.",
|
SetStatus(
|
||||||
result.Manifest.Name),
|
F(
|
||||||
SuccessBrush);
|
"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();
|
RebuildSurface();
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException) when (_lifetimeCts.IsCancellationRequested)
|
catch (OperationCanceledException) when (_lifetimeCts.IsCancellationRequested)
|
||||||
@@ -1015,14 +1031,22 @@ internal sealed class PluginMarketEmbeddedView : UserControl, IDisposable
|
|||||||
|
|
||||||
private static int CompareVersions(string? left, string? right)
|
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));
|
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.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
|
using System.Reflection;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
@@ -20,7 +21,9 @@ internal sealed class AirAppMarketInstallService : IDisposable
|
|||||||
private readonly HttpClient _httpClient;
|
private readonly HttpClient _httpClient;
|
||||||
private readonly ResumableDownloadService _downloadService;
|
private readonly ResumableDownloadService _downloadService;
|
||||||
private readonly AirAppMarketReleaseResolverService _releaseResolverService;
|
private readonly AirAppMarketReleaseResolverService _releaseResolverService;
|
||||||
|
private readonly PendingPluginUpgradeService _pendingUpgradeService;
|
||||||
private readonly string _downloadsDirectory;
|
private readonly string _downloadsDirectory;
|
||||||
|
private readonly Version? _hostVersion;
|
||||||
|
|
||||||
public AirAppMarketInstallService(PluginRuntimeService runtime, string dataDirectory)
|
public AirAppMarketInstallService(PluginRuntimeService runtime, string dataDirectory)
|
||||||
{
|
{
|
||||||
@@ -33,6 +36,8 @@ internal sealed class AirAppMarketInstallService : IDisposable
|
|||||||
_httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("LanMountainDesktop-PluginMarketplace/1.0");
|
_httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("LanMountainDesktop-PluginMarketplace/1.0");
|
||||||
_downloadService = new ResumableDownloadService(_httpClient);
|
_downloadService = new ResumableDownloadService(_httpClient);
|
||||||
_releaseResolverService = new AirAppMarketReleaseResolverService(_httpClient);
|
_releaseResolverService = new AirAppMarketReleaseResolverService(_httpClient);
|
||||||
|
_pendingUpgradeService = new PendingPluginUpgradeService(runtime.PluginsDirectory);
|
||||||
|
_hostVersion = typeof(App).Assembly.GetName().Version;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<AirAppMarketInstallResult> InstallAsync(
|
public async Task<AirAppMarketInstallResult> InstallAsync(
|
||||||
@@ -41,18 +46,6 @@ internal sealed class AirAppMarketInstallService : IDisposable
|
|||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(plugin);
|
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);
|
Directory.CreateDirectory(_downloadsDirectory);
|
||||||
var sources = plugin.GetPackageSourcesInInstallOrder();
|
var sources = plugin.GetPackageSourcesInInstallOrder();
|
||||||
if (sources.Count == 0)
|
if (sources.Count == 0)
|
||||||
@@ -67,6 +60,39 @@ internal sealed class AirAppMarketInstallService : IDisposable
|
|||||||
"PluginMarket",
|
"PluginMarket",
|
||||||
$"Starting install. PluginId='{plugin.Id}'; Version='{plugin.Version}'; Sources='{string.Join(", ", sources.Select(source => source.SourceKind.ToString()))}'.");
|
$"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>();
|
var sourceErrors = new List<string>();
|
||||||
foreach (var source in sources)
|
foreach (var source in sources)
|
||||||
{
|
{
|
||||||
@@ -93,6 +119,88 @@ internal sealed class AirAppMarketInstallService : IDisposable
|
|||||||
return new AirAppMarketInstallResult(false, null, combinedMessage);
|
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(
|
private async Task<AirAppMarketInstallAttemptResult> TryInstallFromSourceAsync(
|
||||||
AirAppMarketPluginEntry plugin,
|
AirAppMarketPluginEntry plugin,
|
||||||
AirAppMarketPluginPackageSourceEntry source,
|
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()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_httpClient.Dispose();
|
_httpClient.Dispose();
|
||||||
@@ -299,4 +472,9 @@ internal sealed class AirAppMarketInstallService : IDisposable
|
|||||||
private sealed record AirAppMarketVerificationResult(
|
private sealed record AirAppMarketVerificationResult(
|
||||||
bool Success,
|
bool Success,
|
||||||
string? ErrorMessage);
|
string? ErrorMessage);
|
||||||
|
|
||||||
|
private sealed record DownloadPackageResult(
|
||||||
|
bool Success,
|
||||||
|
string? PackagePath,
|
||||||
|
string? ErrorMessage);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -305,7 +305,8 @@ internal sealed record AirAppMarketLoadResult(
|
|||||||
internal sealed record AirAppMarketInstallResult(
|
internal sealed record AirAppMarketInstallResult(
|
||||||
bool Success,
|
bool Success,
|
||||||
PluginManifest? Manifest,
|
PluginManifest? Manifest,
|
||||||
string? ErrorMessage);
|
string? ErrorMessage,
|
||||||
|
bool RestartRequired = false);
|
||||||
|
|
||||||
internal sealed class AirAppMarketIndexDocument
|
internal sealed class AirAppMarketIndexDocument
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -773,11 +773,6 @@ public sealed class PluginRuntimeService : IDisposable
|
|||||||
private void ApplyPendingPluginDeletions()
|
private void ApplyPendingPluginDeletions()
|
||||||
{
|
{
|
||||||
var pendingPaths = ReadPendingPluginDeletions();
|
var pendingPaths = ReadPendingPluginDeletions();
|
||||||
if (pendingPaths.Count == 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var remainingPaths = new List<string>();
|
var remainingPaths = new List<string>();
|
||||||
foreach (var path in pendingPaths)
|
foreach (var path in pendingPaths)
|
||||||
{
|
{
|
||||||
@@ -788,6 +783,41 @@ public sealed class PluginRuntimeService : IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
SavePendingPluginDeletions(remainingPaths);
|
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)
|
private string ResolvePluginRemovalTargetPath(PluginCatalogEntry entry)
|
||||||
|
|||||||
Reference in New Issue
Block a user