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>
101 lines
4.4 KiB
C#
101 lines
4.4 KiB
C#
using System.IO;
|
|
using System.IO.Compression;
|
|
using ClaudeDo.Installer.Core;
|
|
|
|
namespace ClaudeDo.Installer.Steps;
|
|
|
|
public sealed class DownloadAndExtractStep : IInstallStep
|
|
{
|
|
private readonly IReleaseClient _releases;
|
|
|
|
public DownloadAndExtractStep(IReleaseClient releases)
|
|
{
|
|
_releases = releases;
|
|
}
|
|
|
|
public string Name => "Download and Extract";
|
|
|
|
public async Task<StepResult> ExecuteAsync(InstallContext ctx, IProgress<string> progress, CancellationToken ct)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(ctx.InstallDirectory))
|
|
return StepResult.Fail("Install directory is not set.");
|
|
|
|
progress.Report("Fetching latest release metadata...");
|
|
var release = await _releases.GetLatestReleaseAsync(ct);
|
|
if (release is null)
|
|
return StepResult.Fail("Could not reach the release server. Check your network connection and try again.");
|
|
|
|
var zipAsset = release.Assets.FirstOrDefault(a =>
|
|
a.Name.StartsWith("ClaudeDo-", StringComparison.OrdinalIgnoreCase) &&
|
|
a.Name.EndsWith("-win-x64.zip", StringComparison.OrdinalIgnoreCase));
|
|
var checksumAsset = release.Assets.FirstOrDefault(a =>
|
|
a.Name.Equals("checksums.txt", StringComparison.OrdinalIgnoreCase));
|
|
|
|
if (zipAsset is null)
|
|
return StepResult.Fail("Release zip asset not found in release metadata.");
|
|
if (checksumAsset is null)
|
|
return StepResult.Fail("checksums.txt not found in release metadata.");
|
|
|
|
var scratchDir = Path.Combine(Path.GetTempPath(), "ClaudeDo-install-" + Guid.NewGuid().ToString("N"));
|
|
Directory.CreateDirectory(scratchDir);
|
|
|
|
try
|
|
{
|
|
var zipPath = Path.Combine(scratchDir, zipAsset.Name);
|
|
var checksumPath = Path.Combine(scratchDir, "checksums.txt");
|
|
|
|
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 =>
|
|
{
|
|
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...");
|
|
await _releases.DownloadAsync(checksumAsset.BrowserDownloadUrl, checksumPath,
|
|
new Progress<long>(_ => { }), ct);
|
|
|
|
progress.Report("Verifying checksum...");
|
|
var map = ChecksumVerifier.ParseChecksumsFile(await File.ReadAllTextAsync(checksumPath, ct));
|
|
if (!map.TryGetValue(zipAsset.Name, out var expectedHash))
|
|
return StepResult.Fail($"No checksum entry for {zipAsset.Name} in checksums.txt.");
|
|
if (!ChecksumVerifier.Verify(zipPath, expectedHash))
|
|
return StepResult.Fail("Checksum mismatch — the downloaded zip may be corrupt or tampered with.");
|
|
|
|
// Only after verification do we touch the install directory.
|
|
progress.Report("Clearing previous app/worker binaries...");
|
|
var appDest = Path.Combine(ctx.InstallDirectory, "app");
|
|
var workerDest = Path.Combine(ctx.InstallDirectory, "worker");
|
|
if (Directory.Exists(appDest)) Directory.Delete(appDest, recursive: true);
|
|
if (Directory.Exists(workerDest)) Directory.Delete(workerDest, recursive: true);
|
|
|
|
progress.Report("Extracting...");
|
|
Directory.CreateDirectory(ctx.InstallDirectory);
|
|
try
|
|
{
|
|
ZipFile.ExtractToDirectory(zipPath, ctx.InstallDirectory, overwriteFiles: true);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return StepResult.Fail(
|
|
$"Extraction failed after old binaries were removed: {ex.Message}. " +
|
|
"Your install directory may be incomplete. Re-run the installer to retry.");
|
|
}
|
|
|
|
ctx.InstalledVersion = release.TagName.TrimStart('v', 'V');
|
|
return StepResult.Ok();
|
|
}
|
|
finally
|
|
{
|
|
try { Directory.Delete(scratchDir, recursive: true); } catch { /* best effort */ }
|
|
}
|
|
}
|
|
}
|