Просмотр исходного кода

完成app-install.service全部功能代码,编译通过,还未单元测试

niujiuru 5 дней назад
Родитель
Сommit
c852f2ae69

+ 8 - 1
Makefile

@@ -33,7 +33,7 @@ D_LIBS2 += -L$(RTU_LINUX_MODULES_PATH)/dh_takephoto/lib/armv7hf -lMVSDK -liImage
 D_LIBS2 += -L$(RTU_LINUX_MODULES_PATH)/dh_takephoto/lib/armv7hf/GenICam/bin -lGCBase_gcc483_v3_0 -lGenApi_gcc483_v3_0 -lLog_gcc483_v3_0 -llog4cpp_gcc483_v3_0 -lMathParser_gcc483_v3_0 -lNodeMapData_gcc483_v3_0 -lXmlParser_gcc483_v3_0
 
 # 编译的目标
-all: networkd timesyncd gnss sshd-mqtt-bridge camera-capture web-ui
+all: networkd timesyncd gnss sshd-mqtt-bridge camera-capture app-install web-ui
 
 # 可执行程序
 networkd : LK_LIBS = $(S_LIBS0) $(S_LIBS1) $(D_LIBS0)
@@ -69,6 +69,13 @@ sshd-mqtt-bridge :
 	$(MAKE) -C $(RTU_LINUX_MODULES_PATH) yfkj_sshd.out target=armv7hf
 	cp -f $(RTU_LINUX_MODULES_PATH)/yfkj_sshd.out ./build/$@
 
+app-install: LK_LIBS =
+app-install:
+	mkdir -p ./build
+	$(GO) mod tidy
+	$(SETGO_ENV) CGO_LDFLAGS="$(LK_LIBS)" $(GO_BUILD) $(GO_FLAGS) -o $@ ./app-install.service/*.go
+	cp -f $@ ./build/ && rm -f $@
+
 web-ui: LK_LIBS =
 web-ui:
 	mkdir -p ./build

BIN
app-install.service/hello.tar.gz


+ 3 - 3
app-install.service/main.go

@@ -26,7 +26,7 @@ const (
 	listenAddr = "127.0.0.1:7004"
 )
 
-var userAppDir = "" // 存储上传的用户应用安装包信息
+var userAppPkgDir = "" // 存储上传的用户应用安装包信息
 
 type program struct {
 	name        string
@@ -44,8 +44,8 @@ func (p *program) Start(s service.Service) error {
 	baseapp.InitLogger(logCfgFile)
 	baseapp.Logger.Infof("[%s] 开始运行, 程序版本: %s, 构建时间: %s", p.name, Version, BuildTime)
 
-	userAppDir = filepath.Join(baseapp.EXEC_DIR, "userapp")
-	if err := os.MkdirAll(userAppDir, 0755); err != nil {
+	userAppPkgDir = filepath.Join(baseapp.EXEC_DIR, "userapp")
+	if err := os.MkdirAll(userAppPkgDir, 0755); err != nil {
 		return err
 	}
 

+ 323 - 32
app-install.service/rpc_handler.go

@@ -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()
+}

+ 31 - 0
app-install.service/test.txt

@@ -0,0 +1,31 @@
+接口测试:
+
+// ping联通测试
+curl -s -X POST http://127.0.0.1:7004/rpc -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"ping","params":{},"id":1}'
+
+// 获取软件版本
+curl -s -X POST http://127.0.0.1:7004/rpc -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"basic.getBuildVer","params":{},"id":2}'
+
+// 获取日志级别
+curl -s -X POST http://127.0.0.1:7004/rpc -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"basic.getLogLevel","params":{},"id":3}'
+
+// 设置日志级别
+curl -s -X POST http://127.0.0.1:7004/rpc -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"basic.setLogLevel","params":{"log_level":"trace"},"id":4}'
+
+// 保存日志设置
+curl -s -X POST http://127.0.0.1:7004/rpc -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"basic.saveLogConf","params":{},"id":5}'
+
+// 导入用户应用
+curl -s -X POST http://127.0.0.1:7004/rpc -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"core.package.import","params":{"file":"/tmp/hello.tar.gz"},"id":6}'
+
+// 删除用户应用
+curl -s -X POST http://127.0.0.1:7004/rpc -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"core.package.remove","params":{},"id":7}'
+
+// 安装用户应用
+curl -s -X POST http://127.0.0.1:7004/rpc -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"core.app.install","params":{},"id":8}'
+
+// 卸载当前应用
+curl -s -X POST http://127.0.0.1:7004/rpc -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"core.app.uninstall","params":{},"id":9}'
+
+// 获取安装信息
+curl -s -X POST http://127.0.0.1:7004/rpc -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"core.getInstallInfo","params":{},"id":10}'

+ 15 - 0
config/yfkj-app-install.service

@@ -0,0 +1,15 @@
+[Unit]
+Description=Yunfei Application installation and uninstallation
+After=yfkj-networkd.service
+Wants=yfkj-networkd.service
+
+[Service]
+Environment="PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin"
+ExecStart=/opt/yfkj/app-install.service/app-install
+Restart=always
+RestartSec=30
+StandardOutput=journal
+StandardError=journal
+
+[Install]
+WantedBy=multi-user.target