Merge claudedo/ebd5a205ecfe40c298e3995f9f92f0a1
This commit is contained in:
@@ -75,7 +75,7 @@ claude mcp add --transport http --scope user claudedo http://127.0.0.1:{External
|
|||||||
```
|
```
|
||||||
Non-fatal if `claude` CLI is missing or too old (prints the manual command). Server name: `claudedo`.
|
Non-fatal if `claude` CLI is missing or too old (prints the manual command). Server name: `claudedo`.
|
||||||
|
|
||||||
**`RegisterAutostartStep`** — creates a per-user Startup-folder shortcut `ClaudeDo Worker.lnk` (`Environment.SpecialFolder.Startup`). Also migrates away from legacy mechanisms:
|
**`RegisterAutostartStep`** — creates a per-user Startup-folder shortcut `ClaudeDo Worker.lnk` (`Environment.SpecialFolder.Startup`); `AutostartShortcut.Install` skips the rewrite (and reports it) when the shortcut already points at the current worker exe, so update/repair runs don't touch it needlessly. Also migrates away from legacy mechanisms, unconditionally on every run (no cached "already migrated" flag — see the comment in the step):
|
||||||
- Deletes legacy Windows service: `sc.exe stop/delete ClaudeDoWorker`
|
- Deletes legacy Windows service: `sc.exe stop/delete ClaudeDoWorker`
|
||||||
- Deletes legacy scheduled task: `schtasks /Delete /TN ClaudeDoWorker`
|
- Deletes legacy scheduled task: `schtasks /Delete /TN ClaudeDoWorker`
|
||||||
|
|
||||||
|
|||||||
@@ -11,13 +11,26 @@ public static class AutostartShortcut
|
|||||||
|
|
||||||
public static string PathIn(string startupDir) => Path.Combine(startupDir, FileName);
|
public static string PathIn(string startupDir) => Path.Combine(startupDir, FileName);
|
||||||
|
|
||||||
public static void Install(string startupDir, string workerExe)
|
/// <summary>Creates or updates the Startup shortcut. Returns false if it already pointed at <paramref name="workerExe"/> and was left untouched.</summary>
|
||||||
|
public static bool Install(string startupDir, string workerExe)
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(startupDir);
|
Directory.CreateDirectory(startupDir);
|
||||||
var workingDir = Path.GetDirectoryName(workerExe) ?? startupDir;
|
var shortcutPath = PathIn(startupDir);
|
||||||
ShortcutFactory.CreateShortcut(PathIn(startupDir), workerExe, workingDir, "ClaudeDo background worker");
|
if (File.Exists(shortcutPath))
|
||||||
|
{
|
||||||
|
var existingTarget = ShortcutFactory.TryGetTarget(shortcutPath);
|
||||||
|
if (existingTarget is not null && PathsEqual(existingTarget, workerExe))
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var workingDir = Path.GetDirectoryName(workerExe) ?? startupDir;
|
||||||
|
ShortcutFactory.CreateShortcut(shortcutPath, workerExe, workingDir, "ClaudeDo background worker");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool PathsEqual(string a, string b) =>
|
||||||
|
string.Equals(Path.GetFullPath(a), Path.GetFullPath(b), StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
public static void Remove(string startupDir)
|
public static void Remove(string startupDir)
|
||||||
{
|
{
|
||||||
var path = PathIn(startupDir);
|
var path = PathIn(startupDir);
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ namespace ClaudeDo.Installer.Core;
|
|||||||
|
|
||||||
public static class ShortcutFactory
|
public static class ShortcutFactory
|
||||||
{
|
{
|
||||||
|
private const int SlgpRawPath = 0x4;
|
||||||
|
|
||||||
public static void CreateShortcut(string shortcutPath, string targetPath, string workingDir, string description)
|
public static void CreateShortcut(string shortcutPath, string targetPath, string workingDir, string description)
|
||||||
{
|
{
|
||||||
var link = (IShellLink)new ShellLink();
|
var link = (IShellLink)new ShellLink();
|
||||||
@@ -18,6 +20,24 @@ public static class ShortcutFactory
|
|||||||
file.Save(shortcutPath, false);
|
file.Save(shortcutPath, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Reads the target path of an existing .lnk, or null if it can't be read (missing/corrupt).</summary>
|
||||||
|
public static string? TryGetTarget(string shortcutPath)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var link = (IShellLink)new ShellLink();
|
||||||
|
((IPersistFile)link).Load(shortcutPath, 0);
|
||||||
|
var sb = new StringBuilder(260);
|
||||||
|
link.GetPath(sb, sb.Capacity, IntPtr.Zero, SlgpRawPath);
|
||||||
|
var path = sb.ToString();
|
||||||
|
return path.Length == 0 ? null : path;
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[ComImport]
|
[ComImport]
|
||||||
[Guid("00021401-0000-0000-C000-000000000046")]
|
[Guid("00021401-0000-0000-C000-000000000046")]
|
||||||
private class ShellLink { }
|
private class ShellLink { }
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ public sealed class RegisterAutostartStep : IInstallStep
|
|||||||
if (!File.Exists(workerExe))
|
if (!File.Exists(workerExe))
|
||||||
return StepResult.Fail($"Worker executable not found: {workerExe}");
|
return StepResult.Fail($"Worker executable not found: {workerExe}");
|
||||||
|
|
||||||
|
// Legacy service/task cleanup below runs unconditionally on every install/update/repair,
|
||||||
|
// even though it's a no-op once migrated. A cached "already migrated" flag could go stale
|
||||||
|
// (e.g. a user re-adds the legacy service) and strand them with it still running; two
|
||||||
|
// extra process starts per run is the price for that migration safety net.
|
||||||
|
|
||||||
// 1) Migrate away the legacy Windows service if present.
|
// 1) Migrate away the legacy Windows service if present.
|
||||||
progress.Report("Checking for legacy worker service...");
|
progress.Report("Checking for legacy worker service...");
|
||||||
var (queryExit, _) = await ProcessRunner.RunAsync("sc.exe", $"query {LegacyServiceName}", null, progress, ct);
|
var (queryExit, _) = await ProcessRunner.RunAsync("sc.exe", $"query {LegacyServiceName}", null, progress, ct);
|
||||||
@@ -38,10 +43,11 @@ public sealed class RegisterAutostartStep : IInstallStep
|
|||||||
await ProcessRunner.RunAsync("schtasks.exe", $"/Delete /TN \"{LegacyTaskName}\" /F", null, progress, ct);
|
await ProcessRunner.RunAsync("schtasks.exe", $"/Delete /TN \"{LegacyTaskName}\" /F", null, progress, ct);
|
||||||
|
|
||||||
// 3) Register per-user autostart via a Startup-folder shortcut.
|
// 3) Register per-user autostart via a Startup-folder shortcut.
|
||||||
progress.Report("Creating Startup shortcut...");
|
progress.Report("Checking Startup shortcut...");
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
AutostartShortcut.Install(AutostartShortcut.DefaultStartupDir, workerExe);
|
var created = AutostartShortcut.Install(AutostartShortcut.DefaultStartupDir, workerExe);
|
||||||
|
progress.Report(created ? "Startup shortcut created." : "Startup shortcut already up to date.");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -22,13 +22,58 @@ public class AutostartShortcutTests
|
|||||||
var workerExe = Path.Combine(workerDir, "ClaudeDo.Worker.exe");
|
var workerExe = Path.Combine(workerDir, "ClaudeDo.Worker.exe");
|
||||||
File.WriteAllText(workerExe, "");
|
File.WriteAllText(workerExe, "");
|
||||||
|
|
||||||
AutostartShortcut.Install(startup, workerExe);
|
var created = AutostartShortcut.Install(startup, workerExe);
|
||||||
|
|
||||||
|
Assert.True(created);
|
||||||
Assert.True(File.Exists(Path.Combine(startup, AutostartShortcut.FileName)));
|
Assert.True(File.Exists(Path.Combine(startup, AutostartShortcut.FileName)));
|
||||||
}
|
}
|
||||||
finally { Directory.Delete(startup, true); Directory.Delete(workerDir, true); }
|
finally { Directory.Delete(startup, true); Directory.Delete(workerDir, true); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Install_is_noop_when_shortcut_already_targets_same_exe()
|
||||||
|
{
|
||||||
|
var startup = TempDir();
|
||||||
|
var workerDir = TempDir();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var workerExe = Path.Combine(workerDir, "ClaudeDo.Worker.exe");
|
||||||
|
File.WriteAllText(workerExe, "");
|
||||||
|
AutostartShortcut.Install(startup, workerExe);
|
||||||
|
var writtenAt = File.GetLastWriteTimeUtc(Path.Combine(startup, AutostartShortcut.FileName));
|
||||||
|
|
||||||
|
var createdAgain = AutostartShortcut.Install(startup, workerExe);
|
||||||
|
|
||||||
|
Assert.False(createdAgain);
|
||||||
|
Assert.Equal(writtenAt, File.GetLastWriteTimeUtc(Path.Combine(startup, AutostartShortcut.FileName)));
|
||||||
|
}
|
||||||
|
finally { Directory.Delete(startup, true); Directory.Delete(workerDir, true); }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Install_rewrites_shortcut_when_target_changed()
|
||||||
|
{
|
||||||
|
var startup = TempDir();
|
||||||
|
var workerDir = TempDir();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var oldExe = Path.Combine(workerDir, "old", "ClaudeDo.Worker.exe");
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(oldExe)!);
|
||||||
|
File.WriteAllText(oldExe, "");
|
||||||
|
AutostartShortcut.Install(startup, oldExe);
|
||||||
|
|
||||||
|
var newExe = Path.Combine(workerDir, "new", "ClaudeDo.Worker.exe");
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(newExe)!);
|
||||||
|
File.WriteAllText(newExe, "");
|
||||||
|
|
||||||
|
var created = AutostartShortcut.Install(startup, newExe);
|
||||||
|
|
||||||
|
Assert.True(created);
|
||||||
|
Assert.Equal(newExe, ShortcutFactory.TryGetTarget(Path.Combine(startup, AutostartShortcut.FileName)));
|
||||||
|
}
|
||||||
|
finally { Directory.Delete(startup, true); Directory.Delete(workerDir, true); }
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Remove_deletes_existing_lnk()
|
public void Remove_deletes_existing_lnk()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user