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: ¬es}
@@ -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]