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 c72a3f4  fix(agentinstall): uninstall the npm copy the scan actually 
found
c72a3f4 is described below

commit c72a3f4ce85531f2cb9a9c578344295f04509d14
Author: Yang Luo <[email protected]>
AuthorDate: Fri Sep 18 18:40:37 2026 +0800

    fix(agentinstall): uninstall the npm copy the scan actually found
---
 agentinstall/host.go         |  9 +++++++++
 agentinstall/job.go          | 28 ++++++++++++++++++++++++----
 agentinstall/plan.go         | 41 ++++++++++++++++++++++++++++++++++++++++-
 web/src/lib/agents.ts        | 16 +++++++++++++++-
 web/src/locales/en/data.json |  3 +++
 web/src/locales/zh/data.json |  3 +++
 6 files changed, 94 insertions(+), 6 deletions(-)

diff --git a/agentinstall/host.go b/agentinstall/host.go
index f11aec6..77ae577 100644
--- a/agentinstall/host.go
+++ b/agentinstall/host.go
@@ -111,6 +111,15 @@ func ownedByUser(path string) bool {
        return within(home, path)
 }
 
+// samePath compares two paths the way the host itself would.
+func samePath(left string, right string) bool {
+       left, right = filepath.Clean(left), filepath.Clean(right)
+       if runtime.GOOS == "windows" {
+               return strings.EqualFold(left, right)
+       }
+       return left == right
+}
+
 // within reports whether path is inside root, comparing whole segments so that
 // a directory is never confused with one whose name merely starts the same.
 func within(root string, path string) bool {
diff --git a/agentinstall/job.go b/agentinstall/job.go
index 7534596..b97d22b 100644
--- a/agentinstall/job.go
+++ b/agentinstall/job.go
@@ -157,7 +157,15 @@ func run(started *job, plan Plan) {
 
        err := cmd.Run()
        if ctx.Err() != nil {
-               err = errors.New("the install did not finish within " + 
jobTimeout.String())
+               err = errors.New("the " + plan.Action + " did not finish within 
" + jobTimeout.String())
+       }
+
+       // Whatever the outcome, what is on disk changed: the next listing has 
to be
+       // read from the host rather than from the scan that ran before this. 
The
+       // job waits for it, since an uninstall is judged by what it finds.
+       installations, scanErr := agent.Scan(true)
+       if err == nil && scanErr == nil {
+               err = confirmRemoved(plan, installations)
        }
 
        started.Lock()
@@ -168,10 +176,22 @@ func run(started *job, plan Plan) {
        }
        started.EndTime = now()
        started.Unlock()
+}
 
-       // Whatever the outcome, what is on disk changed: the next listing has 
to be
-       // read from the host rather than from the scan that ran before this.
-       _, _ = agent.Scan(true)
+// confirmRemoved reports an uninstall that left the installation where it was.
+// An uninstaller exits zero for a tree it found nothing in, so the exit code
+// alone would have the page say the agent had gone.
+func confirmRemoved(plan Plan, installations []agent.Installation) error {
+       if plan.Action != ActionUninstall || plan.removedPath == "" {
+               return nil
+       }
+
+       for _, installation := range installations {
+               if installation.AgentId == plan.AgentId && 
samePath(installation.Path, plan.removedPath) {
+                       return errors.New("the uninstall reported success, but 
the agent is still installed at " + plan.removedPath)
+               }
+       }
+       return nil
 }
 
 func (j *job) snapshot() Job {
diff --git a/agentinstall/plan.go b/agentinstall/plan.go
index 312b0a2..8f9ce39 100644
--- a/agentinstall/plan.go
+++ b/agentinstall/plan.go
@@ -91,6 +91,8 @@ type Plan struct {
 
        program string
        args    []string
+       // removedPath is what an uninstall has to take off the host to have 
worked.
+       removedPath string
 }
 
 // InstallPlan picks the way to install an agent this host does not have.
@@ -189,6 +191,10 @@ func resolve(installation agent.Installation, action 
string, version string) Pla
                return plan
        }
 
+       if action == ActionUninstall {
+               plan.removedPath = installation.Path
+       }
+
        notes := []string{}
        spec := target{installation: installation, packages: packages,
                action: action, version: version, notes: &notes}
@@ -272,7 +278,11 @@ func ownManagerDriver(plan Plan, spec target) (Plan, bool) 
{
                        return plan, false
                }
                if spec.action == ActionUninstall {
-                       return fill(plan, ManagerNpm, program, "uninstall", 
"-g", packages.Npm), true
+                       args := []string{"uninstall", "-g"}
+                       if prefix := npmGlobalPrefix(spec.installation, 
packages.Npm); prefix != "" {
+                               args = append(args, "--prefix", prefix)
+                       }
+                       return fill(plan, ManagerNpm, program, append(args, 
packages.Npm)...), true
                }
                return fill(plan, ManagerNpm, program, "install", "-g", 
packages.Npm+"@"+requestedRelease(spec.version)), true
        case ManagerHomebrew:
@@ -394,6 +404,35 @@ func scriptDriver(plan Plan, spec target) (Plan, bool) {
        return built, true
 }
 
+// npmGlobalPrefix is the prefix the installation a scan found sits in, derived
+// from its own path. npm acts on the prefix its own configuration names, and a
+// host can have several, so a command that does not name this one removes
+// nothing and still exits zero. Empty for a tree that is not an npm global 
root
+// at all - a pnpm store, npx's cache - which npm cannot be pointed at.
+func npmGlobalPrefix(installation agent.Installation, pkg string) string {
+       if installation.InstallMethod != ManagerNpm || installation.Path == "" {
+               return ""
+       }
+
+       dir := installation.Path
+       for range strings.Split(pkg, "/") {
+               dir = filepath.Dir(dir)
+       }
+       if !strings.EqualFold(filepath.Base(dir), "node_modules") {
+               return ""
+       }
+       // A global root is <prefix>/node_modules on Windows and <prefix>/lib/
+       // node_modules everywhere else.
+       prefix := filepath.Dir(dir)
+       if runtime.GOOS == "windows" {
+               return prefix
+       }
+       if filepath.Base(prefix) != "lib" {
+               return ""
+       }
+       return filepath.Dir(prefix)
+}
+
 // requestedRelease is the npm tag or version an install asks for.
 func requestedRelease(version string) string {
        if version == "" {
diff --git a/web/src/lib/agents.ts b/web/src/lib/agents.ts
index 6973b41..aff7b8d 100644
--- a/web/src/lib/agents.ts
+++ b/web/src/lib/agents.ts
@@ -602,7 +602,7 @@ export function useAgentInstall(enabled = true, 
onFinished?: () => void) {
         job.ok ? "success" : "error",
         job.ok
           ? `${i18next.t(installOutcomeKey(job.action))}: 
${job.name}${job.version ? ` ${job.version}` : ""}`
-          : `${i18next.t("agent:Failed to install the agent")}: ${job.error || 
job.name}`,
+          : `${i18next.t(installFailedKey(job.action))}: ${job.error || 
job.name}`,
       );
       finished.current?.();
     });
@@ -669,6 +669,20 @@ function installOutcomeKey(action: string) {
   }
 }
 
+/** The same for one that did not, which an uninstall is no install of. */
+function installFailedKey(action: string) {
+  switch (action) {
+  case "upgrade":
+    return "agent:Failed to upgrade the agent";
+  case "downgrade":
+    return "agent:Failed to move the agent to an older version";
+  case "uninstall":
+    return "agent:Failed to uninstall the agent";
+  default:
+    return "agent:Failed to install the agent";
+  }
+}
+
 /** How often the registries are asked again while a page stays open. */
 const updatePollMs = 10 * 60 * 1000;
 
diff --git a/web/src/locales/en/data.json b/web/src/locales/en/data.json
index d900334..949f710 100644
--- a/web/src/locales/en/data.json
+++ b/web/src/locales/en/data.json
@@ -126,13 +126,16 @@
     "Failed to get agent records": "Failed to get agent records",
     "Failed to get agent sessions": "Failed to get agent sessions",
     "Failed to install the agent": "Failed to install the agent",
+    "Failed to move the agent to an older version": "Failed to move the agent 
to an older version",
     "Failed to read this transcript": "Failed to read this transcript",
     "Failed to save the permissions": "Failed to save the permissions",
     "Failed to scan agents": "Failed to scan agents",
     "Failed to start the agent": "Failed to start the agent",
     "Failed to stop the agent": "Failed to stop the agent",
+    "Failed to uninstall the agent": "Failed to uninstall the agent",
     "Failed to update agent patch": "Failed to update agent patch",
     "Failed to update agent provider": "Failed to update agent provider",
+    "Failed to upgrade the agent": "Failed to upgrade the agent",
     "Failed to write the agent configuration": "Failed to write the agent 
configuration",
     "Fallback hint": "Providers tried, in the order picked, when the bound one 
cannot answer.",
     "Fetch a URL": "Fetch a URL",
diff --git a/web/src/locales/zh/data.json b/web/src/locales/zh/data.json
index a6c74d8..beb9427 100644
--- a/web/src/locales/zh/data.json
+++ b/web/src/locales/zh/data.json
@@ -126,13 +126,16 @@
     "Failed to get agent records": "获取Agent记录失败",
     "Failed to get agent sessions": "获取Agent会话失败",
     "Failed to install the agent": "安装失败",
+    "Failed to move the agent to an older version": "降级失败",
     "Failed to read this transcript": "读取该会话记录失败",
     "Failed to save the permissions": "保存权限失败",
     "Failed to scan agents": "探测Agent失败",
     "Failed to start the agent": "启动Agent失败",
     "Failed to stop the agent": "停止Agent失败",
+    "Failed to uninstall the agent": "卸载失败",
     "Failed to update agent patch": "更新Agent Patch失败",
     "Failed to update agent provider": "更新Agent供应商失败",
+    "Failed to upgrade the agent": "升级失败",
     "Failed to write the agent configuration": "写入Agent配置失败",
     "Fallback hint": "当绑定的供应商无法响应时,按所选顺序依次尝试的备用供应商。",
     "Fetch a URL": "抓取网址",


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

Reply via email to