Skip to content

Commit e9cc1d8

Browse files
committed
feat: configurable uninstall timeout (30min default), winget JSON parsing, orphaned Package Cache scanner, USB device history cleaner
1 parent 4edb02e commit e9cc1d8

5 files changed

Lines changed: 124 additions & 13 deletions

File tree

src/DeepPurge.Cli/Program.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,9 +164,12 @@ private static async Task<int> CmdCleanAsync(ParsedArgs a, CancellationToken ct)
164164

165165
private static async Task<int> CmdUninstallAsync(ParsedArgs a, CancellationToken ct)
166166
{
167-
if (a.Positional.Count == 0) return Fail("usage: deeppurgecli uninstall <name-or-id> [--silent]");
167+
if (a.Positional.Count == 0) return Fail("usage: deeppurgecli uninstall <name-or-id> [--silent] [--timeout <minutes>]");
168168
bool silent = a.HasFlag("silent");
169169
var nameArg = a.Positional[0];
170+
var timeoutStr = a.GetOption("timeout");
171+
if (timeoutStr != null && int.TryParse(timeoutStr, out var mins))
172+
UninstallEngine.UninstallerTimeout = TimeSpan.FromMinutes(mins);
170173

171174
var items = await Task.Run(() => InstalledProgramScanner.GetAllInstalledPrograms(), ct);
172175
var match = items.FirstOrDefault(p =>
@@ -570,7 +573,7 @@ public sealed class ParsedArgs
570573
// when you add a new command that needs them.
571574
private static readonly HashSet<string> ValueOptions = new(StringComparer.OrdinalIgnoreCase)
572575
{
573-
"name", "freq", "time", "day", "args", "export", "format", "program",
576+
"name", "freq", "time", "day", "args", "export", "format", "program", "timeout",
574577
};
575578

576579
public bool HasFlag(string name) => Flags.Contains(name);

src/DeepPurge.Core/FileSystem/JunkFilesCleaner.cs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ public static List<JunkCategory> ScanForJunk()
6767
ScanLogFiles(), ScanCrashDumps(), ScanWerReports(),
6868
// Installer cache
6969
ScanInstallerCache(),
70+
// Package Cache (orphaned installer caches)
71+
ScanPackageCache(),
7072
// App-specific caches
7173
ScanAppCaches(),
7274
// Runtime caches
@@ -462,6 +464,44 @@ private static JunkCategory ScanInstallerCache()
462464
return cat;
463465
}
464466

467+
private static JunkCategory ScanPackageCache()
468+
{
469+
var cat = new JunkCategory
470+
{
471+
Name = "Orphaned Package Cache",
472+
Description = "Installer caches for products that are no longer installed",
473+
IsSelected = false,
474+
};
475+
var cacheRoot = Path.Combine(
476+
Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "Package Cache");
477+
if (!Directory.Exists(cacheRoot)) return cat;
478+
479+
var installed = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
480+
try
481+
{
482+
foreach (var p in Registry.InstalledProgramScanner.GetAllInstalledPrograms())
483+
{
484+
if (!string.IsNullOrEmpty(p.RegistryKeyName)) installed.Add(p.RegistryKeyName);
485+
if (!string.IsNullOrEmpty(p.DisplayName)) installed.Add(p.DisplayName);
486+
}
487+
}
488+
catch { }
489+
490+
try
491+
{
492+
foreach (var dir in Directory.GetDirectories(cacheRoot))
493+
{
494+
var name = Path.GetFileName(dir);
495+
if (installed.Any(i => name.Contains(i, StringComparison.OrdinalIgnoreCase))) continue;
496+
var size = GetDirSize(dir);
497+
if (size > 0)
498+
cat.Files.Add(new JunkFile { Path = dir, Size = size, IsDirectory = true });
499+
}
500+
}
501+
catch { }
502+
return cat;
503+
}
504+
465505
// ─── APP CACHES ───
466506
private static JunkCategory ScanAppCaches()
467507
{

src/DeepPurge.Core/Packages/PackageManagerScanner.cs

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -74,22 +74,46 @@ public static async Task EnrichAsync(
7474

7575
public static List<WingetEntry> QueryWinget(CancellationToken ct = default)
7676
{
77-
var result = new List<WingetEntry>();
78-
string output;
7977
try
8078
{
81-
output = RunProcess(
82-
"winget.exe",
83-
"list --disable-interactivity --accept-source-agreements",
84-
ct);
79+
var jsonOutput = RunProcess("winget.exe",
80+
"list --disable-interactivity --accept-source-agreements --output json", ct);
81+
if (!string.IsNullOrWhiteSpace(jsonOutput) && jsonOutput.TrimStart().StartsWith('['))
82+
return ParseWingetJson(jsonOutput);
8583
}
86-
catch
84+
catch { }
85+
86+
try
8787
{
88-
return result;
88+
var tableOutput = RunProcess("winget.exe",
89+
"list --disable-interactivity --accept-source-agreements", ct);
90+
if (!string.IsNullOrWhiteSpace(tableOutput)) return ParseWingetTable(tableOutput);
8991
}
92+
catch { }
93+
return new();
94+
}
9095

91-
if (string.IsNullOrWhiteSpace(output)) return result;
92-
return ParseWingetTable(output);
96+
internal static List<WingetEntry> ParseWingetJson(string json)
97+
{
98+
var entries = new List<WingetEntry>();
99+
try
100+
{
101+
using var doc = System.Text.Json.JsonDocument.Parse(json);
102+
foreach (var item in doc.RootElement.EnumerateArray())
103+
{
104+
var name = item.TryGetProperty("Name", out var n) ? n.GetString() ?? "" : "";
105+
var id = item.TryGetProperty("Id", out var i) ? i.GetString() ?? "" : "";
106+
var version = item.TryGetProperty("InstalledVersion", out var v)
107+
? v.GetString() ?? ""
108+
: item.TryGetProperty("Version", out var v2) ? v2.GetString() ?? "" : "";
109+
var available = item.TryGetProperty("AvailableVersion", out var a) ? a.GetString() ?? "" : "";
110+
var source = item.TryGetProperty("Source", out var s) ? s.GetString() ?? "" : "";
111+
if (name.Length > 0 && id.Length > 0)
112+
entries.Add(new WingetEntry(id, name, version, available, source));
113+
}
114+
}
115+
catch { }
116+
return entries;
93117
}
94118

95119
/// <summary>

src/DeepPurge.Core/Privacy/EvidenceRemover.cs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ public static List<TraceCategory> ScanAllTraces()
6666
ScanDeliveryOptimization(),
6767
ScanWindowsErrorReporting(),
6868
ScanFontCache(),
69+
ScanUsbDeviceHistory(),
6970
};
7071
cats.RemoveAll(c => c.Items.Count == 0);
7172
return cats;
@@ -446,6 +447,49 @@ private static TraceCategory ScanFontCache()
446447
return cat;
447448
}
448449

450+
private static TraceCategory ScanUsbDeviceHistory()
451+
{
452+
var cat = new TraceCategory
453+
{
454+
Name = "USB Device History",
455+
Description = "Registry records of previously connected USB devices, SetupAPI logs",
456+
};
457+
458+
try
459+
{
460+
using var usbstorKey = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(
461+
@"SYSTEM\CurrentControlSet\Enum\USBSTOR");
462+
if (usbstorKey != null)
463+
{
464+
foreach (var deviceClass in usbstorKey.GetSubKeyNames())
465+
{
466+
using var classKey = usbstorKey.OpenSubKey(deviceClass);
467+
if (classKey == null) continue;
468+
foreach (var serial in classKey.GetSubKeyNames())
469+
{
470+
cat.Items.Add(new TraceItem
471+
{
472+
Path = $@"HKLM\SYSTEM\CurrentControlSet\Enum\USBSTOR\{deviceClass}\{serial}",
473+
SizeBytes = 0,
474+
IsDirectory = false,
475+
});
476+
}
477+
}
478+
}
479+
}
480+
catch { }
481+
482+
var setupApiLog = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows),
483+
"inf", "setupapi.dev.log");
484+
AddFile(cat, setupApiLog);
485+
486+
var setupApiAppLog = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows),
487+
"inf", "setupapi.app.log");
488+
AddFile(cat, setupApiAppLog);
489+
490+
return cat;
491+
}
492+
449493
// ═══════════════════════════════════════════════════════
450494
// Helpers
451495
// ═══════════════════════════════════════════════════════

src/DeepPurge.Core/Uninstall/UninstallEngine.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public class UninstallEngine
2323
private readonly BackupManager _backupManager = new();
2424

2525
private static readonly HashSet<int> UninstallerSuccessCodes = new() { 0, 1641, 3010 };
26-
private static readonly TimeSpan UninstallerTimeout = TimeSpan.FromMinutes(10);
26+
public static TimeSpan UninstallerTimeout { get; set; } = TimeSpan.FromMinutes(30);
2727

2828
public async Task<UninstallResult> UninstallAsync(InstalledProgram program, ScanMode scanMode,
2929
bool createRestorePoint = true, bool runBuiltInUninstaller = true, bool silent = false,

0 commit comments

Comments
 (0)