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 10665e8  fix: only ever take a port from a previous Gateway
10665e8 is described below

commit 10665e85d42042a8c36c2395149e87c46ff066af
Author: Yang Luo <[email protected]>
AuthorDate: Sun Aug 23 13:34:43 2026 +0800

    fix: only ever take a port from a previous Gateway
---
 README.md          |   2 +-
 README_zh.md       |   2 +-
 conf/app.conf      |   6 +--
 main.go            |  25 ++++++---
 scripts/install.sh |   1 +
 service/proxy.go   |   4 +-
 util/daemon.go     |  39 ++++++++++----
 util/port.go       | 103 +++++++++++++++++++++++++++++------
 util/port_test.go  |   2 +-
 util/process.go    |  42 +++++++++------
 util/supervisor.go | 154 -----------------------------------------------------
 11 files changed, 171 insertions(+), 209 deletions(-)

diff --git a/README.md b/README.md
index 0462833..28e7e9e 100644
--- a/README.md
+++ b/README.md
@@ -179,7 +179,7 @@ Gateway prints what it is actually doing when it starts, so 
the result can be ch
 +---------------------------------------------------------------------+
 ```
 
-If a port is taken, Gateway says which process holds it and stops, rather than 
starting half-configured.
+A previous Gateway still holding one of these ports is stopped first, so a 
restart never waits on it. A port held by anything else stays with that 
program: Gateway names the process holding it and stops, rather than taking the 
port or starting half-configured.
 
 ### Recording prompts
 
diff --git a/README_zh.md b/README_zh.md
index 39aafdd..957d384 100644
--- a/README_zh.md
+++ b/README_zh.md
@@ -177,7 +177,7 @@ Gateway 启动时会打印它实际在做什么,所以可以直接看结果而
 +---------------------------------------------------------------------+
 ```
 
-如果端口被占用,Gateway 会指出是哪个进程占着它并停止启动,而不是半配置地跑起来。
+还占着这些端口的上一个 Gateway 会先被停掉,所以重启不用等它。被别的程序占着的端口则留给它:Gateway 
会指出是哪个进程占着并停止启动,而不是抢过端口,也不会半配置地跑起来。
 
 ### 记录提示词
 
diff --git a/conf/app.conf b/conf/app.conf
index 2bea1d8..7ceba26 100644
--- a/conf/app.conf
+++ b/conf/app.conf
@@ -40,9 +40,9 @@ apiKeyEncryptionKey =
 ; The reverse-proxy gateway, i.e. the WAF itself. It is off by default because
 ; ports 80 and 443 usually belong to something else on the host, and binding
 ; them needs root on Linux and macOS. Sites and rules can still be configured
-; while it is off, but nothing is proxied until it is turned on. Turning it on
-; also means the two ports below are taken over on startup, killing whatever
-; holds them, so do not point them at a web server that has to keep running.
+; while it is off, but nothing is proxied until it is turned on. A previous
+; Gateway still holding one of the two ports below is stopped on startup; a 
port
+; held by anything else is left to it and the proxy reports the conflict 
instead.
 gatewayEnabled = false
 gatewayHttpPort = 80
 gatewayHttpsPort = 443
