mirror of
https://github.com/multica-ai/multica.git
synced 2026-06-17 11:48:42 +02:00
* fix(cli): detach daemon from parent console on Windows CREATE_NEW_PROCESS_GROUP alone leaves the daemon attached to the parent console, so closing the launching cmd/PowerShell window fires CTRL_CLOSE_EVENT down the inherited console and takes the daemon with it. Add DETACHED_PROCESS so the child has no console at all; stdout/stderr are already redirected to the log file before spawn. * fix(cli): make `multica update` work while the binary is running on Windows On Windows, a running .exe is opened without FILE_SHARE_WRITE, so the previous os.Rename(tmp, exe) always failed with "Access is denied" — every `multica update` on Windows hit this, because the CLI is updating its own running binary. Windows does allow renaming the running .exe (just not overwriting it), so the new Windows-only replaceBinary moves the running binary to `.old` first, installs the new one, and restores the original if installation fails. A best-effort CleanupStaleUpdateArtifacts runs at CLI/daemon startup to reclaim the leftover `.old` file once the old process has exited. Unix keeps the plain rename-over semantics (the old inode stays valid for the running process). * fix(cli): stop daemon via HTTP /shutdown instead of console ctrl events With DETACHED_PROCESS the Windows daemon shares no console with the stop caller, so `GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, pid)` silently never reaches it — the old code would report "stop sent" while the daemon kept running. Replace the platform-specific stopDaemonProcess with a cross-platform POST to the daemon's HTTP /shutdown endpoint, which cancels the same top-level context the self-restart path already uses. Fall back to `process.Kill()` if the HTTP call fails. Also drops the now-unused stopDaemonProcess / CTRL_BREAK_EVENT wiring, adds handler tests, and updates the DETACHED_PROCESS comment.
61 lines
2.0 KiB
Go
61 lines
2.0 KiB
Go
//go:build windows
|
|
|
|
package cli
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
)
|
|
|
|
// oldBinarySuffix is appended to the previous executable while a new one is
|
|
// being installed. Windows refuses to overwrite a running .exe but allows
|
|
// renaming it, so we shuffle the running binary out of the way first.
|
|
const oldBinarySuffix = ".old"
|
|
|
|
// replaceBinary swaps the running executable for the freshly-downloaded one.
|
|
// Windows holds an exclusive handle on a running .exe, so the rename-over
|
|
// pattern used on Unix fails with "Access is denied". Instead:
|
|
// 1. Clear any stale leftover from a previous update.
|
|
// 2. Move the running executable aside to exePath+".old".
|
|
// 3. Rename the new binary into place.
|
|
// 4. If step 3 fails, restore the original so the user isn't stranded.
|
|
//
|
|
// The leftover .old file is cleaned up on next startup via
|
|
// CleanupStaleUpdateArtifacts.
|
|
func replaceBinary(tmpPath, exePath string) error {
|
|
oldPath := exePath + oldBinarySuffix
|
|
|
|
// Best-effort cleanup; if this fails (file still locked) the next Rename
|
|
// will surface a useful error.
|
|
_ = os.Remove(oldPath)
|
|
|
|
if err := os.Rename(exePath, oldPath); err != nil {
|
|
return fmt.Errorf("move running binary aside: %w", err)
|
|
}
|
|
|
|
if err := os.Rename(tmpPath, exePath); err != nil {
|
|
// Restore so the user isn't left without a multica.exe.
|
|
if rerr := os.Rename(oldPath, exePath); rerr != nil {
|
|
return fmt.Errorf("install new binary: %w (and failed to restore: %v)", err, rerr)
|
|
}
|
|
return fmt.Errorf("install new binary: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// CleanupStaleUpdateArtifacts removes leftover `.old` binaries from previous
|
|
// updates. Windows can't delete a running .exe, so a prior update may have
|
|
// left one behind; once the user restarts, this call reclaims the space.
|
|
func CleanupStaleUpdateArtifacts() {
|
|
exePath, err := os.Executable()
|
|
if err != nil {
|
|
return
|
|
}
|
|
if resolved, err := filepath.EvalSymlinks(exePath); err == nil {
|
|
exePath = resolved
|
|
}
|
|
_ = os.Remove(exePath + oldBinarySuffix)
|
|
}
|