Skip to content

Commit 4923dde

Browse files
committed
Add the option for UniGetUI to kill a process name before an operation with a package is performed
1 parent b03a6f5 commit 4923dde

9 files changed

Lines changed: 189 additions & 16 deletions

File tree

src/UniGetUI.PackageEngine.Operations/AbstractOperation.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public abstract partial class AbstractOperation : IDisposable
2323
public bool Started { get; private set; }
2424
protected bool QUEUE_ENABLED;
2525
protected bool FORCE_HOLD_QUEUE;
26-
private bool IsInnerOperation = false;
26+
private bool IsInnerOperation;
2727

2828
private readonly List<(string, LineType)> LogList = [];
2929
private OperationStatus _status = OperationStatus.InQueue;
@@ -38,7 +38,6 @@ public void ApplyCapabilities(bool admin, bool interactive, bool skiphash, strin
3838
BadgesChanged?.Invoke(this, new BadgeCollection(admin, interactive, skiphash, scope));
3939
}
4040

41-
// private readonly AbstractOperation? requirement;
4241
private readonly IReadOnlyList<InnerOperation> PreOperations = [];
4342
private readonly IReadOnlyList<InnerOperation> PostOperations = [];
4443

src/UniGetUI.PackageEngine.Operations/ProcessOperation.cs renamed to src/UniGetUI.PackageEngine.Operations/AbstractProcessOperation.cs

File renamed without changes.
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
2+
using System.Diagnostics;
3+
using UniGetUI.Core.Tools;
4+
using UniGetUI.PackageEngine.Enums;
5+
6+
namespace UniGetUI.PackageOperations;
7+
8+
public class KillProcessOperation: AbstractOperation
9+
{
10+
private string ProcessName;
11+
public KillProcessOperation(string procName) : base(false)
12+
{
13+
ProcessName = CoreTools.MakeValidFileName(procName);
14+
Metadata.Status = $"Closing process(es) {procName}";
15+
Metadata.Title = $"Closing process(es) {procName}";
16+
Metadata.OperationInformation = " ";
17+
Metadata.SuccessTitle = $"Done!";
18+
Metadata.SuccessMessage = $"Done!";
19+
Metadata.FailureTitle = $"Failed to close process";
20+
Metadata.FailureMessage = $"The process(es) {procName} could not be closed";
21+
}
22+
23+
protected override void ApplyRetryAction(string retryMode)
24+
{
25+
}
26+
27+
protected override async Task<OperationVeredict> PerformOperation()
28+
{
29+
try
30+
{
31+
Line($"Attempting to close all processes with name {ProcessName}...", LineType.Information);
32+
var procs = Process.GetProcessesByName(ProcessName.Replace(".exe", ""));
33+
foreach (var proc in procs)
34+
{
35+
if(proc.HasExited) continue;
36+
Line($"Attempting to close process {ProcessName} with pid={proc.Id}...", LineType.VerboseDetails);
37+
proc.CloseMainWindow();
38+
await Task.WhenAny(proc.WaitForExitAsync(), Task.Delay(1000));
39+
if (!proc.HasExited)
40+
{
41+
Line($"Timeout for process {ProcessName}, attempting to kill...", LineType.Information);
42+
proc.Kill();
43+
}
44+
}
45+
46+
return OperationVeredict.Success;
47+
}
48+
catch (Exception ex)
49+
{
50+
Line(ex.ToString(), LineType.Error);
51+
return OperationVeredict.Failure;
52+
}
53+
}
54+
55+
public override Task<Uri> GetOperationIcon()
56+
=> Task.FromResult(new Uri("about:blank"));
57+
}

src/UniGetUI.PackageEngine.Operations/PackageOperations.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,10 +132,13 @@ public override Task<Uri> GetOperationIcon()
132132
private static IReadOnlyList<InnerOperation> _getPreInstallOps(InstallOptions opts, AbstractOperation? preReq = null)
133133
{
134134
List<InnerOperation> l = new();
135+
if(preReq is not null) l.Add(new(preReq, true));
135136

136-
// Things Things Things
137+
foreach (var process in opts.KillBeforeOperation)
138+
l.Add(new InnerOperation(
139+
new KillProcessOperation(process),
140+
mustSucceed: false));
137141

138-
if(preReq is not null) l.Add(new(preReq, true));
139142
return l;
140143
}
141144

