Files
DeepSeek-Reasonix/desktop/internal/update/layout.go
T
yhh 1db3c9082b feat(desktop): let a bad release be rolled back from inside the app
A release that turns out to be bad currently leaves the user hunting for an
old installer. The version panel lists what has been published and installs
any of them — the same code path forward or back, because a rollback that took
its own path would be the less-tested one. The chosen version is pinned before
the download starts: if the machine dies mid-install, the next launch must not
helpfully update past what the user picked.

The update core moves out of the old shell into desktop/internal/update so
both shells share it. Beyond deduplication that finally makes it compile on
every platform — updater_mac.go could not even be type-checked from Windows,
so the macOS half was only ever verified by CI.

Publishing writes a per-tag index alongside the artifacts, so the catalog the
panel reads is produced by the release itself rather than maintained by hand.

Two things the transport learned the hard way, now covered by tests: a 404 is
not transient and must not burn the full retry budget, and a resumed download
has to verify the range it was actually given — a middle box that answers
`bytes 0-` to a range request will otherwise append the whole file to what is
already on disk.
2026-08-14 05:03:02 -07:00

100 lines
3.1 KiB
Go

package update
import (
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"reasonix/internal/installlayout"
)
// Layout is where this build lives on disk. The three are resolved together
// because they are three answers to one question, and an installer that pairs
// a root from one install with a launcher from another writes a broken one.
type Layout struct {
Executable string // the running binary, symlinks resolved
Root string // owns current.json, or the flat install directory
Launcher string // thin launcher to restart through; else Executable
}
// Here resolves the running build's layout. Every field is empty when the
// executable path cannot be resolved at all — a caller must treat that as "do
// not install", not as "install into the current directory".
func Here() Layout {
exe := currentExecutable()
if exe == "" {
return Layout{}
}
l := Layout{Executable: exe, Root: filepath.Dir(exe)}
if root, err := installlayout.ResolveInstallRoot(exe); err == nil && root != "" {
l.Root = root
}
l.Launcher = launcherIn(l.Root, exe)
return l
}
// Relaunch starts the launcher and returns once it has been started, not once
// it is up: the caller is about to exit so the new process can replace it.
func (l Layout) Relaunch() error {
if l.Launcher == "" {
return os.ErrNotExist
}
args := []string{}
// Only the legacy Guard understands "launch --detach"; the thin launcher
// strips it. Guard is a one-release migration fallback for flat 1.18-1.19.1.
if strings.Contains(strings.ToLower(filepath.Base(l.Launcher)), "guard") {
args = []string{"launch", "--detach"}
}
cmd := exec.Command(l.Launcher, args...)
cmd.Stdout, cmd.Stderr, cmd.Stdin = os.Stdout, os.Stderr, os.Stdin
return cmd.Start()
}
// Versioned reports whether this install carries the v1.20+ versioned layout,
// where a release is published into its own directory and current.json swaps
// last. A flat install predates that and needs the transactional apply path.
func (l Layout) Versioned() bool {
return l.Root != "" && installlayout.HasCurrent(l.Root)
}
func currentExecutable() string {
exe, err := os.Executable()
if err != nil {
return ""
}
if resolved, err := filepath.EvalSymlinks(exe); err == nil {
exe = resolved
}
return exe
}
// launcherIn finds the entry point that survives the running binary being
// replaced. An install missing every candidate relaunches itself, which is
// correct for a portable copy that was never installed.
func launcherIn(root, exe string) string {
for _, name := range launcherNames() {
path := filepath.Join(root, name)
if _, err := os.Stat(path); err == nil {
return path
}
}
// An incomplete install can still have Guard beside the executable when the
// resolved root pointed elsewhere.
if runtime.GOOS == "windows" {
guard := filepath.Join(filepath.Dir(exe), "reasonix-guard.exe")
if _, err := os.Stat(guard); err == nil {
return guard
}
}
return exe
}
func launcherNames() []string {
if runtime.GOOS == "windows" {
return []string{"reasonix-launcher.exe", "Reasonix.exe", "reasonix-guard.exe"}
}
return []string{"reasonix-launcher", "reasonix-guard"}
}