Skip to content

Commit 4fa544b

Browse files
committed
feat(extension-install): implement .modpkg package installation with robust cleanup
- Add InstallFromPackageAsync/InstallFromPackageStreamAsync to IModuleInstallerService - Add ModuleInstallResult to handle installation outcomes with confirmation flow - Add IModuleCleanupService with database-backed pending cleanup persistence - Add ModuleExecutionGuard for module exception isolation and health tracking - Add PendingCleanups table with EF migration for deferred directory cleanup - Add MessageDialog component for Avalonia with theme-aware styling - Add AvaloniaNotificationService for cross-platform notifications Key improvements: - Prevent system module overwrite by user installation - Auto-load modules after installation with menu registration - Handle locked DLL cleanup via retry and deferred cleanup on restart - Cancel pending cleanup when module is reinstalled (prevents accidental deletion) - Unified logging via ILogger (removed Debug.WriteLine) - Fix ALC isolation by adding Avalonia assemblies to SharedAssemblies config
1 parent 21391f9 commit 4fa544b

36 files changed

Lines changed: 3038 additions & 127 deletions
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
## Context
2+
3+
CLI 已有 `.modpkg` 安装能力(`InstallCommand.cs`),但 UI 界面缺乏对应功能。用户需要在扩展管理界面直接安装包文件,且安装后无需重启即可使用。
4+
5+
### 现有架构
6+
- `IModuleInstallerService.InstallFromPathAsync` - 从目录安装(CLI 调用)
7+
- `ModuleListViewModel` - 两个 Host 各有一个版本,包含 `ImportModuleCommand`(目前只支持 manifest.json)
8+
- `IModuleLoader.LoadAsync/UnloadAsync` - 运行时加载/卸载
9+
- CLI `InstallCommand` 实现了完整的 modpkg 解压 → 复制 → 注册流程
10+
11+
## Goals / Non-Goals
12+
13+
### Goals
14+
- 从 UI 选择 `.modpkg` 文件并安装
15+
- 安装后自动加载模块(无需重启)
16+
- 复用 CLI 的核心安装逻辑
17+
- 安装冲突时提示用户确认覆盖
18+
19+
### Non-Goals
20+
- 批量安装多个包
21+
- 从 URL 下载安装
22+
- 拖放安装
23+
24+
## Decisions
25+
26+
### 1. 安装服务层扩展
27+
28+
`IModuleInstallerService` 新增方法:
29+
```csharp
30+
Task<ModuleInstallResult> InstallFromPackageAsync(
31+
string packagePath,
32+
bool overwrite = false,
33+
string? hostType = null,
34+
CancellationToken cancellationToken = default);
35+
```
36+
37+
返回值包含:
38+
- `bool Success`
39+
- `string? ModuleId` - 安装成功时的模块 ID
40+
- `string? Error` - 错误信息
41+
- `bool RequiresConfirmation` - 需要用户确认覆盖
42+
43+
此方法封装 CLI 的解压 → 复制 → 注册逻辑,并返回结果供 UI 层处理。
44+
45+
### 2. Avalonia UI 实现
46+
47+
使用 Avalonia `StorageProvider` API 打开文件选择对话框:
48+
```csharp
49+
var files = await topLevel.StorageProvider.OpenFilePickerAsync(
50+
new FilePickerOpenOptions
51+
{
52+
Title = "Select Module Package",
53+
FileTypeFilter = new[] { new FilePickerFileType("Module Package") { Patterns = new[] { "*.modpkg" } } },
54+
AllowMultiple = false
55+
});
56+
```
57+
58+
优点:
59+
- 跨平台支持良好(Windows/macOS/Linux)
60+
- 无需额外依赖
61+
- Avalonia 官方推荐方式
62+
63+
### 3. Blazor UI 实现
64+
65+
使用 MudBlazor `MudFileUpload` 组件:
66+
```razor
67+
<MudFileUpload T="IBrowserFile" Accept=".modpkg" OnFilesChanged="OnFileSelected">
68+
<ActivatorContent>
69+
<MudButton Variant="Variant.Filled" Color="Color.Primary">
70+
Install Package...
71+
</MudButton>
72+
</ActivatorContent>
73+
</MudFileUpload>
74+
```
75+
76+
文件通过 `IBrowserFile.CopyToAsync` 保存到临时目录后调用安装服务。
77+
78+
### 4. 安装后自动加载
79+
80+
安装完成后:
81+
1. 调用 `IModuleLoader.LoadAsync(modulePath)` 运行时加载
82+
2. 通过 `WeakReferenceMessenger` 发送 `MenuItemsAddedMessage` 更新导航
83+
3. 刷新模块列表 UI
84+
85+
### 5. 覆盖确认流程
86+
87+
当目标目录已存在时:
88+
1. 第一次调用 `InstallFromPackageAsync` 返回 `RequiresConfirmation=true`
89+
2. UI 显示确认对话框
90+
3. 用户确认后以 `overwrite=true` 再次调用
91+
92+
## Risks / Trade-offs
93+
94+
### 风险
95+
- **文件锁定**: 如果模块正在运行,覆盖文件可能失败
96+
- 缓解: 安装前先卸载现有模块
97+
98+
- **部分安装失败**: 复制过程中断可能导致不完整安装
99+
- 缓解: 先复制到临时目录,成功后再移动
100+
101+
### Trade-offs
102+
- 选择在 Core 层实现安装逻辑(而非 Host 层),增加了一定复杂度,但确保了两个 Host 的一致性
103+
104+
## Open Questions
105+
106+
- 是否需要进度指示器?(对于大包)→ 初版暂不实现,包通常较小
107+
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Change: 扩展管理界面安装功能
2+
3+
## Why
4+
当前扩展管理界面仅支持通过手动输入 manifest.json 路径导入开发模块。用户需要从 UI 界面直接选择并安装 `.modpkg` 包文件,安装后立即可用(加载/卸载等功能正常工作),无需重启应用。
5+
6+
## What Changes
7+
- 在 Avalonia Host 扩展管理页面添加"安装包"按钮,打开文件选择对话框
8+
- 在 Blazor Host 扩展管理页面添加文件上传功能
9+
- 新增 `IModuleInstallerService.InstallFromPackageAsync` 方法支持从 `.modpkg` 安装
10+
- 安装流程:解压包 → 复制到用户模块目录 → 注册数据库 → 运行时加载
11+
- 安装后自动刷新模块列表并启用新安装的模块
12+
- 支持覆盖已存在模块(带确认对话框)
13+
14+
## Impact
15+
- Affected specs: `extension-management` (新增)
16+
- Affected code:
17+
- `src/Modulus.Core/Installation/IModuleInstallerService.cs`
18+
- `src/Modulus.Core/Installation/ModuleInstallerService.cs`
19+
- `src/Hosts/Modulus.Host.Avalonia/Shell/ViewModels/ModuleListViewModel.cs`
20+
- `src/Hosts/Modulus.Host.Avalonia/Shell/Views/ModuleListView.axaml`
21+
- `src/Hosts/Modulus.Host.Blazor/Shell/ViewModels/ModuleListViewModel.cs`
22+
- `src/Hosts/Modulus.Host.Blazor/Components/Pages/Modules.razor`
23+
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
## ADDED Requirements
2+
3+
### Requirement: Package Installation Service
4+
扩展安装服务 SHALL 提供从 `.modpkg` 文件安装模块的能力。
5+
6+
#### Scenario: Install from valid modpkg file
7+
- **WHEN** 调用 `InstallFromPackageAsync` 并传入有效的 `.modpkg` 文件路径
8+
- **THEN** 解压包内容到临时目录
9+
- **AND** 验证 `extension.vsixmanifest` 存在且有效
10+
- **AND** 复制文件到用户模块目录 `%APPDATA%/Modulus/Modules/{ModuleId}/`
11+
- **AND** 注册模块到数据库
12+
- **AND** 返回 `Success=true``ModuleId`
13+
14+
#### Scenario: Package file not found
15+
- **WHEN** 指定的 `.modpkg` 文件不存在
16+
- **THEN** 返回 `Success=false`
17+
- **AND** `Error` 包含文件不存在信息
18+
19+
#### Scenario: Invalid package format
20+
- **WHEN** 包文件不是有效的 ZIP 格式或不包含 `extension.vsixmanifest`
21+
- **THEN** 返回 `Success=false`
22+
- **AND** `Error` 包含格式错误信息
23+
24+
#### Scenario: Module already exists without overwrite
25+
- **WHEN** 目标模块目录已存在
26+
- **AND** `overwrite=false`
27+
- **THEN** 返回 `Success=false`
28+
- **AND** `RequiresConfirmation=true`
29+
- **AND** 不修改现有安装
30+
31+
#### Scenario: Module already exists with overwrite
32+
- **WHEN** 目标模块目录已存在
33+
- **AND** `overwrite=true`
34+
- **THEN** 先卸载正在运行的模块(如果有)
35+
- **AND** 删除现有目录
36+
- **AND** 继续安装流程
37+
- **AND** 返回 `Success=true`
38+
39+
### Requirement: Avalonia Package Installation UI
40+
Avalonia Host 扩展管理界面 SHALL 提供文件选择器安装 `.modpkg` 包。
41+
42+
#### Scenario: Open file picker and select package
43+
- **WHEN** 用户点击"Install Package..."按钮
44+
- **THEN** 打开系统文件选择对话框
45+
- **AND** 过滤器仅显示 `.modpkg` 文件
46+
- **AND** 用户选择文件后开始安装流程
47+
48+
#### Scenario: Installation success with auto-load
49+
- **WHEN** `.modpkg` 安装成功
50+
- **THEN** 自动运行时加载新安装的模块
51+
- **AND** 注册模块菜单到导航
52+
- **AND** 刷新模块列表显示新模块
53+
- **AND** 显示成功通知
54+
55+
#### Scenario: Installation requires overwrite confirmation
56+
- **WHEN** 安装返回 `RequiresConfirmation=true`
57+
- **THEN** 显示确认对话框询问是否覆盖
58+
- **AND** 用户确认后以 `overwrite=true` 重新安装
59+
- **AND** 用户取消则中止安装
60+
61+
#### Scenario: Installation failure notification
62+
- **WHEN** 安装过程出错
63+
- **THEN** 显示错误通知包含失败原因
64+
65+
### Requirement: Blazor Package Installation UI
66+
Blazor Host 扩展管理界面 SHALL 提供文件上传安装 `.modpkg` 包。
67+
68+
#### Scenario: Upload and install package
69+
- **WHEN** 用户通过文件上传组件选择 `.modpkg` 文件
70+
- **THEN** 上传文件到临时目录
71+
- **AND** 调用安装服务安装模块
72+
- **AND** 安装成功后自动加载模块
73+
74+
#### Scenario: Upload success with auto-load
75+
- **WHEN** `.modpkg` 上传并安装成功
76+
- **THEN** 自动运行时加载新安装的模块
77+
- **AND** 刷新模块列表显示新模块
78+
- **AND** 显示成功提示
79+
80+
#### Scenario: Upload failure handling
81+
- **WHEN** 文件上传或安装失败
82+
- **THEN** 显示错误提示包含失败原因
83+
- **AND** 清理临时文件
84+
85+
### Requirement: Post-Installation Runtime Load
86+
安装完成后 SHALL 自动加载模块到运行时,无需重启应用。
87+
88+
#### Scenario: Auto-load after installation
89+
- **WHEN** 模块安装成功且 `IsEnabled=true`
90+
- **THEN** 调用 `IModuleLoader.LoadAsync` 加载模块
91+
- **AND** 模块状态变为 `Active`
92+
- **AND** 模块菜单添加到导航
93+
94+
#### Scenario: Load failure marks module error
95+
- **WHEN** 安装成功但运行时加载失败
96+
- **THEN** 模块状态标记为 `Error`
97+
- **AND** 显示加载失败的诊断信息
98+
- **AND** 模块仍然已安装,可重试加载
99+
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
## 1. Core 安装服务扩展
2+
3+
- [x] 1.1 新增 `ModuleInstallResult` 返回类型(Success, ModuleId, Error, RequiresConfirmation)
4+
- [x] 1.2 在 `IModuleInstallerService` 添加 `InstallFromPackageAsync` 方法签名
5+
- [x] 1.3 实现 `ModuleInstallerService.InstallFromPackageAsync`(解压 → 验证 → 复制 → 注册)
6+
- [x] 1.4 处理已存在模块的覆盖逻辑(先卸载运行中模块)
7+
8+
## 2. Avalonia Host UI 实现
9+
10+
- [x] 2.1 在 `ModuleListViewModel` 添加 `InstallPackageAsync` 方法
11+
- [x] 2.2 实现文件选择对话框调用(`StorageProvider.OpenFilePickerAsync`
12+
- [x] 2.3 添加确认覆盖对话框(`INotificationService.ConfirmAsync`
13+
- [x] 2.4 安装后自动调用 `IModuleLoader.LoadAsync` 加载模块
14+
- [x] 2.5 发送 `MenuItemsAddedMessage` 更新导航
15+
- [x] 2.6 在 `ModuleListView.axaml` 添加"Install from Package..."按钮
16+
17+
## 3. Blazor Host UI 实现
18+
19+
- [x] 3.1 在 `ModuleListViewModel` 添加 `InstallFromStreamAsync` 方法
20+
- [x] 3.2 在 `Modules.razor` 添加 `MudFileUpload` 组件
21+
- [x] 3.3 实现文件上传到临时目录逻辑
22+
- [x] 3.4 安装后自动加载模块并刷新列表
23+
- [x] 3.5 添加覆盖确认对话框(`MudDialog`
24+
25+
## 4. 验证
26+
27+
- [ ] 4.1 手动测试:Avalonia 安装 modpkg → 验证加载/卸载/重新加载正常
28+
- [ ] 4.2 手动测试:Blazor 上传安装 modpkg → 验证同上
29+
- [ ] 4.3 测试覆盖安装流程

src/Hosts/Modulus.Host.Avalonia/App.axaml.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ public override void ConfigureServices(IModuleLifecycleContext context)
3131
context.Services.AddSingleton<IUIFactory, AvaloniaUIFactory>();
3232
context.Services.AddSingleton<IViewRegistry, ViewRegistry>();
3333
context.Services.AddSingleton<IThemeService, AvaloniaThemeService>();
34+
context.Services.AddSingleton<INotificationService, AvaloniaNotificationService>();
3435

3536
// Shell Services
3637
context.Services.AddSingleton<IMenuRegistry, MenuRegistry>();
@@ -128,6 +129,8 @@ public override void OnFrameworkInitializationCompleted()
128129
// Repositories & installers (needed at runtime for menu registration)
129130
services.AddScoped<IModuleRepository, ModuleRepository>();
130131
services.AddScoped<IMenuRepository, MenuRepository>();
132+
services.AddScoped<IPendingCleanupRepository, PendingCleanupRepository>();
133+
services.AddSingleton<IModuleCleanupService, ModuleCleanupService>();
131134
services.AddScoped<IModuleInstallerService, ModuleInstallerService>();
132135
services.AddScoped<SystemModuleInstaller>();
133136
services.AddScoped<ModuleIntegrityChecker>();

0 commit comments

Comments
 (0)