diff --git a/main.go b/main.go
index 24ad4e9..62db364 100644
--- a/main.go
+++ b/main.go
@@ -15,6 +15,7 @@
 package main
 
 import (
+       "errors"
        "fmt"
        "os"
        "time"
@@ -52,7 +53,6 @@ func main() {
                return
        }
 
-       util.InitSelfGuard()
        object.InitFlag()
        object.InitAdapter()
        object.CreateTables()
@@ -106,8 +106,8 @@ func main() {
        port := conf.GetHttpPort()
        addr := conf.GetHttpAddr()
 
-       // A previous run still holding one of these ports would keep this one 
from
-       // starting, so it is stopped first. The gateway ports come before the
+       // A previous Gateway still holding one of these ports would keep this 
one
+       // from starting, so it is stopped first. The gateway ports come before 
the
        // management port because service.Start() binds them first.
        stopPorts := []int{}
        if conf.IsGatewayEnabled() {
@@ -115,11 +115,22 @@ func main() {
        }
        stopPorts = append(stopPorts, port)
        for _, stopPort := range stopPorts {
-               if err := util.StopOldInstance(stopPort); err != nil {
-                       // The bind below reports the conflict in full, so a 
failed kill only
-                       // needs a note here and never stops the startup by 
itself.
-                       fmt.Printf("Casbin Gateway: could not free port %d: 
%v\n", stopPort, err)
+               err := util.StopOldInstance(stopPort)
+               if err == nil {
+                       continue
                }
+
+               // A port held by something that is not Gateway stays with it. 
Saying so
+               // here would only precede the bind below, which reports the 
same
+               // conflict along with the remedy for that particular port.
+               var foreign *util.ForeignPortError
+               if errors.As(err, &foreign) {
+                       continue
+               }
+
+               // The bind below reports the conflict in full, so a failed 
kill only
+               // needs a note here and never stops the startup by itself.
+               fmt.Printf("Casbin Gateway: could not free port %d: %v\n", 
stopPort, err)
        }
 
        service.PrintStartupSummary()
diff --git a/scripts/install.sh b/scripts/install.sh
index 6205071..1c0eaca 100755
--- a/scripts/install.sh
+++ b/scripts/install.sh
@@ -167,6 +167,7 @@ Type=simple
 WorkingDirectory=${INSTALL_DIR}
 ExecStart=${INSTALL_DIR}/casbin-gateway
 Restart=on-failure
+RestartPreventExitStatus=78
 
 [Install]
 WantedBy=default.target
diff --git a/service/proxy.go b/service/proxy.go
index 4cc5b8e..1a0cd4c 100644
--- a/service/proxy.go
+++ b/service/proxy.go
@@ -469,8 +469,8 @@ func StopGateway() {
 }
 
 // serveGateway handles what a gateway server returned. StopGateway() closing 
it
-// is the normal way out; anything else is a failure worth crashing on, so the
-// supervisor restarts a proxy that died on its own.
+// is the normal way out; anything else means the proxy died on its own, which
+// is a failure worth crashing on rather than serving nothing in silence.
 func serveGateway(err error) {
        if err != nil && !errors.Is(err, http.ErrServerClosed) {
                panic(err)
diff --git a/util/daemon.go b/util/daemon.go
index c5a3b75..e9d541c 100644
--- a/util/daemon.go
+++ b/util/daemon.go
@@ -15,6 +15,7 @@
 package util
 
 import (
+       "errors"
        "fmt"
        "net"
        "os"
@@ -124,24 +125,42 @@ func stopDaemon(port int) {
                return
        }
 
-       if err := StopOldInstance(port); err != nil {
-               fmt.Printf("Casbin Gateway: could not stop the process on port 
%d: %v\n", port, err)
+       err := StopOldInstance(port)
+       if err == nil {
+               fmt.Println("Casbin Gateway stopped")
+               return
+       }
+
+       // Answering on the port is not the same as being Gateway, so this is 
where
+       // "stop" finds out that the port belongs to something else entirely.
+       var foreign *ForeignPortError
+       if errors.As(err, &foreign) {
+               fmt.Printf("Port %d is held by %s, not by Casbin Gateway, so 
nothing was stopped\n", port, foreign.Holder)
                os.Exit(1)
        }
-       fmt.Println("Casbin Gateway stopped")
+
+       fmt.Printf("Casbin Gateway: could not stop the process on port %d: 
%v\n", port, err)
+       os.Exit(1)
 }
 
 func printStatus(port int) {
-       if isServing(port) {
-               holder := DescribePortHolder(port)
-               if holder == "" {
-                       holder = "running"
-               }
-               fmt.Printf("Casbin Gateway is running on http://localhost:%d 
(%s)\n", port, holder)
+       if !isServing(port) {
+               fmt.Printf("Casbin Gateway is not running on port %d\n", port)
+               return
+       }
+
+       holder := LookupPortHolder(port)
+       if holder == nil {
+               fmt.Printf("Casbin Gateway is running on 
http://localhost:%d\n";, port)
+               return
+       }
+
+       if !holder.Ours {
+               fmt.Printf("Port %d is held by %s, not by Casbin Gateway\n", 
port, holder)
                return
        }
 
-       fmt.Printf("Casbin Gateway is not running on port %d\n", port)
+       fmt.Printf("Casbin Gateway is running on http://localhost:%d (%s)\n", 
port, holder)
 }
 
 // isServing reports whether something already answers on the port, which is 
the
diff --git a/util/port.go b/util/port.go
index 133556e..ebee916 100644
--- a/util/port.go
+++ b/util/port.go
@@ -20,15 +20,26 @@ import (
        "net"
        "os"
        "os/exec"
+       "path/filepath"
        "runtime"
        "strconv"
        "strings"
        "time"
 )
 
-// portLookupTimeout bounds the external "who owns this port" commands so a 
slow
-// lsof or netstat can never hold up startup.
-const portLookupTimeout = 2 * time.Second
+const (
+       // portLookupTimeout bounds the external "who owns this port" commands 
so a
+       // slow lsof or netstat can never hold up startup.
+       portLookupTimeout = 2 * time.Second
+       // procNameMaxLen is how much of another process's name Linux reports, 
from
+       // TASK_COMM_LEN. Anything longer comes back cut to this.
+       procNameMaxLen = 15
+       // ExitCodeFatalConfig marks an exit caused by configuration, such as a 
port
+       // that is already taken. Restarting cannot fix it, so a service 
manager has
+       // to be told not to try; the systemd unit the installer writes says 
so. The
+       // value is the conventional sysexits.h EX_CONFIG.
+       ExitCodeFatalConfig = 78
+)
 
 // ListenTcp binds a TCP listener on all interfaces for the given port. Callers
 // keep the listener and serve on it, so a port that was reported free cannot 
be
@@ -61,9 +72,9 @@ func CheckPortAvailableOn(addr string, port int) error {
 }
 
 // FatalListenError explains a failed bind in plain language and stops the
-// process without a stack trace. It exits with ExitCodeFatalConfig so the
-// supervisor knows this is a configuration problem and does not restart the
-// process straight back into the same conflict.
+// process without a stack trace. It exits with ExitCodeFatalConfig so that a
+// service manager knows this is a configuration problem and does not restart
+// the process straight back into the same conflict.
 //
 // "remedy" completes the sentence "Free the port, or ...", for example
 // `set "gatewayEnabled = false" in conf/app.conf`.
@@ -84,22 +95,84 @@ func FatalListenError(port int, remedy string, err error) {
        os.Exit(ExitCodeFatalConfig)
 }
 
-// DescribePortHolder is a best-effort, read-only lookup of the process that is
-// listening on the port, used to turn "bind failed" into something actionable
-// such as "pid 1234, nginx". It only ever reads: a wrong guess costs an
-// inaccurate hint, never someone else's process.
-func DescribePortHolder(port int) string {
+// PortHolder is the process listening on a port: its pid, its executable name
+// when that could be read, and whether that executable is this same program.
+type PortHolder struct {
+       Pid  int
+       Name string
+       Ours bool
+}
+
+// String renders a holder as "pid 1234, nginx", or "pid 1234" when its name
+// could not be read.
+func (h *PortHolder) String() string {
+       if h.Name == "" {
+               return fmt.Sprintf("pid %d", h.Pid)
+       }
+
+       return fmt.Sprintf("pid %d, %s", h.Pid, h.Name)
+}
+
+// LookupPortHolder is a best-effort, read-only lookup of the process listening
+// on the port. It turns "bind failed" into something actionable such as
+// "pid 1234, nginx", and tells a previous Gateway apart from an unrelated
+// program. It returns nil when nothing is listening or the pid cannot be read.
+func LookupPortHolder(port int) *PortHolder {
        pid := findListenerPid(port)
        if pid <= 0 {
-               return ""
+               return nil
        }
 
        name := findProcessName(pid)
-       if name == "" {
-               return fmt.Sprintf("pid %d", pid)
+       return &PortHolder{Pid: pid, Name: name, Ours: isSelfExecutable(name)}
+}
+
+// DescribePortHolder names the process listening on the port, or "" when there
+// is none to name. It only ever reads: a wrong guess costs an inaccurate hint,
+// never someone else's process.
+func DescribePortHolder(port int) string {
+       holder := LookupPortHolder(port)
+       if holder == nil {
+               return ""
+       }
+
+       return holder.String()
+}
+
+// isSelfExecutable reports whether an executable name is this program's own.
+// It is the check that keeps Gateway from stopping a process that is not a
+// previous Gateway, so it is deliberately conservative: a name that could not
+// be read is never a match.
+func isSelfExecutable(name string) bool {
+       self, err := os.Executable()
+       if err != nil {
+               return false
+       }
+
+       mine, other := executableBaseName(self), executableBaseName(name)
+       if mine == "" || other == "" {
+               return false
+       }
+
+       // Linux reports the name of another process cut to procNameMaxLen, so a
+       // name that long has to be compared as a prefix of this executable's.
+       if len(other) >= procNameMaxLen {
+               return strings.HasPrefix(mine, other)
+       }
+
+       return mine == other
+}
+
+// executableBaseName drops the directory and the ".exe" suffix and lowercases
+// the rest, so that "C:\Program Files\Casbin-Gateway.exe" and
+// "casbin-gateway" are recognised as the same program.
+func executableBaseName(value string) string {
+       value = strings.TrimSpace(value)
+       if value == "" {
+               return ""
        }
 
-       return fmt.Sprintf("pid %d, %s", pid, name)
+       return strings.TrimSuffix(strings.ToLower(filepath.Base(value)), ".exe")
 }
 
 // findListenerPid returns the pid of a process listening on the port, or 0 
when
diff --git a/util/port_test.go b/util/port_test.go
index 5349dab..1059e7b 100644
--- a/util/port_test.go
+++ b/util/port_test.go
@@ -83,7 +83,7 @@ func TestDescribePortHolder(t *testing.T) {
 
 // TestFatalListenError runs the fatal path in a subprocess, because it ends 
the
 // process on purpose. Both halves matter: the message has to name the port, 
and
-// the exit code has to be the one the supervisor treats as "do not restart".
+// the exit code has to be the one a service manager treats as "do not 
restart".
 func TestFatalListenError(t *testing.T) {
        if os.Getenv("CASBIN_GATEWAY_TEST_FATAL_LISTEN") == "1" {
                listener, err := ListenTcp(0)
diff --git a/util/process.go b/util/process.go
index ea3ff0a..7c6e0ea 100644
--- a/util/process.go
+++ b/util/process.go
@@ -29,37 +29,49 @@ const (
        portReleasePollInterval = 100 * time.Millisecond
 )
 
-// StopOldInstance kills whatever process is listening on the port, so that a
-// restart never has to wait for the previous run to be shut down by hand.
+// ForeignPortError reports a port held by a process that is not Casbin 
Gateway.
+// That process keeps running: the port may well be its own, and taking it by
+// force would stop something the rest of the machine depends on.
+type ForeignPortError struct {
+       Port   int
+       Holder string
+}
+
+func (e *ForeignPortError) Error() string {
+       return fmt.Sprintf("port %d is held by %s, which is not Casbin 
Gateway", e.Port, e.Holder)
+}
+
+// StopOldInstance stops a previous Casbin Gateway still listening on the port,
+// so that a restart never has to wait for it to be shut down by hand.
+//
+// Only another copy of this executable is ever stopped. A port held by 
anything
+// else belongs to that program - an nginx on 443, a dev server on 17000 - so 
it
+// is left alone and returned as a ForeignPortError; the caller reports the
+// conflict instead of taking the port by force.
 //
-// This takes the port for Casbin Gateway unconditionally: an unrelated program
-// that happens to be listening there is killed just the same, so the ports in
-// conf/app.conf have to be ones this machine really is allowed to hand over.
 // Only sockets in the LISTEN state count, so a process that merely holds a
 // connection to a remote port with the same number is never touched.
 func StopOldInstance(port int) error {
-       pid := findListenerPid(port)
-       if pid <= 0 || pid == os.Getpid() {
+       holder := LookupPortHolder(port)
+       if holder == nil || holder.Pid == os.Getpid() {
                return nil
        }
 
-       process, err := os.FindProcess(pid)
+       if !holder.Ours {
+               return &ForeignPortError{Port: port, Holder: holder.String()}
+       }
+
+       process, err := os.FindProcess(holder.Pid)
        if err != nil {
                return err
        }
 
-       name := findProcessName(pid)
-
        err = process.Kill()
        if err != nil {
                return err
        }
 
-       if name == "" {
-               fmt.Printf("Casbin Gateway: stopped pid %d, which was holding 
port %d\n", pid, port)
-       } else {
-               fmt.Printf("Casbin Gateway: stopped %s (pid %d), which was 
holding port %d\n", name, pid, port)
-       }
+       fmt.Printf("Casbin Gateway: stopped the previous Gateway (%s), which 
was holding port %d\n", holder, port)
 
        return waitForPortRelease(port)
 }
diff --git a/util/supervisor.go b/util/supervisor.go
deleted file mode 100644
index c58c514..0000000
--- a/util/supervisor.go
+++ /dev/null
@@ -1,154 +0,0 @@
-// Copyright 2025 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 util
-
-import (
-       "fmt"
-       "os"
-       "os/exec"
-       "os/signal"
-       "syscall"
-       "time"
-)
-
-const (
-       // EnvSupervisorKey is the environment variable key to detect if 
running under supervisor
-       EnvSupervisorKey = "CASBIN_GATEWAY_SUPERVISED"
-       // MaxRestarts is the maximum number of restarts within the restart 
window
-       MaxRestarts = 5
-       // RestartWindow is the time window for counting restarts
-       RestartWindow = 5 * time.Minute
-       // RestartDelay is the delay before restarting after a crash
-       RestartDelay = 2 * time.Second
-       // ExitCodeFatalConfig marks an exit caused by configuration, such as a 
port
-       // that is already taken. Restarting cannot fix it, so the supervisor 
stops
-       // instead of looping through the same failure. The value is the 
conventional
-       // sysexits.h EX_CONFIG.
-       ExitCodeFatalConfig = 78
-)
-
-// InitSelfGuard initializes the self-recovery mechanism
-// If not already supervised, it starts a supervisor process and exits
-// If already supervised, it does nothing and returns
-func InitSelfGuard() {
-       // Check if we're already supervised
-       if os.Getenv(EnvSupervisorKey) == "1" {
-               // Already supervised, just return and continue normal execution
-               return
-       }
-
-       // Start as supervisor
-       err := runSupervisor()
-       if err != nil {
-               fmt.Printf("Supervisor error: %v\n", err)
-               os.Exit(1)
-       }
-       // If we get here, supervisor exited cleanly
-       os.Exit(0)
-}
-
-// runSupervisor starts the supervisor that monitors and restarts the main 
process
-func runSupervisor() error {
-       fmt.Println("Starting Casbin Gateway with auto-recovery mechanism...")
-
-       restartTimes := []time.Time{}
-
-       for {
-               // Clean up old restart times outside the window
-               now := time.Now()
-               validRestarts := []time.Time{}
-               for _, t := range restartTimes {
-                       if now.Sub(t) < RestartWindow {
-                               validRestarts = append(validRestarts, t)
-                       }
-               }
-               restartTimes = validRestarts
-
-               // Check if we've exceeded max restarts
-               if len(restartTimes) >= MaxRestarts {
-                       return fmt.Errorf("exceeded maximum restart limit (%d 
restarts in %v), stopping supervisor", MaxRestarts, RestartWindow)
-               }
-
-               // Start the child process
-               cmd := exec.Command(os.Args[0], os.Args[1:]...)
-               cmd.Stdout = os.Stdout
-               cmd.Stderr = os.Stderr
-               cmd.Stdin = os.Stdin
-
-               // Set environment variable to indicate supervised process
-               cmd.Env = append(os.Environ(), fmt.Sprintf("%s=1", 
EnvSupervisorKey))
-
-               // Start the process
-               if err := cmd.Start(); err != nil {
-                       fmt.Printf("Failed to start process: %v\n", err)
-                       return err
-               }
-
-               processStartTime := time.Now()
-               fmt.Printf("Started supervised process with PID: %d\n", 
cmd.Process.Pid)
-
-               // Setup signal handling to forward signals to child
-               sigChan := make(chan os.Signal, 1)
-               signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
-
-               // Wait for process completion or signal
-               doneChan := make(chan error, 1)
-               go func() {
-                       doneChan <- cmd.Wait()
-               }()
-
-               select {
-               case err := <-doneChan:
-                       // Process exited
-                       exitTime := time.Now()
-                       uptime := exitTime.Sub(processStartTime)
-
-                       if err != nil {
-                               // A configuration failure repeats on every 
restart, so pass the
-                               // child's exit code up instead of burning 
through the restart
-                               // budget. The child has already explained 
itself.
-                               if exitErr, ok := err.(*exec.ExitError); ok && 
exitErr.ExitCode() == ExitCodeFatalConfig {
-                                       os.Exit(ExitCodeFatalConfig)
-                               }
-
-                               fmt.Printf("Process crashed after %v: %v\n", 
uptime, err)
-
-                               // Record this restart
-                               restartTimes = append(restartTimes, time.Now())
-
-                               fmt.Printf("Waiting %v before restarting... 
(restart %d/%d)\n",
-                                       RestartDelay, len(restartTimes), 
MaxRestarts)
-                               time.Sleep(RestartDelay)
-
-                               // Continue to restart
-                               continue
-                       } else {
-                               // Clean exit
-                               fmt.Println("Process exited cleanly")
-                               return nil
-                       }
-
-               case sig := <-sigChan:
-                       // Received shutdown signal, forward to child
-                       fmt.Printf("Received signal %v, forwarding to child 
process...\n", sig)
-                       if cmd.Process != nil {
-                               cmd.Process.Signal(sig)
-                       }
-                       // Wait for child to exit
-                       <-doneChan
-                       return nil
-               }
-       }
-}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to