Skip to content

Commit c7ff941

Browse files
committed
Ensure all ADB processes exit before questpatcher quits
(closes #181)
1 parent 6bc6322 commit c7ff941

File tree

3 files changed

+75
-9
lines changed

3 files changed

+75
-9
lines changed

QuestPatcher.Core/AndroidDebugBridge.cs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ public AdbDevice(string id, string model)
6060
/// <summary>
6161
/// Abstraction over using ADB to interact with the Quest.
6262
/// </summary>
63-
public class AndroidDebugBridge
63+
public class AndroidDebugBridge : IDisposable
6464
{
6565
/// <summary>
6666
/// Package names that will not be included in the apps to patch list
@@ -95,6 +95,7 @@ public class AndroidDebugBridge
9595
private readonly IUserPrompter _prompter;
9696
private readonly string _adbExecutableName = OperatingSystem.IsWindows() ? "adb.exe" : "adb";
9797
private readonly Action _quit;
98+
private readonly ProcessUtil _processUtil = new();
9899

99100
/// <summary>
100101
/// The minimum time between checks for the currently open ADB daemon.
@@ -144,7 +145,7 @@ public async Task PrepareAdbPath()
144145
{
145146
// Redownloading ADB - existing installation was not valid
146147
Log.Information("Existing downloaded ADB was out of date or corrupted - fetching again");
147-
await ProcessUtil.InvokeAndCaptureOutput(downloadedAdb, "kill-server"); // Kill server first, otherwise directory will be in use, so can't be deleted.
148+
await _processUtil.InvokeAndCaptureOutput(downloadedAdb, "kill-server"); // Kill server first, otherwise directory will be in use, so can't be deleted.
148149
_adbPath = await _filesDownloader.GetFileLocation(ExternalFileType.PlatformTools, true);
149150
}
150151
else
@@ -166,7 +167,7 @@ private async Task<bool> SetAdbPathIfValid(string adbExecutablePath)
166167
try
167168
{
168169
Log.Verbose("Checking if ADB at {AdbPath} is present and up-to-date", adbExecutablePath);
169-
var outputInfo = await ProcessUtil.InvokeAndCaptureOutput(adbExecutablePath, "version");
170+
var outputInfo = await _processUtil.InvokeAndCaptureOutput(adbExecutablePath, "version");
170171
string output = outputInfo.AllOutput;
171172

172173
Log.Debug("Output from checking ADB version: {VerisonOutput}", output);
@@ -225,7 +226,7 @@ private async Task<List<AdbDevice>> ListDevices()
225226
await PrepareAdbPath();
226227
}
227228

228-
var output = await ProcessUtil.InvokeAndCaptureOutput(_adbPath!, "devices -l");
229+
var output = await _processUtil.InvokeAndCaptureOutput(_adbPath!, "devices -l");
229230
Log.Debug("Listing devices output {Output}", output.AllOutput);
230231

231232
string[] lines = output.StandardOutput.Trim().Split('\n');
@@ -390,7 +391,7 @@ public async Task<ProcessOutput> RunCommand(string command, params int[] allowed
390391
Log.Debug("Executing ADB command: {Command}", $"adb {command}");
391392
while (true)
392393
{
393-
var output = await ProcessUtil.InvokeAndCaptureOutput(_adbPath, $"-s {chosenDeviceId} " + command);
394+
var output = await _processUtil.InvokeAndCaptureOutput(_adbPath, $"-s {chosenDeviceId} " + command);
394395
if (output.StandardOutput.Length > 0)
395396
{
396397
Log.Verbose("Standard output: {StandardOutput}", output.StandardOutput);
@@ -827,5 +828,11 @@ public async Task<bool> FileExists(string path)
827828
var directoryFiles = await ListDirectoryFiles(dirName, true);
828829
return directoryFiles.Contains(Path.GetFileName(path));
829830
}
831+
832+
public void Dispose()
833+
{
834+
_logcatProcess?.Dispose();
835+
_processUtil.Dispose(); // Ensure all ADB processes are killed.
836+
}
830837
}
831838
}

QuestPatcher.Core/ProcessUtil.cs

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
using System.ComponentModel;
1+
using System;
2+
using System.Collections.Generic;
3+
using System.ComponentModel;
24
using System.Diagnostics;
35
using System.Text;
46
using System.Threading.Tasks;
@@ -39,18 +41,27 @@ public struct ProcessOutput
3941
public string? FullPath { get; set; }
4042
}
4143

42-
public static class ProcessUtil
44+
/// <summary>
45+
/// A utility class for invoking processes and capturing their output.
46+
/// Automatically kills all running processes when the instance is disposed.
47+
/// This class is thread safe.
48+
/// </summary>
49+
public class ProcessUtil : IDisposable
4350
{
51+
private readonly HashSet<Process> _runningProcesses = new();
52+
private readonly object _processesLock = new object();
53+
private bool _disposed = false;
54+
4455
/// <summary>
4556
/// Invokes an executable fileName with args and captures its standard and error output.
4657
/// Waits until the process exits before the task completes.
4758
/// </summary>
4859
/// <param name="fileName">File name of the application to call</param>
4960
/// <param name="arguments">Arguments to pass</param>
5061
/// <returns>The standard and error output of the process</returns>
51-
public static async Task<ProcessOutput> InvokeAndCaptureOutput(string fileName, string arguments)
62+
public async Task<ProcessOutput> InvokeAndCaptureOutput(string fileName, string arguments)
5263
{
53-
Process process = new();
64+
using Process process = new();
5465

5566
var startInfo = process.StartInfo;
5667
startInfo.FileName = fileName;
@@ -78,6 +89,11 @@ public static async Task<ProcessOutput> InvokeAndCaptureOutput(string fileName,
7889
};
7990

8091
process.Start();
92+
lock (_processesLock)
93+
{
94+
_runningProcesses.Add(process);
95+
}
96+
8197
string? fullPath = null;
8298
try
8399
{
@@ -92,6 +108,15 @@ public static async Task<ProcessOutput> InvokeAndCaptureOutput(string fileName,
92108
process.BeginErrorReadLine();
93109

94110
await process.WaitForExitAsync();
111+
lock (_processesLock)
112+
{
113+
_runningProcesses.Remove(process);
114+
}
115+
116+
if (_disposed)
117+
{
118+
throw new OperationCanceledException("QuestPatcher closing, process prematurely ended");
119+
}
95120

96121
return new ProcessOutput
97122
{
@@ -101,5 +126,34 @@ public static async Task<ProcessOutput> InvokeAndCaptureOutput(string fileName,
101126
FullPath = fullPath
102127
};
103128
}
129+
130+
public void Dispose()
131+
{
132+
if(_disposed)
133+
{
134+
return;
135+
}
136+
_disposed = true;
137+
138+
lock(_processesLock)
139+
{
140+
if(_runningProcesses.Count > 0)
141+
{
142+
Log.Information("Killing {NumActiveProcesses} active processes", _runningProcesses.Count);
143+
}
144+
145+
foreach (var process in _runningProcesses)
146+
{
147+
try
148+
{
149+
process.Kill();
150+
}
151+
catch (Exception ex)
152+
{
153+
Log.Warning(ex, "Failed to kill process PID {ProcessId} upon exit", process.Id);
154+
}
155+
}
156+
}
157+
}
104158
}
105159
}

QuestPatcher.Core/QuestPatcherService.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,10 @@ public void CleanUp()
9797
{
9898
Log.Debug("Closing QuestPatcher . . .");
9999
_configManager.SaveConfig();
100+
101+
// Kill active ADB processes to ensure that the temp folder can be deleted.
102+
DebugBridge.Dispose();
103+
100104
try
101105
{
102106
Directory.Delete(SpecialFolders.TempFolder, true);
@@ -105,6 +109,7 @@ public void CleanUp()
105109
{
106110
Log.Warning("Failed to delete temporary directory");
107111
}
112+
108113
Log.Debug("Goodbye!");
109114
Log.CloseAndFlush();
110115
}

0 commit comments

Comments
 (0)