diff --git a/src/ClaudeDo.Installer/Core/ShortcutFactory.cs b/src/ClaudeDo.Installer/Core/ShortcutFactory.cs index 5d53eb06..a49c7268 100644 --- a/src/ClaudeDo.Installer/Core/ShortcutFactory.cs +++ b/src/ClaudeDo.Installer/Core/ShortcutFactory.cs @@ -8,24 +8,36 @@ public static class ShortcutFactory { private const int SlgpRawPath = 0x4; + // Both helpers release the ShellLink RCW explicitly. IPersistFile.Load/Save keeps a handle on + // the .lnk for as long as the COM object lives, so leaving it to the GC lets a read immediately + // followed by a write to the same path fail with "used by another process" — exactly what + // AutostartShortcut.Install does when the recorded target changed. public static void CreateShortcut(string shortcutPath, string targetPath, string workingDir, string description) { var link = (IShellLink)new ShellLink(); - link.SetPath(targetPath); - link.SetWorkingDirectory(workingDir); - link.SetDescription(description); - link.SetIconLocation(targetPath, 0); + try + { + link.SetPath(targetPath); + link.SetWorkingDirectory(workingDir); + link.SetDescription(description); + link.SetIconLocation(targetPath, 0); - var file = (IPersistFile)link; - file.Save(shortcutPath, false); + var file = (IPersistFile)link; + file.Save(shortcutPath, false); + } + finally + { + Marshal.FinalReleaseComObject(link); + } } /// Reads the target path of an existing .lnk, or null if it can't be read (missing/corrupt). public static string? TryGetTarget(string shortcutPath) { + IShellLink? link = null; try { - var link = (IShellLink)new ShellLink(); + link = (IShellLink)new ShellLink(); ((IPersistFile)link).Load(shortcutPath, 0); var sb = new StringBuilder(260); link.GetPath(sb, sb.Capacity, IntPtr.Zero, SlgpRawPath); @@ -36,6 +48,10 @@ public static class ShortcutFactory { return null; } + finally + { + if (link is not null) Marshal.FinalReleaseComObject(link); + } } [ComImport]