package sandbox import ( "fmt" "net/http" "net/url" "os" "path/filepath" "runtime" "strings" "time" "github.com/ssgo/httpclient" "github.com/ssgo/log" "github.com/ssgo/u" ) func init() { RegisterRuntime("nodejs", &Runtime{ Check: func(runtimePath, venvPath, projectPath string, uid, gid int, cfg *RuntimeConfig) (*RuntimeSandboxConfig, error) { if err := checkNodejsRuntime(runtimePath, cfg); err != nil { return nil, err } if err := checkNodejsProject(runtimePath, projectPath, uid, gid, cfg); err != nil { return nil, err } nodeExe := getNodeExe(runtimePath) return &RuntimeSandboxConfig{ StartCmd: nodeExe, Envs: map[string]string{ "PATH": filepath.Dir(nodeExe) + string(os.PathListSeparator) + "$PATH", }, }, nil }, }) } func checkNodejsRuntime(runtimePath string, cfg *RuntimeConfig) error { runtimeVersion := filepath.Base(runtimePath) lock := getRuntimeLock("nodejs", "runtime", runtimePath) lock.Lock() defer lock.Unlock() if !u.FileExists(runtimePath) { osPart := u.StringIf(runtime.GOOS == "linux", "linux", u.StringIf(runtime.GOOS == "windows", "win", "darwin")) archPart := u.StringIf(runtime.GOARCH == "arm64", "arm64", "x64") extension := u.StringIf(runtime.GOOS == "windows", ".zip", ".tar.gz") instFile := filepath.Join(filepath.Dir(runtimePath), ".installs", fmt.Sprintf("node-v%s-%s-%s%s", runtimeVersion, osPart, archPart, extension)) if !u.FileExists(instFile) { downloadClient := httpclient.GetClient(3600 * time.Second) downloadClient.EnableRedirect() if cfg.HttpProxy != "" { if proxyURL, err := url.Parse(cfg.HttpProxy); err == nil { downloadClient.GetRawClient().Transport = &http.Transport{Proxy: http.ProxyURL(proxyURL)} } } log.DefaultLogger.Info("[Sandbox] fetching nodejs runtime release list", "from", "https://nodejs.org/dist/index.json", "httpProxy", cfg.HttpProxy) r1 := downloadClient.Get("https://nodejs.org/dist/index.json") if r1.Error != nil { return r1.Error } // 解析JSON数据 var versions []map[string]interface{} if err := r1.To(&versions); err != nil { return fmt.Errorf("failed to parse index.json: %v", err) } // 寻找匹配的版本 var targetVersion string for _, v := range versions { version := v["version"].(string) // 检查版本号是否匹配 if strings.HasPrefix(version, "v"+runtimeVersion) { // 检查是否有对应的文件 files := v["files"].([]interface{}) fileFound := false for _, f := range files { file := f.(string) // 检查是否有对应的平台和架构的文件 if (osPart == "linux" && file == "linux-"+archPart) || (osPart == "darwin" && (file == "osx-"+archPart+"-tar" || file == "osx-x64-tar")) || (osPart == "win" && file == "win-"+archPart+"-zip") { fileFound = true break } } if fileFound { targetVersion = version break } } } if targetVersion == "" { return fmt.Errorf("no binary for version %s on %s-%s", runtimeVersion, osPart, archPart) } // 构建下载URL fileName := fmt.Sprintf("node-%s-%s-%s%s", targetVersion, osPart, archPart, extension) downloadURL := fmt.Sprintf("https://nodejs.org/dist/%s/%s", targetVersion, fileName) log.DefaultLogger.Info("[Sandbox] downloading nodejs runtime", "from", downloadURL, "to", instFile, "httpProxy", cfg.HttpProxy) if _, err := downloadClient.Download(instFile, downloadURL, nil); err != nil { return err } else { log.DefaultLogger.Info("[Sandbox] downloaded nodejs runtime", "version", targetVersion, "os", osPart, "arch", archPart, "url", downloadURL, "to", instFile) } } os.MkdirAll(runtimePath, 0755) if err := u.Extract(instFile, runtimePath, true); err != nil { log.DefaultLogger.Error("[Sandbox] unpack nodejs runtime failed", "version", runtimeVersion, "os", osPart, "arch", archPart, "from", instFile, "err", err.Error()) return err } } return nil } func checkNodejsProject(runtimePath, projectPath string, uid, gid int, cfg *RuntimeConfig) error { lock := getRuntimeLock("nodejs", "project", projectPath) lock.Lock() defer lock.Unlock() if uid != 0 && runtime.GOOS == "linux" { os.Chown(projectPath, uid, gid) } packageFile := filepath.Join(projectPath, "package.json") if u.FileExists(packageFile) { shaFile := filepath.Join(projectPath, ".package.sha1") currentSha := u.Sha1String(u.ReadFileN(packageFile)) oldSha := u.ReadFileN(shaFile) if oldSha != currentSha { npmExe := getNpmExe(runtimePath) npmArgs := []string{"install", "--prefix", projectPath, "--cache", filepath.Join(projectPath, ".npm")} if cfg.Mirror != "" { npmArgs = append(npmArgs, "--registry", cfg.Mirror) } if out, err := RunCmdWithEnv(map[string]string{"PATH": filepath.Dir(npmExe) + string(os.PathListSeparator) + os.Getenv("PATH")}, projectPath, uid, gid, npmExe, npmArgs...); err != nil { log.DefaultLogger.Error("[Sandbox] npm install failed", "runtime", runtimePath, "project", projectPath, "err", err.Error(), "output", out) return err } else { log.DefaultLogger.Info("[Sandbox] npm install success", "runtime", runtimePath, "project", projectPath, "output", out) u.WriteFile(shaFile, currentSha) } } } return nil } func getNodeExe(base string) string { if runtime.GOOS == "windows" { return filepath.Join(base, "node.exe") } return filepath.Join(base, "bin", "node") } func getNpmExe(base string) string { if runtime.GOOS == "windows" { return filepath.Join(base, "npm.cmd") } return filepath.Join(base, "bin", "npm") }