This is an automated email from the ASF dual-hosted git repository. hsluoyz pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/casbin-gateway.git
commit e856fc98b64fff38ce817150ef67cbd53713042f Author: Yang Luo <[email protected]> AuthorDate: Sat Sep 12 09:28:55 2026 +0800 fix(update): restart the desktop shell into the new build --- desktop/instance.go | 39 +++++++++++++---- desktop/restart.go | 121 ++++++++++++++++++++++++++++++++++++++++++++++++++ desktop/tray.go | 12 ++++- main.go | 6 ++- version/launcher.go | 124 ++++++++++++++++++++++++++++++++++++++++++++++++++++ version/restart.go | 7 +-- 6 files changed, 295 insertions(+), 14 deletions(-) diff --git a/desktop/instance.go b/desktop/instance.go index 3bd703d..c237c61 100644 --- a/desktop/instance.go +++ b/desktop/instance.go @@ -27,9 +27,11 @@ import ( ) const ( - showRequest = "show" - showReply = "ok" - instanceWait = 2 * time.Second + showRequest = "show" + // restartRequest is the updated server saying the launcher was replaced too. + restartRequest = "restart" + showReply = "ok" + instanceWait = 2 * time.Second ) // instanceMarker records the loopback port the running tray listens on. The @@ -73,6 +75,10 @@ func raiseRunningInstance() bool { return err == nil && strings.TrimSpace(reply) == showReply } +// heldPort is kept so a restart that could not start the new launcher can name +// this tray in the marker again. +var heldPort int + // holdInstance opens the socket later launches look for. Failing to leaves the // desktop as it was before any of this: a second launch is a second tray. func holdInstance() { @@ -93,6 +99,7 @@ func holdInstance() { return } + heldPort = address.Port go serveInstance(listener) } @@ -100,6 +107,15 @@ func releaseInstance() { _ = os.Remove(instanceMarker()) } +// restoreInstance puts back what releaseInstance took. +func restoreInstance() { + if heldPort == 0 { + return + } + + _ = os.WriteFile(instanceMarker(), []byte(strconv.Itoa(heldPort)), 0o600) +} + func serveInstance(listener net.Listener) { for { conn, err := listener.Accept() @@ -115,12 +131,19 @@ func serveInstanceRequest(conn net.Conn) { _ = conn.SetDeadline(time.Now().Add(instanceWait)) line, err := bufio.NewReader(conn).ReadString('\n') - if err != nil || strings.TrimSpace(line) != showRequest { + if err != nil { return } - // Answering first keeps the launch that is waiting on this from outliving - // the window it asked for. - _, _ = io.WriteString(conn, showReply+"\n") - showWindow() + // Answering first keeps whoever is waiting on this from outliving what it + // asked for. + switch strings.TrimSpace(line) { + case showRequest: + _, _ = io.WriteString(conn, showReply+"\n") + showWindow() + case restartRequest: + _, _ = io.WriteString(conn, showReply+"\n") + conn.Close() + restartLauncher() + } } diff --git a/desktop/restart.go b/desktop/restart.go new file mode 100644 index 0000000..c15968b --- /dev/null +++ b/desktop/restart.go @@ -0,0 +1,121 @@ +// Copyright 2026 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + + "fyne.io/systray" +) + +// handoverEnvKey carries what a launcher on its way out tells the one taking +// over. It is read once and cleared, so nothing started from here inherits it. +const handoverEnvKey = "CASBIN_GATEWAY_HANDOVER" + +const ( + handoverServer = "server" + handoverWindow = "window" + // backupSuffix is what an update renames the launcher it replaces to. + launcherBaseName = "casbin-gateway-desktop" + backupSuffix = ".old" +) + +type handover struct { + taken bool + ownsServer bool + window bool +} + +var handedOver handover + +func takeHandover() handover { + value, found := os.LookupEnv(handoverEnvKey) + if !found { + return handover{} + } + _ = os.Unsetenv(handoverEnvKey) + + state := handover{taken: true} + for _, part := range strings.Split(value, ",") { + switch part { + case handoverServer: + state.ownsServer = true + case handoverWindow: + state.window = true + } + } + return state +} + +// launcherPath is the executable a restart starts. Linux reports the name the +// binary this process runs from was renamed to, which is not the one to start. +func launcherPath() string { + if executable, err := os.Executable(); err == nil { + path := strings.TrimSuffix(strings.TrimSuffix(executable, " (deleted)"), backupSuffix) + if _, err := os.Stat(path); err == nil { + return path + } + } + + name := launcherBaseName + if runtime.GOOS == "windows" { + name += ".exe" + } + return filepath.Join(gatewayHome(), name) +} + +// restartLauncher starts the launcher an update just put in place and leaves. +// The server behind it stays up: this is the shell catching up with the build +// it shows, which otherwise waits for someone to close the app and open it. +func restartLauncher() { + window.Lock() + hadWindow := window.process != nil + window.Unlock() + + closeWindow() + // The marker goes first, so that the launcher started below holds the + // socket rather than handing its launch back to this process. + releaseInstance() + + state := []string{} + ownsServer.Lock() + if ownsServer.value { + state = append(state, handoverServer) + } + ownsServer.Unlock() + if hadWindow { + state = append(state, handoverWindow) + } + + cmd := exec.Command(launcherPath()) + cmd.Dir = gatewayHome() + cmd.Env = append(os.Environ(), handoverEnvKey+"="+strings.Join(state, ",")) + hideConsole(cmd) + if err := cmd.Start(); err != nil { + fmt.Fprintln(os.Stderr, "casbin-gateway-desktop: could not restart into the new version:", err) + restoreInstance() + if hadWindow { + showWindow() + } + return + } + + systray.Quit() +} diff --git a/desktop/tray.go b/desktop/tray.go index 7c96792..8e2479d 100644 --- a/desktop/tray.go +++ b/desktop/tray.go @@ -36,6 +36,10 @@ var ownsServer struct { } func runTray() { + // Read before anything is started from here, so that none of it inherits + // the variable. + handedOver = takeHandover() + go func() { // An archive that was unpacked by hand has no way in but the executable // itself, so the first start is what gives it one. @@ -100,12 +104,16 @@ func onTrayReady() { return } ownsServer.Lock() - ownsServer.value = started + ownsServer.value = started || handedOver.ownsServer ownsServer.Unlock() go watchStatus(mStatus, port) go providers.watch() - showWindow() + // A restart puts back the window that was open, and leaves one that + // was not closed. + if !handedOver.taken || handedOver.window { + showWindow() + } }() for { diff --git a/main.go b/main.go index 1e50a41..adcfa4a 100644 --- a/main.go +++ b/main.go @@ -189,7 +189,11 @@ func main() { } // Nothing is left to roll back to from here: this version has the port. - version.FinishStartup() + if version.FinishStartup() { + // The same update replaced the launcher, and the tray still runs the + // build it took away. + version.RestartDesktopLauncher(port) + } service.PrintStartupSummary() diff --git a/version/launcher.go b/version/launcher.go new file mode 100644 index 0000000..ae816d7 --- /dev/null +++ b/version/launcher.go @@ -0,0 +1,124 @@ +// Copyright 2026 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package version + +import ( + "bufio" + "fmt" + "io" + "net" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/beego/beego" +) + +// The tray listens on a loopback socket and leaves its port in a marker file. +// These mirror its own constants: it is a separate module. +const ( + desktopMarkerName = ".desktop-instance" + desktopRestartWord = "restart" + desktopOkReply = "ok" + desktopDialTimeout = 2 * time.Second + + serveWait = 90 * time.Second + servePoll = 200 * time.Millisecond +) + +// RestartDesktopLauncher tells the tray still running the build this update +// replaced to start the launcher that came with it. Without it the server is +// the new version and the window in front of the reader is not. +func RestartDesktopLauncher(port int) { + go func() { + // The launcher opens its window on this Gateway, so it is only told + // once there is something to open it on. + if !waitUntilServing(port) { + return + } + if err := askDesktop(desktopRestartWord); err != nil { + beego.Error("the desktop launcher was left at the previous version:", err) + } + }() +} + +func waitUntilServing(port int) bool { + deadline := time.Now().Add(serveWait) + for time.Now().Before(deadline) { + conn, err := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", port), desktopDialTimeout) + if err == nil { + conn.Close() + return true + } + time.Sleep(servePoll) + } + return false +} + +// askDesktop sends one request to the tray. No desktop, or a marker that +// outlived the tray which wrote it, is nothing to tell rather than a failure. +func askDesktop(request string) error { + port := desktopInstancePort() + if port == 0 { + return nil + } + + conn, err := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", port), desktopDialTimeout) + if err != nil { + return nil + } + defer conn.Close() + + _ = conn.SetDeadline(time.Now().Add(desktopDialTimeout)) + if _, err := io.WriteString(conn, request+"\n"); err != nil { + return err + } + + reply, err := bufio.NewReader(conn).ReadString('\n') + if err != nil { + return err + } + if strings.TrimSpace(reply) != desktopOkReply { + return fmt.Errorf("the desktop launcher answered %q", strings.TrimSpace(reply)) + } + + return nil +} + +// desktopInstancePort reads the marker the tray left next to the installation, +// falling back to the working directory. +func desktopInstancePort() int { + var dirs []string + if executable, err := executablePath(); err == nil { + dirs = append(dirs, filepath.Dir(executable)) + } + if workingDir, err := os.Getwd(); err == nil { + dirs = append(dirs, workingDir) + } + + for _, dir := range dirs { + content, err := os.ReadFile(filepath.Join(dir, desktopMarkerName)) + if err != nil { + continue + } + if port, err := strconv.Atoi(strings.TrimSpace(string(content))); err == nil && port > 0 { + return port + } + } + + return 0 +} diff --git a/version/restart.go b/version/restart.go index 440438e..65a0a2c 100644 --- a/version/restart.go +++ b/version/restart.go @@ -87,10 +87,10 @@ func BeginStartup() { } // FinishStartup is the update taking. This version is about to serve, so there -// is nothing left to roll back to. -func FinishStartup() { +// is nothing left to roll back to. It reports whether this start was an update. +func FinishStartup() bool { if startedBy == nil { - return + return false } startedBy = nil @@ -98,6 +98,7 @@ func FinishStartup() { _ = os.Remove(filepath.Join(filepath.Dir(executable), restartFileName)) } CleanupBackup() + return true } // RollBackFailedStart puts back the executable an update replaced and starts it, --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
