Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/TrayTidy.App/TrayTidy.App.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<PlatformTarget>x64</PlatformTarget>
<AssemblyName>TrayTidy</AssemblyName>
<RootNamespace>TrayTidy.App</RootNamespace>
<VersionPrefix>0.2.0</VersionPrefix>
<VersionPrefix>0.2.1</VersionPrefix>
<VersionSuffix>beta</VersionSuffix>
<ApplicationManifest>app.manifest</ApplicationManifest>
<ApplicationIcon>Assets\TrayTidy_AppIcon.ico</ApplicationIcon>
Expand Down
19 changes: 15 additions & 4 deletions src/TrayTidy.App/ViewModels/MainViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -598,11 +598,22 @@ private async Task<TerminationResult> 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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,15 +131,31 @@ private static async Task<TerminationResult> 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);
}
}
3 changes: 3 additions & 0 deletions src/TrayTidy.Infrastructure.Windows/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("TrayTidy.Windows.IntegrationTests")]
30 changes: 28 additions & 2 deletions tests/TrayTidy.Windows.IntegrationTests/MainViewModelAiTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -328,6 +346,14 @@ public Task<TerminationResult> TerminateAsync(ApplicationGroup application, Term
Task.FromResult(new TerminationResult(TerminationResultCode.Success, "ok"));
}

private sealed class ThrowingTermination : IApplicationTerminationService
{
public Task<TerminationResult> TerminateAsync(ApplicationGroup application, TerminationMode mode,
CancellationToken cancellationToken = default) =>
Task.FromException<TerminationResult>(new AggregateException(
new System.ComponentModel.Win32Exception(5, "Access is denied.")));
}

private sealed class FakeElevatedTermination : IElevatedTerminationService
{
public Task<TerminationResult> TerminateAsAdministratorAsync(ApplicationGroup application, CancellationToken cancellationToken = default) =>
Expand Down
24 changes: 24 additions & 0 deletions tests/TrayTidy.Windows.IntegrationTests/TerminationSafetyTests.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.ComponentModel;
using TrayTidy.Core;
using TrayTidy.Infrastructure.Windows;

Expand Down Expand Up @@ -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<ProcessSnapshot> current,
out ApplicationGroup application, ProcessSnapshot? original = null)
{
Expand Down