| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635 |
- package main
- import (
- "archive/tar"
- "compress/gzip"
- "context"
- "crypto/md5"
- "encoding/hex"
- "encoding/json"
- "fmt"
- "io"
- "os"
- "os/exec"
- "path"
- "path/filepath"
- "strings"
- "sync"
- "time"
- "hnyfkj.com.cn/rtu/linux/baseapp"
- "hnyfkj.com.cn/rtu/linux/utils/jsonrpc2"
- )
- type PkgInfo struct {
- FileName string `json:"fileName"`
- FileSize int64 `json:"fileSize"`
- CheckMD5Val string `json:"fileMD5"`
- UploadTime string `json:"uploadTime"`
- }
- type AppInfo struct {
- 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 {
- Installed bool `json:"installed"`
- InstallTime string `json:"installTime"`
- }
- type AppIns struct {
- Pkg PkgInfo `json:"pkg"` // 应用的安装包
- App AppInfo `json:"app"` // 应用详细信息
- Ins InsInfo `json:"ins"` // 应用是否安装
- }
- var (
- appMutex sync.Mutex
- )
- const (
- userAppInsDir = "/home/root/yfkj-userapp"
- userAppService = "/etc/systemd/system/yfkj-userapp.service"
- ErrConflict jsonrpc2.ErrCode = -32099 ///////// 资源访问冲突
- )
- // 导入用户安装包
- func pkgImport(ctx context.Context, req *jsonrpc2.Request) *jsonrpc2.Response {
- appMutex.Lock()
- defer appMutex.Unlock()
- var params map[string]string
- if err := json.Unmarshal(req.Params, ¶ms); err != nil {
- return jsonrpc2.BuildError(req, jsonrpc2.ErrInvalidParams, err.Error())
- }
- file, ok := params["file"]
- if !ok || file == "" {
- return jsonrpc2.BuildError(req, jsonrpc2.ErrInvalidParams, "missing or empty 'file' parameter")
- }
- if !strings.HasSuffix(strings.ToLower(filepath.Base(file)), ".tar.gz") {
- return jsonrpc2.BuildError(req, jsonrpc2.ErrInvalidParams, "invalid install package format")
- }
- info, err := os.Stat(file)
- if err != nil {
- return jsonrpc2.BuildError(req, jsonrpc2.ErrInvalidParams, err.Error())
- }
- if !info.Mode().IsRegular() {
- return jsonrpc2.BuildError(req, jsonrpc2.ErrInvalidParams, "invalid install package file")
- }
- if err := os.MkdirAll(userAppPkgDir, 0755); err != nil {
- return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
- }
- if hasImportedPackage(userAppPkgDir) {
- return jsonrpc2.BuildError(req, ErrConflict, "application package already exists")
- }
- appInfo, err := readAppJSON(file)
- if err != nil {
- return jsonrpc2.BuildError(req, jsonrpc2.ErrInvalidParams, err.Error())
- }
- md5Val, err := fileMD5(file)
- if err != nil {
- return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
- }
- savePkgFile := filepath.Join(userAppPkgDir, filepath.Base(file))
- if err := os.Rename(file, savePkgFile); err != nil {
- return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
- }
- appIns := AppIns{
- Pkg: PkgInfo{
- FileName: filepath.Base(file),
- FileSize: info.Size(),
- CheckMD5Val: md5Val,
- UploadTime: time.Now().Format("2006-01-02 15:04:05"),
- },
- App: appInfo,
- Ins: InsInfo{
- Installed: false,
- },
- }
- data, err := json.MarshalIndent(appIns, "", " ")
- if err != nil {
- os.Remove(savePkgFile)
- return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
- }
- 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())
- }
- baseapp.Logger.Infof("[PkgImport] 导入用户应用安装包成功: %s", savePkgFile)
- return jsonrpc2.BuildResponse(req, "success", nil)
- }
- // 删除用户安装包
- func pkgRemove(ctx context.Context, req *jsonrpc2.Request) *jsonrpc2.Response {
- appMutex.Lock()
- defer appMutex.Unlock()
- appInsFile := filepath.Join(userAppPkgDir, "appins.json")
- data, err := os.ReadFile(appInsFile)
- if err == nil {
- var appIns AppIns
- if err := json.Unmarshal(data, &appIns); err == nil {
- if appIns.Ins.Installed { // 用户应用已安装
- return jsonrpc2.BuildError(req, ErrConflict, "application is installed, please uninstall it first")
- }
- }
- } else if !os.IsNotExist(err) {
- return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
- }
- if err := os.RemoveAll(userAppPkgDir); err != nil {
- return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
- }
- if err := os.MkdirAll(userAppPkgDir, 0755); err != nil {
- return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
- }
- baseapp.Logger.Infof("[PkgRemove] 删除用户应用安装包成功")
- return jsonrpc2.BuildResponse(req, "success", nil)
- }
- // 安装包应用安装
- func appInstall(ctx context.Context, req *jsonrpc2.Request) *jsonrpc2.Response {
- appMutex.Lock()
- defer appMutex.Unlock()
- 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)
- }
- // 卸载已安装应用
- func appUninstall(ctx context.Context, req *jsonrpc2.Request) *jsonrpc2.Response {
- appMutex.Lock()
- defer appMutex.Unlock()
- 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(userAppPkgDir, "appins.json")
- data, err := os.ReadFile(appInsFile)
- if err != nil {
- if os.IsNotExist(err) {
- return jsonrpc2.BuildResponse(req, nil, nil)
- }
- return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
- }
- var appIns AppIns
- if err := json.Unmarshal(data, &appIns); err != nil {
- return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
- }
- return jsonrpc2.BuildResponse(req, appIns, nil)
- }
- func readAppJSON(file string) (AppInfo, error) {
- var app AppInfo
- f, err := os.Open(file)
- if err != nil {
- return app, err
- }
- defer f.Close()
- gz, err := gzip.NewReader(f)
- if err != nil {
- return app, err
- }
- defer gz.Close()
- tr := tar.NewReader(gz)
- for {
- header, err := tr.Next()
- if err == io.EOF {
- break
- }
- if err != nil {
- return app, err
- }
- if header.Typeflag != tar.TypeReg {
- continue
- }
- if path.Base(header.Name) != "app.json" {
- continue
- }
- if header.Size > 2<<20 { // 2MB 上限
- return app, fmt.Errorf("app.json 文件过大")
- }
- data, err := io.ReadAll(io.LimitReader(tr, header.Size))
- if err != nil {
- return app, err
- }
- err = json.Unmarshal(data, &app)
- if err != nil {
- return app, err
- }
- if app.Name == "" || app.Version == "" || app.Executable == "" {
- return app, fmt.Errorf("app.json参数不完整")
- }
- if !validRelativePath(app.Executable) {
- return app, fmt.Errorf("invalid executable path")
- }
- for _, p := range app.LibPaths {
- if validRelativePath(p) {
- continue
- }
- return app, fmt.Errorf("invalid library path: %s", p)
- }
- return app, nil
- }
- return app, fmt.Errorf("安装包中未找到 app.json")
- }
- func fileMD5(file string) (string, error) {
- f, err := os.Open(file)
- if err != nil {
- return "", err
- }
- defer f.Close()
- h := md5.New()
- if _, err := io.Copy(h, f); err != nil {
- return "", err
- }
- 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 {
- return false
- }
- return info.Mode().IsRegular()
- }
- func hasImportedPackage(dir string) bool {
- if isRegularFile(filepath.Join(dir, "appins.json")) {
- return true
- }
- entries, err := os.ReadDir(dir)
- if err != nil {
- return false
- }
- for _, entry := range entries {
- if entry.IsDir() {
- continue
- }
- if strings.HasSuffix(strings.ToLower(entry.Name()), ".tar.gz") {
- return true
- }
- }
- return false
- }
- func writeFileAtomic(name string, data []byte, perm os.FileMode) error {
- tmp := name + ".tmp"
- defer os.Remove(tmp)
- f, err := os.OpenFile(tmp, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, perm)
- if err != nil {
- return err
- }
- if _, err := f.Write(data); err != nil {
- f.Close()
- return err
- }
- if err := f.Sync(); err != nil {
- f.Close()
- return err
- }
- if err := f.Close(); err != nil {
- return err
- }
- 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()
- }
|