sandbox/nodejsRuntime.go
Star f9dcf07ba4 first version
supported macOS、linux
2026-03-23 00:35:27 +08:00

164 lines
5.5 KiB
Go

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")
}