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 576f902  fix(agent): find the launcher a package manager keeps outside 
the tree
576f902 is described below

commit 576f902801a9a609220e727bbf1a8e20f3dd9737
Author: Yang Luo <[email protected]>
AuthorDate: Sun Aug 23 17:27:52 2026 +0800

    fix(agent): find the launcher a package manager keeps outside the tree
---
 agent/launch.go      | 101 +++++++++++++++++++++++++++++++++++++++++++++++----
 agent/scan.go        |   2 +-
 agent/scan_common.go |  24 ++++++++++++
 3 files changed, 118 insertions(+), 9 deletions(-)

diff --git a/agent/launch.go b/agent/launch.go
index 236663d..1ac9e52 100644
--- a/agent/launch.go
+++ b/agent/launch.go
@@ -55,16 +55,23 @@ func executableOf(path, execName string) string {
                return ""
        }
 
-       root := nodeModulesRoot(path)
-       if root == "" {
-               return ""
-       }
-       for _, candidate := range npmShims(root, execName) {
-               if isFile(candidate) {
-                       return candidate
+       if root := nodeModulesRoot(path); root != "" {
+               for _, candidate := range npmShims(root, execName) {
+                       if isFile(candidate) {
+                               return candidate
+                       }
+               }
+               for _, dir := range managerBinDirs(root) {
+                       for _, candidate := range shimNames(dir, execName) {
+                               if isFile(candidate) {
+                                       return candidate
+                               }
+                       }
                }
        }
-       return ""
+       // Last, the program the shim would have run: an upgraded or 
half-removed
+       // package manager leaves the tree without its shim.
+       return packagedExecutable(path, execName)
 }
 
 func isFile(path string) bool {
@@ -105,3 +112,81 @@ func npmShims(root, execName string) []string {
        }
        return []string{filepath.Join(prefix, "bin", execName), local}
 }
+
+// managerBinDirs are the shim directories of the package managers that keep 
one
+// for every global package instead of writing beside the tree: pnpm's home and
+// Volta's bin, both recognisable from the package path itself.
+func managerBinDirs(root string) []string {
+       var dirs []string
+       for dir := filepath.Clean(root); ; {
+               parent := filepath.Dir(dir)
+               if parent == dir {
+                       return dirs
+               }
+               switch strings.ToLower(filepath.Base(dir)) {
+               case "pnpm":
+                       dirs = append(dirs, dir)
+               case "volta":
+                       dirs = append(dirs, filepath.Join(dir, "bin"))
+               }
+               dir = parent
+       }
+}
+
+// shimNames are the launcher file names one directory may hold for an agent.
+func shimNames(dir, execName string) []string {
+       if runtime.GOOS == "windows" {
+               return []string{
+                       filepath.Join(dir, execName+".cmd"),
+                       filepath.Join(dir, execName+".exe"),
+                       filepath.Join(dir, execName+".bat"),
+               }
+       }
+       return []string{filepath.Join(dir, execName)}
+}
+
+// packagedExecutable finds the program inside a package tree: npm packages of 
an
+// agent written in a compiled language carry one build per platform, either
+// beside the entry script or under a vendor directory.
+func packagedExecutable(path, execName string) string {
+       var patterns []string
+       for _, dir := range []string{path, filepath.Join(path, "bin"),
+               filepath.Join(path, "vendor", "*"), filepath.Join(path, 
"vendor", "*", "*")} {
+               patterns = append(patterns, filepath.Join(dir, execName+"*"))
+       }
+
+       for _, pattern := range patterns {
+               matches, _ := filepath.Glob(pattern)
+               for _, candidate := range matches {
+                       if isRunnable(candidate, execName) {
+                               return candidate
+                       }
+               }
+       }
+       return ""
+}
+
+// isRunnable reports whether a file inside a package tree is the agent itself
+// rather than one of the scripts or data files sitting beside it.
+func isRunnable(path, execName string) bool {
+       name := filepath.Base(path)
+       extension := filepath.Ext(name)
+       stem := strings.ToLower(strings.TrimSuffix(name, extension))
+       if stem != strings.ToLower(execName) && !strings.HasPrefix(stem, 
strings.ToLower(execName)+"-") {
+               return false
+       }
+
+       info, err := os.Stat(path)
+       if err != nil || !info.Mode().IsRegular() {
+               return false
+       }
+       extension = strings.ToLower(extension)
+       if runtime.GOOS == "windows" {
+               return extension == ".exe" || extension == ".cmd" || extension 
== ".bat"
+       }
+       switch extension {
+       case ".js", ".mjs", ".cjs", ".ts", ".json", ".md":
+               return false
+       }
+       return info.Mode()&0o111 != 0
+}
diff --git a/agent/scan.go b/agent/scan.go
index 944ba6a..6ac0fc1 100644
--- a/agent/scan.go
+++ b/agent/scan.go
@@ -86,7 +86,7 @@ func scanWithTimeout() ([]Installation, error) {
                if err := ctx.Err(); err != nil {
                        return nil, fmt.Errorf("agent scan timed out after %s: 
%w", scanTimeout, err)
                }
-               return installations, nil
+               return dropUnlaunchableTrees(installations), nil
        case <-ctx.Done():
                return nil, fmt.Errorf("agent scan timed out after %s: %w", 
scanTimeout, ctx.Err())
        }
diff --git a/agent/scan_common.go b/agent/scan_common.go
index b25c12d..ed7e483 100644
--- a/agent/scan_common.go
+++ b/agent/scan_common.go
@@ -192,6 +192,30 @@ func dedupeInstallations(installations []Installation) 
[]Installation {
        return result
 }
 
+// dropUnlaunchableTrees removes an installation nothing can start when the 
same
+// agent is installed elsewhere with a launcher. A package manager that 
upgrades
+// or half-removes itself leaves the package directory behind without the shim
+// that ran it, and that leftover is not an installation the user can use.
+func dropUnlaunchableTrees(installations []Installation) []Installation {
+       launchable := map[string]bool{}
+       resolved := make([]bool, len(installations))
+       for i, installation := range installations {
+               resolved[i] = LaunchOf(installation).Executable != ""
+               if resolved[i] {
+                       launchable[installation.AgentId] = true
+               }
+       }
+
+       result := make([]Installation, 0, len(installations))
+       for i, installation := range installations {
+               if !resolved[i] && launchable[installation.AgentId] {
+                       continue
+               }
+               result = append(result, installation)
+       }
+       return result
+}
+
 func canonicalPath(path string) string {
        if resolved, err := filepath.EvalSymlinks(path); err == nil {
                return resolved


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

Reply via email to