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]

Reply via email to