Files
ClaudeDo/src/ClaudeDo.Installer/Pages/InstallPage/InstallPageViewModel.cs
Mika Kuns f599f8d0af
Some checks failed
Release / release (push) Failing after 0s
fix(installer,worker): service hosting, dark theme, uninstall polish
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>
2026-04-15 14:19:09 +02:00

164 lines
5.3 KiB
C#

using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
using ClaudeDo.Installer.Core;
using ClaudeDo.Installer.Steps;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.DependencyInjection;
namespace ClaudeDo.Installer.Pages.InstallPage;
public partial class InstallPageViewModel : ObservableObject, IInstallerPage
{
private readonly InstallContext _context;
private readonly IServiceProvider _serviceProvider;
private InstallPageView? _view;
private CancellationTokenSource? _cts;
public string Title => "Install";
public string Icon => "\uE896";
public int Order => 99;
public bool ShowInWizard => true;
public bool ShowInSettings => false;
public UserControl View => _view ??= new InstallPageView { DataContext = this };
public ObservableCollection<StepViewModel> Steps { get; } = [];
[ObservableProperty] private bool _isInstalling;
[ObservableProperty] private bool _isComplete;
[ObservableProperty] private bool _hasErrors;
[ObservableProperty] private double _overallProgress;
public InstallPageViewModel(InstallContext context, IServiceProvider serviceProvider)
{
_context = context;
_serviceProvider = serviceProvider;
}
public Task LoadAsync()
{
Steps.Clear();
if (_context.Mode == InstallerMode.Update)
{
Steps.Add(new StepViewModel("Stop Worker Service"));
Steps.Add(new StepViewModel("Download and Extract"));
Steps.Add(new StepViewModel("Start Worker Service"));
Steps.Add(new StepViewModel("Write Install Manifest"));
}
else
{
Steps.Add(new StepViewModel("Download and Extract"));
Steps.Add(new StepViewModel("Write Configuration"));
Steps.Add(new StepViewModel("Initialize Database"));
Steps.Add(new StepViewModel("Register Windows Service"));
Steps.Add(new StepViewModel("Create Shortcuts"));
Steps.Add(new StepViewModel("Register in Add/Remove Programs"));
Steps.Add(new StepViewModel("Write Install Manifest"));
}
return Task.CompletedTask;
}
public Task ApplyAsync() => RunInstallAsync();
public bool Validate() => true;
[RelayCommand]
private async Task RunInstallAsync()
{
if (IsInstalling) return;
IsInstalling = true;
IsComplete = false;
HasErrors = false;
OverallProgress = 0;
_cts = new CancellationTokenSource();
var progress = new Progress<StepProgress>(p =>
{
var step = Steps.FirstOrDefault(s => s.Name == p.StepName);
if (step is null) return;
step.Status = p.Status;
if (p.Message is not null)
{
// Messages starting with "\r" overwrite the previous line (live progress).
if (p.Message.StartsWith('\r'))
{
var line = p.Message[1..];
if (step.Messages.Count > 0 && step.Messages[^1].StartsWith(" "))
step.Messages[^1] = line;
else
step.Messages.Add(line);
}
else
{
step.Messages.Add(p.Message);
}
}
if (p.Status is StepStatus.Running && !step.IsExpanded)
step.IsExpanded = true;
if (p.Status is StepStatus.Done or StepStatus.Failed)
{
var completed = Steps.Count(s => s.Status is StepStatus.Done or StepStatus.Failed);
OverallProgress = (double)completed / Steps.Count * 100;
}
});
try
{
IEnumerable<IInstallStep> steps;
if (_context.Mode == InstallerMode.Update)
{
steps = new IInstallStep[]
{
_serviceProvider.GetRequiredService<StopServiceStep>(),
_serviceProvider.GetRequiredService<DownloadAndExtractStep>(),
_serviceProvider.GetRequiredService<StartServiceStep>(),
_serviceProvider.GetRequiredService<WriteInstallManifestStep>(),
};
}
else
{
steps = _serviceProvider.GetServices<IInstallStep>();
}
var runner = new InstallerService(steps);
var results = await runner.ExecuteAsync(_context, progress, _cts.Token);
HasErrors = results.Any(r => !r.Result.Success);
}
catch (OperationCanceledException)
{
HasErrors = true;
}
finally
{
IsInstalling = false;
IsComplete = true;
_cts.Dispose();
_cts = null;
}
}
[RelayCommand]
private void CancelInstall()
{
_cts?.Cancel();
}
[RelayCommand]
private void LaunchApp()
{
var appExe = System.IO.Path.Combine(_context.InstallDirectory, "app", "ClaudeDo.App.exe");
if (System.IO.File.Exists(appExe))
{
Process.Start(new ProcessStartInfo(appExe) { UseShellExecute = true });
Application.Current.Shutdown();
}
}
}