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
The following commit(s) were added to refs/heads/master by this push:
new 5cb5aa5 fix(desktop): hand a second launch to the tray already running
5cb5aa5 is described below
commit 5cb5aa58e2b8c1b84f21d93b1a496438b6e1dc95
Author: Yang Luo <[email protected]>
AuthorDate: Sat Sep 5 00:41:17 2026 +0800
fix(desktop): hand a second launch to the tray already running
---
.gitignore | 3 ++
desktop/instance.go | 126 ++++++++++++++++++++++++++++++++++++++++++++++++++++
desktop/main.go | 8 ++++
desktop/tray.go | 1 +
4 files changed, 138 insertions(+)
diff --git a/.gitignore b/.gitignore
index 5884cba..f6733c1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,6 +21,9 @@
casbin-gateway
casbin-gateway-desktop
+# Written next to the binaries at run time, so a second launch finds the tray
+.desktop-instance
+
# Written next to the binaries at run time, for the window and the shortcuts
casbin-gateway.ico
casbin-gateway.icns
diff --git a/desktop/instance.go b/desktop/instance.go
new file mode 100644
index 0000000..3bd703d
--- /dev/null
+++ b/desktop/instance.go
@@ -0,0 +1,126 @@
+// 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 (
+ "bufio"
+ "fmt"
+ "io"
+ "net"
+ "os"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "time"
+)
+
+const (
+ showRequest = "show"
+ showReply = "ok"
+ instanceWait = 2 * time.Second
+)
+
+// instanceMarker records the loopback port the running tray listens on. The
+// port is ephemeral rather than fixed, because a fixed one would sooner or
+// later be a port something else on the machine wanted - a second Gateway home
+// keeps a marker of its own next to its own data.
+func instanceMarker() string {
+ return filepath.Join(gatewayHome(), ".desktop-instance")
+}
+
+// raiseRunningInstance hands this launch to the tray that is already running.
+// Clicking the shortcut of an app that is open means "show me the window", so
+// what would have been a second tray icon and a second window becomes the one
+// that is there, raised. It reports whether a tray took the launch.
+func raiseRunningInstance() bool {
+ data, err := os.ReadFile(instanceMarker())
+ if err != nil {
+ return false
+ }
+ port, err := strconv.Atoi(strings.TrimSpace(string(data)))
+ if err != nil || port <= 0 {
+ return false
+ }
+
+ // A marker that outlived the tray which wrote it leaves the launch to
run
+ // the tray itself, which is also what overwrites the marker.
+ conn, err := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", port),
instanceWait)
+ if err != nil {
+ return false
+ }
+ defer conn.Close()
+
+ _ = conn.SetDeadline(time.Now().Add(instanceWait))
+ if _, err = io.WriteString(conn, showRequest+"\n"); err != nil {
+ return false
+ }
+
+ // Whatever holds that port now may not be a Gateway tray, so the
launch is
+ // only given up once one has answered.
+ reply, err := bufio.NewReader(conn).ReadString('\n')
+ return err == nil && strings.TrimSpace(reply) == showReply
+}
+
+// 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() {
+ listener, err := net.Listen("tcp", "127.0.0.1:0")
+ if err != nil {
+ fmt.Fprintln(os.Stderr, "casbin-gateway-desktop:", err)
+ return
+ }
+
+ address, ok := listener.Addr().(*net.TCPAddr)
+ if !ok {
+ listener.Close()
+ return
+ }
+ if err = os.WriteFile(instanceMarker(),
[]byte(strconv.Itoa(address.Port)), 0o600); err != nil {
+ fmt.Fprintln(os.Stderr, "casbin-gateway-desktop:", err)
+ listener.Close()
+ return
+ }
+
+ go serveInstance(listener)
+}
+
+func releaseInstance() {
+ _ = os.Remove(instanceMarker())
+}
+
+func serveInstance(listener net.Listener) {
+ for {
+ conn, err := listener.Accept()
+ if err != nil {
+ return
+ }
+ go serveInstanceRequest(conn)
+ }
+}
+
+func serveInstanceRequest(conn net.Conn) {
+ defer conn.Close()
+
+ _ = conn.SetDeadline(time.Now().Add(instanceWait))
+ line, err := bufio.NewReader(conn).ReadString('\n')
+ if err != nil || strings.TrimSpace(line) != showRequest {
+ return
+ }
+
+ // Answering first keeps the launch that is waiting on this from
outliving
+ // the window it asked for.
+ _, _ = io.WriteString(conn, showReply+"\n")
+ showWindow()
+}
diff --git a/desktop/main.go b/desktop/main.go
index a53df95..acebfcb 100644
--- a/desktop/main.go
+++ b/desktop/main.go
@@ -97,6 +97,14 @@ func main() {
return
}
+ // Clicking the shortcut of a Gateway that is already running means
"show me
+ // the window": the launch goes to the tray that is there, rather than
+ // leaving another tray icon and another window behind every time.
+ if raiseRunningInstance() {
+ return
+ }
+
dropOwnConsole()
+ holdInstance()
runTray()
}
diff --git a/desktop/tray.go b/desktop/tray.go
index 45bff82..7c96792 100644
--- a/desktop/tray.go
+++ b/desktop/tray.go
@@ -176,6 +176,7 @@ func toggleAutostart(item *systray.MenuItem) {
func quit() {
closeWindow()
+ releaseInstance()
ownsServer.Lock()
owned := ownsServer.value
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]