|
|
@@ -10,6 +10,7 @@ import (
|
|
|
"fmt"
|
|
|
"io"
|
|
|
"os"
|
|
|
+ "os/exec"
|
|
|
"path"
|
|
|
"path/filepath"
|
|
|
"strings"
|
|
|
@@ -23,18 +24,17 @@ import (
|
|
|
type PkgInfo struct {
|
|
|
FileName string `json:"fileName"`
|
|
|
FileSize int64 `json:"fileSize"`
|
|
|
- FileMD5Hash string `json:"fileHash"`
|
|
|
+ CheckMD5Val string `json:"fileMD5"`
|
|
|
UploadTime string `json:"uploadTime"`
|
|
|
}
|
|
|
|
|
|
type AppInfo struct {
|
|
|
- Name string `json:"name"`
|
|
|
- Version string `json:"version"`
|
|
|
- InstallPath string `json:"installPath"`
|
|
|
- LibraryPath string `json:"libraryPath"`
|
|
|
- Executable string `json:"executable"`
|
|
|
- ReadmeFile string `json:"readmeFile"`
|
|
|
- Description string `json:"description"`
|
|
|
+ Name string `json:"name"`
|
|
|
+ Version string `json:"version"`
|
|
|
+ Executable string `json:"executable"`
|
|
|
+ LibPaths []string `json:"libraryPaths"`
|
|
|
+ Description string `json:"description"`
|
|
|
+ ReadmeFile string `json:"readmeFile"`
|
|
|
}
|
|
|
|
|
|
type InsInfo struct {
|
|
|
@@ -48,9 +48,16 @@ type AppIns struct {
|
|
|
Ins InsInfo `json:"ins"` // 应用是否安装
|
|
|
}
|
|
|
|
|
|
-var appMutex sync.Mutex
|
|
|
+var (
|
|
|
+ appMutex sync.Mutex
|
|
|
+)
|
|
|
+
|
|
|
+const (
|
|
|
+ userAppInsDir = "/home/root/yfkj-userapp"
|
|
|
+ userAppService = "/etc/systemd/system/yfkj-userapp.service"
|
|
|
|
|
|
-const ErrConflict jsonrpc2.ErrCode = -32099 // 资源访问冲突, 拒绝执行
|
|
|
+ ErrConflict jsonrpc2.ErrCode = -32099 ///////// 资源访问冲突
|
|
|
+)
|
|
|
|
|
|
// 导入用户安装包
|
|
|
func pkgImport(ctx context.Context, req *jsonrpc2.Request) *jsonrpc2.Response {
|
|
|
@@ -81,25 +88,25 @@ func pkgImport(ctx context.Context, req *jsonrpc2.Request) *jsonrpc2.Response {
|
|
|
return jsonrpc2.BuildError(req, jsonrpc2.ErrInvalidParams, "invalid install package file")
|
|
|
}
|
|
|
|
|
|
- if err := os.MkdirAll(userAppDir, 0755); err != nil {
|
|
|
+ if err := os.MkdirAll(userAppPkgDir, 0755); err != nil {
|
|
|
return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
|
|
|
}
|
|
|
|
|
|
- if hasImportedPackage(userAppDir) {
|
|
|
+ if hasImportedPackage(userAppPkgDir) {
|
|
|
return jsonrpc2.BuildError(req, ErrConflict, "application package already exists")
|
|
|
}
|
|
|
|
|
|
- app, err := readAppJSON(file)
|
|
|
+ appInfo, err := readAppJSON(file)
|
|
|
if err != nil {
|
|
|
return jsonrpc2.BuildError(req, jsonrpc2.ErrInvalidParams, err.Error())
|
|
|
}
|
|
|
|
|
|
- hash, err := fileMD5(file)
|
|
|
+ md5Val, err := fileMD5(file)
|
|
|
if err != nil {
|
|
|
return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
|
|
|
}
|
|
|
|
|
|
- savePkgFile := filepath.Join(userAppDir, filepath.Base(file))
|
|
|
+ savePkgFile := filepath.Join(userAppPkgDir, filepath.Base(file))
|
|
|
if err := os.Rename(file, savePkgFile); err != nil {
|
|
|
return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
|
|
|
}
|
|
|
@@ -108,10 +115,10 @@ func pkgImport(ctx context.Context, req *jsonrpc2.Request) *jsonrpc2.Response {
|
|
|
Pkg: PkgInfo{
|
|
|
FileName: filepath.Base(file),
|
|
|
FileSize: info.Size(),
|
|
|
- FileMD5Hash: hash,
|
|
|
+ CheckMD5Val: md5Val,
|
|
|
UploadTime: time.Now().Format("2006-01-02 15:04:05"),
|
|
|
},
|
|
|
- App: app,
|
|
|
+ App: appInfo,
|
|
|
Ins: InsInfo{
|
|
|
Installed: false,
|
|
|
},
|
|
|
@@ -123,7 +130,7 @@ func pkgImport(ctx context.Context, req *jsonrpc2.Request) *jsonrpc2.Response {
|
|
|
return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
|
|
|
}
|
|
|
|
|
|
- appInsFile := filepath.Join(userAppDir, "appins.json")
|
|
|
+ appInsFile := filepath.Join(userAppPkgDir, "appins.json")
|
|
|
if err := writeFileAtomic(appInsFile, data, 0644); err != nil {
|
|
|
os.Remove(savePkgFile)
|
|
|
return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
|
|
|
@@ -139,7 +146,7 @@ func pkgRemove(ctx context.Context, req *jsonrpc2.Request) *jsonrpc2.Response {
|
|
|
appMutex.Lock()
|
|
|
defer appMutex.Unlock()
|
|
|
|
|
|
- appInsFile := filepath.Join(userAppDir, "appins.json")
|
|
|
+ appInsFile := filepath.Join(userAppPkgDir, "appins.json")
|
|
|
|
|
|
data, err := os.ReadFile(appInsFile)
|
|
|
if err == nil {
|
|
|
@@ -153,11 +160,11 @@ func pkgRemove(ctx context.Context, req *jsonrpc2.Request) *jsonrpc2.Response {
|
|
|
return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
|
|
|
}
|
|
|
|
|
|
- if err := os.RemoveAll(userAppDir); err != nil {
|
|
|
+ if err := os.RemoveAll(userAppPkgDir); err != nil {
|
|
|
return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
|
|
|
}
|
|
|
|
|
|
- if err := os.MkdirAll(userAppDir, 0755); err != nil {
|
|
|
+ if err := os.MkdirAll(userAppPkgDir, 0755); err != nil {
|
|
|
return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
|
|
|
}
|
|
|
|
|
|
@@ -171,7 +178,98 @@ func appInstall(ctx context.Context, req *jsonrpc2.Request) *jsonrpc2.Response {
|
|
|
appMutex.Lock()
|
|
|
defer appMutex.Unlock()
|
|
|
|
|
|
- return nil
|
|
|
+ appInsFile := filepath.Join(userAppPkgDir, "appins.json")
|
|
|
+
|
|
|
+ data, err := os.ReadFile(appInsFile)
|
|
|
+ if err != nil {
|
|
|
+ return jsonrpc2.BuildError(req, jsonrpc2.ErrInvalidParams, "application package not found")
|
|
|
+ }
|
|
|
+
|
|
|
+ var appIns AppIns
|
|
|
+ if err := json.Unmarshal(data, &appIns); err != nil {
|
|
|
+ return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
|
|
|
+ }
|
|
|
+
|
|
|
+ if appIns.Ins.Installed {
|
|
|
+ return jsonrpc2.BuildError(req, ErrConflict, "application already installed")
|
|
|
+ }
|
|
|
+
|
|
|
+ success := false
|
|
|
+
|
|
|
+ defer func() {
|
|
|
+ if !success {
|
|
|
+ baseapp.Logger.Warnf("[AppInstall] rollback: %s", userAppInsDir)
|
|
|
+
|
|
|
+ systemctlIgnoreError("stop", "yfkj-userapp.service")
|
|
|
+ systemctlIgnoreError("disable", "yfkj-userapp.service")
|
|
|
+
|
|
|
+ os.RemoveAll(userAppInsDir)
|
|
|
+ os.Remove(userAppService)
|
|
|
+
|
|
|
+ systemctlIgnoreError("daemon-reload")
|
|
|
+ }
|
|
|
+ }()
|
|
|
+
|
|
|
+ systemctlIgnoreError("stop", "yfkj-userapp.service")
|
|
|
+
|
|
|
+ if err := os.MkdirAll(userAppInsDir, 0755); err != nil {
|
|
|
+ return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
|
|
|
+ }
|
|
|
+
|
|
|
+ appPkgFile := filepath.Join(userAppPkgDir, appIns.Pkg.FileName)
|
|
|
+
|
|
|
+ if err := extractTarGz(appPkgFile, userAppInsDir); err != nil {
|
|
|
+ return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
|
|
|
+ }
|
|
|
+
|
|
|
+ execFile := filepath.Join(userAppInsDir, appIns.App.Executable)
|
|
|
+
|
|
|
+ info, err := os.Stat(execFile)
|
|
|
+ if err != nil {
|
|
|
+ return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, "executable not found")
|
|
|
+ }
|
|
|
+
|
|
|
+ if !info.Mode().IsRegular() {
|
|
|
+ return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, "invalid executable")
|
|
|
+ }
|
|
|
+
|
|
|
+ if err := os.Chmod(execFile, 0755); err != nil {
|
|
|
+ return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
|
|
|
+ }
|
|
|
+
|
|
|
+ if err := createUserAppService(appIns.App, userAppInsDir); err != nil {
|
|
|
+ return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
|
|
|
+ }
|
|
|
+
|
|
|
+ if err := systemctl("daemon-reload"); err != nil {
|
|
|
+ return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
|
|
|
+ }
|
|
|
+
|
|
|
+ if err := systemctl("enable", "yfkj-userapp.service"); err != nil {
|
|
|
+ return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
|
|
|
+ }
|
|
|
+
|
|
|
+ if err := systemctl("start", "yfkj-userapp.service"); err != nil {
|
|
|
+ return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
|
|
|
+ }
|
|
|
+
|
|
|
+ appIns.Ins.Installed = true
|
|
|
+ appIns.Ins.InstallTime = time.Now().Format("2006-01-02 15:04:05")
|
|
|
+
|
|
|
+ data, err = json.MarshalIndent(appIns, "", " ")
|
|
|
+ if err != nil {
|
|
|
+ return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
|
|
|
+ }
|
|
|
+
|
|
|
+ if err := writeFileAtomic(appInsFile, data, 0644); err != nil {
|
|
|
+ return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
|
|
|
+ }
|
|
|
+
|
|
|
+ success = true
|
|
|
+
|
|
|
+ baseapp.Logger.Infof("[AppInstall] 安装用户应用成功: %s", appIns.App.Name)
|
|
|
+
|
|
|
+ return jsonrpc2.BuildResponse(req, "success", nil)
|
|
|
}
|
|
|
|
|
|
// 卸载已安装应用
|
|
|
@@ -179,12 +277,58 @@ func appUninstall(ctx context.Context, req *jsonrpc2.Request) *jsonrpc2.Response
|
|
|
appMutex.Lock()
|
|
|
defer appMutex.Unlock()
|
|
|
|
|
|
- return nil
|
|
|
+ appInsFile := filepath.Join(userAppPkgDir, "appins.json")
|
|
|
+
|
|
|
+ data, err := os.ReadFile(appInsFile)
|
|
|
+ if err != nil {
|
|
|
+ return jsonrpc2.BuildError(req, jsonrpc2.ErrInvalidParams, "application package not found")
|
|
|
+ }
|
|
|
+
|
|
|
+ var appIns AppIns
|
|
|
+ if err := json.Unmarshal(data, &appIns); err != nil {
|
|
|
+ return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
|
|
|
+ }
|
|
|
+
|
|
|
+ if !appIns.Ins.Installed {
|
|
|
+ return jsonrpc2.BuildError(req, ErrConflict, "application not installed")
|
|
|
+ }
|
|
|
+
|
|
|
+ systemctlIgnoreError("stop", "yfkj-userapp.service")
|
|
|
+
|
|
|
+ systemctlIgnoreError("disable", "yfkj-userapp.service")
|
|
|
+
|
|
|
+ if err := os.Remove(userAppService); err != nil && !os.IsNotExist(err) {
|
|
|
+ return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
|
|
|
+ }
|
|
|
+
|
|
|
+ if err := systemctl("daemon-reload"); err != nil {
|
|
|
+ return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
|
|
|
+ }
|
|
|
+
|
|
|
+ if err := os.RemoveAll(userAppInsDir); err != nil {
|
|
|
+ return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
|
|
|
+ }
|
|
|
+
|
|
|
+ appIns.Ins.Installed = false
|
|
|
+ appIns.Ins.InstallTime = ""
|
|
|
+
|
|
|
+ data, err = json.MarshalIndent(appIns, "", " ")
|
|
|
+ if err != nil {
|
|
|
+ return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
|
|
|
+ }
|
|
|
+
|
|
|
+ if err := writeFileAtomic(appInsFile, data, 0644); err != nil {
|
|
|
+ return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
|
|
|
+ }
|
|
|
+
|
|
|
+ baseapp.Logger.Infof("[AppUninstall] 卸载用户应用成功: %s", appIns.App.Name)
|
|
|
+
|
|
|
+ return jsonrpc2.BuildResponse(req, "success", nil)
|
|
|
}
|
|
|
|
|
|
// 安装包应用信息
|
|
|
func getInstallInfo(ctx context.Context, req *jsonrpc2.Request) *jsonrpc2.Response {
|
|
|
- appInsFile := filepath.Join(userAppDir, "appins.json")
|
|
|
+ appInsFile := filepath.Join(userAppPkgDir, "appins.json")
|
|
|
|
|
|
data, err := os.ReadFile(appInsFile)
|
|
|
if err != nil {
|
|
|
@@ -251,18 +395,19 @@ func readAppJSON(file string) (AppInfo, error) {
|
|
|
return app, err
|
|
|
}
|
|
|
|
|
|
- if app.Name == "" || app.Version == "" || app.InstallPath == "" ||
|
|
|
- app.Executable == "" || app.ReadmeFile == "" {
|
|
|
- return app, fmt.Errorf("app.json 参数不完整")
|
|
|
+ if app.Name == "" || app.Version == "" || app.Executable == "" {
|
|
|
+ return app, fmt.Errorf("app.json参数不完整")
|
|
|
}
|
|
|
|
|
|
- if !filepath.IsAbs(app.InstallPath) {
|
|
|
- return app, fmt.Errorf("installPath必须是绝对路径")
|
|
|
+ if !validRelativePath(app.Executable) {
|
|
|
+ return app, fmt.Errorf("invalid executable path")
|
|
|
}
|
|
|
|
|
|
- clean := filepath.Clean(app.InstallPath)
|
|
|
- if clean != app.InstallPath {
|
|
|
- return app, fmt.Errorf("installPath路径非法")
|
|
|
+ for _, p := range app.LibPaths {
|
|
|
+ if validRelativePath(p) {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ return app, fmt.Errorf("invalid library path: %s", p)
|
|
|
}
|
|
|
|
|
|
return app, nil
|
|
|
@@ -285,6 +430,29 @@ func fileMD5(file string) (string, error) {
|
|
|
return hex.EncodeToString(h.Sum(nil)), nil
|
|
|
}
|
|
|
|
|
|
+func validRelativePath(p string) bool {
|
|
|
+ if p == "" {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ if filepath.IsAbs(p) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ clean := filepath.Clean(p)
|
|
|
+
|
|
|
+ if clean != p {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ if clean == ".." ||
|
|
|
+ strings.HasPrefix(clean, ".."+string(os.PathSeparator)) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ return true
|
|
|
+}
|
|
|
+
|
|
|
func isRegularFile(name string) bool {
|
|
|
info, err := os.Stat(name)
|
|
|
if err != nil {
|
|
|
@@ -342,3 +510,126 @@ func writeFileAtomic(name string, data []byte, perm os.FileMode) error {
|
|
|
|
|
|
return os.Rename(tmp, name)
|
|
|
}
|
|
|
+
|
|
|
+func extractTarGz(src string, dst string) error {
|
|
|
+ f, err := os.Open(src)
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ defer f.Close()
|
|
|
+
|
|
|
+ gz, err := gzip.NewReader(f)
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ defer gz.Close()
|
|
|
+
|
|
|
+ tr := tar.NewReader(gz)
|
|
|
+
|
|
|
+ root := filepath.Clean(dst) + string(os.PathSeparator)
|
|
|
+
|
|
|
+ for {
|
|
|
+ header, err := tr.Next()
|
|
|
+
|
|
|
+ if err == io.EOF {
|
|
|
+ break
|
|
|
+ }
|
|
|
+
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+
|
|
|
+ name := filepath.Join(dst, header.Name)
|
|
|
+ name = filepath.Clean(name)
|
|
|
+
|
|
|
+ if !strings.HasPrefix(name+string(os.PathSeparator), root) {
|
|
|
+ return fmt.Errorf("invalid path: %s", header.Name)
|
|
|
+ }
|
|
|
+
|
|
|
+ switch header.Typeflag {
|
|
|
+ case tar.TypeDir:
|
|
|
+ if err := os.MkdirAll(name, 0755); err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ case tar.TypeReg:
|
|
|
+ if err := os.MkdirAll(filepath.Dir(name), 0755); err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+
|
|
|
+ out, err := os.OpenFile(
|
|
|
+ name,
|
|
|
+ os.O_CREATE|os.O_WRONLY|os.O_TRUNC,
|
|
|
+ os.FileMode(header.Mode),
|
|
|
+ )
|
|
|
+
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+
|
|
|
+ _, err = io.Copy(out, tr)
|
|
|
+
|
|
|
+ out.Close()
|
|
|
+
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ default:
|
|
|
+ return fmt.Errorf("unsupported tar entry: %s", header.Name)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
+func createUserAppService(app AppInfo, installDir string) error {
|
|
|
+ execFile := filepath.Join(installDir, app.Executable)
|
|
|
+
|
|
|
+ libPaths := ""
|
|
|
+ if len(app.LibPaths) > 0 {
|
|
|
+ paths := make([]string, 0, len(app.LibPaths))
|
|
|
+
|
|
|
+ for _, p := range app.LibPaths {
|
|
|
+ paths = append(paths, filepath.Join(installDir, p))
|
|
|
+ }
|
|
|
+
|
|
|
+ libPaths = strings.Join(paths, ":")
|
|
|
+ }
|
|
|
+
|
|
|
+ content := fmt.Sprintf(`[Unit]
|
|
|
+Description=%s Service
|
|
|
+After=yfkj-networkd.service
|
|
|
+Wants=yfkj-networkd.service
|
|
|
+
|
|
|
+[Service]
|
|
|
+Environment="PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin"
|
|
|
+Environment="LD_LIBRARY_PATH=%s"
|
|
|
+WorkingDirectory=%s
|
|
|
+ExecStart=%s
|
|
|
+Restart=always
|
|
|
+RestartSec=5
|
|
|
+StandardOutput=journal
|
|
|
+StandardError=journal
|
|
|
+
|
|
|
+[Install]
|
|
|
+WantedBy=multi-user.target
|
|
|
+`,
|
|
|
+ app.Name,
|
|
|
+ libPaths,
|
|
|
+ installDir,
|
|
|
+ execFile,
|
|
|
+ )
|
|
|
+
|
|
|
+ return os.WriteFile(userAppService, []byte(content), 0644)
|
|
|
+}
|
|
|
+
|
|
|
+func systemctl(args ...string) error {
|
|
|
+ out, err := exec.Command("systemctl", args...).CombinedOutput()
|
|
|
+ if err != nil {
|
|
|
+ return fmt.Errorf("systemctl %v failed: %v %s", args, err, string(out))
|
|
|
+ }
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
+func systemctlIgnoreError(args ...string) {
|
|
|
+ _ = exec.Command("systemctl", args...).Run()
|
|
|
+}
|