fix(installer,worker): service hosting, dark theme, uninstall polish
Some checks failed
Release / release (push) Failing after 0s
Some checks failed
Release / release (push) Failing after 0s
Worker: - Wire UseWindowsService + Microsoft.Extensions.Hosting.WindowsServices so SCM's Service Control Protocol handshake succeeds. Previously the binary exited immediately under sc start, leaving the service registered but never running. Installer: - Pin SDK to .NET 9 (global.json) — SDK 10 dropped win-arm from its RID graph, breaking restore of the WPF project; .NET 9 keeps win-arm AND understands the .slnx solution format. - Force SelfContained=true and default RID=win-x64 when PublishSingleFile is set, so Rider Publish and CLI produce the same bundle. - Dark theme: set Background/Foreground explicitly on WizardWindow and SettingsWindow roots (WPF implicit styles don't cascade to derived Window types). Custom ComboBox template + ComboBoxItem style so dropdowns honour the dark palette instead of system defaults. - Throttle download progress to one report per MB and overwrite the same UI line (\r prefix marker) instead of appending per chunk. - Register ClaudeDo in HKLM\...\Uninstall so it appears in Apps & Features. Copy installer into InstallDir\uninstaller\ for the UninstallString, and schedule a cmd.exe trampoline to handle the self-delete case when Apps & Features launches the copy from inside the install dir. - Treat sc.exe stop exit 1062 (ERROR_SERVICE_NOT_ACTIVE) as success. - Delete the uninstall registry key during UninstallRunner. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -44,9 +44,18 @@ public sealed class DownloadAndExtractStep : IInstallStep
|
||||
var zipPath = Path.Combine(scratchDir, zipAsset.Name);
|
||||
var checksumPath = Path.Combine(scratchDir, "checksums.txt");
|
||||
|
||||
progress.Report($"Downloading {zipAsset.Name} ({zipAsset.Size / (1024 * 1024)} MB)...");
|
||||
var totalMb = zipAsset.Size / (1024 * 1024);
|
||||
progress.Report($"Downloading {zipAsset.Name} ({totalMb} MB)...");
|
||||
long lastReportedMb = -1;
|
||||
await _releases.DownloadAsync(zipAsset.BrowserDownloadUrl, zipPath,
|
||||
new Progress<long>(b => progress.Report($" {b / (1024 * 1024)} MB downloaded")),
|
||||
new Progress<long>(b =>
|
||||
{
|
||||
var mb = b / (1024 * 1024);
|
||||
if (mb == lastReportedMb) return;
|
||||
lastReportedMb = mb;
|
||||
// Leading "\r" tells the UI to overwrite the previous line instead of appending.
|
||||
progress.Report($"\r {mb} / {totalMb} MB downloaded");
|
||||
}),
|
||||
ct);
|
||||
|
||||
progress.Report("Downloading checksums...");
|
||||
|
||||
@@ -27,6 +27,12 @@ public sealed class StopServiceStep : IInstallStep
|
||||
}
|
||||
|
||||
var (stopExit, _) = await ProcessRunner.RunAsync("sc.exe", $"stop {ServiceName}", null, progress, ct);
|
||||
// 1062 = ERROR_SERVICE_NOT_ACTIVE — registered but not running, treat as already stopped.
|
||||
if (stopExit == 1062)
|
||||
{
|
||||
progress.Report("Service was registered but not running.");
|
||||
return StepResult.Ok();
|
||||
}
|
||||
if (stopExit != 0)
|
||||
return StepResult.Fail($"sc.exe stop failed with exit code {stopExit}");
|
||||
|
||||
|
||||
81
src/ClaudeDo.Installer/Steps/WriteUninstallRegistryStep.cs
Normal file
81
src/ClaudeDo.Installer/Steps/WriteUninstallRegistryStep.cs
Normal file
@@ -0,0 +1,81 @@
|
||||
using System.IO;
|
||||
using ClaudeDo.Installer.Core;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace ClaudeDo.Installer.Steps;
|
||||
|
||||
/// <summary>
|
||||
/// Registers ClaudeDo under <c>HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\ClaudeDo</c>
|
||||
/// so it shows up in Windows "Apps & Features" / "Programs and Features".
|
||||
/// Also copies the running installer into the install directory so there is an exe
|
||||
/// for UninstallString to reference after the temp-extracted single-file bundle is gone.
|
||||
/// </summary>
|
||||
public sealed class WriteUninstallRegistryStep : IInstallStep
|
||||
{
|
||||
internal const string UninstallKeyPath = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\ClaudeDo";
|
||||
|
||||
public string Name => "Register in Add/Remove Programs";
|
||||
|
||||
public async Task<StepResult> ExecuteAsync(InstallContext ctx, IProgress<string> progress, CancellationToken ct)
|
||||
{
|
||||
var uninstallDir = Path.Combine(ctx.InstallDirectory, "uninstaller");
|
||||
Directory.CreateDirectory(uninstallDir);
|
||||
var targetExe = Path.Combine(uninstallDir, "ClaudeDo.Installer.exe");
|
||||
|
||||
// Copy the running installer so Apps & Features has a stable exe to launch —
|
||||
// the single-file temp extract is gone once this process exits.
|
||||
var sourceExe = Environment.ProcessPath
|
||||
?? throw new InvalidOperationException("Cannot resolve running installer path.");
|
||||
try
|
||||
{
|
||||
progress.Report("Copying uninstaller binary...");
|
||||
File.Copy(sourceExe, targetExe, overwrite: true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return StepResult.Fail($"Failed to copy uninstaller exe: {ex.Message}");
|
||||
}
|
||||
|
||||
progress.Report("Writing Add/Remove Programs entry...");
|
||||
try
|
||||
{
|
||||
using var key = Registry.LocalMachine.CreateSubKey(UninstallKeyPath, writable: true);
|
||||
if (key is null)
|
||||
return StepResult.Fail("Could not open uninstall registry key (permission denied?).");
|
||||
|
||||
key.SetValue("DisplayName", "ClaudeDo", RegistryValueKind.String);
|
||||
key.SetValue("DisplayVersion", ctx.InstallerVersion ?? "0.0.0", RegistryValueKind.String);
|
||||
key.SetValue("Publisher", "Mika Kuns", RegistryValueKind.String);
|
||||
key.SetValue("InstallLocation", ctx.InstallDirectory, RegistryValueKind.String);
|
||||
key.SetValue("UninstallString", $"\"{targetExe}\"", RegistryValueKind.String);
|
||||
key.SetValue("DisplayIcon", targetExe, RegistryValueKind.String);
|
||||
key.SetValue("NoModify", 1, RegistryValueKind.DWord);
|
||||
key.SetValue("NoRepair", 1, RegistryValueKind.DWord);
|
||||
|
||||
// Best-effort install size (KB) — scan install dir.
|
||||
try
|
||||
{
|
||||
var sizeKb = (int)(DirectorySizeBytes(ctx.InstallDirectory) / 1024);
|
||||
key.SetValue("EstimatedSize", sizeKb, RegistryValueKind.DWord);
|
||||
}
|
||||
catch { /* best-effort only */ }
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return StepResult.Fail($"Failed to write uninstall registry: {ex.Message}");
|
||||
}
|
||||
|
||||
await Task.CompletedTask;
|
||||
return StepResult.Ok();
|
||||
}
|
||||
|
||||
private static long DirectorySizeBytes(string path)
|
||||
{
|
||||
long total = 0;
|
||||
foreach (var file in Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories))
|
||||
{
|
||||
try { total += new FileInfo(file).Length; } catch { /* ignore */ }
|
||||
}
|
||||
return total;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user