update: a restart to update from the open window (Windows, Linux) opens the window again, on the tab it was asked from, instead of the tray icon alone (#275 Aimer779: 设置页更新重启后主窗口消失,只剩托盘) — RelaunchBinary always started magpie tray, autostart's argument, so the new version came up as a tray icon however the update was asked for; the Mac reopens its .app and was fine. The window's Update pill and Settings' "Restart to update" send the tab they're on, the new process starts as magpie gui <tab>; the tray menu's Restart to Update brings the window back if it was open and keeps to the tray if not; autostart stays tray. magpie gui <tab> / magpie app <tab> open the window on that tab, and hand it to a magpie already running. The MAGPIE_REPLACES handover is unchanged: the new process waits for the old one before its single-instance lock. Tested with TestRelaunchArgs and TestRelaunchBinaryArgs (a script in magpie's place is started with gui settings <pid> / tray <pid>); go vet, go build (GUI and -tags nogui, GOOS=linux, GOOS=windows), go test -tags nogui ./... pass. Not tried: a real restart to update on Windows or Linux.
This commit is contained in:
8 files changed
+144
-13
No files matched your search
@@ -9,3 +9,5 @@ const hasGUI = false
|
||||
func runGUI(bool, string) error { return errors.New("this build has no GUI; run `magpie tui`") }
|
||||
|
||||
func runPanel() error { return runGUI(false, "") }
|
||||
|
||||
func runWindow(string) error { return runGUI(true, "") }
|
||||
@@ -16,6 +16,13 @@ func runGUI(showMain bool, link string) error {
|
||||
return gui.Run(version, showMain, link)
|
||||
}
|
||||
|
||||
// runWindow starts the app with its window open on view ("" for the first
|
||||
// tab).
|
||||
func runWindow(view string) error {
|
||||
gui.OpenView = view
|
||||
return runGUI(true, "")
|
||||
}
|
||||
|
||||
// runPanel starts the app with its quick panel open, or toggles the panel
|
||||
// of the one already running.
|
||||
func runPanel() error {
|
||||
|
||||
+14
-3
@@ -10,6 +10,7 @@ import (
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
@@ -95,13 +96,16 @@ func (h *host) HidePanel() { h.panel.Hide() }
|
||||
func (h *host) ShowMain(view string) {
|
||||
h.panel.Hide()
|
||||
if view != "" {
|
||||
h.main.SetURL("/?view=" + view + h.query)
|
||||
h.main.SetURL("/?view=" + url.QueryEscape(view) + h.query)
|
||||
}
|
||||
h.dock(settings.Load(), true)
|
||||
h.main.Show()
|
||||
h.main.Focus()
|
||||
}
|
||||
|
||||
// MainShown says whether the window is up.
|
||||
func (h *host) MainShown() bool { return h.main != nil && h.main.IsVisible() }
|
||||
|
||||
// Import opens the window on an import link, for the user to confirm.
|
||||
func (h *host) Import(link string) {
|
||||
id := stash(link)
|
||||
@@ -349,7 +353,8 @@ func Run(version string, showMain bool, link string) error {
|
||||
menu.Add("Version " + version).SetEnabled(false)
|
||||
restart := menu.Add("Restart to Update").SetHidden(true)
|
||||
restart.OnClick(func(*application.Context) {
|
||||
if restartToUpdate(false) {
|
||||
// the window comes back if it was open; the tray alone if not
|
||||
if restartToUpdate(false, h.MainShown(), "") {
|
||||
h.app.Quit()
|
||||
}
|
||||
})
|
||||
@@ -421,7 +426,7 @@ func Run(version string, showMain bool, link string) error {
|
||||
h.whenReady(h.applyZoom)
|
||||
}
|
||||
if showMain {
|
||||
h.whenReady(func() { h.ShowMain("") })
|
||||
h.whenReady(func() { h.ShowMain(OpenView) })
|
||||
}
|
||||
if OpenPanel {
|
||||
h.whenReady(func() { application.InvokeAsync(h.togglePanel) })
|
||||
@@ -467,6 +472,8 @@ func singleInstance(h *host) *application.SingleInstanceOptions {
|
||||
case len(args) == 1 && args[0] == "tray":
|
||||
case len(args) == 1 && args[0] == "panel":
|
||||
h.whenReady(func() { application.InvokeAsync(h.togglePanel) })
|
||||
case len(args) == 2 && (args[0] == "gui" || args[0] == "app"):
|
||||
h.whenReady(func() { h.ShowMain(args[1]) })
|
||||
default:
|
||||
h.whenReady(func() { h.ShowMain("") })
|
||||
}
|
||||
@@ -542,6 +549,10 @@ func panelOptions(goos, theme string) application.WebviewWindowOptions {
|
||||
// icon runs it (see omarchy.AddWidget).
|
||||
var OpenPanel bool
|
||||
|
||||
// OpenView is the tab the window opens on, as `magpie gui settings` asks:
|
||||
// a restart to update comes back where it was asked for.
|
||||
var OpenView string
|
||||
|
||||
// togglePanel opens the quick panel by the tray icon, or closes it.
|
||||
func (h *host) togglePanel() {
|
||||
if omarchy.Hyprland() {
|
||||
|
||||
@@ -1044,6 +1044,12 @@ async function load() {
|
||||
renderUpdateBadge();
|
||||
}
|
||||
|
||||
// installFrom is what a restart to update tells the app: the window's tab,
|
||||
// for the new version to open its window there (Windows, Linux).
|
||||
function installFrom() {
|
||||
return mode === "window" ? { view } : {};
|
||||
}
|
||||
|
||||
// renderUpdateBadge shows the header's Update pill once a newer magpie is
|
||||
// downloaded (a click restarts into it) or, where magpie can't replace
|
||||
// itself, out (a click opens the release page).
|
||||
@@ -1064,7 +1070,7 @@ async function renderUpdateBadge() {
|
||||
label.textContent = t("Restarting…");
|
||||
// an answer means it didn't: the password prompt dismissed, the swap
|
||||
// failed, or a newer version is out and downloading first
|
||||
const a = await api("update/install", {}).catch(() => ({}));
|
||||
const a = await api("update/install", installFrom()).catch(() => ({}));
|
||||
if (!a) return backAsNew(u.current);
|
||||
if (a) {
|
||||
if (["checking", "downloading"].includes(a.state)) b.dataset.pulling = "1";
|
||||
@@ -8440,7 +8446,7 @@ async function renderUpdate(r, u) {
|
||||
sub.textContent = t("{v} is downloaded", { v: u.latest }) + (u.error ? " · " + u.error : "");
|
||||
// back with an answer only when it didn't restart
|
||||
btn(t("Restart to update"), async () => {
|
||||
const a = await api("update/install", {}).catch(() => ({ state: "error" }));
|
||||
const a = await api("update/install", installFrom()).catch(() => ({ state: "error" }));
|
||||
if (a) return renderUpdate(r, a.current ? a : undefined);
|
||||
sub.textContent = t("Restarting…");
|
||||
backAsNew(u.current);
|
||||
|
||||
+18
-3
@@ -2,6 +2,7 @@ package gui
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
@@ -251,13 +252,18 @@ func updateRoutes(mux *http.ServeMux, w Windows) {
|
||||
// install restarts into the staged version; after a failed download it
|
||||
// downloads it again, and the page restarts once it's in. Only an app
|
||||
// that can't replace itself is sent to the release page.
|
||||
// The window's page names the tab it is on, to come back to it.
|
||||
mux.HandleFunc("POST /api/update/install", func(rw http.ResponseWriter, r *http.Request) {
|
||||
var in struct {
|
||||
View string `json:"view"`
|
||||
}
|
||||
_ = json.NewDecoder(r.Body).Decode(&in)
|
||||
if updates.json().State == "ready" {
|
||||
updates.recheck()
|
||||
}
|
||||
switch j := updates.json(); {
|
||||
case j.State == "ready":
|
||||
if restartToUpdate(isWeb(w)) {
|
||||
if restartToUpdate(isWeb(w), in.View != "" || mainShown(w), in.View) {
|
||||
rw.WriteHeader(http.StatusNoContent)
|
||||
go w.Quit()
|
||||
return
|
||||
@@ -276,7 +282,9 @@ func updateRoutes(mux *http.ServeMux, w Windows) {
|
||||
// restartToUpdate installs the staged version and arranges for it to open
|
||||
// once this process is gone; the caller then quits. magpie web runs on as
|
||||
// the new version in its own place (Web.Wait) instead of opening the app.
|
||||
func restartToUpdate(web bool) bool {
|
||||
// Off the Mac, the new one opens its window on view when window is set, and
|
||||
// only the tray icon when not; the Mac's always opens its window.
|
||||
func restartToUpdate(web, window bool, view string) bool {
|
||||
bundle, exe := updates.bundle, updates.exe
|
||||
if !updates.install(true) {
|
||||
return false
|
||||
@@ -289,10 +297,17 @@ func restartToUpdate(web bool) bool {
|
||||
if bundle != "" {
|
||||
err = update.Relaunch(bundle)
|
||||
} else {
|
||||
err = update.RelaunchBinary(exe)
|
||||
err = update.RelaunchBinary(exe, window, view)
|
||||
}
|
||||
if err != nil {
|
||||
log.Println("update:", err)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// mainShown says whether the app's window is up, for a restart asked for
|
||||
// from the panel to bring it back.
|
||||
func mainShown(w Windows) bool {
|
||||
s, ok := w.(interface{ MainShown() bool })
|
||||
return ok && s.MainShown()
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package update
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// #275: a restart to update from the open window (Windows, Linux) came
|
||||
// back as the tray icon alone; it opens the window again, on its tab, and
|
||||
// only a restart with the window closed stays in the tray, as autostart.
|
||||
func TestRelaunchArgs(t *testing.T) {
|
||||
for _, c := range []struct {
|
||||
window bool
|
||||
view string
|
||||
want []string
|
||||
}{
|
||||
{true, "settings", []string{"gui", "settings"}},
|
||||
{true, "", []string{"gui"}},
|
||||
{false, "", []string{"tray"}},
|
||||
{false, "settings", []string{"tray"}},
|
||||
} {
|
||||
if got := RelaunchArgs(c.window, c.view); !slices.Equal(got, c.want) {
|
||||
t.Errorf("RelaunchArgs(%v, %q) = %q, want %q", c.window, c.view, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RelaunchBinary starts exe with those arguments, and the handover to this
|
||||
// process as before.
|
||||
func TestRelaunchBinaryArgs(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("a shell script stands in for magpie")
|
||||
}
|
||||
for _, c := range []struct {
|
||||
window bool
|
||||
view string
|
||||
want string
|
||||
}{
|
||||
{true, "settings", "gui settings"},
|
||||
{false, "", "tray"},
|
||||
} {
|
||||
// a script and a file of its own each, so a slow start can't be
|
||||
// read as the next one's
|
||||
dir := t.TempDir()
|
||||
out := filepath.Join(dir, "out")
|
||||
exe := filepath.Join(dir, "magpie")
|
||||
script := "#!/bin/sh\necho \"$* $MAGPIE_REPLACES\" > " + out + ".tmp && mv " + out + ".tmp " + out + "\n"
|
||||
if err := os.WriteFile(exe, []byte(script), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := RelaunchBinary(exe, c.window, c.view); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := c.want + " " + strconv.Itoa(os.Getpid())
|
||||
var got string
|
||||
for deadline := time.Now().Add(30 * time.Second); time.Now().Before(deadline); time.Sleep(20 * time.Millisecond) {
|
||||
if b, err := os.ReadFile(out); err == nil {
|
||||
got = strings.TrimSpace(string(b))
|
||||
break
|
||||
}
|
||||
}
|
||||
if got != want {
|
||||
t.Errorf("relaunched with %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -442,11 +442,12 @@ func InstallBinaryAsAdmin(staged, exe string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// RelaunchBinary starts exe again as the tray app. The new process waits
|
||||
// for this one to exit before it takes the gateway's port; see
|
||||
// AwaitPredecessor.
|
||||
func RelaunchBinary(exe string) error {
|
||||
cmd := proc.Command(exe, "tray")
|
||||
// RelaunchBinary starts exe again: with its window on view when window is
|
||||
// set (the window was open), else as the tray app alone, as autostart
|
||||
// starts it. The new process waits for this one to exit before it takes
|
||||
// the gateway's port; see AwaitPredecessor.
|
||||
func RelaunchBinary(exe string, window bool, view string) error {
|
||||
cmd := proc.Command(exe, RelaunchArgs(window, view)...)
|
||||
cmd.Env = append(os.Environ(), "MAGPIE_REPLACES="+strconv.Itoa(os.Getpid()))
|
||||
cmd.Stdin, cmd.Stdout, cmd.Stderr = nil, nil, nil
|
||||
detach(cmd)
|
||||
@@ -456,6 +457,18 @@ func RelaunchBinary(exe string) error {
|
||||
return cmd.Process.Release()
|
||||
}
|
||||
|
||||
// RelaunchArgs is what RelaunchBinary starts magpie with: `gui [view]` for
|
||||
// the window, `tray` for the tray icon alone.
|
||||
func RelaunchArgs(window bool, view string) []string {
|
||||
switch {
|
||||
case !window:
|
||||
return []string{"tray"}
|
||||
case view != "":
|
||||
return []string{"gui", view}
|
||||
}
|
||||
return []string{"gui"}
|
||||
}
|
||||
|
||||
// AwaitPredecessor blocks, for a while at most, until the magpie that
|
||||
// relaunched this one has exited.
|
||||
func AwaitPredecessor() {
|
||||
|
||||
@@ -143,6 +143,11 @@ func run(args []string) error {
|
||||
case "web":
|
||||
return webCmd(args[1:])
|
||||
case "app", "gui":
|
||||
// `magpie gui settings`: the window on that tab, as a restart to
|
||||
// update from it comes back (update.RelaunchArgs)
|
||||
if len(args) > 1 {
|
||||
return runWindow(args[1])
|
||||
}
|
||||
return runGUI(true, "")
|
||||
case "tray":
|
||||
return runGUI(false, "")
|
||||
|
||||
Reference in new issue
Block a user