package toolinstaller import ( "bytes" "fmt" "log" "os" "os/exec" "path/filepath" "strings" "time" "ezops/model" ) // UninstallResult 工具卸载结果(含脚本输出,供审计日志)// Logan.Li type UninstallResult struct { Output string Err error } // Uninstall 卸载指定工具并重置 DB 状态(允许重新安装)// Logan.Li func (ti *ToolInstaller) Uninstall(toolName string) UninstallResult { var tool model.DeployTool if err := ti.db.Where("name = ?", toolName).First(&tool).Error; err != nil { return UninstallResult{Err: fmt.Errorf("tool %s not found in DB: %w", toolName, err)} } ti.mu.Lock() delete(ti.onces, toolName) ti.mu.Unlock() var buf bytes.Buffer if err := ti.uninstallTool(&tool, &buf); err != nil { out := strings.TrimSpace(buf.String()) log.Printf("[ToolInstaller] ERROR uninstalling %s: %v", toolName, err) ti.resetUninstallStatus(tool.ID, out+": "+err.Error()) return UninstallResult{Output: out, Err: err} } ti.resetUninstallStatus(tool.ID, "") out := strings.TrimSpace(buf.String()) log.Printf("[ToolInstaller] %s uninstalled successfully", toolName) return UninstallResult{Output: out} } func (ti *ToolInstaller) resetUninstallStatus(toolID uint, errMsg string) { now := time.Now() updates := map[string]interface{}{ "status": model.ToolNotInstalled, "installed_ver": "", "last_check": &now, } if errMsg != "" { updates["error_msg"] = errMsg } else { updates["error_msg"] = "" } ti.updateStatusMap(toolID, updates) } func (ti *ToolInstaller) updateStatusMap(toolID uint, updates map[string]interface{}) { ti.db.Model(&model.DeployTool{}).Where("id = ?", toolID).Updates(updates) } func (ti *ToolInstaller) uninstallTool(tool *model.DeployTool, buf *bytes.Buffer) error { switch tool.Name { case "jdk": return ti.uninstallJDK(buf) case "maven": return ti.uninstallMaven(buf) case "golang": return ti.uninstallGolang(buf) case "nvm": return ti.uninstallNVM(buf) case "nodejs": return ti.uninstallNodeJS(buf) case "git": return ti.uninstallApkPackage(buf, "git", "git") case "docker-cli": return ti.uninstallApkPackage(buf, "docker-cli", "docker") default: return ti.uninstallBinary(tool, buf) } } func (ti *ToolInstaller) uninstallJDK(buf *bytes.Buffer) error { buf.WriteString("[JDK] Removing symlinks and /usr/local/share/jdk\n") removeIfExists(buf, "/usr/local/bin/java", "/usr/local/bin/javac", "/usr/local/bin/jar") removeDirIfExists(buf, "/usr/local/share/jdk") return nil } func (ti *ToolInstaller) uninstallMaven(buf *bytes.Buffer) error { buf.WriteString("[Maven] Removing symlinks and /usr/local/share/maven\n") removeIfExists(buf, "/usr/local/bin/mvn") removeDirIfExists(buf, "/usr/local/share/maven") return nil } func (ti *ToolInstaller) uninstallGolang(buf *bytes.Buffer) error { buf.WriteString("[Go] Removing GOROOT and symlinks\n") removeIfExists(buf, "/usr/local/bin/go", "/usr/local/bin/gofmt", "/etc/profile.d/golang.sh") removeDirIfExists(buf, "/usr/local/go") return nil } func (ti *ToolInstaller) uninstallNVM(buf *bytes.Buffer) error { buf.WriteString("[NVM] Removing NVM and Node versions\n") removeIfExists(buf, "/usr/local/bin/node", "/usr/local/bin/npm", "/usr/local/bin/npx", "/etc/profile.d/nvm.sh") removeDirIfExists(buf, "/usr/local/share/nvm") return nil } func (ti *ToolInstaller) uninstallNodeJS(buf *bytes.Buffer) error { buf.WriteString("[Node.js] Removing symlinks and NVM node versions\n") removeIfExists(buf, "/usr/local/bin/node", "/usr/local/bin/npm", "/usr/local/bin/npx") nvmSh := "/usr/local/share/nvm/nvm.sh" if st, err := os.Stat(nvmSh); err == nil && !st.IsDir() { script := fmt.Sprintf(`export NVM_DIR="/usr/local/share/nvm" . "%s" CUR=$(nvm current 2>/dev/null || true) if [ -n "$CUR" ] && [ "$CUR" != "none" ] && [ "$CUR" != "system" ]; then nvm uninstall "$CUR" 2>/dev/null || true fi rm -rf "$NVM_DIR/versions/node" `, nvmSh) cmd := exec.Command("bash", "-c", script) out, err := cmd.CombinedOutput() if len(out) > 0 { buf.Write(out) } if err != nil { buf.WriteString(fmt.Sprintf("[Node.js] nvm cleanup warning: %v\n", err)) } } removeDirIfExists(buf, "/usr/local/share/nvm/versions/node") return nil } func (ti *ToolInstaller) uninstallApkPackage(buf *bytes.Buffer, pkg, binary string) error { buf.WriteString(fmt.Sprintf("[%s] Trying apk del %s\n", binary, pkg)) if _, err := exec.LookPath("apk"); err != nil { buf.WriteString(fmt.Sprintf("[%s] apk not found, skip package removal\n", binary)) return nil } cmd := exec.Command("apk", "del", "--no-cache", pkg) out, err := cmd.CombinedOutput() if len(out) > 0 { buf.Write(out) } if err != nil { // 可能未通过 apk 安装,仅标记卸载完成 // Logan.Li buf.WriteString(fmt.Sprintf("[%s] apk del skipped: %v\n", binary, err)) } return nil } func (ti *ToolInstaller) uninstallBinary(tool *model.DeployTool, buf *bytes.Buffer) error { path := tool.InstallPath if path == "" && tool.BinaryName != "" { path = filepath.Join("/usr/local/bin", tool.BinaryName) } if path == "" { return fmt.Errorf("no install path configured for %s", tool.Name) } buf.WriteString(fmt.Sprintf("[%s] Removing %s\n", tool.Name, path)) removeIfExists(buf, path) return nil } func removeIfExists(buf *bytes.Buffer, paths ...string) { for _, p := range paths { if err := os.Remove(p); err != nil { if !os.IsNotExist(err) { buf.WriteString(fmt.Sprintf("remove %s: %v\n", p, err)) } } else { buf.WriteString(fmt.Sprintf("removed %s\n", p)) } } } func removeDirIfExists(buf *bytes.Buffer, dir string) { if err := os.RemoveAll(dir); err != nil { if !os.IsNotExist(err) { buf.WriteString(fmt.Sprintf("remove dir %s: %v\n", dir, err)) } } else { buf.WriteString(fmt.Sprintf("removed dir %s\n", dir)) } }