src/UniGetUI.PackageEngine.Serializable/InstallOptions.cs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ public class InstallOptions: SerializableComponent<InstallOptions>
1919
public bool SkipMinorUpdates { get; set; }
2020
public bool OverridesNextLevelOpts { get; set; }
2121
public bool RemoveDataOnUninstall { get; set; }
22+
public List<string> KillBeforeOperation { get; set; } = [];
23+
2224

2325
public override InstallOptions Copy()
2426
{
@@ -27,9 +29,9 @@ public override InstallOptions Copy()
2729
SkipHashCheck = SkipHashCheck,
2830
Architecture = Architecture,
2931
CustomInstallLocation = CustomInstallLocation,
30-
CustomParameters_Install = CustomParameters_Install,
31-
CustomParameters_Update = CustomParameters_Update,
32-
CustomParameters_Uninstall = CustomParameters_Uninstall,
32+
CustomParameters_Install = CustomParameters_Install.ToList(),
33+
CustomParameters_Update = CustomParameters_Update.ToList(),
34+
CustomParameters_Uninstall = CustomParameters_Uninstall.ToList(),
3335
InstallationScope = InstallationScope,
3436
InteractiveInstallation = InteractiveInstallation,
3537
PreRelease = PreRelease,
@@ -38,6 +40,7 @@ public override InstallOptions Copy()
3840
SkipMinorUpdates = SkipMinorUpdates,
3941
OverridesNextLevelOpts = OverridesNextLevelOpts,
4042
RemoveDataOnUninstall = RemoveDataOnUninstall,
43+
KillBeforeOperation = KillBeforeOperation.ToList(),
4144
};
4245
}
4346

@@ -65,6 +68,7 @@ this.CustomParameters_Uninstall.Count is 0 &&
6568
this.CustomParameters_Uninstall = ReadArrayFromJson(data, "CustomParameters");
6669
}
6770

71+
this.KillBeforeOperation = ReadArrayFromJson(data, nameof(KillBeforeOperation));
6872
this.PreRelease = data[nameof(PreRelease)]?.GetVal<bool>() ?? false;
6973
this.CustomInstallLocation = data[nameof(CustomInstallLocation)]?.GetVal<string>() ?? "";
7074
this.Version = data[nameof(Version)]?.GetVal<string>() ?? "";
@@ -98,6 +102,7 @@ SkipMinorUpdates is not false ||
98102
CustomParameters_Install.Where(x => x != "").Any() ||
99103
CustomParameters_Update.Where(x => x != "").Any() ||
100104
CustomParameters_Uninstall.Where(x => x != "").Any() ||
105+
KillBeforeOperation.Where(x => x != "").Any() ||
101106
CustomInstallLocation.Any() ||
102107
RemoveDataOnUninstall is not false ||
103108
Version.Any();
@@ -115,8 +120,8 @@ public InstallOptions(JsonNode data) : base(data)
115120

116121
public override string ToString()
117122
{
118-
string customparams = CustomParameters_Install.Any() ? string.Join(",", CustomParameters_Install) : "[]";
119-
customparams += CustomParameters_Update.Any() ? string.Join(",", CustomParameters_Update) : "[]";
123+
string customparams = CustomParameters_Install.Any() ? string.Join(",", CustomParameters_Install) : "[],";
124+
customparams += CustomParameters_Update.Any() ? string.Join(",", CustomParameters_Update) : "[],";
120125
customparams += CustomParameters_Uninstall.Any() ? string.Join(",", CustomParameters_Uninstall) : "[]";
121126
return $"<InstallOptions: SkipHashCheck={SkipHashCheck};" +
122127
$"InteractiveInstallation={InteractiveInstallation};" +
@@ -127,6 +132,7 @@ public override string ToString()
127132
$"InstallationScope={CustomInstallLocation};" +
128133
$"CustomParameters={customparams};" +
129134
$"RemoveDataOnUninstall={RemoveDataOnUninstall};" +
135+
$"KillBeforeOperation={KillBeforeOperation};" +
130136
$"PreRelease={PreRelease}>";
131137
}
132138
}

