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
18 changes: 18 additions & 0 deletions docs/release-drafts/v0.3.5.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# PriorityGear v0.3.5 - winget readiness installer fix

v0.3.5 fixes a blocker found while validating the v0.3.4 installer against the winget local lifecycle gate.

## Installer

- Fixes silent uninstall when `PriorityGear.Setup.exe` is launched from the installed version directory.
- Removes the uninstall registration and Start Menu shortcut before removing installed program files.
- Schedules installed program file removal after setup exits when the running setup executable is inside `%ProgramFiles%\PriorityGear`.
- Preserves `%ProgramData%\PriorityGear` rules and logs by default.

## Distribution boundary

- This is still an unsigned GitHub installer release.
- Store, winget acceptance, signing, MSI, and MSIX are not claimed by this release.
- The installer configures `PriorityGear.Service` as a LocalSystem service.
- Changing process priority can affect system stability.
- Arbitrary shared-host `svchost.exe` mutation remains out of scope.
43 changes: 37 additions & 6 deletions src/PriorityGear.Setup/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -356,14 +356,9 @@ internal static async Task UninstallAsync(SetupInstallPlan plan, SetupLog log)
log.Info("Service is not installed.");
}

if (Directory.Exists(plan.BaseInstallDirectory))
{
Directory.Delete(plan.BaseInstallDirectory, recursive: true);
log.Info($"Removed installed program files: {plan.BaseInstallDirectory}");
}

DeleteUninstallRegistration(plan, log);
DeleteStartMenuShortcut(plan, log);
RemoveInstallDirectory(plan, log);
log.Info($"Preserved data directory: {plan.ProgramDataDirectory}");
log.Info("Delete ProgramData manually only if machine rules and logs are no longer needed.");
}
Expand Down Expand Up @@ -620,6 +615,42 @@ private static void DeleteStartMenuShortcut(SetupInstallPlan plan, SetupLog log)
log.Info($"Removed Start Menu shortcut if present: {plan.StartMenuShortcutPath}");
}

private static void RemoveInstallDirectory(SetupInstallPlan plan, SetupLog log)
{
if (!Directory.Exists(plan.BaseInstallDirectory))
{
log.Info($"Installed program files were already absent: {plan.BaseInstallDirectory}");
return;
}

string currentProcessPath = Environment.ProcessPath ?? Application.ExecutablePath;
if (Path.GetFullPath(currentProcessPath).StartsWith(
Path.GetFullPath(plan.BaseInstallDirectory) + Path.DirectorySeparatorChar,
StringComparison.OrdinalIgnoreCase))
{
ScheduleInstallDirectoryRemoval(plan.BaseInstallDirectory, log);
return;
}

Directory.Delete(plan.BaseInstallDirectory, recursive: true);
log.Info($"Removed installed program files: {plan.BaseInstallDirectory}");
}

private static void ScheduleInstallDirectoryRemoval(string installDirectory, SetupLog log)
{
string command = $"timeout /t 2 /nobreak > nul & rmdir /s /q \"{installDirectory}\"";
using Process process = Process.Start(new ProcessStartInfo
{
FileName = "cmd.exe",
Arguments = $"/c {command}",
WorkingDirectory = Environment.GetFolderPath(Environment.SpecialFolder.System),
UseShellExecute = false,
CreateNoWindow = true
Comment thread
tsuchim marked this conversation as resolved.
}) ?? throw new InvalidOperationException("Failed to schedule installed program file removal.");

log.Info($"Scheduled installed program file removal after setup exits: {installDirectory}; cleanup pid={process.Id}");
}

private sealed class ProductionInstallerExecutor(SetupInstallPlan plan, SetupLog log, string payloadDirectory) : IInstallerExecutor
{
public void ValidatePayload()
Expand Down
26 changes: 26 additions & 0 deletions tests/PriorityGear.Setup.Tests/SetupPlanningTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,32 @@ public void StartMenuShortcutTargetsInstalledGuiApp()
Assert.Equal("PriorityGear", spec.Description);
}

[Fact]
public void UninstallRemovesRegistrationAndShortcutBeforeInstallDirectory()
{
string program = File.ReadAllText(Path.Combine(FindRepoRoot(), "src", "PriorityGear.Setup", "Program.cs"));
int deleteRegistration = program.IndexOf("DeleteUninstallRegistration(plan, log);", StringComparison.Ordinal);
int deleteShortcut = program.IndexOf("DeleteStartMenuShortcut(plan, log);", StringComparison.Ordinal);
int removeInstallDirectory = program.IndexOf("RemoveInstallDirectory(plan, log);", StringComparison.Ordinal);

Assert.NotEqual(-1, deleteRegistration);
Assert.NotEqual(-1, deleteShortcut);
Assert.NotEqual(-1, removeInstallDirectory);
Assert.True(deleteRegistration < deleteShortcut);
Assert.True(deleteShortcut < removeInstallDirectory);
}

[Fact]
public void UninstallSchedulesInstallDirectoryRemovalWhenSetupRunsFromInstallRoot()
{
string program = File.ReadAllText(Path.Combine(FindRepoRoot(), "src", "PriorityGear.Setup", "Program.cs"));

Assert.Contains("ScheduleInstallDirectoryRemoval(plan.BaseInstallDirectory, log);", program);
Assert.Contains("timeout /t 2 /nobreak", program);
Assert.Contains("rmdir /s /q", program);
Assert.Contains("WorkingDirectory = Environment.GetFolderPath(Environment.SpecialFolder.System)", program);
}

private static string FindRepoRoot()
{
DirectoryInfo? directory = new(AppContext.BaseDirectory);
Expand Down
Loading