diff --git a/src/UniGetUI.PackageEngine.Operations/KillProcessOperation.cs b/src/UniGetUI.PackageEngine.Operations/KillProcessOperation.cs index d63371b9a3..5cc66a6e25 100644 --- a/src/UniGetUI.PackageEngine.Operations/KillProcessOperation.cs +++ b/src/UniGetUI.PackageEngine.Operations/KillProcessOperation.cs @@ -34,6 +34,7 @@ protected override async Task PerformOperation() $"Attempting to close all processes with name {ProcessName}...", LineType.Information ); + bool processSurvived = false; foreach (var proc in Process.GetProcessesByName(ProcessName.Replace(".exe", ""))) { using (proc) @@ -68,12 +69,13 @@ await Task.WhenAny( $"{ProcessName} with pid={proc.Id} did not exit and will not be killed. You can change this from UniGetUI settings.", LineType.Error ); + processSurvived = true; } } } } - return OperationVeredict.Success; + return processSurvived ? OperationVeredict.Failure : OperationVeredict.Success; } catch (OperationCanceledException) when (CancellationToken.IsCancellationRequested) { diff --git a/src/UniGetUI.PackageEngine.Operations/PackageOperations.cs b/src/UniGetUI.PackageEngine.Operations/PackageOperations.cs index 26e6c41629..c6b97b8f84 100644 --- a/src/UniGetUI.PackageEngine.Operations/PackageOperations.cs +++ b/src/UniGetUI.PackageEngine.Operations/PackageOperations.cs @@ -1473,7 +1473,7 @@ private static IReadOnlyList _getPreInstallOps( return l; foreach (var process in opts.KillBeforeOperation) - l.Add(new InnerOperation(new KillProcessOperation(process), mustSucceed: false)); + l.Add(new InnerOperation(new KillProcessOperation(process), mustSucceed: true)); if (role is OperationType.Install && opts.PreInstallCommand.Any()) l.Add( diff --git a/src/UniGetUI.PackageEngine.Tests/KillProcessOperationTests.cs b/src/UniGetUI.PackageEngine.Tests/KillProcessOperationTests.cs new file mode 100644 index 0000000000..21941a80b0 --- /dev/null +++ b/src/UniGetUI.PackageEngine.Tests/KillProcessOperationTests.cs @@ -0,0 +1,117 @@ +using System.Diagnostics; +using UniGetUI.Core.SettingsEngine; +using UniGetUI.PackageEngine.Enums; +using UniGetUI.PackageOperations; + +namespace UniGetUI.PackageEngine.Tests; + +[Collection(nameof(OperationOrchestrationTestCollection))] +public sealed class KillProcessOperationTests +{ + [Fact] + public async Task FailsWhenAProcessRefusesToCloseAndMayNotBeKilled() + { + Settings.Set(Settings.K.KillProcessesThatRefuseToDie, false); + using var target = WindowlessProcess.Start(); + using var operation = new KillProcessOperation(target.Name); + + await operation.MainThread().WaitAsync(TimeSpan.FromSeconds(30)); + + Assert.Equal(OperationStatus.Failed, operation.Status); + Assert.False(target.Process.HasExited); + } + + [Fact] + public async Task SucceedsWhenAProcessThatRefusesToCloseIsKilled() + { + Settings.Set(Settings.K.KillProcessesThatRefuseToDie, false); + using var target = WindowlessProcess.Start(); + using var operation = new KillProcessOperation(target.Name, forceKill: true); + + await operation.MainThread().WaitAsync(TimeSpan.FromSeconds(30)); + + Assert.Equal(OperationStatus.Succeeded, operation.Status); + Assert.True(target.Process.HasExited); + } + + [Fact] + public async Task SucceedsWhenNoProcessHasTheName() + { + using var operation = new KillProcessOperation($"unigetui-absent-{Guid.NewGuid():N}"); + + await operation.MainThread().WaitAsync(TimeSpan.FromSeconds(30)); + + Assert.Equal(OperationStatus.Succeeded, operation.Status); + } + + private sealed class WindowlessProcess : IDisposable + { + private readonly string _directory; + + public string Name { get; } + public Process Process { get; } + + private WindowlessProcess(string directory, string name, Process process) + { + _directory = directory; + Name = name; + Process = process; + } + + public static WindowlessProcess Start() + { + string directory = Path.Combine(Path.GetTempPath(), $"unigetui-killtest-{Guid.NewGuid():N}"); + Directory.CreateDirectory(directory); + string name = $"unigetui-killtest-{Guid.NewGuid():N}"; + + string source; + string arguments; + if (OperatingSystem.IsWindows()) + { + source = Path.Combine(Environment.SystemDirectory, "cmd.exe"); + arguments = "/d /c ping -n 120 127.0.0.1 >nul"; + name += ".exe"; + } + else + { + source = File.Exists("/bin/sleep") ? "/bin/sleep" : "/usr/bin/sleep"; + arguments = "120"; + } + + string executable = Path.Combine(directory, name); + File.Copy(source, executable); + if (!OperatingSystem.IsWindows()) + File.SetUnixFileMode(executable, File.GetUnixFileMode(source)); + + var process = Process.Start( + new ProcessStartInfo(executable, arguments) + { + CreateNoWindow = true, + UseShellExecute = false, + } + )!; + return new WindowlessProcess(directory, name, process); + } + + public void Dispose() + { + try + { + if (!Process.HasExited) + { + Process.Kill(entireProcessTree: true); + Process.WaitForExit(10000); + } + } + catch (InvalidOperationException) { } + + Process.Dispose(); + try + { + Directory.Delete(_directory, recursive: true); + } + catch (IOException) { } + catch (UnauthorizedAccessException) { } + } + } +} diff --git a/src/UniGetUI.PackageEngine.Tests/PackageOperationsTests.cs b/src/UniGetUI.PackageEngine.Tests/PackageOperationsTests.cs index 7ae28d9285..cc16cb0ac8 100644 --- a/src/UniGetUI.PackageEngine.Tests/PackageOperationsTests.cs +++ b/src/UniGetUI.PackageEngine.Tests/PackageOperationsTests.cs @@ -140,12 +140,12 @@ public void InstallOperationBuildsPrerequisitesKillListAndPreCommand() inner => { Assert.IsType(inner.Operation); - Assert.False(inner.MustSucceed); + Assert.True(inner.MustSucceed); }, inner => { Assert.IsType(inner.Operation); - Assert.False(inner.MustSucceed); + Assert.True(inner.MustSucceed); }, inner => {