src/UniGetUI/Controls/MenuForPackage.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ public partial class BetterTabViewItem : TabViewItem
7676
public BetterTabViewItem()
7777
{
7878
IsClosable = false;
79+
CanDrag = false;
7980
}
8081

8182
public void LoadText()

src/UniGetUI/Pages/DialogPages/InstallOptions_Package.xaml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,16 @@
1111
xmlns:widgets="using:UniGetUI.Interface.Widgets"
1212
MaxWidth="700"
1313
mc:Ignorable="d">
14+
<Page.Resources>
15+
<DataTemplate x:Key="ProcessTemplate" x:DataType="local:IOP_Proc">
16+
<StackPanel Orientation="Horizontal">
17+
<Viewbox Width="16">
18+
<FontIcon Glyph="&#xECAA;" />
19+
</Viewbox>
20+
<TextBlock Padding="8,0,0,0" Text="{x:Bind Name}" />
21+
</StackPanel>
22+
</DataTemplate>
23+
</Page.Resources>
1424
<Grid>
1525
<Grid.RowDefinitions>
1626
<RowDefinition Height="*" />
@@ -131,6 +141,7 @@
131141
HorizontalAlignment="Stretch"
132142
Background="{ThemeResource SystemChromeAltHighColor}"
133143
IsAddTabButtonVisible="False"
144+
CanReorderTabs="False"
134145
TabWidthMode="SizeToContent">
135146
<widgets:BetterTabViewItem
136147
IconName="Settings"
@@ -210,6 +221,24 @@
210221
<widgets:TranslatedTextBlock Text="Skip minor updates for this package" />
211222
</CheckBox>
212223
</controls:WrapPanel>
224+
<controls:TokenizingTextBox
225+
x:Name="KillProcessesBox"
226+
MaxWidth="620"
227+
HorizontalAlignment="Left"
228+
ItemsSource="{x:Bind ProcessesToKill, Mode=TwoWay}"
229+
Loaded="KillProcessesBox_Loaded"
230+
PlaceholderText="process1.exe,process2.exe"
231+
SuggestedItemTemplate="{StaticResource ProcessTemplate}"
232+
SuggestedItemsSource="{x:Bind SuggestedProcesses, Mode=OneWay}"
233+
TextChanged="KillProcessesBox_TextChanged"
234+
TextMemberPath="Text"
235+
TokenDelimiter=","
236+
TokenItemAdding="KillProcessesBox_TokenItemAdding"
237+
TokenItemTemplate="{StaticResource ProcessTemplate}">
238+
<controls:TokenizingTextBox.Header>
239+
<widgets:TranslatedTextBlock Text="Processes to close before this package is installed, updated or uninstalled:" />
240+
</controls:TokenizingTextBox.Header>
241+
</controls:TokenizingTextBox>
213242
</StackPanel>
214243
</controls:Case>
215244

src/UniGetUI/Pages/DialogPages/InstallOptions_Package.xaml.cs

Lines changed: 84 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1+
using System.Collections.ObjectModel;
2+
using System.ComponentModel;
13
using System.Data;
4+
using System.Diagnostics;
25
using System.Runtime.InteropServices;
6+
using CommunityToolkit.WinUI.Controls;
37
using Microsoft.Extensions.Options;
48
using Microsoft.UI.Xaml;
59
using Microsoft.UI.Xaml.Controls;
@@ -13,6 +17,7 @@
1317
using UniGetUI.PackageEngine.Serializable;
1418
using UniGetUI.Pages.SettingsPages.GeneralPages;
1519
using Windows.ApplicationModel.Activation;
20+
using Windows.Services.Maps;
1621
using Architecture = UniGetUI.PackageEngine.Enums.Architecture;
1722

