diff --git a/src/TrayTidy.App/TrayTidy.App.csproj b/src/TrayTidy.App/TrayTidy.App.csproj
index 7cbc2f7..4634f6c 100644
--- a/src/TrayTidy.App/TrayTidy.App.csproj
+++ b/src/TrayTidy.App/TrayTidy.App.csproj
@@ -9,7 +9,7 @@
x64
TrayTidy
TrayTidy.App
- 0.2.0
+ 0.2.1
beta
app.manifest
Assets\TrayTidy_AppIcon.ico
diff --git a/src/TrayTidy.App/ViewModels/MainViewModel.cs b/src/TrayTidy.App/ViewModels/MainViewModel.cs
index fc048b1..fb75476 100644
--- a/src/TrayTidy.App/ViewModels/MainViewModel.cs
+++ b/src/TrayTidy.App/ViewModels/MainViewModel.cs
@@ -598,11 +598,22 @@ private async Task TerminateAndRecordAsync(
ApplicationGroup model, string displayName, TerminationMode mode, string sessionId)
{
var started = DateTimeOffset.UtcNow;
- var result = await _termination.TerminateAsync(model, mode);
- if (mode == TerminationMode.ForceTree && result.Code == TerminationResultCode.AccessDenied)
+ TerminationResult result;
+ try
+ {
+ result = await _termination.TerminateAsync(model, mode);
+ if (mode == TerminationMode.ForceTree && result.Code == TerminationResultCode.AccessDenied)
+ {
+ OperationMessage = $"{displayName} 需要管理员权限,正在等待 Windows 确认……";
+ result = await _elevatedTermination.TerminateAsAdministratorAsync(model);
+ }
+ }
+ catch (Exception ex)
{
- OperationMessage = $"{displayName} 需要管理员权限,正在等待 Windows 确认……";
- result = await _elevatedTermination.TerminateAsAdministratorAsync(model);
+ Log.Error(ex, "Application termination failed unexpectedly; appKey={AppKey}; mode={Mode}",
+ model.AppKey, mode);
+ result = new TerminationResult(TerminationResultCode.Failed,
+ "终止操作遇到意外错误,未确认进程已终止。请查看本地日志。");
}
await _cleanup.AddAsync(new CleanupHistoryEntry(0, sessionId, started, DateTimeOffset.UtcNow,
model.AppKey, displayName,
diff --git a/src/TrayTidy.Infrastructure.Windows/ApplicationTerminationService.cs b/src/TrayTidy.Infrastructure.Windows/ApplicationTerminationService.cs
index b241d82..68ae93a 100644
--- a/src/TrayTidy.Infrastructure.Windows/ApplicationTerminationService.cs
+++ b/src/TrayTidy.Infrastructure.Windows/ApplicationTerminationService.cs
@@ -131,15 +131,31 @@ private static async Task ForceAsync(ApplicationGroup applica
{
continue;
}
- catch (Win32Exception ex) when (ex.NativeErrorCode == 5)
+ catch (Exception ex) when (ex is Win32Exception or UnauthorizedAccessException or
+ AggregateException or NotSupportedException or TimeoutException)
{
- return new TerminationResult(TerminationResultCode.AccessDenied, "当前权限不足,无法终止该进程。 ");
- }
- catch (Exception ex) when (ex is Win32Exception or NotSupportedException or TimeoutException)
- {
- return new TerminationResult(TerminationResultCode.Failed, $"强制终止失败:{ex.GetType().Name}");
+ return MapForceException(ex);
}
}
return new TerminationResult(TerminationResultCode.Success, "已强制终止进程树。 ");
}
+
+ internal static TerminationResult MapForceException(Exception exception)
+ {
+ if (ContainsAccessDenied(exception))
+ return new TerminationResult(TerminationResultCode.AccessDenied,
+ "当前权限不足,无法终止该进程。 ");
+
+ return new TerminationResult(TerminationResultCode.Failed,
+ $"强制终止失败:{exception.GetType().Name}");
+ }
+
+ private static bool ContainsAccessDenied(Exception exception)
+ {
+ if (exception is UnauthorizedAccessException or Win32Exception { NativeErrorCode: 5 })
+ return true;
+
+ return exception is AggregateException aggregate &&
+ aggregate.Flatten().InnerExceptions.Any(ContainsAccessDenied);
+ }
}
diff --git a/src/TrayTidy.Infrastructure.Windows/AssemblyInfo.cs b/src/TrayTidy.Infrastructure.Windows/AssemblyInfo.cs
new file mode 100644
index 0000000..609f79a
--- /dev/null
+++ b/src/TrayTidy.Infrastructure.Windows/AssemblyInfo.cs
@@ -0,0 +1,3 @@
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("TrayTidy.Windows.IntegrationTests")]
diff --git a/tests/TrayTidy.Windows.IntegrationTests/MainViewModelAiTests.cs b/tests/TrayTidy.Windows.IntegrationTests/MainViewModelAiTests.cs
index 27e7fff..27bd912 100644
--- a/tests/TrayTidy.Windows.IntegrationTests/MainViewModelAiTests.cs
+++ b/tests/TrayTidy.Windows.IntegrationTests/MainViewModelAiTests.cs
@@ -156,12 +156,30 @@ public async Task New_scan_discards_late_ai_result_from_old_generation()
public void Auto_analysis_is_disabled_by_default() =>
Assert.False(AiAdvisorSettings.Default.AutoAnalyzeAfterScan);
+ [Fact]
+ public async Task Unexpected_termination_exception_is_reported_without_escaping_the_command()
+ {
+ await using var viewModel = CreateViewModel(
+ new FakeAiCoordinator(), new FakeDialog(), AppSettings.Default,
+ new ThrowingTermination());
+ await viewModel.InitializeAsync(startScan: true);
+ var item = Assert.Single(viewModel.Applications);
+
+ await viewModel.CloseApplicationCommand.ExecuteAsync(item);
+
+ Assert.True(viewModel.IsOperationVisible);
+ Assert.False(viewModel.IsOperationBusy);
+ Assert.False(viewModel.CanForceRemaining);
+ Assert.Contains("意外错误", viewModel.OperationMessage, StringComparison.Ordinal);
+ }
+
private static MainViewModel CreateViewModel(
FakeAiCoordinator coordinator,
FakeDialog dialog,
- AppSettings settings) => new(
+ AppSettings settings,
+ IApplicationTerminationService? termination = null) => new(
new FakeScanner(),
- new FakeTermination(),
+ termination ?? new FakeTermination(),
new FakeElevatedTermination(),
new FakeActivation(),
new FakeRules(),
@@ -328,6 +346,14 @@ public Task TerminateAsync(ApplicationGroup application, Term
Task.FromResult(new TerminationResult(TerminationResultCode.Success, "ok"));
}
+ private sealed class ThrowingTermination : IApplicationTerminationService
+ {
+ public Task TerminateAsync(ApplicationGroup application, TerminationMode mode,
+ CancellationToken cancellationToken = default) =>
+ Task.FromException(new AggregateException(
+ new System.ComponentModel.Win32Exception(5, "Access is denied.")));
+ }
+
private sealed class FakeElevatedTermination : IElevatedTerminationService
{
public Task TerminateAsAdministratorAsync(ApplicationGroup application, CancellationToken cancellationToken = default) =>
diff --git a/tests/TrayTidy.Windows.IntegrationTests/TerminationSafetyTests.cs b/tests/TrayTidy.Windows.IntegrationTests/TerminationSafetyTests.cs
index a1466b3..9d1c205 100644
--- a/tests/TrayTidy.Windows.IntegrationTests/TerminationSafetyTests.cs
+++ b/tests/TrayTidy.Windows.IntegrationTests/TerminationSafetyTests.cs
@@ -1,3 +1,4 @@
+using System.ComponentModel;
using TrayTidy.Core;
using TrayTidy.Infrastructure.Windows;
@@ -146,6 +147,29 @@ public async Task Resource_activity_does_not_cancel_explicit_graceful_selection(
Assert.Contains("没有可用的顶层窗口", result.Message, StringComparison.Ordinal);
}
+ [Fact]
+ public void Aggregate_access_denied_is_mapped_to_the_elevation_result()
+ {
+ var exception = new AggregateException("Not all processes in process tree could be terminated.",
+ new Win32Exception(5, "Access is denied."));
+
+ var result = ApplicationTerminationService.MapForceException(exception);
+
+ Assert.Equal(TerminationResultCode.AccessDenied, result.Code);
+ Assert.Contains("权限不足", result.Message, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void Aggregate_non_permission_failure_is_reported_without_throwing()
+ {
+ var exception = new AggregateException(new Win32Exception(87, "The parameter is incorrect."));
+
+ var result = ApplicationTerminationService.MapForceException(exception);
+
+ Assert.Equal(TerminationResultCode.Failed, result.Code);
+ Assert.Contains(nameof(AggregateException), result.Message, StringComparison.Ordinal);
+ }
+
private static ApplicationTerminationService CreateService(IReadOnlyList current,
out ApplicationGroup application, ProcessSnapshot? original = null)
{