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 7ebc454819b0470f8781bfa13af9a4d7a652170a Author: Yang Luo <[email protected]> AuthorDate: Sat Sep 12 13:45:09 2026 +0800 fix(install): stop the running Gateway before replacing it --- desktop/gateway.go | 25 +++++++++++++++++++++ desktop/instance.go | 60 ++++++++++++++++++++++++++++++++++++++++++------- desktop/main.go | 11 +++++++++ scripts/install.ps1 | 65 +++++++++++++++++++++++++++++++++++++++++++++++------ scripts/install.sh | 23 +++++++++++++++++-- 5 files changed, 167 insertions(+), 17 deletions(-) diff --git a/desktop/gateway.go b/desktop/gateway.go index 2876790..4809b65 100644 --- a/desktop/gateway.go +++ b/desktop/gateway.go @@ -184,3 +184,28 @@ func stopServer() error { } return nil } + +// quitRunning stops the whole of a Gateway that is running out of this home: +// the tray and its window through the socket they listen on, and the server +// behind them whether or not the tray was what started it. It reports whether +// there was anything to stop. +// +// This is what an installer calls before it replaces the executables. Windows +// will not write over one that is running, and everywhere else an install that +// left the old process serving would look like an update that did nothing. +func quitRunning() bool { + port := instancePort() + stopped := askInstance(quitRequest) + if stopped { + waitForInstanceGone(port) + } + + if !isServing(httpPort()) { + return stopped + } + if err := stopServer(); err != nil { + fmt.Fprintln(os.Stderr, "casbin-gateway-desktop: could not stop the Gateway server:", err) + return stopped + } + return true +} diff --git a/desktop/instance.go b/desktop/instance.go index c237c61..0856838 100644 --- a/desktop/instance.go +++ b/desktop/instance.go @@ -28,10 +28,15 @@ import ( const ( showRequest = "show" + quitRequest = "quit" // restartRequest is the updated server saying the launcher was replaced too. restartRequest = "restart" - showReply = "ok" + okReply = "ok" instanceWait = 2 * time.Second + // quitWait bounds the wait for a tray that agreed to go: it answers first + // and lets go of the socket a moment later. + quitWait = 15 * time.Second + quitPoll = 100 * time.Millisecond ) // instanceMarker records the loopback port the running tray listens on. The @@ -47,12 +52,27 @@ func instanceMarker() string { // 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 { + return askInstance(showRequest) +} + +// instancePort is where the running tray listens, or 0 when nothing names it. +func instancePort() int { data, err := os.ReadFile(instanceMarker()) if err != nil { - return false + return 0 } port, err := strconv.Atoi(strings.TrimSpace(string(data))) if err != nil || port <= 0 { + return 0 + } + return port +} + +// askInstance sends one request to the tray that is already running and reports +// whether it took it. +func askInstance(request string) bool { + port := instancePort() + if port == 0 { return false } @@ -65,14 +85,34 @@ func raiseRunningInstance() bool { defer conn.Close() _ = conn.SetDeadline(time.Now().Add(instanceWait)) - if _, err = io.WriteString(conn, showRequest+"\n"); err != nil { + if _, err = io.WriteString(conn, request+"\n"); err != nil { return false } - // Whatever holds that port now may not be a Gateway tray, so the launch is + // Whatever holds that port now may not be a Gateway tray, so the request is // only given up once one has answered. reply, err := bufio.NewReader(conn).ReadString('\n') - return err == nil && strings.TrimSpace(reply) == showReply + return err == nil && strings.TrimSpace(reply) == okReply +} + +// waitForInstanceGone blocks until nothing answers on the port the tray was +// listening on, which is the process itself going away rather than the marker +// it deletes on the way out. An installer replacing the executable that process +// runs from has to wait for exactly that. +func waitForInstanceGone(port int) { + if port == 0 { + return + } + + deadline := time.Now().Add(quitWait) + for time.Now().Before(deadline) { + conn, err := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", port), instanceWait) + if err != nil { + return + } + conn.Close() + time.Sleep(quitPoll) + } } // heldPort is kept so a restart that could not start the new launcher can name @@ -136,13 +176,17 @@ func serveInstanceRequest(conn net.Conn) { } // Answering first keeps whoever is waiting on this from outliving what it - // asked for. + // asked for - and, for a quit, from waiting on a process on its way out. switch strings.TrimSpace(line) { case showRequest: - _, _ = io.WriteString(conn, showReply+"\n") + _, _ = io.WriteString(conn, okReply+"\n") showWindow() + case quitRequest: + _, _ = io.WriteString(conn, okReply+"\n") + conn.Close() + quit() case restartRequest: - _, _ = io.WriteString(conn, showReply+"\n") + _, _ = io.WriteString(conn, okReply+"\n") conn.Close() restartLauncher() } diff --git a/desktop/main.go b/desktop/main.go index acebfcb..cd167b5 100644 --- a/desktop/main.go +++ b/desktop/main.go @@ -56,6 +56,17 @@ func main() { return } + // "quit" stops the Gateway that is running - window, tray and server - which + // is what the installers do before they replace the executables it runs + // from. It exits 2 when there was nothing running, so that an install which + // stopped one knows to start it again. + if len(os.Args) > 1 && os.Args[1] == "quit" { + if !quitRunning() { + os.Exit(2) + } + return + } + // "open-agent-link <url>" is what a URL scheme Gateway captured opens with. // The server routes the link; it is started from here because this // executable is windowed and flashes no console on the way. diff --git a/scripts/install.ps1 b/scripts/install.ps1 index c062032..c1e8819 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -9,7 +9,8 @@ # Optional environment variables: # INSTALL_DIR where the executable and its data live # (default: $env:LOCALAPPDATA\casbin-gateway) -# NO_START set to any value to install without starting Gateway +# NO_START set to any value to install without starting Gateway; one that +# was already running is stopped for the update and started again # NO_AUTOSTART set to any value to skip the login-time startup entry # NO_SHORTCUT set to any value to skip the desktop and Start menu shortcuts @@ -30,6 +31,34 @@ $ProgressPreference = 'SilentlyContinue' function Write-Info { param([string]$Message) Write-Host $Message } +# Windows will not write over an executable that is running. The Gateway this +# install replaces is stopped first, but an agent can still hold one of its MCP +# servers open, so a file that is locked anyway is renamed out of the way — that +# Windows does allow — and the Gateway deletes the ".old" copy on its next start. +function Install-Executable { + param([string]$Source, [string]$Destination) + + try { + Copy-Item -Path $Source -Destination $Destination -Force + return + } + catch { + if (-not (Test-Path $Destination)) { + throw "cannot write $Destination ($_)" + } + } + + $Name = Split-Path $Destination -Leaf + try { + Remove-Item -Path "$Destination.old" -Force -ErrorAction SilentlyContinue + Rename-Item -Path $Destination -NewName "$Name.old" -Force + Copy-Item -Path $Source -Destination $Destination -Force + } + catch { + throw "cannot replace $Name in $(Split-Path $Destination -Parent), something is still running it - close the agents that use Gateway and try again ($_)" + } +} + # ── pick the archive for this machine ───────────────────────────────────────── # Only the x86_64 archive is published; Windows on ARM runs it under the # built-in x64 emulation. @@ -62,15 +91,34 @@ try { New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null $DesktopExePath = Join-Path $InstallDir 'casbin-gateway-desktop.exe' - foreach ($executable in @('casbin-gateway.exe', 'casbin-gateway-desktop.exe')) { + + # ── stop the Gateway that is running ────────────────────────────────────── + # Without this the install failed outright: Windows holds the running + # executables open, so there was no way to update without quitting Gateway + # from its tray icon first. The launcher from the archive is what stops it, + # because an installation old enough to have no "quit" command cannot stop + # itself, and because it runs from the temporary directory rather than from + # the files about to be replaced. CASBIN_GATEWAY_HOME points it at the + # installation instead of at the directory it was unpacked into. + $WasRunning = $false + if (Test-Path (Join-Path $InstallDir 'casbin-gateway.exe')) { + $env:CASBIN_GATEWAY_HOME = $InstallDir try { - Copy-Item -Path (Join-Path $Unpacked $executable) -Destination (Join-Path $InstallDir $executable) -Force + $quit = Start-Process -FilePath (Join-Path $Unpacked 'casbin-gateway-desktop.exe') ` + -ArgumentList 'quit' -WorkingDirectory $InstallDir -Wait -PassThru + $WasRunning = $quit.ExitCode -eq 0 } catch { - # Windows locks a running executable, so this is what an upgrade over - # a started Gateway looks like. - throw "cannot replace $executable in $InstallDir, quit Casbin Gateway from its tray icon and try again ($_)" + Write-Info "Could not stop the running Casbin Gateway: $_" } + Remove-Item Env:\CASBIN_GATEWAY_HOME -ErrorAction SilentlyContinue + if ($WasRunning) { + Write-Info 'Stopped the running Gateway, and will start it again once it is updated' + } + } + + foreach ($executable in @('casbin-gateway.exe', 'casbin-gateway-desktop.exe')) { + Install-Executable -Source (Join-Path $Unpacked $executable) -Destination (Join-Path $InstallDir $executable) } foreach ($legalFile in @('LICENSE', 'NOTICE', 'DISCLAIMER')) { @@ -149,7 +197,10 @@ Write-Info 'Closing its window leaves it running in the tray; quit it from there Write-Info 'Without the window: "casbin-gateway start", "casbin-gateway stop", "casbin-gateway status".' Write-Info '' -if ($env:NO_START) { +# A Gateway that was running when this started is started again even with +# NO_START: the install is what stopped it, and leaving the machine without one +# is not what "install without starting" asked for. +if ($env:NO_START -and -not $WasRunning) { Write-Info 'Start it from the "Casbin Gateway" shortcut, or with: casbin-gateway start' return } diff --git a/scripts/install.sh b/scripts/install.sh index 9436e49..0dd91e1 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -12,7 +12,8 @@ # (default: $HOME/.local/share/casbin-gateway) # BIN_DIR where the "casbin-gateway" command is placed # (default: $HOME/.local/bin) -# NO_START set to any value to install without starting Gateway +# NO_START set to any value to install without starting Gateway; one that +# was already running is stopped for the update and started again # NO_AUTOSTART set to any value to skip the login-time startup entry # NO_SHORTCUT set to any value to skip the macOS bundle or the desktop entry @@ -82,6 +83,21 @@ rm -rf "${stage}" mkdir -p "${stage}" tar -xzf "${tmpDir}/${archive}" -C "${stage}" --strip-components=1 +# ── stop the Gateway that is running ────────────────────────────────────────── +# Without this an install over a running Gateway put the new executables in +# place and left the old process serving, so nothing appeared to change until +# that process was stopped by hand. The launcher from the archive is what stops +# it, because an installation old enough to have no "quit" command cannot stop +# itself. CASBIN_GATEWAY_HOME points that launcher at the installation rather +# than at the staging directory it runs from. +wasRunning="" +chmod 755 "${stage}/casbin-gateway-desktop" +if [[ -e "${INSTALL_DIR}/casbin-gateway" ]] && + CASBIN_GATEWAY_HOME="${INSTALL_DIR}" "${stage}/casbin-gateway-desktop" quit; then + wasRunning="yes" + info "Stopped the running Gateway, and will start it again once it is updated" +fi + for executable in casbin-gateway casbin-gateway-desktop; do mv -f "${stage}/${executable}" "${INSTALL_DIR}/${executable}" chmod 755 "${INSTALL_DIR}/${executable}" @@ -186,7 +202,10 @@ if [[ -n "${autostartNote}" ]]; then fi info "" -if [[ -n "${NO_START}" ]]; then +# A Gateway that was running when this started is started again even with +# NO_START: the install is what stopped it, and leaving the machine without one +# is not what "install without starting" asked for. +if [[ -n "${NO_START}" && -z "${wasRunning}" ]]; then info "Start it from the Casbin Gateway application, or with: casbin-gateway start" exit 0 fi --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