1823
// To learn more about WinUI, the WinUI project structure,
@@ -32,6 +37,10 @@ public sealed partial class InstallOptionsPage : Page
3237
private readonly string packageInstallLocation;
3338
private bool _uiLoaded;
3439

40+
public ObservableCollection<IOP_Proc> ProcessesToKill = new();
41+
private readonly ObservableCollection<IOP_Proc> _runningProcesses = new();
42+
public ObservableCollection<IOP_Proc> SuggestedProcesses = new();
43+
3544
public InstallOptionsPage(IPackage package, InstallOptions options) : this(package, OperationType.None, options) { }
3645
public InstallOptionsPage(IPackage package, OperationType operation, InstallOptions options)
3746
{
@@ -82,6 +91,7 @@ async Task LoadImage()
8291
_ = LoadImage();
8392
DialogTitle.Text = CoreTools.Translate("{0} installation options", package.Name);
8493
PlaceholderText.Text = CoreTools.Translate("{0} Install options are currently locked because {0} follows the default install options.", package.Name);
94+
KillProcessesBox.PlaceholderText = CoreTools.Translate("Write here the process names, separed by commas (,)");
8595

8696
packageInstallLocation = Package.Manager.DetailsHelper.GetInstallLocation(package) ?? CoreTools.Translate("Unset or unknown");
8797

@@ -154,20 +164,35 @@ async Task LoadImage()
154164
}
155165
}
156166

167+
foreach(var p in Options.KillBeforeOperation)
168+
{
169+
ProcessesToKill.Add(new(p));
170+
}
157171

158172
if (Options.CustomInstallLocation == "") CustomInstallLocation.Text = packageInstallLocation;
159173
else CustomInstallLocation.Text = Options.CustomInstallLocation;
160174

161-
162175
CustomParameters1.Text = string.Join(' ', Options.CustomParameters_Install);
163176
CustomParameters2.Text = string.Join(' ', Options.CustomParameters_Update);
164177
CustomParameters3.Text = string.Join(' ', Options.CustomParameters_Uninstall);
165178

166179
_uiLoaded = true;
167180
EnableDisableControls(operation);
168181
LoadIgnoredUpdates();
182+
_ = _loadProcesses();
169183
}
170184

