feat(installer): register autostart via Startup shortcut, drop scheduled task

Replaces schtasks /Create with AutostartShortcut.Install; migrates away
legacy scheduled task and Windows service on upgrade. Removes ScheduledTaskXml.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
mika kuns
2026-06-01 12:09:07 +02:00
parent e2bb43ad6d
commit 133f2d2f1d
3 changed files with 9 additions and 97 deletions

View File

@@ -1,52 +0,0 @@
using System.Security;
namespace ClaudeDo.Installer.Core;
public static class ScheduledTaskXml
{
public static string Build(string userId, string workerExePath, int restartIntervalMinutes)
{
var minutes = restartIntervalMinutes < 1 ? 1 : restartIntervalMinutes;
var user = SecurityElement.Escape(userId);
var cmd = SecurityElement.Escape(workerExePath);
return $"""
<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
<RegistrationInfo>
<Description>ClaudeDo background worker (per-user).</Description>
</RegistrationInfo>
<Triggers>
<LogonTrigger>
<Enabled>true</Enabled>
<UserId>{user}</UserId>
</LogonTrigger>
</Triggers>
<Principals>
<Principal id="Author">
<UserId>{user}</UserId>
<LogonType>InteractiveToken</LogonType>
<RunLevel>LeastPrivilege</RunLevel>
</Principal>
</Principals>
<Settings>
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
<AllowHardTerminate>true</AllowHardTerminate>
<StartWhenAvailable>true</StartWhenAvailable>
<Hidden>true</Hidden>
<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
<RestartOnFailure>
<Interval>PT{minutes}M</Interval>
<Count>3</Count>
</RestartOnFailure>
</Settings>
<Actions Context="Author">
<Exec>
<Command>{cmd}</Command>
</Exec>
</Actions>
</Task>
""";
}
}

View File

@@ -1,12 +1,11 @@
using System.IO; using System.IO;
using System.Security.Principal;
using ClaudeDo.Installer.Core; using ClaudeDo.Installer.Core;
namespace ClaudeDo.Installer.Steps; namespace ClaudeDo.Installer.Steps;
public sealed class RegisterAutostartStep : IInstallStep public sealed class RegisterAutostartStep : IInstallStep
{ {
public const string TaskName = "ClaudeDoWorker"; public const string LegacyTaskName = "ClaudeDoWorker";
private const string LegacyServiceName = "ClaudeDoWorker"; private const string LegacyServiceName = "ClaudeDoWorker";
public string Name => "Register Autostart"; public string Name => "Register Autostart";
@@ -34,24 +33,19 @@ public sealed class RegisterAutostartStep : IInstallStep
} }
} }
// 2) Register (or replace) the per-user logon task. // 2) Migrate away the legacy logon scheduled task if present (best-effort).
var userId = WindowsIdentity.GetCurrent().Name; progress.Report("Removing legacy logon task...");
var minutes = Math.Max(1, ctx.RestartDelayMs / 60000); await ProcessRunner.RunAsync("schtasks.exe", $"/Delete /TN \"{LegacyTaskName}\" /F", null, progress, ct);
var xml = ScheduledTaskXml.Build(userId, workerExe, minutes);
var xmlPath = Path.Combine(Path.GetTempPath(), $"ClaudeDoWorker-{Guid.NewGuid():N}.xml"); // 3) Register per-user autostart via a Startup-folder shortcut.
await File.WriteAllTextAsync(xmlPath, xml, new System.Text.UnicodeEncoding(false, true), ct); progress.Report("Creating Startup shortcut...");
try try
{ {
progress.Report("Registering logon task..."); AutostartShortcut.Install(AutostartShortcut.DefaultStartupDir, workerExe);
var (exit, output) = await ProcessRunner.RunAsync(
"schtasks.exe", $"/Create /TN \"{TaskName}\" /XML \"{xmlPath}\" /F", null, progress, ct);
if (exit != 0)
return StepResult.Fail($"schtasks /Create failed (exit {exit}): {output}");
} }
finally catch (Exception ex)
{ {
try { File.Delete(xmlPath); } catch { /* best effort */ } return StepResult.Fail($"Failed to create Startup shortcut: {ex.Message}");
} }
return StepResult.Ok(); return StepResult.Ok();

View File

@@ -1,30 +0,0 @@
using ClaudeDo.Installer.Core;
namespace ClaudeDo.Installer.Tests;
public class ScheduledTaskXmlTests
{
[Fact]
public void Build_EmbedsUserExeAndLogonTrigger()
{
var xml = ScheduledTaskXml.Build(
userId: "MACHINE\\mika",
workerExePath: @"C:\Program Files\ClaudeDo\worker\ClaudeDo.Worker.exe",
restartIntervalMinutes: 1);
Assert.Contains("<LogonTrigger>", xml);
Assert.Contains("<UserId>MACHINE\\mika</UserId>", xml);
Assert.Contains("<LogonType>InteractiveToken</LogonType>", xml);
Assert.Contains("<Hidden>true</Hidden>", xml);
Assert.Contains("<RunLevel>LeastPrivilege</RunLevel>", xml);
Assert.Contains(@"C:\Program Files\ClaudeDo\worker\ClaudeDo.Worker.exe", xml);
Assert.Contains("<Interval>PT1M</Interval>", xml);
}
[Fact]
public void Build_ClampsRestartIntervalToOneMinuteMinimum()
{
var xml = ScheduledTaskXml.Build("M\\u", @"C:\w.exe", restartIntervalMinutes: 0);
Assert.Contains("<Interval>PT1M</Interval>", xml);
}
}