diff --git a/README.en.md b/README.en.md
index 45b8ff4..cbfad4a 100644
--- a/README.en.md
+++ b/README.en.md
@@ -11,7 +11,7 @@ An efficient intranet office platform that includes various tools such as Word,
English | [简体中文](README.md)
-[FAQ](./docs/Faq.md) | [API](./docs/Api.md)
+[FAQ](./docs/Faq.md) | [API](./docs/Api.md) | [AppStore](./docs/Store.md)
### Install
diff --git a/README.md b/README.md
index 3aa4902..af7400f 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
GodoOS
-一款高效的内网办公平台,内含word/excel/ppt/pdf/内网聊天/白板/思维导图等多个办公系统工具,支持原生文件存储。平台界面精仿windows风格,操作简便,同时保持低资源消耗和高性能运行。无需注册即可自动连接内网用户,实现即时通讯和文件共享。
+一款高效的内网办公平台,内含word/excel/ppt/pdf/内网聊天/白板/思维导图等多个办公系统工具,支持原生文件存储。平台界面精仿windows风格,操作简便,同时保持低资源消耗和高性能运行。无需注册即可自动连接内网用户,实现即时通讯和文件共享。灵活高配置的应用商店,可无限扩展。
@@ -11,7 +11,7 @@
[English](README.en.md) | 简体中文
-[FAQ](./docs/Faq.md) | [API](./docs/Api.md)
+[FAQ](./docs/Faq.md) | [API](./docs/Api.md) | [应用开发](./docs/Store.md)
@@ -48,6 +48,8 @@
- ***精小,打包后仅65M,确包含了所有的办公套件***
- ***可无限扩展,支持自定义应用***
- ***golang开发后端,低资源消耗和高性能***
+- ***支持多平台,Windows、Linux、MacOS***
+- ***只会html和拥有可执行的应用,简单学习一下应用商店配置即可开发出复杂的应用***
## 🚧 开发进程
- 2024年7月,发布v1.0.0版本,发布后,项目进入第二阶段。
diff --git a/docs/Api.md b/docs/Api.md
index a5087c8..8c0e5a0 100644
--- a/docs/Api.md
+++ b/docs/Api.md
@@ -16,6 +16,11 @@
- **Content-Type**: `application/json`
- **响应体**: 目录内容列表
+#### 示例
+```bash
+PATH_PARAM="/path/to/directory"
+curl -X GET "http://localhost:56780/files/read?path=$PATH_PARAM"
+```
---
### 获取文件或目录状态
@@ -33,6 +38,12 @@
- **Content-Type**: `application/json`
- **响应体**: 文件或目录的状态信息
+#### 示例
+```bash
+# 获取文件或目录状态
+PATH_PARAM="/path/to/file_or_directory"
+curl -X GET "http://localhost:56780/files/stat?path=$PATH_PARAM"
+```
---
### 更改文件权限
@@ -51,6 +62,12 @@
- **Content-Type**: `application/json`
- **响应体**: 操作结果
+#### 示例
+```bash
+# 更改文件权限
+JSON='{"path":"/path/to/file","mode":"0644"}'
+curl -X POST -H "Content-Type: application/json" -d "$JSON" "http://localhost:56780/files/chmod"
+```
---
### 检查文件或目录是否存在
@@ -67,7 +84,12 @@
#### 响应
- **Content-Type**: `application/json`
- **响应体**: 存在性检查结果
-
+#### 示例
+```bash
+# 检查文件或目录是否存在
+PATH_PARAM="/path/to/file_or_directory"
+curl -X GET "http://localhost:56780/files/exists?path=$PATH_PARAM"
+```
---
### 读取文件内容
@@ -84,7 +106,12 @@
#### 响应
- **Content-Type**: `application/json`
- **响应体**: 文件内容
-
+#### 示例
+```bash
+# 读取文件内容
+PATH_PARAM="/path/to/file"
+curl -X GET "http://localhost:56780/files/readfile?path=$PATH_PARAM"
+```
---
### 删除文件
@@ -101,7 +128,12 @@
#### 响应
- **Content-Type**: `application/json`
- **响应体**: 操作结果
-
+#### 示例
+```bash
+# 删除文件
+PATH_PARAM="/path/to/file"
+curl -X GET "http://localhost:56780/files/unlink?path=$PATH_PARAM"
+```
---
### 清空文件系统
@@ -115,7 +147,11 @@
#### 响应
- **Content-Type**: `application/json`
- **响应体**: 操作结果
-
+#### 示例
+```bash
+# 清空文件系统
+curl -X GET "http://localhost:56780/files/clear"
+```
---
### 重命名文件或目录
@@ -132,7 +168,13 @@
#### 响应
- **Content-Type**: `application/json`
- **响应体**: 操作结果
-
+#### 示例
+```bash
+# 重命名文件或目录
+OLD_PATH_PARAM="/path/to/old_file_or_directory"
+NEW_PATH_PARAM="/path/to/new_file_or_directory"
+curl -X GET "http://localhost:56780/files/rename?oldPath=$OLD_PATH_PARAM&newPath=$NEW_PATH_PARAM"
+```
---
### 创建目录
@@ -149,7 +191,12 @@
#### 响应
- **Content-Type**: `application/json`
- **响应体**: 操作结果
-
+#### 示例
+```bash
+# 创建目录
+DIR_PATH_PARAM="/path/to/new_directory"
+curl -X POST "http://localhost:56780/files/mkdir?dirPath=$DIR_PATH_PARAM"
+```
---
### 删除目录
@@ -166,7 +213,12 @@
#### 响应
- **Content-Type**: `application/json`
- **响应体**: 操作结果
-
+#### 示例
+```bash
+# 删除目录
+DIR_PATH_PARAM="/path/to/directory"
+curl -X GET "http://localhost:56780/files/rmdir?dirPath=$DIR_PATH_PARAM"
+```
---
### 复制文件
@@ -183,7 +235,13 @@
#### 响应
- **Content-Type**: `application/json`
- **响应体**: 操作结果
-
+#### 示例
+```bash
+# 复制文件
+SRC_PATH_PARAM="/path/to/source_file"
+DST_PATH_PARAM="/path/to/destination_file"
+curl -X GET "http://localhost:56780/files/copyfile?srcPath=$SRC_PATH_PARAM&dstPath=$DST_PATH_PARAM"
+```
---
### 写入文件
@@ -204,7 +262,13 @@
#### 响应
- **Content-Type**: `application/json`
- **响应体**: 操作结果
-
+#### 示例
+```bash
+# 写入文件
+FILE_PATH_PARAM="/path/to/file"
+CONTENT="This is the content to be written."
+curl -X POST -F "content=@-$CONTENT" "http://localhost:56780/files/writefile?filePath=$FILE_PATH_PARAM"
+```
---
### 追加文件内容
@@ -225,7 +289,13 @@
#### 响应
- **Content-Type**: `application/json`
- **响应体**: 操作结果
-
+#### 示例
+```bash
+# 追加文件内容
+FILE_PATH_PARAM="/path/to/file"
+CONTENT="This is the content to be appended."
+curl -X POST -F "content=@-$CONTENT" "http://localhost:56780/files/appendfile?filePath=$FILE_PATH_PARAM"
+```
---
### 文件系统事件监听
@@ -237,3 +307,4 @@
- **path** (监听的文件或目录路径)
- **callback** (事件回调函数)
- **errback** (错误回调函数)
+
diff --git a/docs/Markdown.md b/docs/Markdown.md
index 18e2cca..8f88b5e 100644
--- a/docs/Markdown.md
+++ b/docs/Markdown.md
@@ -1,7 +1,3 @@
----
-title: Markdown编辑器
-icon: circle-info
----
## 概览
Markdown 编辑器允许您使用丰富的文本编辑体验创建和编辑 Markdown 文件。它包括实时保存、文件导入/导出以及访问最近的 Markdown 文件等功能。
diff --git a/docs/Store.md b/docs/Store.md
new file mode 100644
index 0000000..c8d0257
--- /dev/null
+++ b/docs/Store.md
@@ -0,0 +1,159 @@
+## 应用管理
+
+### 如何添加一个应用
+
+1. 在用户目录.godoos/run下建立一个应用文件夹。
+2. 一个应用需要包含两个配置文件,在应用根目录下创建`install.json`和`store.json`两个文件。 配置文件格式如下:
+- install.json
+```json
+{
+ "name": "", // string, 应用程序名称。
+ "url": "", // string, 应用程序下载地址。
+ "webUrl":"", // string, 如何设置,应用程序将显示到桌面。
+ "isDev": true, // boolean, 是否为开发环境。
+ "needDownload": true, // boolean, 是否需要下载。
+ "needInstall": true, // boolean, 是否需要安装。
+ "version": "1.0.0", // string, 应用程序版本。
+ "icon": "", // string, 应用程序图标,为可访问的网络地址。
+ "checkProgress": true, // boolean, 表示是否显示启动和停止。
+ "hasRestart": true, // boolean, 是否需要重启。
+ "setting": true // boolean, 是否需要设置。
+}
+
+```
+如果不需要后端进程的web应用,store.json可以不用配置。
+
+install.json的struct为:
+```json
+type InstallInfo struct {
+ Name string `json:"name"` // 应用程序名称。重要,必须和应用的目录名称一致。
+ URL string `json:"url"` // 应用程序下载地址。
+ WebUrl string `json:"webUrl"` // 应用程序的网页地址。
+ IsDev bool `json:"isDev"` // 标志位,表示是否为开发者版本。
+ NeedDownload bool `json:"needDownload"` // 标志位,表示是否需要下载。
+ NeedInstall bool `json:"needInstall"` // 标志位,表示是否需要安装。
+ Version string `json:"version"` // 应用程序的版本号。
+ Desc string `json:"desc"` // 应用程序的描述信息。
+ Icon string `json:"icon"` // 应用程序的图标路径。
+ CheckProgress bool `json:"checkProgress"` // 标志位,表示是否显示启动和停止。
+ HasRestart bool `json:"hasRestart"` // 标志位,表示安装后是否需要重启。
+ Setting bool `json:"setting"` // 标志位,表示是否需要配置。
+}
+```
+
+- store.json
+
+```json
+{
+ "name": "mysql5.7", // string, 应用程序名称。可不设置,会继承自install.json
+ "icon": "mysql.png", // string, 应用程序图标,一般放在应用程序static目录下。
+ "setting": {
+ "binPath": "{exePath}/bin/mysqld.exe", // string, 重要,必须设置。为启动程序路径。
+ "confPath": "{exePath}/my.ini",// string, 重要,必须设置。为配置文件路径。
+ "progressName": "mysqld.exe",// string, 进程名称。如果为单线程可不设置。
+ "isOn": true // boolean, 是否启动守护进程
+ },
+ "config": { // object, 配置文件。里面的配置可以任意填写,和cmds配合使用。
+ },
+ "cmds": {},// object, 命令集。
+ "install": { // object, 安装配置。
+ "envs": [],// object[], 环境变量。
+ "cmds": []// object[], 启动命令。
+ },
+ "start": {// object, 启动配置。
+ "envs": [],// object[], 环境变量。
+ "cmds": []// object[], 启动命令。
+ }
+}
+```
+store.json的struct为
+```json
+type StoreInfo struct {
+ Name string `json:"name"` // 应用程序的名称。
+ Icon string `json:"icon"` // 应用程序的图标路径。
+ Setting Setting `json:"setting"` // 应用程序的配置信息。
+ Config map[string]any `json:"config"` // 应用程序的配置信息映射。
+ Cmds map[string][]Cmd `json:"cmds"` // 应用程序的命令集合。
+ Install Install `json:"install"` // 安装应用程序的信息。
+ Start Install `json:"start"` // 启动应用程序的信息。
+}
+```
+Setting的结构体为:
+```json
+// 包含应用程序的二进制文件路径、配置文件路径等关键设置信息。
+type Setting struct {
+ BinPath string `json:"binPath"` // 应用程序二进制文件的路径。
+ ConfPath string `json:"confPath"` // 应用程序配置文件的路径。
+ ProgressName string `json:"progressName"` // 进程的名称。
+ IsOn bool `json:"isOn"` //是否守护进程运行。
+}
+```
+Cmd的结构体为:
+```json
+type Cmd struct {
+ Name string `json:"name"` // 命令的名称。
+ FilePath string `json:"filePath,omitempty"` // 命令文件的路径。
+ Content string `json:"content,omitempty"` // 命令的内容。
+ BinPath string `json:"binPath,omitempty"` // 执行命令的二进制文件路径。
+ TplPath string `json:"tplPath,omitempty"` // 命令的模板路径。
+ Cmds []string `json:"cmds,omitempty"` // 要执行的子命令列表。
+ Waiting int `json:"waiting"` // 等待的时间。
+ Kill bool `json:"kill"` // 标志位,表示是否需要终止之前的命令。如果setting中设置了progressName会优先杀死整个进程
+ Envs []Item `json:"envs"` // 命令执行时的环境变量。
+}
+```
+Install的struct为:
+```json
+// Install 描述了安装过程中的环境变量和命令列表。
+type Install struct {
+ Envs []Item `json:"envs"` // 安装过程中需要的环境变量。
+ Cmds []string `json:"cmds"` // 安装过程中需要执行的命令列表。
+}
+// Item 是一个通用的键值对结构体,用于表示配置项或环境变量等。
+type Item struct {
+ Name string `json:"name"` // 配置项的名称。
+ Value any `json:"value"` // 配置项的值。
+}
+```
+
+3. 在应用商店添加应用,选择本地添加,输入应用的目录名字(不需要填写整个目录)。
+
+### 配置文件`store.json`说明
+
+1. `install`和`start`配置可以调用`cmds`里的命令。
+2. `cmds`里的命令也可以调用自身的命令。
+3. 所有的命令都可以串联使用。
+
+### 如何设置配置
+1. 在应用目录下建立`static`目录,并创建`index.html`文件。`install.json`中`setting`设置为`true`。
+前端配置样列
+```js
+const postData = {
+dataDir: dataDir,// 此处对应store.json中的config配置项
+logDir: logDir,// 此处对应store.json中的config配置项
+port: port, // 此处对应store.json中的config配置项
+name: "mysql5.7",// 应用名称
+cmdKey: "setting"// 命令键,cmds的name
+ }
+const comp = await fetch('http://localhost:56780/store/setting', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(postData)
+});
+```
+关键要设置好`name`和`cmdKey`,`cmdKey`对应`store.json`中的`cmds`数组配置项中的`name`,一个数组中又可以配置一系列的命令,可以参考Cmd的结构体
+
+### 内置应用说明
+- 系列封装了一些用于处理进程控制和文件操作的功能函数,以下是各函数的详细描述:
+1. `start` 启动应用
+2. `stop` 停止应用
+3. `restart` 重启应用
+4. `exec` 执行命令 必须设置binPath和cmds
+5. `writeFile` 写入文件 必须设置filePath和content,以config为依据,对content进行替换
+6. `changeFile` 修改文件 必须设置filePath和tplPath,以config为依据,在模板文件中进行替换
+7. `deleteFile` 删除文件
+8. `unzip` 解压文件 必须设置filePath和content,content为解压目录
+9. `zip` 压缩文件 必须设置filePath和content,filePath为将要压缩的文件夹,content为压缩后的文件名
+10. `mkdir` 创建文件夹 必须设置FilePath,FilePath为创建的文件夹路径
diff --git a/frontend/components.d.ts b/frontend/components.d.ts
index 3eb1fa7..baeb150 100644
--- a/frontend/components.d.ts
+++ b/frontend/components.d.ts
@@ -44,6 +44,7 @@ declare module 'vue' {
ElCard: typeof import('element-plus/es')['ElCard']
ElCol: typeof import('element-plus/es')['ElCol']
ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
+ ElDialog: typeof import('element-plus/es')['ElDialog']
ElDrawer: typeof import('element-plus/es')['ElDrawer']
ElEmpty: typeof import('element-plus/es')['ElEmpty']
ElForm: typeof import('element-plus/es')['ElForm']
diff --git a/frontend/src/assets/store.json b/frontend/src/assets/store.json
index 325920d..f4be2b9 100644
--- a/frontend/src/assets/store.json
+++ b/frontend/src/assets/store.json
@@ -4,19 +4,12 @@
"url": "https://gitee.com/ruitao_admin/godoos-mysql5.7/releases/download/mysql5.7/mysql5.7.zip",
"needDownload": true,
"needInstall": true,
+ "isDev":true,
"version": "5.7.37",
"icon": "https://img0.baidu.com/it/u=1087008535,3349487719&fm=253&fmt=auto&app=138&f=PNG?w=969&h=500",
"checkProgress": true,
"hasRestart": true,
- "setting": [
- {
- "path": "setting.html",
- "lang":{
- "cn-zh": "设置",
- "en": "Setting"
- }
- }
- ]
+ "setting": true
},
{
"name": "nginx",
diff --git a/frontend/src/assets/store.scss b/frontend/src/assets/store.scss
index e819416..e0de62b 100644
--- a/frontend/src/assets/store.scss
+++ b/frontend/src/assets/store.scss
@@ -221,6 +221,7 @@
display: flex;
flex-direction: row;
flex-wrap: wrap;
+ gap: 10px;
justify-content: flex-start;
align-items: flex-start;
padding: 10px;
diff --git a/frontend/src/components/store/AddApp.vue b/frontend/src/components/store/AddApp.vue
index 72279b6..e643520 100644
--- a/frontend/src/components/store/AddApp.vue
+++ b/frontend/src/components/store/AddApp.vue
@@ -1,11 +1,17 @@
diff --git a/frontend/src/components/store/Store.vue b/frontend/src/components/store/Store.vue
index 59870b0..56641cc 100644
--- a/frontend/src/components/store/Store.vue
+++ b/frontend/src/components/store/Store.vue
@@ -24,15 +24,17 @@ onMounted(async () => {
async function getList() {
if (currentCate.value == 'add') return;
storeList.value = storeInitList
+
// const apiUrl = getSystemKey("apiUrl");
// const storeUrl = apiUrl + '/store/storelist?cate=' + currentCate.value
- // fetch(storeUrl).then(res => {
- // res.json().then(data => {
- // storeList.value = data
- // })
- // }).catch(() => {
- // notifyError(t("store.errorList"))
- // })
+ // const res = await fetch(storeUrl)
+ // if (!res.ok) {
+ // notifyError(t('store.getStoreListError'))
+ // return
+ // }
+ // const data = await res.json()
+ // //console.log(data)
+ // storeList.value = data
await checkProgress()
isready.value = true;
}
@@ -41,8 +43,8 @@ async function checkProgress() {
if (!completion.ok) {
return
}
- let res:any = await completion.json()
- if(!res || res.length < 1){
+ let res: any = await completion.json()
+ if (!res || res.length < 1) {
res = []
}
storeList.value.forEach((item: any, index: number) => {
@@ -50,7 +52,7 @@ async function checkProgress() {
console.log(pitem)
if (pitem) {
storeList.value[index].isRuning = pitem.running
- }else{
+ } else {
storeList.value[index].isRuning = false
}
})
@@ -69,7 +71,7 @@ function setCache() {
}
async function install(item: any) {
- if (item.needDownload) {
+ if (item.needDownload && !item.isDev) {
await download(item)
}
if (item.needInstall) {
@@ -84,8 +86,11 @@ async function install(item: any) {
notifyError(res.message)
return
}
+ if(res.data){
+ item.icon = res.data
+ }
}
- if(item.isOut){
+ if (item.isOut) {
item.progress = 0
const completion = await fetch(apiUrl + '/store/installOut?url=' + item.url)
if (!completion.ok) {
@@ -98,10 +103,10 @@ async function install(item: any) {
return
}
}
- if (item.isWeb) {
+ if (item.webUrl) {
sys.fs.writeFile(
`${sys._options.userLocation}Desktop/${item.name}.url`,
- `link::url::${item.url}::${item.icon}`
+ `link::url::${item.webUrl}::${item.icon}`
);
}
@@ -165,26 +170,26 @@ async function download(item: any) {
}
}
async function pauseApp(item: any) {
- const res:any = await fetch(apiUrl + '/store/stop/' + item.name)
- if(!res.ok){
+ const res: any = await fetch(apiUrl + '/store/stop/' + item.name)
+ if (!res.ok) {
const msg = await res.text()
notifyError(msg)
return
}
- setTimeout(async() => {
+ setTimeout(async () => {
await checkProgress()
}, 1000)
-
+
}
async function restartApp(item: any) {
await fetch(apiUrl + '/store/restart/' + item.name)
- setTimeout(async() => {
+ setTimeout(async () => {
await checkProgress()
}, 1000)
}
async function startApp(item: any) {
await fetch(apiUrl + '/store/start/' + item.name)
- setTimeout(async() => {
+ setTimeout(async () => {
await checkProgress()
}, 1000)
}
@@ -204,25 +209,10 @@ async function startApp(item: any) {
-
{{ currentTitle }}
-
0 {
+ cmd = exec.Command(storeInfo.Setting.BinPath, storeInfo.Start.Cmds...)
+ } else {
+ cmd = exec.Command(storeInfo.Setting.BinPath)
+ }
+ if runtime.GOOS == "windows" {
+ // 在Windows上,通过设置CreationFlags来隐藏窗口
+ cmd = SetHideConsoleCursor(cmd)
+ }
+ if err := cmd.Start(); err != nil {
+ //log.Println("Error starting script:", err)
+ return fmt.Errorf("error starting script: %v", err)
+ }
+
+ // 启动脚本命令并返回可能的错误
+ go func(cmd *exec.Cmd) {
+ RegisterProcess(storeInfo.Name, storeInfo.Setting.ProgressName, storeInfo.Setting.IsOn, cmd)
+ if err := cmd.Wait(); err != nil {
+ log.Println("Error starting script:", err)
+ return
+ }
+
+ }(cmd)
+
+ return nil
+}
+func runRestart(storeInfo StoreInfo) error {
+ err := runStop(storeInfo)
+ if err != nil {
+ return fmt.Errorf("failed to stop process %s: %w", storeInfo.Name, err)
+ }
+ return runStart(storeInfo)
+}
+func runExec(storeInfo StoreInfo, cmdParam Cmd) error {
+ err := SetEnvs(cmdParam.Envs)
+ if err != nil {
+ return fmt.Errorf("failed to set start environment variable %s: %w", storeInfo.Name, err)
+ }
+ log.Printf("bin path:%v", cmdParam.BinPath)
+ log.Printf("cmds:%v", cmdParam.Cmds)
+ cmd := exec.Command(cmdParam.BinPath, cmdParam.Cmds...)
+ if runtime.GOOS == "windows" {
+ // 在Windows上,通过设置CreationFlags来隐藏窗口
+ cmd = SetHideConsoleCursor(cmd)
+ }
+ if err := cmd.Start(); err != nil {
+ return fmt.Errorf("failed to run exec process %s: %w", storeInfo.Name, err)
+ }
+ go func() {
+ if cmdParam.Waiting > 0 {
+ if err = cmd.Wait(); err != nil {
+ log.Printf("failed to wait exec process %s: %s", storeInfo.Name, err.Error())
+ return
+ }
+ time.Sleep(time.Second * time.Duration(cmdParam.Waiting))
+ }
+ if cmdParam.Kill {
+ if storeInfo.Setting.ProgressName != "" {
+ if err := KillProcessByName(storeInfo.Setting.ProgressName); err != nil {
+ log.Printf("failed to kill process %s: %s", storeInfo.Setting.ProgressName, err.Error())
+ }
+ } else {
+ if err := cmd.Process.Kill(); err != nil {
+ log.Printf("failed to kill process %s: %s", storeInfo.Name, err.Error())
+ return
+ }
+ }
+
+ }
+ //log.Printf("run exec process %s, name is %s", storeInfo.Name, cmdParam.Name)
+ }()
+
+ return nil
+}
+func WriteFile(cmd Cmd) error {
+ if cmd.FilePath != "" {
+ content := cmd.Content
+ if content != "" {
+ err := os.WriteFile(cmd.FilePath, []byte(content), 0644)
+ if err != nil {
+ return fmt.Errorf("failed to write to file: %w", err)
+ }
+ }
+ }
+ return nil
+}
+func ChangeFile(storeInfo StoreInfo, cmd Cmd) error {
+ if cmd.FilePath != "" && cmd.TplPath != "" && libs.PathExists(cmd.TplPath) && libs.PathExists(cmd.FilePath) {
+ content, err := os.ReadFile(cmd.TplPath)
+ if err != nil {
+ return fmt.Errorf("failed to read tpl file: %w", err)
+ }
+ exePath := GetExePath(storeInfo.Name)
+ contentstr := strings.ReplaceAll(string(content), "{exePath}", exePath)
+ contentstr = ChangeConfig(storeInfo, contentstr)
+ if err := os.WriteFile(cmd.FilePath, []byte(contentstr), 0644); err != nil {
+ return fmt.Errorf("failed to write to file: %w", err)
+ }
+ }
+ return nil
+}
+func DeleteFile(cmd Cmd) error {
+ if cmd.FilePath != "" {
+ err := os.Remove(cmd.FilePath)
+ if err != nil {
+ return fmt.Errorf("failed to delete file: %w", err)
+ }
+ }
+ return nil
+}
+func MkDir(cmd Cmd) error {
+ err := os.MkdirAll(cmd.FilePath, os.ModePerm)
+ if err != nil {
+ return fmt.Errorf("failed to make dir: %w", err)
+ }
+ return nil
+}
+func Unzip(cmd Cmd) error {
+ return files.Decompress(cmd.FilePath, cmd.Content)
+}
+func Zip(cmd Cmd) error {
+ return files.Encompress(cmd.FilePath, cmd.Content)
+}
diff --git a/godo/store/install.go b/godo/store/install.go
index 13dcb06..758af35 100644
--- a/godo/store/install.go
+++ b/godo/store/install.go
@@ -3,79 +3,49 @@ package store
import (
"encoding/json"
"fmt"
- "godo/files"
"godo/libs"
"log"
"net/http"
"os"
- "os/exec"
"path/filepath"
"regexp"
- "runtime"
"strings"
- "time"
)
-type StoreInfo struct {
- Name string `json:"name"`
- URL string `json:"url"`
- NeedDownload bool `json:"needDownload"`
- Icon string `json:"icon"`
- Setting Setting `json:"setting"`
- Config map[string]any `json:"config"`
- Cmds map[string][]Cmd `json:"cmds"`
- Install Install `json:"install"`
- Start Install `json:"start"`
-}
-type Setting struct {
- BinPath string `json:"binPath"`
- ConfPath string `json:"confPath"`
- DataDir string `json:"dataDir"`
- LogDir string `json:"logDir"`
-}
-
-type Item struct {
- Name string `json:"name"`
- Value any `json:"value"`
-}
-type Cmd struct {
- Name string `json:"name"`
- FilePath string `json:"filePath,omitempty"`
- Content string `json:"content,omitempty"`
- BinPath string `json:"binPath,omitempty"`
- Cmds []string `json:"cmds,omitempty"`
- Waiting int `json:"waiting"`
- Kill bool `json:"kill"`
- Envs []Item `json:"envs"`
-}
-
-type Install struct {
- Envs []Item `json:"envs"`
- Cmds []string `json:"cmds"`
-}
-
func InstallHandler(w http.ResponseWriter, r *http.Request) {
pluginName := r.URL.Query().Get("name")
if pluginName == "" {
libs.ErrorMsg(w, "the app name is empty!")
return
}
- exeDir := libs.GetRunDir()
- exePath := filepath.Join(exeDir, pluginName)
- log.Printf("the app path is %s", exePath)
+ exePath := GetExePath(pluginName)
+ //log.Printf("the app path is %s", exePath)
if !libs.PathExists(exePath) {
libs.ErrorMsg(w, "the app path is not exists!")
return
}
- installFile := filepath.Join(exePath, "install.json")
- if !libs.PathExists(installFile) {
- libs.ErrorMsg(w, "the install.json is not exists!")
+ installInfo, err := GetInstallInfo(pluginName)
+ if err != nil {
+ libs.ErrorMsg(w, "the install.json is error:"+err.Error())
+ return
+ }
+ if pluginName != installInfo.Name {
+ libs.ErrorMsg(w, "the app name must equal the install.json!")
+ return
+ }
+ if !installInfo.NeedInstall {
+ libs.SuccessMsg(w, "success", "the app is installed!")
+ return
+ }
+ storeFile := filepath.Join(exePath, "store.json")
+ if !libs.PathExists(storeFile) {
+ libs.ErrorMsg(w, "the store.json is not exists!")
return
}
var storeInfo StoreInfo
- content, err := os.ReadFile(installFile)
+ content, err := os.ReadFile(storeFile)
if err != nil {
- libs.ErrorMsg(w, "cant read the install.json!")
+ libs.ErrorMsg(w, "cant read the store.json!")
return
}
//设置 info.json
@@ -85,17 +55,16 @@ func InstallHandler(w http.ResponseWriter, r *http.Request) {
err = json.Unmarshal(contentBytes, &storeInfo)
if err != nil {
log.Printf("Error during unmarshal: %v", err)
- libs.ErrorMsg(w, "the install.json is error: "+err.Error())
+ libs.ErrorMsg(w, "the store.json is error: "+err.Error())
return
}
replacePlaceholdersInCmds(&storeInfo)
- infoFile := filepath.Join(exePath, "info.json")
- err = SaveStoreInfo(storeInfo, infoFile)
+ storeInfo.Name = installInfo.Name
+ err = SaveInfoFile(storeInfo)
if err != nil {
- libs.ErrorMsg(w, "save the info.json is error!")
+ libs.ErrorMsg(w, "the store info.json is error: "+err.Error())
return
}
-
//设置config
if libs.PathExists(storeInfo.Setting.ConfPath + ".tpl") {
err = SaveStoreConfig(storeInfo, exePath)
@@ -109,20 +78,27 @@ func InstallHandler(w http.ResponseWriter, r *http.Request) {
libs.ErrorMsg(w, "install the app is error!")
return
}
+ var res string
//复制static目录
staticPath := filepath.Join(exePath, "static")
if libs.PathExists(staticPath) {
staticDir := libs.GetStaticDir()
targetPath := filepath.Join(staticDir, pluginName)
- err = os.Rename(staticPath, targetPath)
- if err != nil {
- // libs.ErrorMsg(w, "copy the static is error!")
- // return
- log.Printf("copy the static is error! %v", err)
+ if !libs.PathExists(targetPath) {
+ err = os.Rename(staticPath, targetPath)
+ if err != nil {
+ log.Printf("copy the static is error! %v", err)
+ }
+ iconPath := filepath.Join(targetPath, storeInfo.Icon)
+ if libs.PathExists(iconPath) {
+ res = "http://localhost:56780/static/" + pluginName + "/" + storeInfo.Icon
+ }
}
+
}
- libs.SuccessMsg(w, "success", "install the app success!")
+ libs.SuccessMsg(w, res, "install the app success!")
}
+
func UnInstallHandler(w http.ResponseWriter, r *http.Request) {
pluginName := r.URL.Query().Get("name")
if pluginName == "" {
@@ -136,8 +112,16 @@ func UnInstallHandler(w http.ResponseWriter, r *http.Request) {
// return
log.Printf("stop the app is error! %s", err)
}
- exeDir := libs.GetRunDir()
- exePath := filepath.Join(exeDir, pluginName)
+ installInfo, err := GetInstallInfo(pluginName)
+ if err != nil {
+ libs.ErrorMsg(w, "the install.json is error:"+err.Error())
+ return
+ }
+ if installInfo.IsDev {
+ libs.SuccessMsg(w, "success", "uninstall the app success!")
+ return
+ }
+ exePath := GetExePath(pluginName)
//log.Printf("the app path is %s", exePath)
if libs.PathExists(exePath) {
err := os.RemoveAll(exePath)
@@ -158,6 +142,28 @@ func UnInstallHandler(w http.ResponseWriter, r *http.Request) {
libs.SuccessMsg(w, "success", "uninstall the app success!")
}
+func GetInstallInfo(pluginName string) (InstallInfo, error) {
+ var installInfo InstallInfo
+ exePath := GetExePath(pluginName)
+ installFile := filepath.Join(exePath, "install.json")
+ if !libs.PathExists(installFile) {
+ return installInfo, fmt.Errorf("install.json is not exist:%s", installFile)
+ }
+ content, err := os.ReadFile(installFile)
+ if err != nil {
+ return installInfo, err
+ }
+ err = json.Unmarshal(content, &installInfo)
+ if err != nil {
+ return installInfo, err
+ }
+ return installInfo, nil
+}
+func GetExePath(pluginName string) string {
+ exeDir := libs.GetRunDir()
+ return filepath.Join(exeDir, pluginName)
+}
+
func convertValueToString(value any) string {
var strValue string
switch v := value.(type) {
@@ -187,157 +193,69 @@ func InstallStore(storeInfo StoreInfo) error {
}
return nil
}
-func RunCmds(storeInfo StoreInfo, cmdKey string) error {
- cmds := storeInfo.Cmds[cmdKey]
- for _, cmd := range cmds {
-
- if cmd.Name == "stop" {
- runStop(storeInfo)
- }
- if cmd.Name == "start" {
- runStart(storeInfo)
- }
- if cmd.Name == "restart" {
- runRestart(storeInfo)
- }
- if cmd.Name == "exec" {
- runExec(storeInfo, cmd)
- }
- if cmd.Name == "writeFile" {
- err := WriteFile(cmd)
- if err != nil {
- return fmt.Errorf("failed to write to file: %w", err)
- }
- }
- if cmd.Name == "deleteFile" {
- err := DeleteFile(cmd)
- if err != nil {
- return fmt.Errorf("failed to delete file: %w", err)
- }
- }
- if cmd.Name == "unzip" {
- err := Unzip(cmd)
- if err != nil {
- return fmt.Errorf("failed to unzip file: %w", err)
- }
- }
- if cmd.Name == "zip" {
- err := Zip(cmd)
- if err != nil {
- return fmt.Errorf("failed to unzip file: %w", err)
- }
- }
- if cmd.Name == "mkdir" {
- err := MkDir(cmd)
- if err != nil {
- return fmt.Errorf("failed to create directory: %w", err)
- }
+func RunCmd(storeInfo StoreInfo, cmd Cmd) error {
+ if cmd.Name == "stop" {
+ runStop(storeInfo)
+ }
+ if cmd.Name == "start" {
+ runStart(storeInfo)
+ }
+ if cmd.Name == "restart" {
+ runRestart(storeInfo)
+ }
+ if cmd.Name == "exec" {
+ runExec(storeInfo, cmd)
+ }
+ if cmd.Name == "writeFile" {
+ err := WriteFile(cmd)
+ if err != nil {
+ return fmt.Errorf("failed to write to file: %w", err)
}
}
- return nil
-}
-func runStop(storeInfo StoreInfo) error {
- return StopCmd(storeInfo.Name)
-}
-func runStart(storeInfo StoreInfo) error {
- err := SetEnvs(storeInfo.Start.Envs)
- if err != nil {
- return fmt.Errorf("failed to set start environment variable %s: %w", storeInfo.Name, err)
- }
- if !libs.PathExists(storeInfo.Setting.BinPath) {
- return fmt.Errorf("script file %s does not exist", storeInfo.Setting.BinPath)
- }
- var cmd *exec.Cmd
- if len(storeInfo.Start.Cmds) > 0 {
- cmd = exec.Command(storeInfo.Setting.BinPath, storeInfo.Start.Cmds...)
- } else {
- cmd = exec.Command(storeInfo.Setting.BinPath)
- }
- if runtime.GOOS == "windows" {
- // 在Windows上,通过设置CreationFlags来隐藏窗口
- cmd = SetHideConsoleCursor(cmd)
- }
- // 启动脚本命令并返回可能的错误
- go func(cmd *exec.Cmd) {
- if err := cmd.Start(); err != nil {
- log.Println("Error starting script:", err)
- return
- }
- RegisterProcess(storeInfo.Name, cmd)
- }(cmd)
-
- return nil
-}
-func runRestart(storeInfo StoreInfo) error {
- err := runStop(storeInfo)
- if err != nil {
- return fmt.Errorf("failed to stop process %s: %w", storeInfo.Name, err)
- }
- return runStart(storeInfo)
-}
-func runExec(storeInfo StoreInfo, cmdParam Cmd) error {
- err := SetEnvs(cmdParam.Envs)
- if err != nil {
- return fmt.Errorf("failed to set start environment variable %s: %w", storeInfo.Name, err)
- }
- log.Printf("bin path:%v", cmdParam.BinPath)
- log.Printf("cmds:%v", cmdParam.Cmds)
- cmd := exec.Command(cmdParam.BinPath, cmdParam.Cmds...)
- if runtime.GOOS == "windows" {
- // 在Windows上,通过设置CreationFlags来隐藏窗口
- cmd = SetHideConsoleCursor(cmd)
- }
- if err := cmd.Start(); err != nil {
- return fmt.Errorf("failed to run exec process %s: %w", storeInfo.Name, err)
- }
- if cmdParam.Waiting > 0 {
- if err = cmd.Wait(); err != nil {
- return fmt.Errorf("failed to wait for exec process %s: %w", storeInfo.Name, err)
- }
- time.Sleep(time.Second * time.Duration(cmdParam.Waiting))
- }
- if cmdParam.Kill {
- if err := cmd.Process.Kill(); err != nil {
- return fmt.Errorf("failed to kill process: %v", err)
+ if cmd.Name == "changeFile" {
+ err := ChangeFile(storeInfo, cmd)
+ if err != nil {
+ return fmt.Errorf("failed to change to file: %w", err)
}
}
- log.Printf("run exec process %s, name is %s", storeInfo.Name, cmdParam.Name)
- return nil
-}
-func WriteFile(cmd Cmd) error {
- if cmd.FilePath != "" {
- content := cmd.Content
- if content != "" {
- err := os.WriteFile(cmd.FilePath, []byte(content), 0644)
- if err != nil {
- return fmt.Errorf("failed to write to file: %w", err)
- }
- }
- }
- return nil
-}
-func DeleteFile(cmd Cmd) error {
- if cmd.FilePath != "" {
- err := os.Remove(cmd.FilePath)
+ if cmd.Name == "deleteFile" {
+ err := DeleteFile(cmd)
if err != nil {
return fmt.Errorf("failed to delete file: %w", err)
}
}
- return nil
-}
-func MkDir(cmd Cmd) error {
- err := os.MkdirAll(cmd.FilePath, os.ModePerm)
- if err != nil {
- return fmt.Errorf("failed to make dir: %w", err)
+ if cmd.Name == "unzip" {
+ err := Unzip(cmd)
+ if err != nil {
+ return fmt.Errorf("failed to unzip file: %w", err)
+ }
+ }
+ if cmd.Name == "zip" {
+ err := Zip(cmd)
+ if err != nil {
+ return fmt.Errorf("failed to unzip file: %w", err)
+ }
+ }
+ if cmd.Name == "mkdir" {
+ err := MkDir(cmd)
+ if err != nil {
+ return fmt.Errorf("failed to create directory: %w", err)
+ }
}
return nil
}
-func Unzip(cmd Cmd) error {
- return files.Decompress(cmd.FilePath, cmd.Content)
-}
-func Zip(cmd Cmd) error {
- return files.Encompress(cmd.FilePath, cmd.Content)
+func RunCmds(storeInfo StoreInfo, cmdKey string) error {
+ cmds := storeInfo.Cmds[cmdKey]
+ for _, cmd := range cmds {
+ if _, ok := storeInfo.Cmds[cmd.Name]; ok {
+ RunCmds(storeInfo, cmd.Name)
+ } else {
+ RunCmd(storeInfo, cmd)
+ }
+ }
+ return nil
}
+
func SetEnvs(envs []Item) error {
if len(envs) > 0 {
for _, item := range envs {
@@ -352,6 +270,11 @@ func SetEnvs(envs []Item) error {
}
return nil
}
+func SaveInfoFile(storeInfo StoreInfo) error {
+ exePath := GetExePath(storeInfo.Name)
+ infoFile := filepath.Join(exePath, "info.json")
+ return SaveStoreInfo(storeInfo, infoFile)
+}
func SaveStoreInfo(storeInfo StoreInfo, infoFile string) error {
// 使用 json.MarshalIndent 直接获取内容的字节切片
content, err := json.MarshalIndent(storeInfo, "", " ")
diff --git a/godo/store/progress.go b/godo/store/progress.go
index 3c672ae..0391aa6 100644
--- a/godo/store/progress.go
+++ b/godo/store/progress.go
@@ -10,12 +10,15 @@ import (
)
type Process struct {
- Name string `json:"name"`
- Running bool `json:"running"`
- ExitCode int `json:"exitCode"`
- Cmd *exec.Cmd `json:"cmd"`
- Waiting bool `json:"waiting"`
- LastPing time.Time `json:"lastPing"`
+ Name string `json:"name"`
+ Running bool `json:"running"`
+ ExitCode int `json:"exitCode"`
+ Cmd *exec.Cmd `json:"cmd"`
+ Pid int `json:"pid"`
+ ProgressName string `json:"progressName"`
+ Waiting bool `json:"waiting"`
+ IsOn bool `json:"isOn"`
+ LastPing time.Time `json:"lastPing"`
}
var (
@@ -26,13 +29,16 @@ var (
func respondWithError(w http.ResponseWriter, code int, message string) {
http.Error(w, message, code)
}
-func RegisterProcess(name string, cmdstr *exec.Cmd) {
+func RegisterProcess(name string, progressName string, isOn bool, cmdstr *exec.Cmd) {
processesMu.Lock()
defer processesMu.Unlock()
processes[name] = &Process{
- Name: name,
- Running: true,
- Cmd: cmdstr,
+ Name: name,
+ Running: true,
+ Cmd: cmdstr,
+ Pid: cmdstr.Process.Pid,
+ IsOn: isOn,
+ ProgressName: progressName,
}
}
func GetCmd(name string) *Process {
@@ -42,20 +48,9 @@ func GetCmd(name string) *Process {
return processes[name]
}
func Status(w http.ResponseWriter, r *http.Request) {
- // processesMu.RLock()
- // defer processesMu.RUnlock()
-
var ps []Process
for name, cmd := range processes {
- // if cmd.Cmd.ProcessState != nil && cmd.Cmd.ProcessState.Exited() {
- // cmd.Running = false
- // // 进程已经退出
- // ps = append(ps, Process{Name: name, Running: false, Waiting: cmd.Waiting, LastPing: cmd.LastPing, ExitCode: cmd.Cmd.ProcessState.ExitCode()})
- // } else {
- // // 进程仍在运行
- // ps = append(ps, Process{Name: name, Running: true, Waiting: cmd.Waiting, LastPing: cmd.LastPing})
- // }
- ps = append(ps, Process{Name: name, Running: cmd.Running, Waiting: cmd.Waiting, LastPing: cmd.LastPing, ExitCode: cmd.Cmd.ProcessState.ExitCode()})
+ ps = append(ps, Process{Name: name, Running: cmd.Running, Waiting: cmd.Waiting, Pid: cmd.Pid, LastPing: cmd.LastPing, ExitCode: cmd.Cmd.ProcessState.ExitCode()})
}
jsonBytes, err := json.MarshalIndent(ps, "", " ")
diff --git a/godo/store/start.go b/godo/store/start.go
index ef4b80a..0f9609c 100644
--- a/godo/store/start.go
+++ b/godo/store/start.go
@@ -1,12 +1,9 @@
package store
import (
- "encoding/json"
"fmt"
"godo/libs"
- "log"
"net/http"
- "os"
"os/exec"
"path/filepath"
@@ -49,27 +46,6 @@ func StartAll(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "All processes started.")
}
-func GetStoreInfo(name string) (StoreInfo, error) {
- var storeInfo StoreInfo
- exeDir := libs.GetRunDir()
- infoPath := filepath.Join(exeDir, name, "info.json")
- if !libs.PathExists(infoPath) {
- return storeInfo, fmt.Errorf("process information for '%s' not found", name)
- }
-
- content, err := os.ReadFile(infoPath)
- if err != nil {
- return storeInfo, fmt.Errorf("failed to read info.json: %v", err)
- }
- if err := json.Unmarshal(content, &storeInfo); err != nil {
- return storeInfo, fmt.Errorf("failed to unmarshal info.json: %v", err)
- }
- scriptPath := storeInfo.Setting.BinPath
- if !libs.PathExists(scriptPath) {
- return storeInfo, fmt.Errorf("script file '%s' not found", scriptPath)
- }
- return storeInfo, nil
-}
// ExecuteScript 执行指定名称的脚本。
// 参数:
@@ -77,42 +53,14 @@ func GetStoreInfo(name string) (StoreInfo, error) {
// 返回值:
// 返回可能遇到的错误,如果执行成功,则返回nil。
func ExecuteScript(name string) error {
- cmd, exists := processes[name]
- if !exists {
- log.Printf("process information for '%s' not found", name)
- //return fmt.Errorf("process information for '%s' not found", name)
- storeInfo, err := GetStoreInfo(name)
- if err != nil {
- return err
- }
- err = runStart(storeInfo)
- if err != nil {
- return fmt.Errorf("failed to run script: %v", err)
- }
- return nil
-
+ storeInfo, err := GetStoreInfo(name)
+ if err != nil {
+ return err
}
- if cmd.Running {
- return nil
+ err = runStart(storeInfo)
+ if err != nil {
+ return fmt.Errorf("failed to run script: %v", err)
}
-
- // if err := cmd.Cmd.Start(); err != nil {
- // log.Printf("failed to start process %s: %v", name, err)
- // return nil
- // }
- processes[name].Running = true
- go func() {
-
- if err := cmd.Cmd.Start(); err != nil {
- log.Printf("failed to start process %s: %v", name, err)
- return
- }
- // processesMu.Lock()
- // defer processesMu.Unlock()
-
- processes[name].Cmd = cmd.Cmd
- }()
-
return nil
}
func RunOutHandler(w http.ResponseWriter, r *http.Request) {
diff --git a/godo/store/stop.go b/godo/store/stop.go
index 4ddc2c7..2986233 100644
--- a/godo/store/stop.go
+++ b/godo/store/stop.go
@@ -4,6 +4,10 @@ import (
"fmt"
"log"
"net/http"
+ "os/exec"
+ "runtime"
+ "strconv"
+ "strings"
"time"
"github.com/gorilla/mux"
@@ -11,6 +15,7 @@ import (
)
func KillByPid(pid int) error {
+ log.Println("Killing process with PID:", pid)
p, err := process.NewProcess(int32(pid))
if err != nil {
return fmt.Errorf("failed to create process object: %w", err)
@@ -23,15 +28,25 @@ func KillByPid(pid int) error {
return nil
}
func StopCmd(name string) error {
+ processesMu.Lock()
+ defer processesMu.Unlock()
cmd, ok := processes[name]
if !ok {
return fmt.Errorf("Process not found")
}
- processes[name].Running = false
- err := KillByPid(cmd.Cmd.Process.Pid)
- if err != nil {
- return fmt.Errorf("failed to kill process: %w", err)
+ //processes[name].Running = false
+ if cmd.ProgressName != "" {
+ err := KillProcessByName(cmd.ProgressName)
+ if err != nil {
+ return fmt.Errorf("failed to kill process: %w", err)
+ }
+ } else {
+ err := KillByPid(cmd.Pid)
+ if err != nil {
+ return fmt.Errorf("failed to kill process: %w", err)
+ }
}
+ delete(processes, name)
return nil
}
func StopProcess(w http.ResponseWriter, r *http.Request) {
@@ -54,7 +69,8 @@ func StopAllHandler() error {
if err := KillByPid(cmd.Cmd.Process.Pid); err != nil {
return fmt.Errorf("failed to stop process %s: %v", name, err)
}
- processes[name].Running = false
+ //processes[name].Running = false
+ delete(processes, name)
}
return nil
}
@@ -75,15 +91,15 @@ func ReStartProcess(w http.ResponseWriter, r *http.Request) {
name := vars["name"]
// Stop the process first
- if cmd, ok := processes[name]; ok {
- if err := KillByPid(cmd.Cmd.Process.Pid); err != nil {
+ if _, ok := processes[name]; ok {
+ if err := StopCmd(name); err != nil {
// respondWithError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to stop process %s before restart: %v", name, err))
// return
log.Printf("Failed to stop process %s before restart: %v", name, err)
}
// processesMu.Lock()
// defer processesMu.Unlock()
- processes[name].Running = false
+ //processes[name].Running = false
} else {
respondWithError(w, http.StatusNotFound, fmt.Sprintf("Process %s not found to restart", name))
return
@@ -98,3 +114,82 @@ func ReStartProcess(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "Process %s restarted.", name)
}
+func killByPids(pids []int) error {
+ for _, pid := range pids {
+ if err := KillByPid(pid); err != nil {
+ return fmt.Errorf("failed to kill process with PID %d: %w", pid, err)
+ }
+ fmt.Printf("Killed process with PID %d\n", pid)
+ }
+ return nil
+}
+
+// KillProcessByName 终止所有与给定名称匹配的进程
+func KillProcessByName(processName string) error {
+ switch runtime.GOOS {
+ case "windows":
+ // Windows 系统下的实现
+ pids, err := findPidsWindows(processName)
+ if err != nil {
+ return err
+ }
+ err = killByPids(pids)
+ if err != nil {
+ return err
+ }
+ default:
+ // Unix/Linux 系统下的实现
+ pids, err := findPidsUnix(processName)
+ if err != nil {
+ return err
+ }
+ err = killByPids(pids)
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// findPidsUnix 在 Unix/Linux 系统下查找具有指定名称的进程的 PID
+func findPidsUnix(name string) ([]int, error) {
+ cmd := exec.Command("pgrep", "-f", name)
+ output, err := cmd.Output()
+ if err != nil {
+ return nil, fmt.Errorf("failed to execute pgrep: %w", err)
+ }
+ pids := strings.Fields(string(output))
+ result := make([]int, len(pids))
+ for i, pidStr := range pids {
+ pid, err := strconv.Atoi(pidStr)
+ if err != nil {
+ return nil, fmt.Errorf("failed to convert PID to integer: %w", err)
+ }
+ result[i] = pid
+ }
+ return result, nil
+}
+
+// findPidsWindows 在 Windows 系统下查找具有指定名称的进程的 PID
+func findPidsWindows(name string) ([]int, error) {
+ cmd := exec.Command("tasklist")
+ output, err := cmd.Output()
+ if err != nil {
+ return nil, fmt.Errorf("failed to execute tasklist: %w", err)
+ }
+ lines := strings.Split(string(output), "\r\n")
+ var pids []int
+ for _, line := range lines {
+ fields := strings.Fields(line)
+ if len(fields) >= 3 {
+ if strings.Contains(strings.ToLower(fields[0]), strings.ToLower(name)) {
+ pid, err := strconv.Atoi(fields[1])
+ if err != nil {
+ return nil, fmt.Errorf("failed to convert PID to integer: %w", err)
+ }
+ pids = append(pids, pid)
+ }
+ }
+ }
+ return pids, nil
+}
diff --git a/godo/store/store.go b/godo/store/store.go
index d0ae65a..d5188c4 100644
--- a/godo/store/store.go
+++ b/godo/store/store.go
@@ -6,10 +6,13 @@ import (
"godo/libs"
"io"
"net/http"
+ "os"
+ "path/filepath"
"runtime"
+ "strings"
)
-func GetStoreInfoHandler(w http.ResponseWriter, r *http.Request) {
+func GetStoreListHandler(w http.ResponseWriter, r *http.Request) {
cate := r.URL.Query().Get("cate")
os := runtime.GOOS
arch := runtime.GOARCH
@@ -39,3 +42,76 @@ func GetStoreInfoHandler(w http.ResponseWriter, r *http.Request) {
}
}
+func StoreSettingHandler(w http.ResponseWriter, r *http.Request) {
+ var req map[string]any
+ err := json.NewDecoder(r.Body).Decode(&req)
+ if err != nil {
+ libs.ErrorMsg(w, err.Error())
+ return
+ }
+ if req["name"] == nil {
+ libs.ErrorMsg(w, "name is required")
+ return
+ }
+ if req["cmdKey"] == nil {
+ libs.ErrorMsg(w, "cmdKey is required")
+ return
+ }
+ name := req["name"].(string)
+ cmdKey := req["cmdKey"].(string)
+ // 替换字符串类型的值中的反斜杠
+ for key, value := range req {
+ if strValue, ok := value.(string); ok {
+ req[key] = strings.ReplaceAll(strValue, "\\", "/")
+ }
+ }
+ storeInfo, err := GetStoreInfo(name)
+ if err != nil {
+ libs.ErrorMsg(w, err.Error())
+ return
+ }
+ // 更新storeInfo.Config
+ for k, v := range req {
+ if k != "name" && k != "cmdKey" {
+ storeInfo.Config[k] = v // 如果k存在,则更新;如果不存在,则新增
+ }
+ }
+ err = SaveInfoFile(storeInfo)
+ if err != nil {
+ libs.ErrorMsg(w, "the store info.json is error: "+err.Error())
+ return
+ }
+ _, ok := storeInfo.Cmds[cmdKey]
+ if !ok {
+ libs.ErrorMsg(w, "cmdKey is not found")
+ return
+ }
+ err = RunCmds(storeInfo, cmdKey)
+ if err != nil {
+ libs.ErrorMsg(w, err.Error())
+ return
+ }
+ libs.SuccessMsg(w, "success", "success")
+
+}
+func GetStoreInfo(name string) (StoreInfo, error) {
+ var storeInfo StoreInfo
+ exeDir := libs.GetRunDir()
+ infoPath := filepath.Join(exeDir, name, "info.json")
+ if !libs.PathExists(infoPath) {
+ return storeInfo, fmt.Errorf("process information for '%s' not found", name)
+ }
+
+ content, err := os.ReadFile(infoPath)
+ if err != nil {
+ return storeInfo, fmt.Errorf("failed to read info.json: %v", err)
+ }
+ if err := json.Unmarshal(content, &storeInfo); err != nil {
+ return storeInfo, fmt.Errorf("failed to unmarshal info.json: %v", err)
+ }
+ scriptPath := storeInfo.Setting.BinPath
+ if !libs.PathExists(scriptPath) {
+ return storeInfo, fmt.Errorf("script file '%s' not found", scriptPath)
+ }
+ return storeInfo, nil
+}
diff --git a/godo/store/types.go b/godo/store/types.go
new file mode 100644
index 0000000..bb05c31
--- /dev/null
+++ b/godo/store/types.go
@@ -0,0 +1,65 @@
+package store
+
+// InstallInfo 描述了应用程序的安装信息。
+// 包含应用程序的名称、下载地址、版本号等关键安装信息。
+type InstallInfo struct {
+ Name string `json:"name"` // 应用程序名称。
+ URL string `json:"url"` // 应用程序下载地址。
+ WebUrl string `json:"webUrl"` // 应用程序的网页地址。
+ IsDev bool `json:"isDev"` // 标志位,表示是否为开发者版本。
+ NeedDownload bool `json:"needDownload"` // 标志位,表示是否需要下载。
+ NeedInstall bool `json:"needInstall"` // 标志位,表示是否需要安装。
+ Version string `json:"version"` // 应用程序的版本号。
+ Desc string `json:"desc"` // 应用程序的描述信息。
+ Icon string `json:"icon"` // 应用程序的图标路径。
+ CheckProgress bool `json:"checkProgress"` // 标志位,表示是否显示启动和停止。
+ HasRestart bool `json:"hasRestart"` // 标志位,表示安装后是否需要重启。
+ Setting bool `json:"setting"` // 标志位,表示是否需要配置。
+}
+
+// StoreInfo 维护了应用程序商店的信息。
+// 包含应用程序的名称、图标、配置信息等。
+type StoreInfo struct {
+ Name string `json:"name"` // 应用程序商店的名称。
+ Icon string `json:"icon"` // 应用程序商店的图标路径。
+ Setting Setting `json:"setting"` // 应用程序商店的配置信息。
+ Config map[string]any `json:"config"` // 应用程序的配置信息映射。
+ Cmds map[string][]Cmd `json:"cmds"` // 应用程序的命令集合。
+ Install Install `json:"install"` // 安装应用程序的信息。
+ Start Install `json:"start"` // 启动应用程序的信息。
+}
+
+// Setting 描述了应用程序的设置信息。
+// 包含应用程序的二进制文件路径、配置文件路径等关键设置信息。
+type Setting struct {
+ BinPath string `json:"binPath"` // 应用程序二进制文件的路径。
+ ConfPath string `json:"confPath"` // 应用程序配置文件的路径。
+ ProgressName string `json:"progressName"` // 进程的名称。
+ IsOn bool `json:"isOn"` //是否守护进程运行。
+}
+
+// Item 是一个通用的键值对结构体,用于表示配置项或环境变量等。
+type Item struct {
+ Name string `json:"name"` // 配置项的名称。
+ Value any `json:"value"` // 配置项的值。
+}
+
+// Cmd 描述了一个命令的详细信息。
+// 包含命令的名称、执行路径、环境变量等。
+type Cmd struct {
+ Name string `json:"name"` // 命令的名称。
+ FilePath string `json:"filePath,omitempty"` // 命令文件的路径。
+ Content string `json:"content,omitempty"` // 命令的内容。
+ BinPath string `json:"binPath,omitempty"` // 执行命令的二进制文件路径。
+ TplPath string `json:"tplPath,omitempty"` // 命令的模板路径。
+ Cmds []string `json:"cmds,omitempty"` // 要执行的子命令列表。
+ Waiting int `json:"waiting"` // 等待的时间。
+ Kill bool `json:"kill"` // 标志位,表示是否需要终止之前的命令。
+ Envs []Item `json:"envs"` // 命令执行时的环境变量。
+}
+
+// Install 描述了安装过程中的环境变量和命令列表。
+type Install struct {
+ Envs []Item `json:"envs"` // 安装过程中需要的环境变量。
+ Cmds []string `json:"cmds"` // 安装过程中需要执行的命令列表。
+}