185+
private async Task _loadProcesses()
186+
{
187+
var processNames = await Task.Run(() =>
188+
Process.GetProcesses().Select(p => p.ProcessName).Distinct().ToList());
189+
190+
_runningProcesses.Clear();
191+
foreach (var name in processNames)
192+
{
193+
if(name.Any()) _runningProcesses.Add(new(name + ".exe"));
194+
}
195+
}
171196
private void EnableDisableControls(OperationType operation)
172197
{
173198
if(FollowGlobalOptionsSwitch.IsOn)
@@ -188,13 +213,17 @@ private void EnableDisableControls(OperationType operation)
188213

189214
AdminCheckBox.IsEnabled = Package.Manager.Capabilities.CanRunAsAdmin;
190215
InteractiveCheckBox.IsEnabled = Package.Manager.Capabilities.CanRunInteractively;
191-
HashCheckbox.IsEnabled = operation != OperationType.Uninstall && Package.Manager.Capabilities.CanSkipIntegrityChecks;
192-
ArchitectureComboBox.IsEnabled = operation != OperationType.Uninstall && Package.Manager.Capabilities.SupportsCustomArchitectures;
216+
HashCheckbox.IsEnabled =
217+
operation is not OperationType.Uninstall
218+
&& Package.Manager.Capabilities.CanSkipIntegrityChecks;
219+
220+
ArchitectureComboBox.IsEnabled =
221+
operation is not OperationType.Uninstall
222+
&& Package.Manager.Capabilities.SupportsCustomArchitectures;
223+
193224
VersionComboBox.IsEnabled =
194-
(operation == OperationType.Install
195-
|| operation == OperationType.None)
196-
&& (Package.Manager.Capabilities.SupportsCustomVersions
197-
|| Package.Manager.Capabilities.SupportsPreRelease);
225+
operation is OperationType.Install or OperationType.None
226+
&& (Package.Manager.Capabilities.SupportsCustomVersions || Package.Manager.Capabilities.SupportsPreRelease);
198227
ScopeCombo.IsEnabled = Package.Manager.Capabilities.SupportsCustomScopes;
199228
ResetDir.IsEnabled = Package.Manager.Capabilities.SupportsCustomLocations;
200229
SelectDir.IsEnabled = Package.Manager.Capabilities.SupportsCustomLocations;
@@ -269,6 +298,9 @@ public async Task<InstallOptions> GetUpdatedOptions(bool updateIgnoredUpdates =
269298
Options.CustomParameters_Uninstall = CustomParameters3.Text.Split(' ').ToList();
270299
Options.PreRelease = VersionComboBox.SelectedValue.ToString() == CoreTools.Translate("PreRelease");
271300

301+
Options.KillBeforeOperation.Clear();
302+
foreach(var p in ProcessesToKill) Options.KillBeforeOperation.Add(p.Name);
303+
272304
if (VersionComboBox.SelectedValue.ToString() != CoreTools.Translate("PreRelease") && VersionComboBox.SelectedValue.ToString() != CoreTools.Translate("Latest"))
273305
{
274306
Options.Version = VersionComboBox.SelectedValue.ToString() ?? "";
@@ -363,5 +395,50 @@ private void GoToSecureSettings_Click(object sender, RoutedEventArgs e)
363395
Close?.Invoke(this, EventArgs.Empty);
364396
MainApp.Instance.MainWindow.NavigationPage.OpenSettingsPage(typeof(Administrator));
365397
}
398+
399+
private void KillProcessesBox_TokenItemAdding(TokenizingTextBox sender, TokenItemAddingEventArgs args)
400+
{
401+
args.Item = _runningProcesses.FirstOrDefault((item) => item.Name.Contains(args.TokenText));
402+
if(args.Item is null)
403+
{
404+
string text = args.TokenText;
405+
if (!text.EndsWith(".exe")) text += ".exe";
406+
args.Item = new IOP_Proc(text);
407+
}
408+
}
409+
410+
private async void KillProcessesBox_TextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args)
411+
{
412+
var text = KillProcessesBox.Text;
413+
await Task.Delay(100);
414+
if (text != KillProcessesBox.Text)
415+
return;
416+
417+
SuggestedProcesses.Clear();
418+
if (text.Trim() != "")
419+
{
420+
if (!text.EndsWith(".exe"))
421+
text = text.Trim() + ".exe";
422+
SuggestedProcesses.Add(new(text));
423+
foreach (var item in _runningProcesses.Where(x => x.Name.Contains(KillProcessesBox.Text)))
424+
{
425+
SuggestedProcesses.Add(item);
426+
}
427+
}
428+
}
429+
430+
private void KillProcessesBox_Loaded(object sender, RoutedEventArgs e)
431+
{
432+
433+
}
434+
}
435+
436+
public class IOP_Proc
437+
{
438+
public readonly string Name;
439+
public IOP_Proc(string name)
440+
{
441+
Name = name;
442+
}
366443
}
367444
}

src/UniGetUI/UniGetUI.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
<PackageReference Include="CommunityToolkit.WinUI.Controls.Segmented" Version="8.2.250402" />
7474
<PackageReference Include="CommunityToolkit.WinUI.Controls.SettingsControls" Version="8.2.250402" />
7575
<PackageReference Include="CommunityToolkit.WinUI.Controls.Sizers" Version="8.2.250402" />
76+
<PackageReference Include="CommunityToolkit.WinUI.Controls.TokenizingTextBox" Version="8.2.250402" />
7677
<PackageReference Include="CommunityToolkit.WinUI.Converters" Version="8.2.250402" />
7778
<PackageReference Include="CommunityToolkit.WinUI.Media" Version="8.2.250402" />
7879
<PackageReference Include="H.NotifyIcon.WinUI" Version="2.3.0" />

0 commit comments

Comments
 (0)