From a517d4a2db2d0e89e864161420f6280cf9e41f70 Mon Sep 17 00:00:00 2001 From: Wangjunqi123 Date: Tue, 11 Mar 2025 09:47:09 +0800 Subject: [PATCH] initialize repo --- .gitignore | 17 + LICENSE | 194 ++++ cmd/agent/conf/config.go | 59 ++ cmd/agent/conf/meta.go | 15 + cmd/agent/global/debug.go | 19 + cmd/agent/global/file.go | 50 + cmd/agent/global/global.go | 33 + cmd/agent/global/os.go | 25 + cmd/agent/logger/sdkLogger.go | 20 + cmd/agent/logs_agent.yaml.template | 12 + cmd/agent/logtools/journald/client.go | 639 ++++++++++++ cmd/agent/logtools/journald/meta.go | 57 ++ cmd/agent/logtools/logClientsManager.go | 152 +++ cmd/agent/main.go | 66 ++ cmd/agent/resourcemanage/resourcemanage.go | 200 ++++ cmd/agent/signal/signalMonitor.go | 33 + cmd/agent/webserver/engine.go | 35 + cmd/agent/webserver/handle.go | 61 ++ cmd/public/debug.go | 19 + cmd/public/journald_meta.go | 56 ++ cmd/server/conf/config.go | 60 ++ cmd/server/conf/meta.go | 20 + cmd/server/global/IsIPandPORTValid.go | 39 + cmd/server/global/file.go | 50 + cmd/server/global/global.go | 27 + cmd/server/logger/sdkLogger.go | 20 + cmd/server/logs_server.yaml.template | 17 + cmd/server/main.go | 72 ++ cmd/server/pluginclient/meta.go | 24 + cmd/server/pluginclient/pluginClient.go | 125 +++ cmd/server/resourcemanage/resourcemanage.go | 206 ++++ cmd/server/signal/signalMonitor.go | 34 + cmd/server/webserver/engine.go | 129 +++ .../webserver/frontendResource/static.go | 35 + .../webserver/frontendResource/staticPro.go | 54 + cmd/server/webserver/handle.go | 123 +++ cmd/server/webserver/middleware/logger.go | 65 ++ cmd/server/webserver/proxy/TcpHijackProxy.go | 86 ++ cmd/server/webserver/proxy/forwardProxy.go | 475 +++++++++ cmd/server/webserver/proxy/meta.go | 14 + cmd/server/webserver/proxy/wsproxymanager.go | 72 ++ go.mod | 42 + go.sum | 107 ++ scripts/PilotGo-plugin-logs-agent.service | 13 + scripts/PilotGo-plugin-logs-server.service | 13 + scripts/PilotGo-plugin-logs.spec | 88 ++ web/.gitignore | 24 + web/README.md | 9 + web/index.html | 20 + web/package.json | 24 + web/public/vite.svg | 1 + web/src/App.vue | 16 + web/src/api/log.ts | 15 + web/src/api/request.ts | 41 + web/src/assets/vue.svg | 1 + web/src/main.ts | 31 + web/src/stores/log.ts | 31 + web/src/style.scss | 69 ++ web/src/view/LogStream.vue | 386 ++++++++ web/src/view/socket.ts | 175 ++++ web/src/view/utils.ts | 65 ++ web/src/vite-env.d.ts | 8 + web/tsconfig.json | 25 + web/tsconfig.node.json | 11 + web/vite.config.ts | 26 + web/yarn.lock | 930 ++++++++++++++++++ 66 files changed, 5680 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 cmd/agent/conf/config.go create mode 100644 cmd/agent/conf/meta.go create mode 100644 cmd/agent/global/debug.go create mode 100755 cmd/agent/global/file.go create mode 100644 cmd/agent/global/global.go create mode 100644 cmd/agent/global/os.go create mode 100644 cmd/agent/logger/sdkLogger.go create mode 100644 cmd/agent/logs_agent.yaml.template create mode 100644 cmd/agent/logtools/journald/client.go create mode 100644 cmd/agent/logtools/journald/meta.go create mode 100644 cmd/agent/logtools/logClientsManager.go create mode 100644 cmd/agent/main.go create mode 100644 cmd/agent/resourcemanage/resourcemanage.go create mode 100644 cmd/agent/signal/signalMonitor.go create mode 100644 cmd/agent/webserver/engine.go create mode 100644 cmd/agent/webserver/handle.go create mode 100644 cmd/public/debug.go create mode 100644 cmd/public/journald_meta.go create mode 100644 cmd/server/conf/config.go create mode 100644 cmd/server/conf/meta.go create mode 100644 cmd/server/global/IsIPandPORTValid.go create mode 100755 cmd/server/global/file.go create mode 100755 cmd/server/global/global.go create mode 100644 cmd/server/logger/sdkLogger.go create mode 100644 cmd/server/logs_server.yaml.template create mode 100644 cmd/server/main.go create mode 100644 cmd/server/pluginclient/meta.go create mode 100644 cmd/server/pluginclient/pluginClient.go create mode 100644 cmd/server/resourcemanage/resourcemanage.go create mode 100644 cmd/server/signal/signalMonitor.go create mode 100644 cmd/server/webserver/engine.go create mode 100644 cmd/server/webserver/frontendResource/static.go create mode 100644 cmd/server/webserver/frontendResource/staticPro.go create mode 100644 cmd/server/webserver/handle.go create mode 100644 cmd/server/webserver/middleware/logger.go create mode 100644 cmd/server/webserver/proxy/TcpHijackProxy.go create mode 100644 cmd/server/webserver/proxy/forwardProxy.go create mode 100644 cmd/server/webserver/proxy/meta.go create mode 100644 cmd/server/webserver/proxy/wsproxymanager.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 scripts/PilotGo-plugin-logs-agent.service create mode 100644 scripts/PilotGo-plugin-logs-server.service create mode 100644 scripts/PilotGo-plugin-logs.spec create mode 100644 web/.gitignore create mode 100644 web/README.md create mode 100644 web/index.html create mode 100644 web/package.json create mode 100644 web/public/vite.svg create mode 100644 web/src/App.vue create mode 100644 web/src/api/log.ts create mode 100644 web/src/api/request.ts create mode 100644 web/src/assets/vue.svg create mode 100644 web/src/main.ts create mode 100644 web/src/stores/log.ts create mode 100644 web/src/style.scss create mode 100644 web/src/view/LogStream.vue create mode 100644 web/src/view/socket.ts create mode 100644 web/src/view/utils.ts create mode 100644 web/src/vite-env.d.ts create mode 100644 web/tsconfig.json create mode 100644 web/tsconfig.node.json create mode 100644 web/vite.config.ts create mode 100644 web/yarn.lock diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4aba796 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +cmd/logs_server.yaml +cmd/logs_agent.yaml + +build +logs-agent +logs-server +PilotGo-plugin-logs-server +PilotGo-plugin-logs-agent + +cmd/server/webserver/frontendResource/assets/ +cmd/server/webserver/frontendResource/index.html +cmd/server/webserver/frontendResource/vite.svg + +web/node_modules +web/dist/ + +vendor/ \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f6c2697 --- /dev/null +++ b/LICENSE @@ -0,0 +1,194 @@ +木兰宽松许可证,第2版 + +木兰宽松许可证,第2版 + +2020年1月 http://license.coscl.org.cn/MulanPSL2 + +您对“软件”的复制、使用、修改及分发受木兰宽松许可证,第2版(“本许可证”)的如下条款的约束: + +0. 定义 + +“软件” 是指由“贡献”构成的许可在“本许可证”下的程序和相关文档的集合。 + +“贡献” 是指由任一“贡献者”许可在“本许可证”下的受版权法保护的作品。 + +“贡献者” 是指将受版权法保护的作品许可在“本许可证”下的自然人或“法人实体”。 + +“法人实体” 是指提交贡献的机构及其“关联实体”。 + +“关联实体” 是指,对“本许可证”下的行为方而言,控制、受控制或与其共同受控制的机构,此处的控制是 +指有受控方或共同受控方至少50%直接或间接的投票权、资金或其他有价证券。 + +1. 授予版权许可 + +每个“贡献者”根据“本许可证”授予您永久性的、全球性的、免费的、非独占的、不可撤销的版权许可,您可 +以复制、使用、修改、分发其“贡献”,不论修改与否。 + +2. 授予专利许可 + +每个“贡献者”根据“本许可证”授予您永久性的、全球性的、免费的、非独占的、不可撤销的(根据本条规定 +撤销除外)专利许可,供您制造、委托制造、使用、许诺销售、销售、进口其“贡献”或以其他方式转移其“贡 +献”。前述专利许可仅限于“贡献者”现在或将来拥有或控制的其“贡献”本身或其“贡献”与许可“贡献”时的“软 +件”结合而将必然会侵犯的专利权利要求,不包括对“贡献”的修改或包含“贡献”的其他结合。如果您或您的“ +关联实体”直接或间接地,就“软件”或其中的“贡献”对任何人发起专利侵权诉讼(包括反诉或交叉诉讼)或 +其他专利维权行动,指控其侵犯专利权,则“本许可证”授予您对“软件”的专利许可自您提起诉讼或发起维权 +行动之日终止。 + +3. 无商标许可 + +“本许可证”不提供对“贡献者”的商品名称、商标、服务标志或产品名称的商标许可,但您为满足第4条规定 +的声明义务而必须使用除外。 + +4. 分发限制 + +您可以在任何媒介中将“软件”以源程序形式或可执行形式重新分发,不论修改与否,但您必须向接收者提供“ +本许可证”的副本,并保留“软件”中的版权、商标、专利及免责声明。 + +5. 免责声明与责任限制 + +“软件”及其中的“贡献”在提供时不带任何明示或默示的担保。在任何情况下,“贡献者”或版权所有者不对 +任何人因使用“软件”或其中的“贡献”而引发的任何直接或间接损失承担责任,不论因何种原因导致或者基于 +何种法律理论,即使其曾被建议有此种损失的可能性。 + +6. 语言 + +“本许可证”以中英文双语表述,中英文版本具有同等法律效力。如果中英文版本存在任何冲突不一致,以中文 +版为准。 + +条款结束 + +如何将木兰宽松许可证,第2版,应用到您的软件 + +如果您希望将木兰宽松许可证,第2版,应用到您的新软件,为了方便接收者查阅,建议您完成如下三步: + +1, 请您补充如下声明中的空白,包括软件名、软件的首次发表年份以及您作为版权人的名字; + +2, 请您在软件包的一级目录下创建以“LICENSE”为名的文件,将整个许可证文本放入该文件中; + +3, 请将如下声明文本放入每个源文件的头部注释中。 + +Copyright (c) [Year] [name of copyright holder] +[Software Name] is licensed under Mulan PSL v2. +You can use this software according to the terms and conditions of the Mulan +PSL v2. +You may obtain a copy of Mulan PSL v2 at: + http://license.coscl.org.cn/MulanPSL2 +THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY +KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. +See the Mulan PSL v2 for more details. + +Mulan Permissive Software License,Version 2 + +Mulan Permissive Software License,Version 2 (Mulan PSL v2) + +January 2020 http://license.coscl.org.cn/MulanPSL2 + +Your reproduction, use, modification and distribution of the Software shall +be subject to Mulan PSL v2 (this License) with the following terms and +conditions: + +0. Definition + +Software means the program and related documents which are licensed under +this License and comprise all Contribution(s). + +Contribution means the copyrightable work licensed by a particular +Contributor under this License. + +Contributor means the Individual or Legal Entity who licenses its +copyrightable work under this License. + +Legal Entity means the entity making a Contribution and all its +Affiliates. + +Affiliates means entities that control, are controlled by, or are under +common control with the acting entity under this License, ‘control’ means +direct or indirect ownership of at least fifty percent (50%) of the voting +power, capital or other securities of controlled or commonly controlled +entity. + +1. Grant of Copyright License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to you a perpetual, worldwide, royalty-free, non-exclusive, +irrevocable copyright license to reproduce, use, modify, or distribute its +Contribution, with modification or not. + +2. Grant of Patent License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to you a perpetual, worldwide, royalty-free, non-exclusive, +irrevocable (except for revocation under this Section) patent license to +make, have made, use, offer for sale, sell, import or otherwise transfer its +Contribution, where such patent license is only limited to the patent claims +owned or controlled by such Contributor now or in future which will be +necessarily infringed by its Contribution alone, or by combination of the +Contribution with the Software to which the Contribution was contributed. +The patent license shall not apply to any modification of the Contribution, +and any other combination which includes the Contribution. If you or your +Affiliates directly or indirectly institute patent litigation (including a +cross claim or counterclaim in a litigation) or other patent enforcement +activities against any individual or entity by alleging that the Software or +any Contribution in it infringes patents, then any patent license granted to +you under this License for the Software shall terminate as of the date such +litigation or activity is filed or taken. + +3. No Trademark License + +No trademark license is granted to use the trade names, trademarks, service +marks, or product names of Contributor, except as required to fulfill notice +requirements in section 4. + +4. Distribution Restriction + +You may distribute the Software in any medium with or without modification, +whether in source or executable forms, provided that you provide recipients +with a copy of this License and retain copyright, patent, trademark and +disclaimer statements in the Software. + +5. Disclaimer of Warranty and Limitation of Liability + +THE SOFTWARE AND CONTRIBUTION IN IT ARE PROVIDED WITHOUT WARRANTIES OF ANY +KIND, EITHER EXPRESS OR IMPLIED. IN NO EVENT SHALL ANY CONTRIBUTOR OR +COPYRIGHT HOLDER BE LIABLE TO YOU FOR ANY DAMAGES, INCLUDING, BUT NOT +LIMITED TO ANY DIRECT, OR INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES ARISING +FROM YOUR USE OR INABILITY TO USE THE SOFTWARE OR THE CONTRIBUTION IN IT, NO +MATTER HOW IT’S CAUSED OR BASED ON WHICH LEGAL THEORY, EVEN IF ADVISED OF +THE POSSIBILITY OF SUCH DAMAGES. + +6. Language + +THIS LICENSE IS WRITTEN IN BOTH CHINESE AND ENGLISH, AND THE CHINESE VERSION +AND ENGLISH VERSION SHALL HAVE THE SAME LEGAL EFFECT. IN THE CASE OF +DIVERGENCE BETWEEN THE CHINESE AND ENGLISH VERSIONS, THE CHINESE VERSION +SHALL PREVAIL. + +END OF THE TERMS AND CONDITIONS + +How to Apply the Mulan Permissive Software License,Version 2 +(Mulan PSL v2) to Your Software + +To apply the Mulan PSL v2 to your work, for easy identification by +recipients, you are suggested to complete following three steps: + +i. Fill in the blanks in following statement, including insert your software +name, the year of the first publication of your software, and your name +identified as the copyright owner; + +ii. Create a file named "LICENSE" which contains the whole context of this +License in the first directory of your software package; + +iii. Attach the statement to the appropriate annotated syntax at the +beginning of each source file. + +Copyright (c) [Year] [name of copyright holder] +[Software Name] is licensed under Mulan PSL v2. +You can use this software according to the terms and conditions of the Mulan +PSL v2. +You may obtain a copy of Mulan PSL v2 at: + http://license.coscl.org.cn/MulanPSL2 +THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY +KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. +See the Mulan PSL v2 for more details. diff --git a/cmd/agent/conf/config.go b/cmd/agent/conf/config.go new file mode 100644 index 0000000..564ecc5 --- /dev/null +++ b/cmd/agent/conf/config.go @@ -0,0 +1,59 @@ +/* + * Copyright (c) KylinSoft Co., Ltd. 2024.All rights reserved. + * PilotGo-plugin-logs licensed under the Mulan Permissive Software License, Version 2. + * See LICENSE file for more details. + * Author: Wangjunqi123 + * Date: Mon Dec 16 08:43:58 2024 +0800 + */ +package conf + +import ( + "flag" + "fmt" + "os" + "path" + + "gitee.com/openeuler/PilotGo-plugin-logs/cmd/agent/global" + "gitee.com/openeuler/PilotGo/sdk/logger" + "gopkg.in/yaml.v2" +) + +var Global_Config *ServerConfig + +const config_type = "logs_agent.yaml" + +var config_dir string + +type ServerConfig struct { + Logs *LogsConf + Logopts *logger.LogOpts `yaml:"log"` +} + +func ConfigFile() string { + configfilepath := path.Join(config_dir, config_type) + + return configfilepath +} + +func InitConfig() { + flag.StringVar(&config_dir, "conf", "./", "logs plugin configuration directory") + flag.Usage = func() { + fmt.Fprintf(os.Stderr, "Usage: %s -conf /path/to/logs-agent.yaml(default:./) \n", os.Args[0]) + } + flag.Parse() + + bytes, err := global.FileReadBytes(ConfigFile()) + if err != nil { + flag.Usage() + fmt.Printf("open file failed: %s, %s\n", ConfigFile(), err.Error()) + os.Exit(1) + } + + Global_Config = &ServerConfig{} + + err = yaml.Unmarshal(bytes, Global_Config) + if err != nil { + fmt.Printf("yaml unmarshal failed: %s\n", err.Error()) + os.Exit(1) + } +} diff --git a/cmd/agent/conf/meta.go b/cmd/agent/conf/meta.go new file mode 100644 index 0000000..6631e4b --- /dev/null +++ b/cmd/agent/conf/meta.go @@ -0,0 +1,15 @@ +/* + * Copyright (c) KylinSoft Co., Ltd. 2024.All rights reserved. + * PilotGo-plugin-logs licensed under the Mulan Permissive Software License, Version 2. + * See LICENSE file for more details. + * Author: Wangjunqi123 + * Date: Mon Dec 16 08:43:58 2024 +0800 + */ +package conf + +type LogsConf struct { + Https_enabled bool `yaml:"https_enabled"` + CertFile string `yaml:"cert_file"` + KeyFile string `yaml:"key_file"` + Addr string `yaml:"server_listen_addr"` +} diff --git a/cmd/agent/global/debug.go b/cmd/agent/global/debug.go new file mode 100644 index 0000000..762aefc --- /dev/null +++ b/cmd/agent/global/debug.go @@ -0,0 +1,19 @@ +/* + * Copyright (c) KylinSoft Co., Ltd. 2024.All rights reserved. + * PilotGo-plugin-logs licensed under the Mulan Permissive Software License, Version 2. + * See LICENSE file for more details. + * Author: Wangjunqi123 + * Date: Mon Dec 16 08:43:58 2024 +0800 + */ +package global + +import ( + "fmt" + "runtime" +) + +func DebugStackTrace() { + var buf [4096]byte + n := runtime.Stack(buf[:], false) + fmt.Printf("StackTrace (length: %d)\n%s\n", n, buf[:n]) +} diff --git a/cmd/agent/global/file.go b/cmd/agent/global/file.go new file mode 100755 index 0000000..84e63db --- /dev/null +++ b/cmd/agent/global/file.go @@ -0,0 +1,50 @@ +/* + * Copyright (c) KylinSoft Co., Ltd. 2024.All rights reserved. + * PilotGo-plugin-logs licensed under the Mulan Permissive Software License, Version 2. + * See LICENSE file for more details. + * Author: Wangjunqi123 + * Date: Mon Dec 16 08:43:58 2024 +0800 + */ +package global + +import ( + "io" + "os" + + "github.com/pkg/errors" +) + +func FileReadString(filePath string) (string, error) { + content, err := os.ReadFile(filePath) + if err != nil { + return "", errors.New(err.Error()) + } + + return string(content), nil +} + +func FileReadBytes(filePath string) ([]byte, error) { + f, err := os.Open(filePath) + if err != nil { + return nil, errors.New(err.Error()) + } + defer f.Close() + + var content []byte + readbuff := make([]byte, 1024*4) + for { + n, err := f.Read(readbuff) + if err != nil { + if err == io.EOF { + if n != 0 { + content = append(content, readbuff[:n]...) + } + break + } + return nil, errors.New(err.Error()) + } + content = append(content, readbuff[:n]...) + } + + return content, nil +} diff --git a/cmd/agent/global/global.go b/cmd/agent/global/global.go new file mode 100644 index 0000000..d53e346 --- /dev/null +++ b/cmd/agent/global/global.go @@ -0,0 +1,33 @@ +/* + * Copyright (c) KylinSoft Co., Ltd. 2024.All rights reserved. + * PilotGo-plugin-logs licensed under the Mulan Permissive Software License, Version 2. + * See LICENSE file for more details. + * Author: Wangjunqi123 + * Date: Mon Dec 16 08:43:58 2024 +0800 + */ +package global + +import ( + "context" + "time" + + "gitee.com/openeuler/PilotGo-plugin-logs/cmd/agent/resourcemanage" +) + +var ( + RootCtx = context.Background() +) + +var ERManager *resourcemanage.ErrorReleaseManagement + +const ( + ReadCmdStderrTimeout = 5 * time.Second + + HeartbeatPeriod = 5 * time.Second // 日志采集组件状态检测周期 +) + +var OsName string + +func init() { + +} diff --git a/cmd/agent/global/os.go b/cmd/agent/global/os.go new file mode 100644 index 0000000..0475913 --- /dev/null +++ b/cmd/agent/global/os.go @@ -0,0 +1,25 @@ +/* + * Copyright (c) KylinSoft Co., Ltd. 2024.All rights reserved. + * PilotGo-plugin-logs licensed under the Mulan Permissive Software License, Version 2. + * See LICENSE file for more details. + * Author: Wangjunqi123 + * Date: Mon Dec 16 08:43:58 2024 +0800 + */ +package global + +import ( + "strings" + + "github.com/pkg/errors" +) + +func InitOSName() { + contents, err := FileReadString("/etc/system-release") + if err != nil { + ERManager.ErrorTransmit("global", "error", errors.Errorf("fail to init os name: %s", err.Error()), true, false) + } + OsName = strings.Split(contents, " ")[0] + if OsName != "openEuler" && OsName != "Kylin" { + ERManager.ErrorTransmit("global", "error", errors.Errorf("unsupport os version: %s", OsName), true, false) + } +} diff --git a/cmd/agent/logger/sdkLogger.go b/cmd/agent/logger/sdkLogger.go new file mode 100644 index 0000000..ac251b4 --- /dev/null +++ b/cmd/agent/logger/sdkLogger.go @@ -0,0 +1,20 @@ +/* + * Copyright (c) KylinSoft Co., Ltd. 2024.All rights reserved. + * PilotGo-plugin-logs licensed under the Mulan Permissive Software License, Version 2. + * See LICENSE file for more details. + * Author: Wangjunqi123 + * Date: Mon Dec 16 08:43:58 2024 +0800 + */ +package logger + +import ( + "gitee.com/openeuler/PilotGo-plugin-logs/cmd/agent/conf" + "gitee.com/openeuler/PilotGo/sdk/logger" +) + +func InitLogger() { + err := logger.Init(conf.Global_Config.Logopts) + if err != nil { + logger.Fatal("%s", err.Error()) + } +} diff --git a/cmd/agent/logs_agent.yaml.template b/cmd/agent/logs_agent.yaml.template new file mode 100644 index 0000000..d3bc939 --- /dev/null +++ b/cmd/agent/logs_agent.yaml.template @@ -0,0 +1,12 @@ +logs: + https_enabled: false + cert_file: "" + key_file: "" +# 插件服务端服务器监听地址 + server_listen_addr: "0.0.0.0:9995" +log: + level: debug + driver: file # 可选stdout和file。stdout:输出到终端控制台;file:输出到path下的指定文件。 + path: /opt/PilotGo/plugin/logs/agent/log/logs-agent.log + max_file: 1 + max_size: 10485760 diff --git a/cmd/agent/logtools/journald/client.go b/cmd/agent/logtools/journald/client.go new file mode 100644 index 0000000..b61cde7 --- /dev/null +++ b/cmd/agent/logtools/journald/client.go @@ -0,0 +1,639 @@ +/* + * Copyright (c) KylinSoft Co., Ltd. 2024.All rights reserved. + * PilotGo-plugin-logs licensed under the Mulan Permissive Software License, Version 2. + * See LICENSE file for more details. + * Author: Wangjunqi123 + * Date: Mon Dec 16 08:43:58 2024 +0800 + */ +package journald + +import ( + "bufio" + "context" + "encoding/json" + "fmt" + "io" + "os/exec" + "os/user" + "sort" + "strconv" + "strings" + "sync" + "time" + + "github.com/pkg/errors" + + "gitee.com/openeuler/PilotGo-plugin-logs/cmd/agent/global" + "gitee.com/openeuler/PilotGo-plugin-logs/cmd/public" + "github.com/gorilla/websocket" +) + +var ( + FollowLogDefaultOptions = []string{"--quiet", "--utc", "--output=json"} + + UnitListDefaultOptions = []string{"list-units", "--no-legend", "--type=service", "--no-pager"} +) + +type JournaldClient struct { + ID string + + Active bool + + wswriteMutex sync.Mutex + wsreadMutex sync.Mutex + + wsconn *websocket.Conn + + defaultOptions []string + options *public.JournalctlOptions + + CancelC context.Context + CancelF context.CancelFunc + + Jcmd *exec.Cmd + + wg sync.WaitGroup + once sync.Once + + dataCh chan *public.StdoutData + errCh chan []byte + CloseReadMsgCh chan struct{} + closeWriteMsgCh chan struct{} + + // 打印stderr信息时的超时控制 + timeout time.Duration + + UnitsMap map[string][]string + + PageEntryBuff PageEntryBuffSortByTimestamp +} + +func CreateJournaldClient(_conn *websocket.Conn, _timeout time.Duration) *JournaldClient { + cancelCtx, cancelFunc := context.WithCancel(JournaldCtx) + return &JournaldClient{ + wsconn: _conn, + defaultOptions: FollowLogDefaultOptions, + options: nil, + CancelC: cancelCtx, + CancelF: cancelFunc, + Jcmd: nil, + dataCh: make(chan *public.StdoutData, 10), + errCh: make(chan []byte, 10), + CloseReadMsgCh: make(chan struct{}, 10), + closeWriteMsgCh: make(chan struct{}, 10), + timeout: _timeout, + UnitsMap: make(map[string][]string), + } +} + +func (jclient *JournaldClient) ReadMessageFromClient() { +OuterLoop: + for { + select { + case <-jclient.CloseReadMsgCh: + global.ERManager.ErrorTransmit("journald", "warn", errors.New("jclient.ReadMessageFromClient() exit: cancelctx canceled"), false, false) + return + default: + jclient.wsreadMutex.Lock() + msgType, jmsgBytes, err := jclient.wsconn.ReadMessage() + if err != nil { + if websocket.IsCloseError(err, websocket.CloseAbnormalClosure, websocket.CloseNormalClosure) { + if jclient.Active { + global.ERManager.ErrorTransmit("journald", "error", errors.Errorf("websocket client %s closed: %s", jclient.wsconn.RemoteAddr().String(), err.Error()), false, false) + jclient.Close(true, false, false) + } + return + } + if jclient.Active { + global.ERManager.ErrorTransmit("journald", "error", errors.Errorf("error while reading message(msgType: %d): %s, %s", msgType, err.Error(), jmsgBytes), false, false) + jclient.Close(true, false, false) + } + return + } + jclient.wsreadMutex.Unlock() + + jmsg := &public.JMessage{} + if err := json.Unmarshal(jmsgBytes, jmsg); jmsgBytes != nil && err != nil { + global.ERManager.ErrorTransmit("journald", "error", errors.Errorf("error while unmarshalling json options: %s, jmsg: %+v", err.Error(), string(jmsgBytes)), false, true) + jclient.Close(true, false, false) + return + } + + jclient.once.Do(func() { + global.ERManager.ErrorTransmit("journald", "debug", errors.Errorf("jmsg.type: %+v, jmsg.joptions:%+v, jmsg.data: %+v", jmsg.Type, jmsg.JOptions, jmsg.Data), false, false) + }) + + switch jmsg.Type { + case public.UpdateOptionsMsg: + // TODO: 连续发送相同的查询请求暂时跳过 + if jclient.options == jmsg.JOptions { + time.Sleep(1 * time.Second) + continue OuterLoop + } + + if jclient.Jcmd != nil { + global.ERManager.ErrorTransmit("journald", "info", errors.Errorf("==========%-50s==========", "reset journalctl options"), false, false) + global.ERManager.ErrorTransmit("journald", "info", errors.Errorf("jmsg.type: %+v, jmsg.joptions:%+v, jmsg.data: %+v", jmsg.Type, jmsg.JOptions, jmsg.Data), false, false) + // 释放上一次查询的资源 + jclient.Close(false, false, false) + } + + cancelCtx, cancelFunc := context.WithCancel(JournaldCtx) + jclient.CancelC = cancelCtx + jclient.CancelF = cancelFunc + jclient.options = jmsg.JOptions + jclient.PageEntryBuff = nil + jclient.Jcmd = exec.Command("journalctl", jclient.assembleOptions(jclient.defaultOptions, jmsg.JOptions)...) + go jclient.WriteMessageToClient() + jclient.ProcessData(jclient.Jcmd, public.LogEntryData) + case public.UnitListMsg: + cmd := exec.Command("systemctl", UnitListDefaultOptions...) + go jclient.WriteMessageToClient() + jclient.ProcessData(cmd, public.UnitData) + case public.UpdatePageMsg: + if len(jclient.PageEntryBuff) == 0 { + continue OuterLoop + } + jclient.options.From = jmsg.JOptions.From + jclient.options.Size = jmsg.JOptions.Size + start_index, end_index := jclient.options.From, jclient.options.From+jclient.options.Size + if len(jclient.PageEntryBuff) <= end_index { + end_index = len(jclient.PageEntryBuff) + } + if len(jclient.PageEntryBuff) <= start_index { + start_index = len(jclient.PageEntryBuff) + } + + jclient.dataCh <- &public.StdoutData{ + Type: public.LogEntryData, + Data: strings.Join(jclient.PageEntryBuff[start_index:end_index], "\n"), + } + default: + global.ERManager.ErrorTransmit("journald", "error", errors.Errorf("unsupport message type: %+v", jmsg), false, false) + } + } + } +} + +func (jclient *JournaldClient) ProcessData(_cmd *exec.Cmd, _data_type public.StdoutDataType) { + cmd_stdout, err := _cmd.StdoutPipe() + if err != nil { + global.ERManager.ErrorTransmit("journald", "error", errors.Errorf("cannot get stdout pipe: %s", err), false, false) + jclient.Close(true, false, false) + return + } + cmd_stderr, err := _cmd.StderrPipe() + if err != nil { + global.ERManager.ErrorTransmit("journald", "error", errors.Errorf("cannot get stderr pipe: %s", err), false, false) + jclient.Close(true, false, false) + return + } + + jclient.wg.Add(1) + go jclient.readFromStderr(cmd_stderr) + jclient.wg.Add(1) + go jclient.readFromStdout(_data_type, cmd_stdout) + global.ERManager.Wg.Add(1) + go func(__cmd *exec.Cmd) { + defer global.ERManager.Wg.Done() + __cmd.WaitDelay = time.Second * 2 + err = __cmd.Run() + if err != nil { + global.ERManager.ErrorTransmit("journald", "error", errors.Errorf("err while running cmd: %d, %s", __cmd.ProcessState.ExitCode(), err.Error()), false, false) + // 主动kill journalctl process,exitcode: -1, 不释放资源 + if __cmd.ProcessState.ExitCode() != -1 { + jclient.Close(false, false, true) + return + } + } + // 分页查询模式下command执行完成时不调用cancelFunc + // if jclient.options == nil || !jclient.options.Notail { + // jclient.Close(false, false, false) + // } + }(_cmd) +} + +func (jclient *JournaldClient) WriteMessageToClient() { + for { + select { + case <-jclient.closeWriteMsgCh: + global.ERManager.ErrorTransmit("journald", "warn", errors.New("cancelctx canceled, jclient.WriteMessageToClient() exit"), false, false) + return + case data, open := <-jclient.dataCh: + if !open { + global.ERManager.ErrorTransmit("journald", "warn", errors.New("journalctl stdout pipe closed"), false, false) + jclient.Close(true, false, false) + return + } + + var err error + jdata := &public.StdoutData{} + switch data.Type { + // 日志条目查询 + case public.LogEntryData: + jdata.Type = public.LogEntryData + if jclient.options.Notail { + // 分页查询 + var raw_page_entries []string + var ok bool + + buffdata, ok := data.Data.(string) + if !ok { + global.ERManager.ErrorTransmit("journald", "error", errors.Errorf("fail to assert page enties json to string: %+v(%T)", data.Data, data.Data), false, true) + jclient.Close(true, true, false) + return + } + + if buffdata != "abnormal" { + if len(jclient.PageEntryBuff) == 0 { + jclient.PageEntryBuff = strings.Split(buffdata, "\n") + jclient.PageEntryBuff = jclient.PageEntryBuff[:len(jclient.PageEntryBuff)-1] + sort.Sort(jclient.PageEntryBuff) + start_index, end_index := jclient.options.From, jclient.options.Size + if len(jclient.PageEntryBuff) <= jclient.options.Size { + end_index = len(jclient.PageEntryBuff) + } + raw_page_entries = jclient.PageEntryBuff[start_index:end_index] + } else { + raw_page_entries = strings.Split(buffdata, "\n") + if !ok { + global.ERManager.ErrorTransmit("journald", "error", errors.Errorf("fail to assert page enties json to []string: %+v(%T)", data.Data, data.Data), false, true) + jclient.Close(true, true, false) + return + } + } + + page_entries := make([]map[string]interface{}, 0) + for _, entry_json := range raw_page_entries { + if len(entry_json) == 0 { + continue + } + raw_entry := map[string]interface{}{} + if err := json.Unmarshal([]byte(entry_json), &raw_entry); err != nil { + global.ERManager.ErrorTransmit("journald", "error", errors.Errorf("fail to unmarshal Journald JSON: %s; raw data: %+v(%d)", err, entry_json, len(entry_json)), false, true) + jclient.Close(true, true, false) + return + } + page_entries = append(page_entries, jclient.generateEntry(raw_entry)) + } + + jdata.Data = &public.PageData{ + Total: len(jclient.PageEntryBuff), + Hits: page_entries, + } + } else { + jdata.Data = nil + } + } else { + // 实时查询 + raw_entry := map[string]interface{}{} + jsondata, ok := data.Data.(string) + if !ok { + global.ERManager.ErrorTransmit("journald", "error", errors.Errorf("fail to assert follow mode json to string: %+v(%T)", data.Data, data.Data), false, true) + jclient.Close(true, true, false) + return + } + if jsondata != "abnormal" { + if err := json.Unmarshal([]byte(jsondata), &raw_entry); err != nil { + global.ERManager.ErrorTransmit("journald", "error", errors.Errorf("fail to unmarshal Journald JSON: %s; raw data: %s(%d)", err, data.Data.(string), len(data.Data.(string))), false, true) + jclient.Close(true, true, false) + return + } + jdata.Data = jclient.generateEntry(raw_entry) + } else { + jdata.Data = nil + } + } + // 服务单元查询 + case public.UnitData: + jdata.Type = public.UnitData + if data.Data.(string) != "abnormal" { + systemd_units_raw := strings.Split(data.Data.(string), "\n") + if err := jclient.generateUnitsMap(systemd_units_raw); err != nil { + global.ERManager.ErrorTransmit("journald", "error", errors.Wrap(err, " "), false, true) + } + jdata.Data = jclient.UnitsMap + } else { + jdata.Data = nil + } + } + + jmsg := &public.JMessage{ + Type: public.DataMsg, + Data: jdata, + } + jmsgBytes, err := json.Marshal(jmsg) + if err != nil { + global.ERManager.ErrorTransmit("journald", "error", errors.Errorf("fail to marshal message: %s", err.Error()), false, true) + } + + if jclient.wsconn == nil { + break + } + + jclient.wswriteMutex.Lock() + if err := jclient.wsconn.WriteMessage(websocket.TextMessage, jmsgBytes); err != nil { + global.ERManager.ErrorTransmit("journald", "error", errors.Errorf("error while writing message to ws client: %s", err.Error()), false, true) + } + jclient.wswriteMutex.Unlock() + } + } +} + +func (jclient *JournaldClient) generateEntry(_raw_entry map[string]interface{}) map[string]interface{} { + entry := map[string]interface{}{} + if _raw_entry["__REALTIME_TIMESTAMP"].(string) != "" { + timestamp_int64, err := strconv.ParseInt(_raw_entry["__REALTIME_TIMESTAMP"].(string), 10, 64) + if err != nil { + global.ERManager.ErrorTransmit("journald", "error", errors.Errorf("fail to parse timestamp %s: %s", _raw_entry["__REALTIME_TIMESTAMP"].(string), err.Error()), false, true) + entry["timestamp"] = _raw_entry["__REALTIME_TIMESTAMP"].(string)[:13] + } + entry["timestamp"] = strconv.Itoa(int(timestamp_int64 / 1000)) + } + if _raw_entry["PRIORITY"] != nil && _raw_entry["PRIORITY"].(string) != "" { + entry["level"] = _raw_entry["PRIORITY"].(string) + } + if _raw_entry["MESSAGE"].(string) != "" { + entry["message"] = _raw_entry["MESSAGE"].(string) + } + if _raw_entry["_TRANSPORT"].(string) != "" { + switch _raw_entry["_TRANSPORT"].(string) { + case "journal": + if _raw_entry["UNIT"] != nil { + entry["targetname"] = _raw_entry["UNIT"].(string) + } else if _raw_entry["SYSLOG_IDENTIFIER"] != nil { + entry["targetname"] = _raw_entry["SYSLOG_IDENTIFIER"].(string) + } + // TODO: 暂时无法提供syslog日志 + case "syslog", "kernel", "audit": + if _raw_entry["SYSLOG_IDENTIFIER"] != nil { + entry["targetname"] = _raw_entry["SYSLOG_IDENTIFIER"].(string) + } + } + } + return entry +} + +func (jclient *JournaldClient) generateUnitsMap(_systemd_units_raw []string) error { + user_units := []string{} + user_, err := user.Lookup("root") + if err != nil { + return errors.Errorf("fail to get user: %s", err) + } + user_units = append(user_units, fmt.Sprintf("%v:%v", user_.Username, user_.Uid)) + + user_, err = user.Current() + if err != nil { + return errors.Errorf("fail to get user: %s", err) + } + if user_.Username != "root" { + user_units = append(user_units, fmt.Sprintf("%v:%v", user_.Username, user_.Uid)) + } + + jclient.UnitsMap["user"] = user_units + + jclient.UnitsMap["transport"] = []string{"audit", "kernel"} + + systemd_units := []string{} + for _, line := range _systemd_units_raw { + if line != "" { + switch global.OsName { + case "openEuler": + unit_name := "" + line_split_by_space := strings.Split(strings.TrimLeft(line, " "), " ") + for _, e := range line_split_by_space { + if strings.Contains(e, ".service") { + unit_name = strings.Split(e, ".service")[0] + break + } + } + systemd_units = append(systemd_units, unit_name) + case "Kylin": + unit_name := "" + line_split_by_space := strings.Split(line, " ") + for _, e := range line_split_by_space { + if strings.Contains(e, ".service") { + unit_name = strings.Split(e, ".service")[0] + break + } + } + systemd_units = append(systemd_units, unit_name) + } + } + } + jclient.UnitsMap["systemd"] = systemd_units + return nil +} + +func (jclient *JournaldClient) readFromStdout(_type public.StdoutDataType, _stdout io.ReadCloser) { + defer jclient.wg.Done() + defer _stdout.Close() + + reader := bufio.NewReader(_stdout) + for { + select { + case <-jclient.CancelC.Done(): + global.ERManager.ErrorTransmit("journald", "warn", errors.New("jclient.readFromStdout() exit, cancelctx canceled"), false, false) + return + default: + dataT := &public.StdoutData{} + switch _type { + case public.LogEntryData: + dataT.Type = public.LogEntryData + if jclient.options.Notail { + text, err := jclient.readAllOnce(_stdout) + if err != nil { + if strings.Contains(err.Error(), "EOF") { + global.ERManager.ErrorTransmit("journald", "error", errors.Wrap(err, "jclient.readFromStdout() exit: "), false, false) + dataT.Data = "abnormal" + jclient.dataCh <- dataT + return + } + global.ERManager.ErrorTransmit("journald", "error", errors.Wrap(err, "jclient.readFromStdout() exit: "), false, true) + dataT.Data = "abnormal" + jclient.dataCh <- dataT + return + } + dataT.Data = text + } else { + text := jclient.readOneLineOnce(reader) + if text == "" { + dataT.Data = "abnormal" + jclient.dataCh <- dataT + global.ERManager.ErrorTransmit("journald", "debug", errors.New("jclient.readFromStdout() exit: EOF"), false, false) + return + } + dataT.Data = text + } + case public.UnitData: + dataT.Type = public.UnitData + text, err := jclient.readAllOnce(_stdout) + if text == "" && err != nil { + if strings.Contains(err.Error(), "EOF") { + global.ERManager.ErrorTransmit("journald", "error", errors.Wrap(err, "jclient.readFromStdout() exit: "), false, false) + dataT.Data = "abnormal" + jclient.dataCh <- dataT + return + } + global.ERManager.ErrorTransmit("journald", "error", errors.Wrap(err, "jclient.readFromStdout() exit: "), false, true) + dataT.Data = "abnormal" + jclient.dataCh <- dataT + return + } + dataT.Data = text + } + jclient.dataCh <- dataT + } + } +} + +func (jclient *JournaldClient) readOneLineOnce(_reader *bufio.Reader) string { + bytes, err := _reader.ReadBytes('\n') + if err != nil { + return "" + } + + return string(bytes) +} + +func (jclient *JournaldClient) readAllOnce(_reader io.ReadCloser) (string, error) { + bytes, err := io.ReadAll(_reader) + if len(bytes) == 0 { + return "", errors.New("cannot read from cmd stdout: EOF") + } + if err != nil { + return string(bytes), errors.Errorf("cannot read from cmd stdout(bytes: %s): %s", string(bytes), err) + } + return string(bytes), nil +} + +func (jclient *JournaldClient) readFromStderr(_stderr io.ReadCloser) { + defer jclient.wg.Done() + defer _stderr.Close() + reader := bufio.NewReader(_stderr) + for { + select { + case <-jclient.CancelC.Done(): + global.ERManager.ErrorTransmit("journald", "warn", errors.New("jclient.readFromStderr() exit: cancelctx canceled"), false, false) + return + default: + data, err := reader.ReadBytes('\n') + if err != nil { + if errors.Is(err, io.EOF) { + global.ERManager.ErrorTransmit("journald", "error", errors.New("jclient.readFromStderr() exit: EOF"), false, false) + return + } + global.ERManager.ErrorTransmit("journald", "error", errors.Errorf("jclient.readFromStderr() exit: %s", err), false, false) + return + } + jclient.errCh <- data + } + } +} + +func (jclient *JournaldClient) assembleOptions(_initOptions []string, _options *public.JournalctlOptions) []string { + if _options.Notail { + _initOptions = append(_initOptions, "--no-tail") + } else { + _initOptions = append(_initOptions, "--follow") + } + if _options.Notail && _options.Since != "" && _options.Until != "" { + _initOptions = append(_initOptions, "--since", _options.Since, "--until", _options.Until) + } + if _options.Unit != "" { + _initOptions = append(_initOptions, "--unit", _options.Unit) + } + if _options.Identifier != "" { + _initOptions = append(_initOptions, "--identifier", _options.Identifier) + } + if _options.Severity != "" { + _initOptions = append(_initOptions, "--priority", _options.Severity) + } + if _options.Transport != "" { + _initOptions = append(_initOptions, fmt.Sprintf("_TRANSPORT=%s", _options.Transport)) + } + if _options.User != "" { + uid := strings.Split(_options.User, ":")[1] + if uid == "" { + global.ERManager.ErrorTransmit("journald", "error", errors.Errorf("user field in options is invalid: %s", _options.User), false, false) + } + _initOptions = append(_initOptions, "_UID="+uid) + } + return _initOptions +} + +func (jclient *JournaldClient) ReturnJournalctlOptions() *public.JournalctlOptions { + return jclient.options +} + +/* +_closeconn: 是否关闭websocket连接 + +_closechan: 是否关闭dataCh和errCh + +_printstderr: 因journalctl command执行异常而调用releasesource,且打印stderr错误信息 +*/ +func (jclient *JournaldClient) Close(_closeconn, _closechan, _printstderr bool) { + global.ERManager.ErrorTransmit("journald", "info", errors.Errorf("==========%-50s==========", fmt.Sprintf("journald client %s call close", jclient.ID)), false, false) + + if _closeconn && jclient.wsconn != nil { + jclient.Active = false + jclient.wswriteMutex.Lock() + if err := jclient.wsconn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")); err != nil { + global.ERManager.ErrorTransmit("journald", "error", errors.Errorf("write close message to wsconn failed: %s", err.Error()), false, false) + } + jclient.wswriteMutex.Unlock() + jclient.wsconn.Close() + } + + // 是否打印journalctl command stderr错误信息 + if _printstderr { + readStderrTimeout, cancel := context.WithTimeout(JournaldCtx, jclient.timeout) + defer cancel() + OuterLoop: + for { + select { + case <-readStderrTimeout.Done(): + global.ERManager.ErrorTransmit("journald", "warn", errors.New("read stderr timeout, kill journalctl process"), false, false) + break OuterLoop + case stderrLine, isOpen := <-jclient.errCh: + if string(stderrLine) != "" { + global.ERManager.ErrorTransmit("journald", "error", errors.New(string(stderrLine)), false, false) + } + if !isOpen { + break OuterLoop + } + } + } + } + + if jclient.Jcmd != nil && jclient.Jcmd.Process != nil { + if err := jclient.Jcmd.Process.Kill(); err != nil { + global.ERManager.ErrorTransmit("journald", "error", errors.Errorf("cannot kill journalctl process: %s", err), false, false) + } + } + + /* + jclient.readFromStderr() + jclient.readFromStdout() + */ + jclient.CancelF() + /* + jclient.readFromStderr() + jclient.readFromStdout() + */ + jclient.wg.Wait() + global.ERManager.ErrorTransmit("journald", "info", errors.Errorf("journald client:%s all goroutines done", jclient.ID), false, false) + + jclient.closeWriteMsgCh <- struct{}{} + + if _closechan { + jclient.once.Do(func() { + close(jclient.dataCh) + close(jclient.errCh) + }) + } + + time.Sleep(1000 * time.Millisecond) + global.ERManager.ErrorTransmit("journald", "info", errors.Errorf("==========%-50s==========", fmt.Sprintf("journald client %s call close done", jclient.ID)), false, false) +} diff --git a/cmd/agent/logtools/journald/meta.go b/cmd/agent/logtools/journald/meta.go new file mode 100644 index 0000000..b58111c --- /dev/null +++ b/cmd/agent/logtools/journald/meta.go @@ -0,0 +1,57 @@ +/* + * Copyright (c) KylinSoft Co., Ltd. 2024.All rights reserved. + * PilotGo-plugin-logs licensed under the Mulan Permissive Software License, Version 2. + * See LICENSE file for more details. + * Author: Wangjunqi123 + * Date: Mon Dec 16 08:43:58 2024 +0800 + */ +package journald + +import ( + "context" + "encoding/json" + "sort" + "strconv" + + "gitee.com/openeuler/PilotGo-plugin-logs/cmd/agent/global" + "github.com/pkg/errors" +) + +var JournaldCtx, JournaldCancel = context.WithCancel(global.RootCtx) + +var _ sort.Interface = PageEntryBuffSortByTimestamp{} + +type PageEntryBuffSortByTimestamp []string + +func (peb PageEntryBuffSortByTimestamp) Len() int { + return len(peb) +} + +func (peb PageEntryBuffSortByTimestamp) Swap(i, j int) { + peb[i], peb[j] = peb[j], peb[i] +} + +func (peb PageEntryBuffSortByTimestamp) Less(i, j int) bool { + i_raw_entry := map[string]interface{}{} + if err := json.Unmarshal([]byte(peb[i]), &i_raw_entry); err != nil { + global.ERManager.ErrorTransmit("logtools", "error", errors.Errorf("fail to unmarshal Journald JSON: %s; raw data: %+v(%d) ***", err, peb[i], len(peb[i])), false, true) + return true + } + i_timestamp_int64, err := strconv.ParseInt(i_raw_entry["__REALTIME_TIMESTAMP"].(string), 10, 64) + if err != nil { + global.ERManager.ErrorTransmit("logtools", "error", errors.Errorf("fail to parse timestamp %s: %s", i_raw_entry["__REALTIME_TIMESTAMP"].(string), err.Error()), false, true) + return true + } + + j_raw_entry := map[string]interface{}{} + if err := json.Unmarshal([]byte(peb[j]), &j_raw_entry); err != nil { + global.ERManager.ErrorTransmit("logtools", "error", errors.Errorf("fail to unmarshal Journald JSON: %s; raw data: %+v(%d) ***", err, peb[j], len(peb[j])), false, false) + return true + } + j_timestamp_int64, err := strconv.ParseInt(j_raw_entry["__REALTIME_TIMESTAMP"].(string), 10, 64) + if err != nil { + global.ERManager.ErrorTransmit("logtools", "error", errors.Errorf("fail to parse timestamp %s: %s", j_raw_entry["__REALTIME_TIMESTAMP"].(string), err.Error()), false, false) + return true + } + return int(i_timestamp_int64) < int(j_timestamp_int64) +} diff --git a/cmd/agent/logtools/logClientsManager.go b/cmd/agent/logtools/logClientsManager.go new file mode 100644 index 0000000..f9b44a6 --- /dev/null +++ b/cmd/agent/logtools/logClientsManager.go @@ -0,0 +1,152 @@ +/* + * Copyright (c) KylinSoft Co., Ltd. 2024.All rights reserved. + * PilotGo-plugin-logs licensed under the Mulan Permissive Software License, Version 2. + * See LICENSE file for more details. + * Author: Wangjunqi123 + * Date: Mon Dec 16 08:43:58 2024 +0800 + */ +package logtools + +import ( + "fmt" + "os/exec" + "sync" + "time" + + "gitee.com/openeuler/PilotGo-plugin-logs/cmd/agent/global" + "gitee.com/openeuler/PilotGo-plugin-logs/cmd/agent/logtools/journald" + "gitee.com/openeuler/PilotGo-plugin-logs/cmd/public" + "github.com/pkg/errors" +) + +var LogCollector *LogClientManagement + +const ( + JournaldLogClientType int = iota +) + +type LogClientManagement struct { + // key: web client id + journaldClients map[string]*journald.JournaldClient + + // 终止采集组件状态检测 + heartbeatDone chan struct{} + + // 终止周期性采集日志时间轴数据 + timelineDone chan struct{} + + once sync.Once +} + +func CreateLogClientsManager() { + LogCollector = &LogClientManagement{ + journaldClients: make(map[string]*journald.JournaldClient), + heartbeatDone: make(chan struct{}), + timelineDone: make(chan struct{}), + } + + go LogCollector.heartbeatDetect() + // go LogCollector.logTimeline() +} + +func (lcm *LogClientManagement) Add(_type int, _id string, _c interface{}) error { + switch _type { + case JournaldLogClientType: + jc, ok := _c.(*journald.JournaldClient) + if !ok { + return fmt.Errorf("fail to add log collect client: %+v", _c) + } + lcm.journaldClients[_id] = jc + } + return nil +} + +func (lcm *LogClientManagement) Get(_type int, _id string) (interface{}, bool) { + switch _type { + case JournaldLogClientType: + jc, ok := lcm.journaldClients[_id] + if ok { + return jc, ok + } + } + return nil, false +} + +func (lcm *LogClientManagement) Delete(_type int, _id string) { + switch _type { + case JournaldLogClientType: + delete(lcm.journaldClients, _id) + } +} + +func (lcm *LogClientManagement) ReturnLogClients(_type int) interface{} { + switch _type { + case JournaldLogClientType: + return lcm.journaldClients + } + return nil +} + +func (lcm *LogClientManagement) heartbeatDetect() { + for { + select { + case <-lcm.heartbeatDone: + return + case <-time.After(global.HeartbeatPeriod): + for _id, _jc := range lcm.journaldClients { + if !_jc.Active { + global.ERManager.ErrorTransmit("logtools", "info", errors.Errorf("remove web client: %s", _id), false, false) + lcm.Delete(JournaldLogClientType, _id) + } + } + } + } +} + +func (lcm *LogClientManagement) LogTimeline() error { + for { + select { + case <-lcm.timelineDone: + return fmt.Errorf("timeline done channel closed") + case t := <-time.After(60 * time.Second): + if len(lcm.journaldClients) == 0 { + return fmt.Errorf("no active journald clients") + } + + tmpjclient := journald.CreateJournaldClient(nil, global.ReadCmdStderrTimeout) + cmd := exec.Command("systemctl", journald.UnitListDefaultOptions...) + tmpjclient.ProcessData(cmd, public.UnitData) + go tmpjclient.WriteMessageToClient() + + entry_count := make(map[string]string) + for _, _unit := range tmpjclient.UnitsMap["systemd"] { + since := t.Local().Format("2024-01-02 15:04:05") + until := t.Add(-60 * time.Second).Local().Format("2024-01-02 15:04:05") + cmd = exec.Command("/bin/bash", "-c", "journalctl", "--quiet", "--no-pager", "--unit", _unit+".service", "--since", since, "--until", until, "|", "wc", "-l") + outputBytes, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("log timeline cmd error: %s, output: %v", err.Error(), string(outputBytes)) + } + entry_count[_unit] = string(outputBytes) + } + + fmt.Printf("\033[32m>>>\033[0m entry count: %+v\n", entry_count) + + tmpjclient.Close(false, true, false) + } + } + +} + +func (lcm *LogClientManagement) CloseAll() { + lcm.once.Do(func() { + close(lcm.heartbeatDone) + }) + + // lcm.timelineDoneCh <- struct{}{} + for id, jc := range lcm.journaldClients { + global.ERManager.ErrorTransmit("logtools", "info", errors.Errorf("shutdown journald client: %s", id), false, false) + jc.CloseReadMsgCh <- struct{}{} + jc.Close(true, true, false) + } +} diff --git a/cmd/agent/main.go b/cmd/agent/main.go new file mode 100644 index 0000000..57c6125 --- /dev/null +++ b/cmd/agent/main.go @@ -0,0 +1,66 @@ +/* + * Copyright (c) KylinSoft Co., Ltd. 2024.All rights reserved. + * PilotGo-plugin-logs licensed under the Mulan Permissive Software License, Version 2. + * See LICENSE file for more details. + * Author: Wangjunqi123 + * Date: Mon Dec 16 08:43:58 2024 +0800 + */ +package main + +import ( + "gitee.com/openeuler/PilotGo-plugin-logs/cmd/agent/conf" + "gitee.com/openeuler/PilotGo-plugin-logs/cmd/agent/global" + "gitee.com/openeuler/PilotGo-plugin-logs/cmd/agent/logger" + "gitee.com/openeuler/PilotGo-plugin-logs/cmd/agent/logtools" + "gitee.com/openeuler/PilotGo-plugin-logs/cmd/agent/resourcemanage" + "gitee.com/openeuler/PilotGo-plugin-logs/cmd/agent/signal" + "gitee.com/openeuler/PilotGo-plugin-logs/cmd/agent/webserver" + sdklogger "gitee.com/openeuler/PilotGo/sdk/logger" +) + +func main() { + /* + init config + */ + conf.InitConfig() + + /* + init logger + */ + logger.InitLogger() + + /* + init error control、resource release、goroutine end management + */ + ermanager, err := resourcemanage.CreateErrorReleaseManager(global.RootCtx, Close) + if err != nil { + sdklogger.Fatal(err.Error()) + } + global.ERManager = ermanager + + /* + 日志采集组件管理 + */ + logtools.CreateLogClientsManager() + + /* + + */ + global.InitOSName() + + /* + init web server + */ + webserver.InitWebserver() + + /* + 终止进程信号监听 + */ + signal.SignalMonitoring() +} + +func Close() { + if logtools.LogCollector != nil { + logtools.LogCollector.CloseAll() + } +} diff --git a/cmd/agent/resourcemanage/resourcemanage.go b/cmd/agent/resourcemanage/resourcemanage.go new file mode 100644 index 0000000..1900072 --- /dev/null +++ b/cmd/agent/resourcemanage/resourcemanage.go @@ -0,0 +1,200 @@ +/* + * Copyright (c) KylinSoft Co., Ltd. 2024.All rights reserved. + * PilotGo-plugin-logs licensed under the Mulan Permissive Software License, Version 2. + * See LICENSE file for more details. + * Author: Wangjunqi123 + * Date: Mon Dec 16 08:43:58 2024 +0800 + */ +package resourcemanage + +import ( + "context" + "fmt" + "os" + "sync" + "time" + + "gitee.com/openeuler/PilotGo/sdk/logger" + "github.com/pkg/errors" +) + +const ( + green string = "\x1b[97;104m" + reset string = "\x1b[0m" +) + +type ResourceReleaseFunction func() + +type FinalError struct { + Err error + + Module string + + Severity string + + Cancel context.CancelFunc + + PrintStack bool + + ExitAfterPrint bool +} + +func (e *FinalError) Error() string { + return fmt.Sprintf("%+v", e.Err) +} + +type ErrorReleaseManagement struct { + ErrChan chan error + + errEndChan chan struct{} + + // cancelCtx: 控制 ERManager 本身资源释放的上下文,子上下文:errortransmit + cancelCtx context.Context + cancelFunc context.CancelFunc + + // GoCancelCtx: ERManager 用于控制项目指定goroutine优雅退出的上下文 + GoCancelCtx context.Context + GoCancelFunc context.CancelFunc + + Wg sync.WaitGroup + + // releaseFunc: 项目整体资源释放回调函数 + releaseFunc ResourceReleaseFunction +} + +func CreateErrorReleaseManager(_ctx context.Context, _releaseFunc ResourceReleaseFunction) (*ErrorReleaseManagement, error) { + if _ctx == nil || _releaseFunc == nil { + return nil, fmt.Errorf("context or closeFunc is nil") + } + + ErrorM := &ErrorReleaseManagement{ + ErrChan: make(chan error, 20), + errEndChan: make(chan struct{}), + releaseFunc: _releaseFunc, + } + ErrorM.cancelCtx, ErrorM.cancelFunc = context.WithCancel(_ctx) + ErrorM.GoCancelCtx, ErrorM.GoCancelFunc = context.WithCancel(_ctx) + + go ErrorM.errorFactory() + + return ErrorM, nil +} + +func (erm *ErrorReleaseManagement) errorFactory() { + for { + select { + case <-erm.errEndChan: + logger.Info("error management stopped") + return + case _error := <-erm.ErrChan: + if _error == nil { + continue + } + + _terror, ok := _error.(*FinalError) + if !ok { + logger.Error("plain error: %s", _error.Error()) + continue + } + + if _terror.Err != nil { + if !_terror.PrintStack && !_terror.ExitAfterPrint { + erm.output(_terror) + } else if _terror.PrintStack && !_terror.ExitAfterPrint { + logger.ErrorStack(erm.errorStackMsg(_terror.Module), _terror.Err) + } else if !_terror.PrintStack && _terror.ExitAfterPrint { + erm.output(_terror) + _terror.Cancel() + } else if _terror.PrintStack && _terror.ExitAfterPrint { + logger.ErrorStack(erm.errorStackMsg(_terror.Module), _terror.Err) + _terror.Cancel() + } + } + } + } +} + +func (erm *ErrorReleaseManagement) ResourceRelease() { + erm.releaseFunc() + + erm.GoCancelFunc() + + erm.Wg.Wait() + + time.Sleep(500 * time.Millisecond) + + close(erm.errEndChan) + close(erm.ErrChan) +} + +/* +@severity: debug info warn error + +@err: 最终生成的error + +@exit_after_print: 打印完异常日志后是否结束主程序 + +@print_stack: 是否打印异常日志错误链,打印错误链时默认severity为error +*/ +func (erm *ErrorReleaseManagement) ErrorTransmit(_module, _severity string, _err error, _exit_after_print, _print_stack bool) { + if _exit_after_print { + ctx, cancel := context.WithCancel(erm.cancelCtx) + erm.ErrChan <- &FinalError{ + Err: _err, + Cancel: cancel, + Module: _module, + Severity: _severity, + PrintStack: _print_stack, + ExitAfterPrint: _exit_after_print, + } + <-ctx.Done() + erm.ResourceRelease() + os.Exit(1) + } + + erm.ErrChan <- &FinalError{ + Err: _err, + Cancel: nil, + Module: _module, + Severity: _severity, + PrintStack: _print_stack, + ExitAfterPrint: _exit_after_print, + } +} + +func (erm *ErrorReleaseManagement) logFormat(_err error, _module string) string { + if len(_module) > 10 { + _module = _module[:10] + } + log := fmt.Sprintf("%v %s %-10s %s %+v", + time.Now().Format("2006-01-02 15:04:05"), + green, _module, reset, + _err.Error(), + ) + return log +} + +func (erm *ErrorReleaseManagement) errorStackMsg(_module string) string { + if len(_module) > 10 { + _module = _module[:10] + } + return fmt.Sprintf("%v %s %-10s %s", + time.Now().Format("2006-01-02 15:04:05"), + green, _module, reset, + ) +} + +func (erm *ErrorReleaseManagement) output(_err *FinalError) { + switch _err.Severity { + case "debug": + logger.Debug("%s", erm.logFormat(errors.Cause(_err.Err), _err.Module)) + case "info": + logger.Info("%s", erm.logFormat(errors.Cause(_err.Err), _err.Module)) + case "warn": + logger.Warn("%s", erm.logFormat(errors.Cause(_err.Err), _err.Module)) + case "error": + logger.Error("%s", erm.logFormat(errors.Cause(_err.Err), _err.Module)) + default: + logger.Error("%s", erm.logFormat(errors.Cause(_err.Err), _err.Module)) + } +} diff --git a/cmd/agent/signal/signalMonitor.go b/cmd/agent/signal/signalMonitor.go new file mode 100644 index 0000000..125206a --- /dev/null +++ b/cmd/agent/signal/signalMonitor.go @@ -0,0 +1,33 @@ +/* + * Copyright (c) KylinSoft Co., Ltd. 2024.All rights reserved. + * PilotGo-plugin-logs licensed under the Mulan Permissive Software License, Version 2. + * See LICENSE file for more details. + * Author: Wangjunqi123 + * Date: Mon Dec 16 08:43:58 2024 +0800 + */ +package signal + +import ( + "os" + "os/signal" + "syscall" + + "gitee.com/openeuler/PilotGo-plugin-logs/cmd/agent/global" + "github.com/pkg/errors" +) + +func SignalMonitoring() { + ch := make(chan os.Signal, 1) + + signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) + for s := range ch { + switch s { + case syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT: + global.ERManager.ErrorTransmit("signal", "info", errors.Errorf("signal interrupt: %s", s.String()), false, false) + global.ERManager.ResourceRelease() + os.Exit(1) + default: + global.ERManager.ErrorTransmit("signal", "warn", errors.Errorf("unknown signal: %s\n", s.String()), false, false) + } + } +} diff --git a/cmd/agent/webserver/engine.go b/cmd/agent/webserver/engine.go new file mode 100644 index 0000000..3e85e2f --- /dev/null +++ b/cmd/agent/webserver/engine.go @@ -0,0 +1,35 @@ +/* + * Copyright (c) KylinSoft Co., Ltd. 2024.All rights reserved. + * PilotGo-plugin-logs licensed under the Mulan Permissive Software License, Version 2. + * See LICENSE file for more details. + * Author: Wangjunqi123 + * Date: Mon Dec 16 08:43:58 2024 +0800 + */ +package webserver + +import ( + "net/http" + + "gitee.com/openeuler/PilotGo-plugin-logs/cmd/agent/conf" + "gitee.com/openeuler/PilotGo-plugin-logs/cmd/agent/global" + "github.com/pkg/errors" +) + +func InitWebserver() { + http.HandleFunc("/ws/entry", entryHandle) + + go func() { + global.ERManager.ErrorTransmit("webserver", "info", errors.Errorf("WebSocket server started on %s", conf.Global_Config.Logs.Addr), false, false) + if conf.Global_Config.Logs.Https_enabled { + if err := http.ListenAndServeTLS(conf.Global_Config.Logs.Addr, conf.Global_Config.Logs.CertFile, conf.Global_Config.Logs.KeyFile, nil); err != nil { + global.ERManager.ErrorTransmit("webserver", "error", errors.Errorf("Error starting server: %s", err), true, false) + return + } + } else { + if err := http.ListenAndServe(conf.Global_Config.Logs.Addr, nil); err != nil { + global.ERManager.ErrorTransmit("webserver", "error", errors.Errorf("Error starting server: %s", err), true, false) + return + } + } + }() +} diff --git a/cmd/agent/webserver/handle.go b/cmd/agent/webserver/handle.go new file mode 100644 index 0000000..e5116db --- /dev/null +++ b/cmd/agent/webserver/handle.go @@ -0,0 +1,61 @@ +/* + * Copyright (c) KylinSoft Co., Ltd. 2024.All rights reserved. + * PilotGo-plugin-logs licensed under the Mulan Permissive Software License, Version 2. + * See LICENSE file for more details. + * Author: Wangjunqi123 + * Date: Mon Dec 16 08:43:58 2024 +0800 + */ +package webserver + +import ( + "fmt" + "net/http" + "strings" + + "gitee.com/openeuler/PilotGo-plugin-logs/cmd/agent/global" + "gitee.com/openeuler/PilotGo-plugin-logs/cmd/agent/logtools" + "gitee.com/openeuler/PilotGo-plugin-logs/cmd/agent/logtools/journald" + "github.com/gorilla/websocket" + "github.com/pkg/errors" +) + +var upgrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + CheckOrigin: func(r *http.Request) bool { + return true // 允许来自任何来源的连接 + }, +} + +func entryHandle(_w http.ResponseWriter, _r *http.Request) { + global.ERManager.ErrorTransmit("webserver", "debug", errors.Errorf( + "r.method: %v, r.proto: %v, r.host: %v, r.header: %+v, r.URL.scheme: %v, r.URL.host: %v, r.URL.path: %v", + _r.Method, _r.Proto, _r.Host, _r.Header, _r.URL.Scheme, _r.URL.Host, _r.URL.Path), + false, false) + + conn, err := upgrader.Upgrade(_w, _r, nil) + if err != nil { + global.ERManager.ErrorTransmit("webserver", "error", errors.Errorf("Error while upgrading connection: %s", err.Error()), false, false) + _w.Write([]byte(fmt.Sprintf("Error while upgrading connection: %s", err.Error()))) + return + } + defer conn.Close() + + global.ERManager.ErrorTransmit("webserver", "info", errors.Errorf("connected to ws client: %s", strings.Split(_r.Header.Get("X-Forwarded-For"), ",")[0]), false, false) + + jclient := journald.CreateJournaldClient(conn, global.ReadCmdStderrTimeout) + jclient.ID = _r.Header.Get("clientId") + + jclient.Active = true + if logtools.LogCollector == nil { + global.ERManager.ErrorTransmit("webserver", "error", errors.New("logcollector is nil"), false, false) + _w.Write([]byte("logcollector is nil")) + return + } + if err := logtools.LogCollector.Add(logtools.JournaldLogClientType, jclient.ID, jclient); err != nil { + global.ERManager.ErrorTransmit("webserver", "error", errors.New(err.Error()), false, false) + _w.Write([]byte(err.Error())) + return + } + jclient.ReadMessageFromClient() +} diff --git a/cmd/public/debug.go b/cmd/public/debug.go new file mode 100644 index 0000000..4149002 --- /dev/null +++ b/cmd/public/debug.go @@ -0,0 +1,19 @@ +/* + * Copyright (c) KylinSoft Co., Ltd. 2024.All rights reserved. + * PilotGo-plugin-logs licensed under the Mulan Permissive Software License, Version 2. + * See LICENSE file for more details. + * Author: Wangjunqi123 + * Date: Mon Dec 16 08:43:58 2024 +0800 + */ +package public + +import ( + "fmt" + "runtime" +) + +func DebugStackTrace() { + var buf [4096]byte + n := runtime.Stack(buf[:], false) + fmt.Printf("StackTrace (length: %d)\n%s\n", n, buf[:n]) +} diff --git a/cmd/public/journald_meta.go b/cmd/public/journald_meta.go new file mode 100644 index 0000000..712d0e7 --- /dev/null +++ b/cmd/public/journald_meta.go @@ -0,0 +1,56 @@ +/* + * Copyright (c) KylinSoft Co., Ltd. 2024.All rights reserved. + * PilotGo-plugin-logs licensed under the Mulan Permissive Software License, Version 2. + * See LICENSE file for more details. + * Author: Wangjunqi123 + * Date: Mon Dec 16 08:43:58 2024 +0800 + */ +package public + +type JournalctlOptions struct { + Since string `json:"since"` + Until string `json:"until"` + Unit string `json:"unit"` + Identifier string `json:"identifier"` + Severity string `json:"severity"` + Transport string `json:"transport"` + Notail bool `json:"notail"` + User string `json:"user"` // root:0 + From int `json:"from"` + Size int `json:"size"` +} + +type JMessage struct { + Type int `json:"type"` + JOptions *JournalctlOptions `json:"joptions"` + Data interface{} `json:"data"` +} + +// 客户端与logs agent之间websocket通信的消息类型 +const ( + UpdateOptionsMsg int = iota + AgentAddrMsg + UnitListMsg + ConnectedMsg + DataMsg + UpdatePageMsg + DialFailedMsg +) + +type StdoutDataType int + +type StdoutData struct { + Type StdoutDataType `json:"type"` + Data interface{} `json:"data"` +} + +// shell命令stdout数据类型 +const ( + LogEntryData StdoutDataType = iota + UnitData +) + +type PageData struct { + Total int `json:"total"` + Hits []map[string]interface{} `json:"hits"` +} diff --git a/cmd/server/conf/config.go b/cmd/server/conf/config.go new file mode 100644 index 0000000..93618e7 --- /dev/null +++ b/cmd/server/conf/config.go @@ -0,0 +1,60 @@ +/* + * Copyright (c) KylinSoft Co., Ltd. 2024.All rights reserved. + * PilotGo-plugin-logs licensed under the Mulan Permissive Software License, Version 2. + * See LICENSE file for more details. + * Author: Wangjunqi123 + * Date: Mon Dec 16 08:43:58 2024 +0800 + */ +package conf + +import ( + "flag" + "fmt" + "os" + "path" + + "gitee.com/openeuler/PilotGo-plugin-logs/cmd/server/global" + "gitee.com/openeuler/PilotGo/sdk/logger" + "gopkg.in/yaml.v2" +) + +var Global_Config *ServerConfig + +const config_type = "logs_server.yaml" + +var config_dir string + +type ServerConfig struct { + Logs *LogsConf + PilotGo *PilotGoConf + Logopts *logger.LogOpts `yaml:"log"` +} + +func ConfigFile() string { + configfilepath := path.Join(config_dir, config_type) + + return configfilepath +} + +func InitConfig() { + flag.StringVar(&config_dir, "conf", "./", "logs plugin configuration directory") + flag.Usage = func() { + fmt.Fprintf(os.Stderr, "Usage: %s -conf /path/to/logs.yaml(default:./) \n", os.Args[0]) + } + flag.Parse() + + bytes, err := global.FileReadBytes(ConfigFile()) + if err != nil { + flag.Usage() + fmt.Printf("open file failed: %s, %s\n", ConfigFile(), err.Error()) + os.Exit(1) + } + + Global_Config = &ServerConfig{} + + err = yaml.Unmarshal(bytes, Global_Config) + if err != nil { + fmt.Printf("yaml unmarshal failed: %s\n", err.Error()) + os.Exit(1) + } +} diff --git a/cmd/server/conf/meta.go b/cmd/server/conf/meta.go new file mode 100644 index 0000000..4253f7f --- /dev/null +++ b/cmd/server/conf/meta.go @@ -0,0 +1,20 @@ +/* + * Copyright (c) KylinSoft Co., Ltd. 2024.All rights reserved. + * PilotGo-plugin-logs licensed under the Mulan Permissive Software License, Version 2. + * See LICENSE file for more details. + * Author: Wangjunqi123 + * Date: Mon Dec 16 08:43:58 2024 +0800 + */ +package conf + +type LogsConf struct { + Https_enabled bool `yaml:"https_enabled"` + CertFile string `yaml:"cert_file"` + KeyFile string `yaml:"key_file"` + Addr string `yaml:"server_listen_addr"` + Addr_target string `yaml:"server_target_addr"` +} + +type PilotGoConf struct { + Addr string `yaml:"addr"` +} diff --git a/cmd/server/global/IsIPandPORTValid.go b/cmd/server/global/IsIPandPORTValid.go new file mode 100644 index 0000000..fde4a64 --- /dev/null +++ b/cmd/server/global/IsIPandPORTValid.go @@ -0,0 +1,39 @@ +/* + * Copyright (c) KylinSoft Co., Ltd. 2024.All rights reserved. + * PilotGo-plugin-topology licensed under the Mulan Permissive Software License, Version 2. + * See LICENSE file for more details. + * Author: Wangjunqi123 + * Date: Mon Nov 4 14:30:13 2024 +0800 + */ +package global + +import ( + "fmt" + "net" + "time" + + "github.com/pkg/errors" +) + +const ( + req_timeout = 500 * time.Millisecond +) + +// 检测IP是否可达 +func IsIPandPORTValid(ip, port string) bool { + addr, err := net.ResolveIPAddr("ip", ip) + if err != nil { + ERManager.ErrorTransmit("global", "error", errors.Errorf("fail to judge addr valid: %s", err.Error()), false, false) + return false + } + + // 设置连接超时时间 + conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%s", addr.String(), port), req_timeout) + if err != nil { + ERManager.ErrorTransmit("global", "error", errors.Errorf("fail to judge addr valid: %s", err.Error()), false, false) + return false + } + + conn.Close() + return true +} diff --git a/cmd/server/global/file.go b/cmd/server/global/file.go new file mode 100755 index 0000000..84e63db --- /dev/null +++ b/cmd/server/global/file.go @@ -0,0 +1,50 @@ +/* + * Copyright (c) KylinSoft Co., Ltd. 2024.All rights reserved. + * PilotGo-plugin-logs licensed under the Mulan Permissive Software License, Version 2. + * See LICENSE file for more details. + * Author: Wangjunqi123 + * Date: Mon Dec 16 08:43:58 2024 +0800 + */ +package global + +import ( + "io" + "os" + + "github.com/pkg/errors" +) + +func FileReadString(filePath string) (string, error) { + content, err := os.ReadFile(filePath) + if err != nil { + return "", errors.New(err.Error()) + } + + return string(content), nil +} + +func FileReadBytes(filePath string) ([]byte, error) { + f, err := os.Open(filePath) + if err != nil { + return nil, errors.New(err.Error()) + } + defer f.Close() + + var content []byte + readbuff := make([]byte, 1024*4) + for { + n, err := f.Read(readbuff) + if err != nil { + if err == io.EOF { + if n != 0 { + content = append(content, readbuff[:n]...) + } + break + } + return nil, errors.New(err.Error()) + } + content = append(content, readbuff[:n]...) + } + + return content, nil +} diff --git a/cmd/server/global/global.go b/cmd/server/global/global.go new file mode 100755 index 0000000..973bfdd --- /dev/null +++ b/cmd/server/global/global.go @@ -0,0 +1,27 @@ +/* + * Copyright (c) KylinSoft Co., Ltd. 2024.All rights reserved. + * PilotGo-plugin-logs licensed under the Mulan Permissive Software License, Version 2. + * See LICENSE file for more details. + * Author: Wangjunqi123 + * Date: Mon Dec 16 08:43:58 2024 +0800 + */ +package global + +import ( + "context" + "time" + + "gitee.com/openeuler/PilotGo-plugin-logs/cmd/server/resourcemanage" +) + +var ( + RootCtx = context.Background() + + HeartbeatPeriod = 5 * time.Second // 日志采集组件状态检测周期s +) + +var ERManager *resourcemanage.ErrorReleaseManagement + +func init() { + +} diff --git a/cmd/server/logger/sdkLogger.go b/cmd/server/logger/sdkLogger.go new file mode 100644 index 0000000..3709ce3 --- /dev/null +++ b/cmd/server/logger/sdkLogger.go @@ -0,0 +1,20 @@ +/* + * Copyright (c) KylinSoft Co., Ltd. 2024.All rights reserved. + * PilotGo-plugin-logs licensed under the Mulan Permissive Software License, Version 2. + * See LICENSE file for more details. + * Author: Wangjunqi123 + * Date: Mon Dec 16 08:43:58 2024 +0800 + */ +package logger + +import ( + "gitee.com/openeuler/PilotGo-plugin-logs/cmd/server/conf" + "gitee.com/openeuler/PilotGo/sdk/logger" +) + +func InitLogger() { + err := logger.Init(conf.Global_Config.Logopts) + if err != nil { + logger.Fatal(err.Error()) + } +} diff --git a/cmd/server/logs_server.yaml.template b/cmd/server/logs_server.yaml.template new file mode 100644 index 0000000..54ecf82 --- /dev/null +++ b/cmd/server/logs_server.yaml.template @@ -0,0 +1,17 @@ +logs: + https_enabled: false + cert_file: "" + key_file: "" +# 插件服务端服务器监听地址 + server_listen_addr: "0.0.0.0:9994" +# +# 远程客户端与插件服务端建立连接时插件的地址 + server_target_addr: "localhost:9994" +PilotGo: + addr: "localhost:8888" +log: + level: debug + driver: file # 可选stdout和file。stdout:输出到终端控制台;file:输出到path下的指定文件。 + path: /opt/PilotGo/plugin/logs/server/log/logs-server.log + max_file: 1 + max_size: 10485760 diff --git a/cmd/server/main.go b/cmd/server/main.go new file mode 100644 index 0000000..9299e2d --- /dev/null +++ b/cmd/server/main.go @@ -0,0 +1,72 @@ +/* + * Copyright (c) KylinSoft Co., Ltd. 2024.All rights reserved. + * PilotGo-plugin-logs licensed under the Mulan Permissive Software License, Version 2. + * See LICENSE file for more details. + * Author: Wangjunqi123 + * Date: Mon Dec 16 08:43:58 2024 +0800 + */ +package main + +import ( + "gitee.com/openeuler/PilotGo-plugin-logs/cmd/server/conf" + "gitee.com/openeuler/PilotGo-plugin-logs/cmd/server/global" + "gitee.com/openeuler/PilotGo-plugin-logs/cmd/server/logger" + "gitee.com/openeuler/PilotGo-plugin-logs/cmd/server/pluginclient" + "gitee.com/openeuler/PilotGo-plugin-logs/cmd/server/resourcemanage" + "gitee.com/openeuler/PilotGo-plugin-logs/cmd/server/signal" + "gitee.com/openeuler/PilotGo-plugin-logs/cmd/server/webserver" + "gitee.com/openeuler/PilotGo-plugin-logs/cmd/server/webserver/proxy" + sdklogger "gitee.com/openeuler/PilotGo/sdk/logger" +) + +func main() { + /* + init config + */ + conf.InitConfig() + + /* + init logger + */ + logger.InitLogger() + + /* + init error control、resource release、goroutine end management + */ + ermanager, err := resourcemanage.CreateErrorReleaseManager(global.RootCtx, Close) + if err != nil { + sdklogger.Fatal("%s", err.Error()) + } + global.ERManager = ermanager + + /* + init plugin client + */ + pluginclient.InitPluginClient() + + /* + websocket proxy management + */ + proxy.CreateWebsocketProxyManagement() + + /* + init web server + */ + webserver.InitWebServer() + // proxy.InitWebServerTcpHijackProxy() + + /* + 业务模块 + */ + + /* + 终止进程信号监听 + */ + signal.SignalMonitoring() +} + +func Close() { + if proxy.WebsocketProxyManager != nil { + proxy.WebsocketProxyManager.CloseAll() + } +} diff --git a/cmd/server/pluginclient/meta.go b/cmd/server/pluginclient/meta.go new file mode 100644 index 0000000..58dad3f --- /dev/null +++ b/cmd/server/pluginclient/meta.go @@ -0,0 +1,24 @@ +/* + * Copyright (c) KylinSoft Co., Ltd. 2024.All rights reserved. + * PilotGo-plugin-logs licensed under the Mulan Permissive Software License, Version 2. + * See LICENSE file for more details. + * Author: Wangjunqi123 + * Date: Mon Dec 16 08:43:58 2024 +0800 + */ +package pluginclient + +import "gitee.com/openeuler/PilotGo/sdk/plugin/client" + +const Version = "1.0.1" + +var PluginInfo = &client.PluginInfo{ + Name: "logs", + Version: Version, + Description: "logs plugin for PilotGo", + Author: "wangjunqi", + Email: "wangjunqi@kylinos.cn", + Url: "", // 客户端建立连接的插件服务端地址,非插件配置文件中web服务器的监听地址 + Icon: "Reading", + MenuName: "主机日志", + PluginType: "micro-app", +} diff --git a/cmd/server/pluginclient/pluginClient.go b/cmd/server/pluginclient/pluginClient.go new file mode 100644 index 0000000..131a56e --- /dev/null +++ b/cmd/server/pluginclient/pluginClient.go @@ -0,0 +1,125 @@ +/* + * Copyright (c) KylinSoft Co., Ltd. 2024.All rights reserved. + * PilotGo-plugin-logs licensed under the Mulan Permissive Software License, Version 2. + * See LICENSE file for more details. + * Author: Wangjunqi123 + * Date: Mon Dec 16 08:43:58 2024 +0800 + */ +package pluginclient + +import ( + "context" + "fmt" + "sync" + + "github.com/pkg/errors" + + "gitee.com/openeuler/PilotGo-plugin-logs/cmd/server/conf" + "gitee.com/openeuler/PilotGo-plugin-logs/cmd/server/global" + "gitee.com/openeuler/PilotGo/sdk/common" + "gitee.com/openeuler/PilotGo/sdk/plugin/client" +) + +var Global_Client *client.Client + +var Global_Context context.Context + +func InitPluginClient() { + if conf.Global_Config != nil && conf.Global_Config.Logs.Https_enabled { + PluginInfo.Url = fmt.Sprintf("https://%s", conf.Global_Config.Logs.Addr_target) + } else if conf.Global_Config != nil && !conf.Global_Config.Logs.Https_enabled { + PluginInfo.Url = fmt.Sprintf("http://%s", conf.Global_Config.Logs.Addr_target) + } else { + global.ERManager.ErrorTransmit("pluginclient", "error", errors.New("Global_Config is nil"), true, false) + } + + Global_Client = client.DefaultClient(PluginInfo) + + // 注册插件扩展点 + var ex []common.Extention + me1 := &common.MachineExtention{ + Type: common.ExtentionMachine, + Name: "安装日志agent", + URL: "/plugin/logs/api/runcommand?type=install", + Permission: "plugin.logs.agent/install", + } + me2 := &common.MachineExtention{ + Type: common.ExtentionMachine, + Name: "卸载日志agent", + URL: "/plugin/logs/api/runcommand?type=uninstall", + Permission: "plugin.logs.agent/uninstall", + } + pe1 := &common.PageExtention{ + Type: common.ExtentionPage, + Name: "日志查询", + URL: "/page", + Permission: "plugin.logs.page/menu", + } + // be1 := &common.BatchExtention{ + // Type: common.ExtentionBatch, + // Name: "批次扩展", + // URL: "/batch", + // Permission: "plugin.logs/function", + // } + ex = append(ex, pe1, me1, me2) + Global_Client.RegisterExtention(ex) + + tag_cb := func(uuids []string) []common.Tag { + machines, err := Global_Client.MachineList() + if err != nil { + return nil + } + + var mu sync.Mutex + var wg sync.WaitGroup + var tags []common.Tag + for _, m := range machines { + wg.Add(1) + go func(_m *common.MachineNode) { + if global.IsIPandPORTValid(_m.IP, "9995") { + tag := common.Tag{ + UUID: _m.UUID, + Type: common.TypeOk, + Data: "日志", + } + mu.Lock() + tags = append(tags, tag) + mu.Unlock() + } else { + tag := common.Tag{ + UUID: _m.UUID, + Type: common.TypeError, + Data: "", + } + mu.Lock() + tags = append(tags, tag) + mu.Unlock() + } + wg.Done() + }(m) + } + wg.Wait() + return tags + } + Global_Client.OnGetTags(tag_cb) + + addPermissions() + + Global_Context = context.Background() +} + +func addPermissions() { + var pe []common.Permission + p1 := common.Permission{ + Resource: "logs", + Operate: "menu", + } + + p2 := common.Permission{ + Resource: "logs_operate", + Operate: "button", + } + + p := append(pe, p1, p2) + Global_Client.RegisterPermission(p) +} diff --git a/cmd/server/resourcemanage/resourcemanage.go b/cmd/server/resourcemanage/resourcemanage.go new file mode 100644 index 0000000..2d8b4a2 --- /dev/null +++ b/cmd/server/resourcemanage/resourcemanage.go @@ -0,0 +1,206 @@ +/* + * Copyright (c) KylinSoft Co., Ltd. 2024.All rights reserved. + * PilotGo-plugin-logs licensed under the Mulan Permissive Software License, Version 2. + * See LICENSE file for more details. + * Author: Wangjunqi123 + * Date: Mon Dec 16 08:43:58 2024 +0800 + */ +package resourcemanage + +import ( + "context" + "fmt" + "os" + "sync" + "time" + + "gitee.com/openeuler/PilotGo/sdk/logger" + "github.com/pkg/errors" +) + +const ( + green string = "\x1b[97;104m" + reset string = "\x1b[0m" +) + +type ResourceReleaseFunction func() + +type FinalError struct { + Err error + + Module string + + Severity string + + Cancel context.CancelFunc + + PrintStack bool + + ExitAfterPrint bool +} + +func (e *FinalError) Error() string { + return fmt.Sprintf("%+v", e.Err) +} + +type ErrorReleaseManagement struct { + ErrChan chan error + + errEndChan chan struct{} + + // cancelCtx: 控制 ERManager 本身资源释放的上下文,子上下文:errortransmit + cancelCtx context.Context + cancelFunc context.CancelFunc + + // GoCancelCtx: ERManager 用于控制项目指定goroutine优雅退出的上下文 + GoCancelCtx context.Context + GoCancelFunc context.CancelFunc + + Wg sync.WaitGroup + + // releaseFunc: 项目整体资源释放回调函数 + releaseFunc ResourceReleaseFunction +} + +func CreateErrorReleaseManager(_ctx context.Context, _releaseFunc ResourceReleaseFunction) (*ErrorReleaseManagement, error) { + if _ctx == nil || _releaseFunc == nil { + return nil, fmt.Errorf("context or closeFunc is nil") + } + + ErrorM := &ErrorReleaseManagement{ + ErrChan: make(chan error, 20), + errEndChan: make(chan struct{}), + releaseFunc: _releaseFunc, + } + ErrorM.cancelCtx, ErrorM.cancelFunc = context.WithCancel(_ctx) + ErrorM.GoCancelCtx, ErrorM.GoCancelFunc = context.WithCancel(_ctx) + + go ErrorM.errorFactory() + + return ErrorM, nil +} + +func (erm *ErrorReleaseManagement) errorFactory() { + for { + select { + case <-erm.errEndChan: + logger.Info("error management stopped") + return + case _error := <-erm.ErrChan: + if _error == nil { + continue + } + + _terror, ok := _error.(*FinalError) + if !ok { + logger.Error("plain error: %s", _error.Error()) + continue + } + + if _terror.Err != nil { + if !_terror.PrintStack && !_terror.ExitAfterPrint { + erm.output(_terror) + } else if _terror.PrintStack && !_terror.ExitAfterPrint { + logger.ErrorStack(erm.errorStackMsg(_terror.Module), _terror.Err) + } else if !_terror.PrintStack && _terror.ExitAfterPrint { + erm.output(_terror) + _terror.Cancel() + } else if _terror.PrintStack && _terror.ExitAfterPrint { + logger.ErrorStack(erm.errorStackMsg(_terror.Module), _terror.Err) + _terror.Cancel() + } + } + } + } +} + +func (erm *ErrorReleaseManagement) ResourceRelease() { + erm.releaseFunc() + + erm.GoCancelFunc() + + erm.Wg.Wait() + + close(erm.errEndChan) + close(erm.ErrChan) + + time.Sleep(100 * time.Millisecond) +} + +/* +@severity: debug info warn error + +@err: 最终生成的error + +@exit_after_print: 打印完异常日志后是否结束主程序 + +@print_stack: 是否打印异常日志错误链,打印错误链时默认severity为error +*/ +func (erm *ErrorReleaseManagement) ErrorTransmit(_module, _severity string, _err error, _exit_after_print, _print_stack bool) { + defer func() { + if r := recover(); r != nil { + logger.Error("(ErrorTransmit)send on closed channel: %+v", r) + } + }() + + if _exit_after_print { + ctx, cancel := context.WithCancel(erm.cancelCtx) + erm.ErrChan <- &FinalError{ + Err: _err, + Cancel: cancel, + Module: _module, + Severity: _severity, + PrintStack: _print_stack, + ExitAfterPrint: _exit_after_print, + } + <-ctx.Done() + erm.ResourceRelease() + os.Exit(1) + } + + erm.ErrChan <- &FinalError{ + Err: _err, + Cancel: nil, + Module: _module, + Severity: _severity, + PrintStack: _print_stack, + ExitAfterPrint: _exit_after_print, + } +} + +func (erm *ErrorReleaseManagement) logFormat(_err error, _module string) string { + if len(_module) > 10 { + _module = _module[:10] + } + log := fmt.Sprintf("%v %s %-10s %s %+v", + time.Now().Format("2006-01-02 15:04:05"), + green, _module, reset, + _err.Error(), + ) + return log +} + +func (erm *ErrorReleaseManagement) errorStackMsg(_module string) string { + if len(_module) > 10 { + _module = _module[:10] + } + return fmt.Sprintf("%v %s %-10s %s", + time.Now().Format("2006-01-02 15:04:05"), + green, _module, reset, + ) +} + +func (erm *ErrorReleaseManagement) output(_err *FinalError) { + switch _err.Severity { + case "debug": + logger.Debug(erm.logFormat(errors.Cause(_err.Err), _err.Module)) + case "info": + logger.Info(erm.logFormat(errors.Cause(_err.Err), _err.Module)) + case "warn": + logger.Warn(erm.logFormat(errors.Cause(_err.Err), _err.Module)) + case "error": + logger.Error(erm.logFormat(errors.Cause(_err.Err), _err.Module)) + default: + logger.Error(erm.logFormat(errors.Cause(_err.Err), _err.Module)) + } +} diff --git a/cmd/server/signal/signalMonitor.go b/cmd/server/signal/signalMonitor.go new file mode 100644 index 0000000..8d3ac64 --- /dev/null +++ b/cmd/server/signal/signalMonitor.go @@ -0,0 +1,34 @@ +/* + * Copyright (c) KylinSoft Co., Ltd. 2024.All rights reserved. + * PilotGo-plugin-logs licensed under the Mulan Permissive Software License, Version 2. + * See LICENSE file for more details. + * Author: Wangjunqi123 + * Date: Mon Dec 16 08:43:58 2024 +0800 + */ +package signal + +import ( + "os" + "os/signal" + "syscall" + + "gitee.com/openeuler/PilotGo-plugin-logs/cmd/server/global" + "gitee.com/openeuler/PilotGo/sdk/logger" + "github.com/pkg/errors" +) + +func SignalMonitoring() { + ch := make(chan os.Signal, 1) + + signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) + for s := range ch { + switch s { + case syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT: + global.ERManager.ErrorTransmit("signal", "info", errors.Errorf("signal interrupt: %s", s.String()), false, false) + global.ERManager.ResourceRelease() + os.Exit(1) + default: + logger.Warn("unknown signal-> %s\n", s.String()) + } + } +} diff --git a/cmd/server/webserver/engine.go b/cmd/server/webserver/engine.go new file mode 100644 index 0000000..baf9616 --- /dev/null +++ b/cmd/server/webserver/engine.go @@ -0,0 +1,129 @@ +/* + * Copyright (c) KylinSoft Co., Ltd. 2024.All rights reserved. + * PilotGo-plugin-logs licensed under the Mulan Permissive Software License, Version 2. + * See LICENSE file for more details. + * Author: Wangjunqi123 + * Date: Mon Dec 16 08:43:58 2024 +0800 + */ +package webserver + +import ( + "context" + "net/http" + "strings" + "time" + + "gitee.com/openeuler/PilotGo-plugin-logs/cmd/server/conf" + "gitee.com/openeuler/PilotGo-plugin-logs/cmd/server/global" + "gitee.com/openeuler/PilotGo-plugin-logs/cmd/server/pluginclient" + "gitee.com/openeuler/PilotGo-plugin-logs/cmd/server/webserver/frontendResource" + "gitee.com/openeuler/PilotGo-plugin-logs/cmd/server/webserver/middleware" + "gitee.com/openeuler/PilotGo/sdk/logger" + "github.com/gin-gonic/gin" + "github.com/pkg/errors" +) + +func InitWebServer() { + if pluginclient.Global_Client == nil { + logger.Fatal("Global_Client is nil") + } + + engine := gin.New() + engine.Use(gin.Recovery(), middleware.Logger([]string{ + "/plugin_manage/bind", + "/", + })) + gin.SetMode(gin.ReleaseMode) + pluginclient.Global_Client.RegisterHandlers(engine) + pluginRouter(engine) + proxyRouter(engine) + frontendResource.StaticRouter(engine) + + web := &http.Server{ + Addr: conf.Global_Config.Logs.Addr, + Handler: engine, + } + + global.ERManager.Wg.Add(1) + go func() { + global.ERManager.ErrorTransmit("webserver", "info", errors.Errorf("logs server started on %s", conf.Global_Config.Logs.Addr), false, false) + if conf.Global_Config.Logs.Https_enabled { + if err := web.ListenAndServeTLS(conf.Global_Config.Logs.CertFile, conf.Global_Config.Logs.KeyFile); err != nil { + if strings.Contains(err.Error(), "Server closed") { + err = errors.New(err.Error()) + global.ERManager.ErrorTransmit("webserver", "info", err, false, false) + return + } + err = errors.Errorf("%s, addr: %s", err.Error(), conf.Global_Config.Logs.Addr) + global.ERManager.ErrorTransmit("webserver", "error", err, true, true) + } + } + if err := web.ListenAndServe(); err != nil { + if strings.Contains(err.Error(), "Server closed") { + err = errors.New(err.Error()) + global.ERManager.ErrorTransmit("webserver", "info", err, false, false) + return + } + err = errors.New(err.Error()) + global.ERManager.ErrorTransmit("webserver", "error", err, true, true) + } + }() + + go func() { + defer global.ERManager.Wg.Done() + + <-global.ERManager.GoCancelCtx.Done() + + global.ERManager.ErrorTransmit("webserver", "info", errors.New("shutting down web server..."), false, false) + + ctx, cancel := context.WithTimeout(global.RootCtx, 1*time.Second) + defer cancel() + + if err := web.Shutdown(ctx); err != nil { + global.ERManager.ErrorTransmit("webserver", "error", errors.Errorf("web server shutdown error: %s", err.Error()), false, false) + } else { + global.ERManager.ErrorTransmit("webserver", "info", errors.New("web server stopped"), false, false) + } + }() + + time.Sleep(100 * time.Millisecond) + pluginclient.Global_Client.Wait4Bind() +} + +func pluginRouter(_engine *gin.Engine) { + pilotgoApi := _engine.Group("/plugin/logs/api") + { + pilotgoApi.GET("/ip_list", GetIpListHandle) + + pilotgoApi.POST("/runcommand", RunCommandHandle) + } +} + +func proxyRouter(_engine *gin.Engine) { + _engine.GET("/ws/proxy", WebsocketProxyHandle) +} + +// func testRouter(_engine *gin.Engine) { +// _engine.PUT("/files/:filename", func(_ctx *gin.Context) { +// filename, ok := _ctx.Params.Get("filename") +// if !ok { +// _ctx.JSON(http.StatusBadRequest, "path param error") +// return +// } + +// file, err := os.Create(fmt.Sprintf("/home/wjq/%s", filename)) +// if err != nil { +// _ctx.JSON(http.StatusBadRequest, fmt.Sprintf("fail to create file: %s, %s", filename, err.Error())) +// return +// } +// defer file.Close() + +// _, err = io.Copy(file, _ctx.Request.Body) +// if err != nil { +// _ctx.JSON(http.StatusBadRequest, fmt.Sprintf("io.copy error: %s", err.Error())) +// return +// } + +// _ctx.JSON(http.StatusCreated, "") +// }) +// } diff --git a/cmd/server/webserver/frontendResource/static.go b/cmd/server/webserver/frontendResource/static.go new file mode 100644 index 0000000..89fe006 --- /dev/null +++ b/cmd/server/webserver/frontendResource/static.go @@ -0,0 +1,35 @@ +/* + * Copyright (c) KylinSoft Co., Ltd. 2024.All rights reserved. + * PilotGo-plugin-logs licensed under the Mulan Permissive Software License, Version 2. + * See LICENSE file for more details. + * Author: Wangjunqi123 + * Date: Mon Dec 16 08:43:58 2024 +0800 + */ +//go:build !production +// +build !production + +package frontendResource + +import ( + "net/http" + "strings" + + "github.com/gin-gonic/gin" +) + +func StaticRouter(router *gin.Engine) { + static := router.Group("/plugin/logs") + { + static.Static("/assets", "../web/dist/assets") + static.StaticFile("/", "../web/dist/index.html") + + // 解决页面刷新404的问题 + router.NoRoute(func(c *gin.Context) { + if !strings.HasPrefix(c.Request.RequestURI, "/plugin/logs/api") { + c.File("../web/dist/index.html") + return + } + c.AbortWithStatus(http.StatusNotFound) + }) + } +} diff --git a/cmd/server/webserver/frontendResource/staticPro.go b/cmd/server/webserver/frontendResource/staticPro.go new file mode 100644 index 0000000..0af75c8 --- /dev/null +++ b/cmd/server/webserver/frontendResource/staticPro.go @@ -0,0 +1,54 @@ +/* + * Copyright (c) KylinSoft Co., Ltd. 2024.All rights reserved. + * PilotGo-plugin-logs licensed under the Mulan Permissive Software License, Version 2. + * See LICENSE file for more details. + * Author: Wangjunqi123 + * Date: Mon Dec 16 08:43:58 2024 +0800 + */ +//go:build production +// +build production + +package frontendResource + +import ( + "embed" + "errors" + "io/fs" + "mime" + "net/http" + "strings" + + "gitee.com/openeuler/PilotGo-plugin-logs/cmd/server/global" + "github.com/gin-gonic/gin" +) + +//go:embed assets index.html +var StaticFiles embed.FS + +func StaticRouter(router *gin.Engine) { + sf, err := fs.Sub(StaticFiles, "assets") + if err != nil { + global.ERManager.ErrorTransmit("webserver", "warn", errors.New(err.Error()), false, false) + return + } + + mime.AddExtensionType(".js", "application/javascript") + static := router.Group("/plugin/logs") + { + static.StaticFS("/assets", http.FS(sf)) + static.GET("/", func(c *gin.Context) { + c.FileFromFS("/", http.FS(StaticFiles)) + }) + + } + + // 解决页面刷新404的问题 + router.NoRoute(func(c *gin.Context) { + if !strings.HasPrefix(c.Request.RequestURI, "/plugin/logs/api") { + c.FileFromFS("/", http.FS(StaticFiles)) + return + } + c.AbortWithStatus(http.StatusNotFound) + }) + +} diff --git a/cmd/server/webserver/handle.go b/cmd/server/webserver/handle.go new file mode 100644 index 0000000..9fdf110 --- /dev/null +++ b/cmd/server/webserver/handle.go @@ -0,0 +1,123 @@ +/* + * Copyright (c) KylinSoft Co., Ltd. 2024.All rights reserved. + * PilotGo-plugin-logs licensed under the Mulan Permissive Software License, Version 2. + * See LICENSE file for more details. + * Author: Wangjunqi123 + * Date: Mon Dec 16 08:43:58 2024 +0800 + */ +package webserver + +import ( + "net/http" + "strings" + + "gitee.com/openeuler/PilotGo-plugin-logs/cmd/server/global" + "gitee.com/openeuler/PilotGo-plugin-logs/cmd/server/pluginclient" + "gitee.com/openeuler/PilotGo-plugin-logs/cmd/server/webserver/proxy" + "gitee.com/openeuler/PilotGo/sdk/common" + "gitee.com/openeuler/PilotGo/sdk/response" + "github.com/gin-gonic/gin" + "github.com/pkg/errors" +) + +var ResultOptMsg = []string{"安装成功", "卸载成功"} + +const ( + CommandInstall_Cmd = "yum install -y PilotGo-plugin-logs-agent && (echo '安装成功'; systemctl start PilotGo-plugin-logs-agent) || echo '安装失败'" + CommandRemove_Cmd = "yum remove -y PilotGo-plugin-logs-agent && echo '卸载成功' || echo '卸载失败'" +) + +// 运行远程命令安装、卸载exporter +func RunCommandHandle(_ctx *gin.Context) { + d := &struct { + MachineUUIDs []string `json:"uuids"` + }{} + if err := _ctx.ShouldBind(d); err != nil { + response.Fail(_ctx, nil, "parameter error") + global.ERManager.ErrorTransmit("webserver", "error", errors.Errorf("fail to bind batch param: %s", err.Error()), false, false) + return + } + + var command string + command_type := _ctx.Query("type") + if command_type == "install" { + command = CommandInstall_Cmd + } else if command_type == "uninstall" { + command = CommandRemove_Cmd + } else { + response.Fail(_ctx, nil, "请重新检查命令参数type") + global.ERManager.ErrorTransmit("webserver", "error", errors.New("fail to resolve query param"), false, false) + return + } + + run_result := func(result []*common.CmdResult) { + for _, res := range result { + switch command_type { + case "install": + if !strings.Contains(res.Stdout, "成功") && !strings.Contains(res.Stdout, "success") { + global.ERManager.ErrorTransmit("webserver", "error", errors.Errorf("failed runcommand result(%s, %d): %s", res.MachineIP, res.RetCode, res.Stderr), false, false) + } + case "uninstall": + if !strings.Contains(res.Stdout, "成功") && !strings.Contains(res.Stdout, "success") { + global.ERManager.ErrorTransmit("webserver", "error", errors.Errorf("failed runcommand result(%s, %d): %s", res.MachineIP, res.RetCode, res.Stderr), false, false) + } + } + } + } + dd := &common.Batch{ + MachineUUIDs: d.MachineUUIDs, + } + err := pluginclient.Global_Client.RunCommandAsync(dd, command, run_result) + if err != nil { + response.Fail(_ctx, nil, err.Error()) + global.ERManager.ErrorTransmit("webserver", "error", errors.Errorf("fail to %s PilotGo-plugin-logs-agent to %v: %s", command_type, dd.MachineUUIDs, err.Error()), false, false) + return + } + response.Success(_ctx, nil, "指令下发完成") +} + +func GetIpListHandle(_ctx *gin.Context) { + if pluginclient.Global_Client == nil { + err := errors.New("Global_Client is nil") + response.Fail(_ctx, nil, err.Error()) + global.ERManager.ErrorTransmit("webserver", "error", err, true, false) + return + } + + // ttcode + // if cookie, err := _ctx.Request.Cookie("ws_session_id"); err != nil { + // fmt.Printf(">>>cookie: %+v\n", err.Error()) + // } else { + // fmt.Printf(">>>cookie: %+v\n", cookie) + // } + + machine_list, err := pluginclient.Global_Client.MachineList() + if err != nil { + err = errors.New(err.Error()) + response.Fail(_ctx, nil, err.Error()) + global.ERManager.ErrorTransmit("agentmanager", "error", err, false, false) + } + + machine_ip_list := []string{} + for _, m := range machine_list { + if global.IsIPandPORTValid(m.IP, "9995") { + machine_ip_list = append(machine_ip_list, m.IP) + } else { + continue + } + } + response.Success(_ctx, machine_ip_list, "") +} + +func WebsocketProxyHandle(_ctx *gin.Context) { + wsproxy := proxy.NewWebsocketForwardProxy() + wsproxy.ID = _ctx.Request.Header.Get("clientId") + wsproxy.Active = true + if proxy.WebsocketProxyManager == nil { + global.ERManager.ErrorTransmit("webserver", "error", errors.New("WebsocketProxyManager is nil"), true, false) + _ctx.JSON(http.StatusInternalServerError, "WebsocketProxyManager is nil") + return + } + proxy.WebsocketProxyManager.Add(wsproxy.ID, wsproxy) + wsproxy.ServeHTTP(_ctx.Writer, _ctx.Request) +} diff --git a/cmd/server/webserver/middleware/logger.go b/cmd/server/webserver/middleware/logger.go new file mode 100644 index 0000000..7b834c3 --- /dev/null +++ b/cmd/server/webserver/middleware/logger.go @@ -0,0 +1,65 @@ +/* + * Copyright (c) KylinSoft Co., Ltd. 2024.All rights reserved. + * PilotGo-plugin-logs licensed under the Mulan Permissive Software License, Version 2. + * See LICENSE file for more details. + * Author: Wangjunqi123 + * Date: Mon Dec 16 08:43:58 2024 +0800 + */ +package middleware + +import ( + "time" + + "gitee.com/openeuler/PilotGo-plugin-logs/cmd/server/global" + "github.com/gin-gonic/gin" + "github.com/pkg/errors" +) + +func Logger(_skipPaths []string) gin.HandlerFunc { + var skip map[string]struct{} + + if len(_skipPaths) > 0 { + skip = make(map[string]struct{}, len(_skipPaths)) + + for _, path := range _skipPaths { + skip[path] = struct{}{} + } + } + + return func(c *gin.Context) { + start := time.Now() + path := c.Request.URL.Path + raw := c.Request.URL.RawQuery + + c.Next() + + if _, ok := skip[path]; !ok { + endTime := time.Now() + latency := endTime.Sub(start) + method := c.Request.Method + statusCode := c.Writer.Status() + clientIP := c.ClientIP() + errorMessage := c.Errors.ByType(gin.ErrorTypePrivate).String() + + if raw != "" { + path = path + "?" + raw + } + + if latency > time.Minute { + latency = latency.Truncate(time.Second) + } + + global.ERManager.ErrorTransmit("gin", "debug", errors.Errorf("|%3d| %-13v | %-15s |%-7s %#v", + statusCode, + latency, + clientIP, + method, + path), + false, false, + ) + if errorMessage != "" { + global.ERManager.ErrorTransmit("gin", "error", errors.New(errorMessage), false, false) + } + } + } +} diff --git a/cmd/server/webserver/proxy/TcpHijackProxy.go b/cmd/server/webserver/proxy/TcpHijackProxy.go new file mode 100644 index 0000000..be10659 --- /dev/null +++ b/cmd/server/webserver/proxy/TcpHijackProxy.go @@ -0,0 +1,86 @@ +/* + * Copyright (c) KylinSoft Co., Ltd. 2024.All rights reserved. + * PilotGo-plugin-logs licensed under the Mulan Permissive Software License, Version 2. + * See LICENSE file for more details. + * Author: Wangjunqi123 + * Date: Mon Dec 16 08:43:58 2024 +0800 + */ +package proxy + +import ( + "fmt" + "io" + "net" + "net/http" + + "gitee.com/openeuler/PilotGo/sdk/logger" +) + +type WebsocketTcpHijackProxy struct{} + +func InitWebServerTcpHijackProxy() { + logger.Info("logs websocket proxy server started on %s", "10.41.107.29:9994") + go func() { + if err := http.ListenAndServe("10.41.107.29:9994", &WebsocketTcpHijackProxy{}); err != nil { + logger.Fatal(err.Error()) + } + }() +} + +func (h *WebsocketTcpHijackProxy) ServeHTTP(_w http.ResponseWriter, _r *http.Request) { + if _r.Method == http.MethodConnect { + h.websocketConnectProxyHandle(_w, _r) + } +} + +// websocket代理:通过劫持客户端tcp连接及与目标服务器建立tcp连接的方式转发 +func (h *WebsocketTcpHijackProxy) websocketConnectProxyHandle(_w http.ResponseWriter, _r *http.Request) { + target_netconn, err := net.Dial("tcp", _r.Host) + if err != nil { + logger.Error("%d, Unable to connect to target", http.StatusServiceUnavailable) + http.Error(_w, "Unable to connect to target", http.StatusServiceUnavailable) + return + } + defer target_netconn.Close() + + _w.WriteHeader(http.StatusOK) + + hijacker, ok := _w.(http.Hijacker) + if !ok { + logger.Error("%d, Hijacking not supported", http.StatusInternalServerError) + http.Error(_w, "Hijacking not supported", http.StatusInternalServerError) + return + } + client_netconn, _, err := hijacker.Hijack() + if err != nil { + logger.Error("%d, Hijack failed", http.StatusServiceUnavailable) + client_netconn.Write([]byte(fmt.Sprintf("Unable to connect to target, code: %d", http.StatusServiceUnavailable))) + return + } + defer client_netconn.Close() + + go io.Copy(target_netconn, client_netconn) + io.Copy(client_netconn, target_netconn) +} + +// 实现两个tcp net.conn间的数据传输,暂时弃用 +func (h *WebsocketTcpHijackProxy) TransferMessages(_srcConn, _destConn net.Conn, _err_ch chan error) { + for { + buf := make([]byte, 4096) + n, err := _srcConn.Read(buf) + if err != nil { + if err == io.EOF { + continue + } + _err_ch <- err + return + } + if n > 0 { + _, err = _destConn.Write(buf[:n]) + if err != nil { + _err_ch <- err + return + } + } + } +} diff --git a/cmd/server/webserver/proxy/forwardProxy.go b/cmd/server/webserver/proxy/forwardProxy.go new file mode 100644 index 0000000..494da4c --- /dev/null +++ b/cmd/server/webserver/proxy/forwardProxy.go @@ -0,0 +1,475 @@ +/* + * Copyright (c) KylinSoft Co., Ltd. 2024.All rights reserved. + * PilotGo-plugin-logs licensed under the Mulan Permissive Software License, Version 2. + * See LICENSE file for more details. + * Author: Wangjunqi123 + * Date: Mon Dec 16 08:43:58 2024 +0800 + */ +package proxy + +import ( + "context" + "crypto/tls" + "encoding/json" + "fmt" + "net" + "net/http" + "strings" + "sync" + "time" + + "gitee.com/openeuler/PilotGo-plugin-logs/cmd/public" + "gitee.com/openeuler/PilotGo-plugin-logs/cmd/server/global" + "gitee.com/openeuler/PilotGo/sdk/utils/httputils" + "github.com/gorilla/websocket" + "github.com/pkg/errors" +) + +var ( + DefaultUpgrader = &websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + CheckOrigin: func(r *http.Request) bool { + return true + }, + } + + DefaultDialer = &websocket.Dialer{ + Proxy: nil, + HandshakeTimeout: 45 * time.Second, + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + } + + err error +) + +type WebsocketError struct { + Code int + SrcConn *websocket.Conn + DstConn *websocket.Conn + SingleConn *websocket.Conn + Text string +} + +const ( + WebsocketProxyReadError int = iota + WebsocketProxyWriteError + WebsocketProxySingleError +) + +func (we *WebsocketError) Error() string { + str := "" + switch we.Code { + case WebsocketProxyReadError: + str = fmt.Sprintf("websocketerror(read): %s", we.Text) + case WebsocketProxyWriteError: + str = fmt.Sprintf("websocketerror(write): %s", we.Text) + case WebsocketProxySingleError: + str = fmt.Sprintf("websocketerror: %s", we.Text) + } + return str +} + +type WebsocketForwardProxy struct { + ID string + + Active bool + + targetURL string + + Upgrader *websocket.Upgrader + + Dialer *websocket.Dialer + + errChan chan error + errEndChan chan struct{} + + client_wsconn *websocket.Conn + target_wsconn *websocket.Conn + + client_closemsg string + target_closemsg string + + wg sync.WaitGroup + once sync.Once + + clientWriteMutex sync.Mutex + clientReadMutex sync.Mutex + targetWriteMutex sync.Mutex + targetReadMutex sync.Mutex + + CancelCtx context.Context + CancelFunc context.CancelFunc + + request *http.Request + responseWriter http.ResponseWriter +} + +func NewWebsocketForwardProxy() *WebsocketForwardProxy { + ctx, cancel := context.WithCancel(WebsocketProxyCtx) + return &WebsocketForwardProxy{ + Upgrader: DefaultUpgrader, + Dialer: DefaultDialer, + errChan: make(chan error, 1), + errEndChan: make(chan struct{}, 1), + client_closemsg: "", + target_closemsg: "", + CancelCtx: ctx, + CancelFunc: cancel, + } +} + +// websocket代理:双端websocket连接转发 +func (w *WebsocketForwardProxy) ServeHTTP(_w http.ResponseWriter, _r *http.Request) { + global.ERManager.ErrorTransmit("webserver", "debug", errors.Errorf( + "r.method: %v, r.proto: %v, r.host: %v, r.header: %+v, r.URL.scheme: %v, r.URL.host: %v, r.URL.path: %v", + _r.Method, _r.Proto, _r.Host, _r.Header, _r.URL.Scheme, _r.URL.Host, _r.URL.Path), false, false) + + w.responseWriter = _w + w.request = _r + + w.client_wsconn, err = w.Upgrader.Upgrade(_w, _r, nil) + if err != nil { + global.ERManager.ErrorTransmit("webserver", "error", errors.Errorf("failed to upgrade client connection to WebSocket: %s", err.Error()), false, false) + http.Error(_w, fmt.Sprintf("failed to upgrade client connection to WebSocket: %s", err.Error()), http.StatusBadGateway) + w.Close(true, false, false) + return + } + + if err := w.readMessageAgentAddr(); err != nil { + global.ERManager.ErrorTransmit("webserver", "error", errors.Wrap(err, " "), false, false) + w.client_closemsg = errors.Cause(err).Error() + w.Close(true, false, false) + return + } + + if err := w.dialTarget(_r); err != nil { + global.ERManager.ErrorTransmit("webserver", "error", errors.Wrap(err, " "), false, false) + w.client_closemsg = errors.Cause(err).Error() + w.Close(true, false, false) + return + } + + w.wg.Add(1) + go w.writeMessage2Client(public.ConnectedMsg) + go w.processError() +} + +func (w *WebsocketForwardProxy) dialTarget(_r *http.Request) error { + w.CancelCtx, w.CancelFunc = context.WithCancel(WebsocketProxyCtx) + w.target_wsconn, _, err = w.Dialer.Dial(w.targetURL, w.targetDirector(_r)) + if err != nil { + return errors.Errorf("dial to target WebSocket failed: %s", err.Error()) + } + + go w.transferMessages(w.client_wsconn, w.target_wsconn, true) + go w.transferMessages(w.target_wsconn, w.client_wsconn, false) + return nil +} + +func (w *WebsocketForwardProxy) processError() { + for { + select { + case <-w.errEndChan: + global.ERManager.ErrorTransmit("webserver", "info", errors.New("processError done"), false, false) + return + case err := <-w.errChan: + if err == nil { + continue + } + global.ERManager.ErrorTransmit("webserver", "error", errors.New(err.Error()), false, false) + wserr := err.(*WebsocketError) + w.client_closemsg = wserr.Text + w.target_closemsg = wserr.Text + } + } +} + +func (w *WebsocketForwardProxy) targetDirector(_r *http.Request) http.Header { + header := http.Header{} + + header.Set("Host", _r.Host) + + if clientIP, clientPort, err := net.SplitHostPort(_r.RemoteAddr); err == nil { + + if prior, ok := _r.Header["X-Forwarded-For"]; ok { + clientIP = strings.Join(prior, ", ") + ", " + clientIP + } + header.Set("X-Forwarded-For", clientIP+":"+clientPort) + } + + header.Set("X-Forwarded-Proto", "http") + if _r.TLS != nil { + header.Set("X-Forwarded-Proto", "https") + } + + header.Set("clientId", w.ID) + return header +} + +func (w *WebsocketForwardProxy) ResponseDirector(_resp *http.Response, _header *http.Header) { + if hdr := _resp.Header.Get("Sec-Websocket-Protocol"); hdr != "" { + _header.Set("Sec-Websocket-Protocol", hdr) + } + if hdr := _resp.Header.Get("Set-Cookie"); hdr != "" { + _header.Set("Set-Cookie", hdr) + } +} + +func (w *WebsocketForwardProxy) transferMessages(_srcConn, _dstConn *websocket.Conn, isC2T bool) { + defer func() { + global.ERManager.ErrorTransmit("webserver", "info", errors.Errorf("transferMessages goroutine done(%v->%v), forward: %v", _srcConn.RemoteAddr().String(), _dstConn.RemoteAddr().String(), isC2T), false, false) + + if r := recover(); r != nil { + global.ERManager.ErrorTransmit("webserver", "warn", errors.Errorf("(transfermessages)send on closed channel: %+v", r), false, false) + } + }() + + for { + select { + case <-w.CancelCtx.Done(): + w.errChan <- &WebsocketError{ + Code: WebsocketProxySingleError, + SrcConn: _srcConn, + DstConn: _dstConn, + Text: fmt.Sprintf("transferMessages goroutine exit(%v->%v), context canceled, forward: %v", _srcConn.RemoteAddr().String(), _dstConn.RemoteAddr().String(), isC2T), + } + return + default: + if isC2T { + w.clientReadMutex.Lock() + } else { + w.targetReadMutex.Lock() + } + messageType, message, err := _srcConn.ReadMessage() + if err != nil { + if websocket.IsCloseError(err, websocket.CloseAbnormalClosure, websocket.CloseNormalClosure) { + w.errChan <- &WebsocketError{ + Code: WebsocketProxyReadError, + SrcConn: _srcConn, + DstConn: _dstConn, + Text: fmt.Sprintf("websocket src conn %s closed(%v->%v): %s", _srcConn.RemoteAddr().String(), _srcConn.RemoteAddr().String(), _dstConn.RemoteAddr().String(), err.Error()), + } + // 远端关闭连接,释放资源 + w.Close(true, false, false) + if isC2T { + w.clientReadMutex.Unlock() + } else { + w.targetReadMutex.Unlock() + } + return + } + w.errChan <- &WebsocketError{ + Code: WebsocketProxyReadError, + SrcConn: _srcConn, + DstConn: _dstConn, + Text: fmt.Sprintf("error while reading message(%v->%v, msgType: %d): %s, %s", _srcConn.RemoteAddr().String(), _dstConn.RemoteAddr().String(), messageType, err.Error(), message), + } + if isC2T { + w.clientReadMutex.Unlock() + } else { + w.targetReadMutex.Unlock() + } + return + } + if isC2T { + w.clientReadMutex.Unlock() + } else { + w.targetReadMutex.Unlock() + } + + if isC2T { + jmsg := &public.JMessage{} + if err := json.Unmarshal(message, jmsg); message != nil && err != nil { + global.ERManager.ErrorTransmit("journald", "error", errors.Errorf("error while unmarshalling json options: %s, jmsg: %+v", err.Error(), string(message)), false, true) + w.Close(true, false, false) + return + } + switch jmsg.Type { + case public.AgentAddrMsg: + w.Close(false, false, true) + ishttp, err := httputils.ServerIsHttp("http://" + jmsg.Data.(string)) + if err != nil { + w.writeMessage2Client(public.DialFailedMsg) + global.ERManager.ErrorTransmit("journald", "error", errors.Errorf("fail to detect remote http/https: %s", err.Error()), false, true) + w.Close(true, false, false) + return + } + if ishttp { + w.targetURL = fmt.Sprintf("ws://%s/ws/entry", jmsg.Data.(string)) + } + if !ishttp { + w.targetURL = fmt.Sprintf("wss://%s/ws/entry", jmsg.Data.(string)) + } + if err := w.dialTarget(w.request); err != nil { + global.ERManager.ErrorTransmit("journald", "error", errors.Wrap(err, " "), false, true) + w.Close(true, false, false) + } + w.wg.Add(1) + go w.writeMessage2Client(public.ConnectedMsg) + return + } + } + + if isC2T { + w.targetWriteMutex.Lock() + } else { + w.clientWriteMutex.Lock() + } + if err := _dstConn.WriteMessage(messageType, message); err != nil { + w.errChan <- &WebsocketError{ + Code: WebsocketProxyWriteError, + SrcConn: _srcConn, + DstConn: _dstConn, + Text: fmt.Sprintf("error while writing message(%v->%v): %s", _srcConn.RemoteAddr().String(), _dstConn.RemoteAddr().String(), err.Error()), + } + if isC2T { + w.targetWriteMutex.Lock() + } else { + w.clientWriteMutex.Lock() + } + return + } + if isC2T { + w.targetWriteMutex.Unlock() + } else { + w.clientWriteMutex.Unlock() + } + } + } +} + +// 向客户端发送已与目标服务器建立连接消息 +func (w *WebsocketForwardProxy) writeMessage2Client(_jmsg_type int) { + defer w.wg.Done() + defer global.ERManager.ErrorTransmit("webserver", "info", errors.Errorf("writeMessage2Client goroutine done, jmsg type: %d", _jmsg_type), false, false) + + for { + select { + case <-w.CancelCtx.Done(): + w.errChan <- &WebsocketError{ + Code: WebsocketProxySingleError, + DstConn: w.client_wsconn, + SrcConn: w.target_wsconn, + Text: fmt.Sprintf("writeMessage2Client goroutine, jmsg type: %d, context canceled", _jmsg_type), + } + return + default: + jmsg := &public.JMessage{Type: _jmsg_type} + jmsgBytes, err := json.Marshal(jmsg) + if err != nil { + w.errChan <- &WebsocketError{ + Code: WebsocketProxySingleError, + SingleConn: w.target_wsconn, + Text: fmt.Sprintf("error while marshalling json jmessage: %s", err.Error()), + } + return + } + + w.clientWriteMutex.Lock() + if err := w.client_wsconn.WriteMessage(websocket.TextMessage, jmsgBytes); err != nil { + w.errChan <- &WebsocketError{ + Code: WebsocketProxyWriteError, + SrcConn: w.target_wsconn, + DstConn: w.client_wsconn, + Text: fmt.Sprintf("error while writing message %d(%v->%v): %s", _jmsg_type, w.target_wsconn.RemoteAddr().String(), w.client_wsconn.RemoteAddr().String(), err.Error()), + } + } + w.clientWriteMutex.Unlock() + return + } + } +} + +// 从客户端读取目标服务器地址 +func (w *WebsocketForwardProxy) readMessageAgentAddr() error { + w.clientReadMutex.Lock() + _, jmsgBytes, err := w.client_wsconn.ReadMessage() + if err != nil { + if websocket.IsCloseError(err, websocket.CloseAbnormalClosure, websocket.CloseNormalClosure) { + w.clientReadMutex.Unlock() + return errors.Errorf("error while reading message: %s", err.Error()) + } + w.clientReadMutex.Unlock() + return errors.Errorf("error while reading message: %s", err.Error()) + } + w.clientReadMutex.Unlock() + + jmsg := &public.JMessage{} + if err := json.Unmarshal(jmsgBytes, jmsg); jmsgBytes != nil && err != nil { + return errors.Errorf("error while unmarshalling json jmessage: %s, %s", err.Error(), string(jmsgBytes)) + } + // 确保与client建立websocket连接之后client发送的第一条消息为AgentAddrMsg + if jmsg.Type != public.AgentAddrMsg { + return errors.Errorf("the first message must be the agent addr: %d", jmsg.Type) + } + + target_addr := jmsg.Data.(string) + ishttp, err := httputils.ServerIsHttp("http://" + target_addr) + if err != nil { + w.writeMessage2Client(public.DialFailedMsg) + return errors.Errorf("fail to detect remote http/https: %s", err.Error()) + } + if ishttp { + w.targetURL = fmt.Sprintf("ws://%s/ws/entry", target_addr) + } + if !ishttp { + w.targetURL = fmt.Sprintf("wss://%s/ws/entry", target_addr) + } + return nil +} + +func (w *WebsocketForwardProxy) Close(_close_A, _close_C, _close_T bool) { + /* + w.writeMessage2Client() + w.transferMessages() + */ + w.CancelFunc() + + if _close_A { + w.once.Do(func() { + if w.client_wsconn != nil { + w.clientWriteMutex.Lock() + if err := w.client_wsconn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, w.client_closemsg)); err != nil { + global.ERManager.ErrorTransmit("webserver", "error", errors.Errorf("write close message to client_wsconn error: %s", err.Error()), false, false) + } + w.clientWriteMutex.Unlock() + w.client_wsconn.Close() + } + if w.target_wsconn != nil { + w.targetWriteMutex.Lock() + if err := w.target_wsconn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, w.target_closemsg)); err != nil { + global.ERManager.ErrorTransmit("webserver", "error", errors.Errorf("write close message to target_wsconn error: %s", err.Error()), false, false) + } + w.targetWriteMutex.Unlock() + w.target_wsconn.Close() + } + + /* + w.writeMessageConnectedWithTarget() + */ + w.wg.Wait() + global.ERManager.ErrorTransmit("webserver", "info", errors.New("WebsocketForwardProxy all waitgroup goroutines done"), false, false) + + close(w.errEndChan) + close(w.errChan) + + w.Active = false + + time.Sleep(100 * time.Millisecond) + }) + } + + if _close_T { + if w.target_wsconn != nil { + w.targetWriteMutex.Lock() + if err := w.target_wsconn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, w.target_closemsg)); err != nil { + global.ERManager.ErrorTransmit("webserver", "error", errors.Errorf("write close message to target_wsconn error: %s", err.Error()), false, false) + } + w.targetWriteMutex.Unlock() + w.target_wsconn.Close() + } + } +} diff --git a/cmd/server/webserver/proxy/meta.go b/cmd/server/webserver/proxy/meta.go new file mode 100644 index 0000000..7da9bd4 --- /dev/null +++ b/cmd/server/webserver/proxy/meta.go @@ -0,0 +1,14 @@ +/* + * Copyright (c) KylinSoft Co., Ltd. 2024.All rights reserved. + * PilotGo-plugin-logs licensed under the Mulan Permissive Software License, Version 2. + * See LICENSE file for more details. + * Author: Wangjunqi123 + * Date: Mon Dec 16 08:43:58 2024 +0800 + */ +package proxy + +import "context" + +var WebsocketProxyCtx = context.Background() + +// var WebsocketProxy *WebsocketForwardProxy diff --git a/cmd/server/webserver/proxy/wsproxymanager.go b/cmd/server/webserver/proxy/wsproxymanager.go new file mode 100644 index 0000000..bf1fdb6 --- /dev/null +++ b/cmd/server/webserver/proxy/wsproxymanager.go @@ -0,0 +1,72 @@ +/* + * Copyright (c) KylinSoft Co., Ltd. 2024.All rights reserved. + * PilotGo-plugin-logs licensed under the Mulan Permissive Software License, Version 2. + * See LICENSE file for more details. + * Author: Wangjunqi123 + * Date: Mon Dec 16 08:43:58 2024 +0800 + */ +package proxy + +import ( + "sync" + "time" + + "gitee.com/openeuler/PilotGo-plugin-logs/cmd/server/global" + "github.com/pkg/errors" +) + +var WebsocketProxyManager *WebsocketProxyManagement + +type WebsocketProxyManagement struct { + // key: web client id + WebsocketProxyMap map[string]*WebsocketForwardProxy + + // 终止采集组件状态检测 + heartbeatDone chan struct{} + + once sync.Once +} + +func CreateWebsocketProxyManagement() { + WebsocketProxyManager = &WebsocketProxyManagement{ + WebsocketProxyMap: make(map[string]*WebsocketForwardProxy), + heartbeatDone: make(chan struct{}), + } + + go WebsocketProxyManager.heartbeatDetect() +} + +func (wpm *WebsocketProxyManagement) Add(_id string, _wsproxy *WebsocketForwardProxy) { + wpm.WebsocketProxyMap[_id] = _wsproxy +} + +func (wpm *WebsocketProxyManagement) Delete(_id string) { + delete(wpm.WebsocketProxyMap, _id) +} + +func (wpm *WebsocketProxyManagement) heartbeatDetect() { + for { + select { + case <-wpm.heartbeatDone: + return + case <-time.After(global.HeartbeatPeriod): + for _id, _jc := range wpm.WebsocketProxyMap { + if !_jc.Active { + global.ERManager.ErrorTransmit("webserver", "info", errors.Errorf("remove websocket proxy client: %s", _id), false, false) + wpm.Delete(_id) + } + } + } + } +} + +func (wpm *WebsocketProxyManagement) CloseAll() { + wpm.once.Do(func() { + close(wpm.heartbeatDone) + }) + + for id, wsproxy := range wpm.WebsocketProxyMap { + global.ERManager.ErrorTransmit("webserver", "info", errors.Errorf("shutdown websocket proxy: %s", id), false, false) + wsproxy.Close(true, false, false) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..93e1a4e --- /dev/null +++ b/go.mod @@ -0,0 +1,42 @@ +module gitee.com/openeuler/PilotGo-plugin-logs + +go 1.20 + +require ( + gitee.com/openeuler/PilotGo/sdk v0.0.0-20250121031234-c5439c613a24 + github.com/gin-gonic/gin v1.9.1 + github.com/gorilla/websocket v1.5.3 + github.com/pkg/errors v0.9.1 + gopkg.in/yaml.v2 v2.4.0 +) + +require ( + github.com/bytedance/sonic v1.9.1 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.14.1 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible // indirect + github.com/lestrrat-go/strftime v1.0.6 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.1.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.11 // indirect + golang.org/x/arch v0.3.0 // indirect + golang.org/x/crypto v0.21.0 // indirect + golang.org/x/net v0.23.0 // indirect + golang.org/x/sys v0.18.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/protobuf v1.33.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..1211062 --- /dev/null +++ b/go.sum @@ -0,0 +1,107 @@ +gitee.com/openeuler/PilotGo/sdk v0.0.0-20250121031234-c5439c613a24 h1:zy8FZHMehE/W08f6ASaz1scr5MyMy3Sm13/tiG0SaHQ= +gitee.com/openeuler/PilotGo/sdk v0.0.0-20250121031234-c5439c613a24/go.mod h1:Et2wSYAR8hj9jMnSqwtyi8fA79q63tLvPcHwySiRG+g= +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= +github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= +github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.14.1 h1:9c50NUPC30zyuKprjL3vNZ0m5oG+jU0zvx4AqHGnv4k= +github.com/go-playground/validator/v10 v10.14.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= +github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc h1:RKf14vYWi2ttpEmkA4aQ3j4u9dStX2t4M8UM6qqNsG8= +github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc/go.mod h1:kopuH9ugFRkIXf3YoqHKyrJ9YfUFsckUU9S7B+XP+is= +github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible h1:Y6sqxHMyB1D2YSzWkLibYKgg+SwmyFU9dF2hn6MdTj4= +github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible/go.mod h1:ZQnN8lSECaebrkQytbHj4xNgtg8CR7RYXnPok8e0EHA= +github.com/lestrrat-go/strftime v1.0.6 h1:CFGsDEt1pOpFNU+TJB0nhz9jl+K0hZSLE205AhTIGQQ= +github.com/lestrrat-go/strftime v1.0.6/go.mod h1:f7jQKgV5nnJpYgdEasS+/y7EsTb8ykN2z68n3TtcTaw= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= +github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= +golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/scripts/PilotGo-plugin-logs-agent.service b/scripts/PilotGo-plugin-logs-agent.service new file mode 100644 index 0000000..d51d9cb --- /dev/null +++ b/scripts/PilotGo-plugin-logs-agent.service @@ -0,0 +1,13 @@ +[Unit] +Description=PilotGo plugin logs agent +Requires=network-online.target +After=network-online.target + +[Service] +Type=simple +Restart=always +RestartSec=3s +ExecStart=/opt/PilotGo/plugin/logs/agent/PilotGo-plugin-logs-agent -conf /opt/PilotGo/plugin/logs/server + +[Install] +WantedBy=multi-user.target diff --git a/scripts/PilotGo-plugin-logs-server.service b/scripts/PilotGo-plugin-logs-server.service new file mode 100644 index 0000000..87f6845 --- /dev/null +++ b/scripts/PilotGo-plugin-logs-server.service @@ -0,0 +1,13 @@ +[Unit] +Description=PilotGo plugin logs server +Requires=network-online.target +After=network-online.target + +[Service] +Type=simple +Restart=always +RestartSec=3s +ExecStart=/opt/PilotGo/plugin/logs/server/PilotGo-plugin-logs-server -conf /opt/PilotGo/plugin/logs/server + +[Install] +WantedBy=multi-user.target diff --git a/scripts/PilotGo-plugin-logs.spec b/scripts/PilotGo-plugin-logs.spec new file mode 100644 index 0000000..69214ed --- /dev/null +++ b/scripts/PilotGo-plugin-logs.spec @@ -0,0 +1,88 @@ +%define debug_package %{nil} + +Name: PilotGo-plugin-logs +Version: 1.0.0 +Release: 1 +Summary: logs plugin for PilotGo +License: MulanPSL-2.0 +URL: https://gitee.com/openeuler/PilotGo-plugin-logs +Source0: https://gitee.com/src-openeuler/PilotGo-plugin-logs/%{name}-%{version}.tar.gz + +ExclusiveArch: x86_64 aarch64 + +BuildRequires: systemd +BuildRequires: golang +BuildRequires: nodejs +BuildRequires: npm + +%description +logs plugin for PilotGo + +%package server +Summary: PilotGo-plugin-logs server +Provides: pilotgo-plugin-logs-server = %{version}-%{release} + +%description server +PilotGo-plugin-logs server. + +%package agent +Summary: PilotGo-plugin-logs agent +Provides: pilotgo-plugin-logs-agent = %{version}-%{release} + +%description agent +PilotGo-plugin-logs agent. + +%prep +%autosetup -p1 -n %{name}-%{version} + +%build +# web +pushd web +npm run install +npm run build +popd +cp -rf web/dist/* cmd/server/webserver/frontendResource/ +# server +pushd cmd/server +GOWORK=off GO111MODULE=on go build -tags=production -o PilotGo-plugin-logs-server main.go +popd +# agent +pushd cmd/agent +GOWORK=off GO111MODULE=on go build -o PilotGo-plugin-logs-agent main.go +popd + +%install +mkdir -p %{buildroot}/opt/PilotGo/plugin/logs/server/log +mkdir -p %{buildroot}/opt/PilotGo/plugin/logs/agent/log +# server +install -D -m 0755 %{_builddir}/PilotGo-plugin-logs/cmd/server/PilotGo-plugin-logs-server %{buildroot}/opt/PilotGo/plugin/logs/server +install -D -m 0644 %{_builddir}/PilotGo-plugin-logs/cmd/server/logs_server.yaml.template %{buildroot}/opt/PilotGo/plugin/logs/server/logs_server.yaml +install -D -m 0644 %{_builddir}/PilotGo-plugin-logs/scripts/PilotGo-plugin-logs-server.service %{buildroot}%{_unitdir}/PilotGo-plugin-logs-server.service +# agent +install -D -m 0755 %{_builddir}/PilotGo-plugin-logs/cmd/agent/PilotGo-plugin-logs-agent %{buildroot}/opt/PilotGo/plugin/logs/agent +install -D -m 0644 %{_builddir}/PilotGo-plugin-logs/cmd/agent/logs_agent.yaml.template %{buildroot}/opt/PilotGo/plugin/logs/agent/logs_agent.yaml +install -D -m 0644 %{_builddir}/PilotGo-plugin-logs/scripts/PilotGo-plugin-logs-agent.service %{buildroot}%{_unitdir}/PilotGo-plugin-logs-agent.service + +%files server +%dir /opt/PilotGo +%dir /opt/PilotGo/plugin +%dir /opt/PilotGo/plugin/logs +%dir /opt/PilotGo/plugin/logs/server +%dir /opt/PilotGo/plugin/logs/server/log +/opt/PilotGo/plugin/logs/server/PilotGo-plugin-logs-server +/opt/PilotGo/plugin/logs/server/logs_server.yaml +%{_unitdir}/PilotGo-plugin-logs-server.service + +%files agent +%dir /opt/PilotGo +%dir /opt/PilotGo/plugin +%dir /opt/PilotGo/plugin/logs +%dir /opt/PilotGo/plugin/logs/agent +%dir /opt/PilotGo/plugin/logs/agent/log +/opt/PilotGo/plugin/logs/agent/PilotGo-plugin-logs-agent +/opt/PilotGo/plugin/logs/agent/logs_agent.yaml +%{_unitdir}/PilotGo-plugin-logs-agent.service + +%changelog +* Fri Feb 28 2025 wangjunqi - 1.0.0-1 +- initialize diff --git a/web/.gitignore b/web/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/web/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/web/README.md b/web/README.md new file mode 100644 index 0000000..0bfecb0 --- /dev/null +++ b/web/README.md @@ -0,0 +1,9 @@ +# Vue 3 + TypeScript + Vite + +This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 ` + + diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..195d5f9 --- /dev/null +++ b/web/package.json @@ -0,0 +1,24 @@ +{ + "name": "web", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "axios": "^1.7.2", + "element-plus": "^2.9.0", + "pinia": "^2.2.8", + "vue": "^3.4.21" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.4", + "sass": "^1.82.0", + "typescript": "^5.2.2", + "vite": "^5.2.0", + "vue-tsc": "^2.0.6" + } +} diff --git a/web/public/vite.svg b/web/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/web/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/src/App.vue b/web/src/App.vue new file mode 100644 index 0000000..1ad188e --- /dev/null +++ b/web/src/App.vue @@ -0,0 +1,16 @@ + + + + + diff --git a/web/src/api/log.ts b/web/src/api/log.ts new file mode 100644 index 0000000..d9dc595 --- /dev/null +++ b/web/src/api/log.ts @@ -0,0 +1,15 @@ +/* + * Copyright (c) KylinSoft Co., Ltd. 2024.All rights reserved. + * PilotGo-plugin-logs licensed under the Mulan Permissive Software License, Version 2. + * See LICENSE file for more details. + * Author: Wangjunqi123 + * Date: Mon Dec 16 08:43:58 2024 +0800 + */ +import request from "./request"; + +export function getIpList() { + return request({ + url: "/plugin/logs/api/ip_list", + method: "get" + }) +} \ No newline at end of file diff --git a/web/src/api/request.ts b/web/src/api/request.ts new file mode 100644 index 0000000..2e53713 --- /dev/null +++ b/web/src/api/request.ts @@ -0,0 +1,41 @@ +/* + * Copyright (c) KylinSoft Co., Ltd. 2024.All rights reserved. + * PilotGo-plugin-logs licensed under the Mulan Permissive Software License, Version 2. + * See LICENSE file for more details. + * Author: Wangjunqi123 + * Date: Mon Dec 16 08:43:58 2024 +0800 + */ +import axios from 'axios'; + +// 1.创建axios实例 +const request = axios.create({ + baseURL: '', + timeout: 5000, + headers: { + 'Content-Type': 'application/json', + } +}); + +// 2.1添加请求拦截器 +request.interceptors.request.use( + (config) => { + return config; + }, + (error) => { + return Promise.reject(error); + }, +); + +// 2.2添加响应拦截器 +request.interceptors.response.use( + (response: any) => { + return response; + }, + (error) => { + if (error.response) { + return Promise.reject(error.response.data); + } + }, +); + +export default request; diff --git a/web/src/assets/vue.svg b/web/src/assets/vue.svg new file mode 100644 index 0000000..770e9d3 --- /dev/null +++ b/web/src/assets/vue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/src/main.ts b/web/src/main.ts new file mode 100644 index 0000000..9f50044 --- /dev/null +++ b/web/src/main.ts @@ -0,0 +1,31 @@ +/* + * Copyright (c) KylinSoft Co., Ltd. 2024.All rights reserved. + * PilotGo-plugin-logs licensed under the Mulan Permissive Software License, Version 2. + * See LICENSE file for more details. + * Author: Wangjunqi123 + * Date: Mon Dec 16 08:43:58 2024 +0800 + */ +import { createApp } from 'vue' +import './style.scss' +import App from './App.vue' + +import ElementPlus from 'element-plus' +import 'element-plus/dist/index.css' +import * as ElementPlusIconsVue from '@element-plus/icons-vue' +import { createPinia } from 'pinia' + +import { useLogStore } from './stores/log' + +const app = createApp(App) +const pinia = createPinia() +app.use(pinia) +app.use(ElementPlus) +for (const [key, component] of Object.entries(ElementPlusIconsVue)) { + app.component(key, component) +} +app.mount('#app') + +// 监听应用卸载生命周期 +window.addEventListener('unmount',() => { + useLogStore().clientId = 0; +}) \ No newline at end of file diff --git a/web/src/stores/log.ts b/web/src/stores/log.ts new file mode 100644 index 0000000..51bd278 --- /dev/null +++ b/web/src/stores/log.ts @@ -0,0 +1,31 @@ +/* + * Copyright (c) KylinSoft Co., Ltd. 2024.All rights reserved. + * PilotGo-plugin-logs licensed under the Mulan Permissive Software License, Version 2. + * See LICENSE file for more details. + * Author: Wangjunqi123 + * Date: Mon Dec 16 08:43:58 2024 +0800 + */ +import { ref } from 'vue' +import { defineStore } from 'pinia' +interface LogSearchList { + ip:string, + timeRange:[Date,Date]; + level:string; + service:{label:string,value:string}; + realTime:boolean; +} +export const useLogStore = defineStore('log', () => { + const search_list = ref([] as LogSearchList[]); + const ws_isOpen = ref(false); + const clientId = ref(parseInt(Math.random() * 100000+'')); // websocket标识id,初始化为随机数 + const updateLogList = (param:any) => { + let ip_index = search_list.value.findIndex(item => item.ip === param.ip); + ip_index !== -1 ? search_list.value[ip_index] = param : search_list.value.push(param); + } + + const $reset = () => { + search_list.value = []; + ws_isOpen.value = false; + } + return {clientId,ws_isOpen,search_list,updateLogList,$reset} +}) \ No newline at end of file diff --git a/web/src/style.scss b/web/src/style.scss new file mode 100644 index 0000000..23beb6f --- /dev/null +++ b/web/src/style.scss @@ -0,0 +1,69 @@ +/* 1.重置样式 */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} +html { + width: 100%; + height: 100%; + font-family: Inter, 'Helvetica Neue', Helvetica, 'PingFang SC', + 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif; +} +/* 2.盒模型 */ +body { + width: 100%; + height: 100%; + line-height: 2.4; +} + +/* 3.字体和排版 */ +h1, h2, h3, h4, h5, h6 { + font-weight: bold; +} + +p { + margin-bottom: 15px; +} + +/* 4.颜色和背景 */ +body { + background-color: #f5f5f5; + color: #333; +} + +/* 5.布局和定位 */ +.container { + width: 100%; + // max-width: 1200px; + margin: 0 auto; +} + +/* 6.表单控件 */ +input[type="text"], +input[type="email"], +textarea { + padding: 10px; + border-radius: 4px; +} + +/* 7.响应式设计 */ +@media (max-width: 768px) { + .container { + padding-left: 15px; + padding-right: 15px; + } +} + +/* 8.滚动条样式修改 */ +::-webkit-scrollbar { + width: 6px; +} +::-webkit-scrollbar-thumb { + background-color: #0003; + border-radius: 10px; + transition: all .2s ease-in-out; +} +::-webkit-scrollbar-track { + border-radius: 10px; +} \ No newline at end of file diff --git a/web/src/view/LogStream.vue b/web/src/view/LogStream.vue new file mode 100644 index 0000000..63bb683 --- /dev/null +++ b/web/src/view/LogStream.vue @@ -0,0 +1,386 @@ + + + + + + \ No newline at end of file diff --git a/web/src/view/socket.ts b/web/src/view/socket.ts new file mode 100644 index 0000000..a92d99d --- /dev/null +++ b/web/src/view/socket.ts @@ -0,0 +1,175 @@ +/* + * Copyright (c) KylinSoft Co., Ltd. 2024.All rights reserved. + * PilotGo-plugin-logs licensed under the Mulan Permissive Software License, Version 2. + * See LICENSE file for more details. + * Author: Wangjunqi123 + * Date: Mon Dec 16 08:43:58 2024 +0800 + */ +import { useLogStore } from '../stores/log' +import { ElMessage } from 'element-plus' +interface socket { + websocket: any + connectURL: string + socket_open: boolean + hearbeat_timer: any + hearbeat_interval: number + is_reonnect: boolean + reconnect_count: number + reconnect_current: number + ronnect_number: number + reconnect_timer: any + reconnect_interval: number + init: (receiveMessage: Function | null, socketUrl: String) => any + receive: (message: any) => void + heartbeat: () => void + send: (data: any, callback?: any) => void + close: () => void + reconnect: () => void +} + +let wsProtocol = window.location.protocol === 'http' ? 'ws://' : 'wss://'; +const socket: socket = { + websocket: null, + connectURL: wsProtocol+"localhost:8888/plugin/ws/logs", + // 开启标识 + socket_open: false, + // 心跳timer + hearbeat_timer: null, + // 心跳发送频率 + hearbeat_interval: 45000, + // 是否自动重连 + is_reonnect: false, + // 重连次数 + reconnect_count: 100, + // 已发起重连次数 + reconnect_current: 1, + // 网络错误提示此时 + ronnect_number: 0, + // 重连timer + reconnect_timer: null, + // 重连频率 + reconnect_interval: 5000, + + init: (receiveMessage: Function | null, _socketUrl?: String) => { + if (!('WebSocket' in window)) { + ElMessage.warning('浏览器不支持WebSocket') + return null + } + // 已经创建过连接不再重复创建 + /* if (socket.websocket) { + return socket.websocket + } */ + + socket.websocket = new WebSocket(socket.connectURL+`?clientId=${useLogStore().clientId}`) + socket.websocket.onmessage = (e: any) => { + if (receiveMessage) { + receiveMessage(e) + } + } + + socket.websocket.onclose = (e: any) => { + console.log('检测到关闭',e) + useLogStore().ws_isOpen = false; + clearInterval(socket.hearbeat_interval) + socket.socket_open = false + + // 需要重新连接 + if (socket.is_reonnect) { + socket.reconnect_timer = setTimeout(() => { + // 超过重连次数 + if (socket.reconnect_current > socket.reconnect_count) { + clearTimeout(socket.reconnect_timer) + socket.is_reonnect = false + return + } + + // 记录重连次数 + socket.reconnect_current++ + console.log("重连次数:", socket.reconnect_current) + socket.reconnect() + }, socket.reconnect_interval) + } + } + + // 连接成功 + socket.websocket.onopen = function () { + console.log('ws连接成功') + useLogStore().ws_isOpen = true; + socket.socket_open = true + socket.is_reonnect = false + // 开启心跳 + // socket.heartbeat() + } + + // 连接发生错误 + socket.websocket.onerror = function (e:any) { + console.log('连接发生错误',e) + useLogStore().ws_isOpen = false; + } + }, + + send: (data, callback = null) => { + // 开启状态直接发送 + if (socket.websocket.readyState === socket.websocket.OPEN) { + socket.websocket.send(JSON.stringify(data)) + if (callback) { + callback() + } + + // 正在开启状态,则等待1s后重新调用 + } else { + clearInterval(socket.hearbeat_timer) + /* if (socket.ronnect_number < 1) { + ElMessage({ + type: 'error', + message: 'error', + duration: 1000, + }) + } */ + socket.ronnect_number++ + } + }, + + receive: (message: any) => { + let params = JSON.parse(message.data).data; + return params + }, + + heartbeat: () => { + if (socket.hearbeat_timer) { + clearInterval(socket.hearbeat_timer) + } + + socket.hearbeat_timer = setInterval(() => { + let data = { + content: 'ping', + } + var sendDara = { + encryption_type: 'base64', + data: data, + } + socket.send(sendDara) + }, socket.hearbeat_interval) + }, + + close: () => { + clearInterval(socket.hearbeat_interval) + socket.is_reonnect = false + socket.websocket.close() + }, + + /** + * 重新连接 + */ + reconnect: () => { + console.log("进入重新连接") + if (socket.websocket && !socket.is_reonnect) { + socket.close() + } + + socket.init(null, '/event') + }, +} + +export default socket + diff --git a/web/src/view/utils.ts b/web/src/view/utils.ts new file mode 100644 index 0000000..43e268b --- /dev/null +++ b/web/src/view/utils.ts @@ -0,0 +1,65 @@ +/* + * Copyright (c) KylinSoft Co., Ltd. 2024.All rights reserved. + * PilotGo-plugin-logs licensed under the Mulan Permissive Software License, Version 2. + * See LICENSE file for more details. + * Author: Wangjunqi123 + * Date: Mon Dec 16 08:43:58 2024 +0800 + */ +/* +* 1.formatDate(new Date(1533686888 * 1000), "YYYY-MM-DD HH:ii:ss");// 2019-07-09 19:44:01 +* 2.formatDate(new Date(1562672641 * 1000), "YYYY-MM-DD 周W");//2019-07-09 周二 +*/ +//时间戳转年月 +export const formatDate = (date: any, formatStr: String) => { + let arrWeek = ['日', '一', '二', '三', '四', '五', '六'], + str = formatStr.replace(/yyyy|YYYY/, date.getFullYear()).replace(/yy|YY/, $addZero(date.getFullYear() % 100, + 2)).replace(/mm|MM/, $addZero(date.getMonth() + 1, 2)).replace(/m|M/g, date.getMonth() + 1).replace( + /dd|DD/, $addZero(date.getDate(), 2)).replace(/d|D/g, date.getDate()).replace(/hh|HH/, $addZero(date + .getHours(), 2)).replace(/h|H/g, date.getHours()).replace(/ii|II/, $addZero(date.getMinutes(), 2)) + .replace(/i|I/g, date.getMinutes()).replace(/ss|SS/, $addZero(date.getSeconds(), 2)).replace(/s|S/g, date + .getSeconds()).replace(/w/g, date.getDay()).replace(/W/g, arrWeek[date.getDay()]); + return str + + } + function $addZero(v: any, size: number) { + for (var i = 0, len: number = size - (v + "").length; i < len; i++) { + v = "0" + v + } + return v + "" + } + + // 日志等级 + export let levels = [ + { + value: '0', + label: 'emergency', + }, + { + value: '1', + label: 'alert', + }, + { + value: '2', + label: 'critical', + }, + { + value: '3', + label: 'error', + }, + { + value: '4', + label: 'warning', + }, + { + value: '5', + label: 'notice', + }, + { + value: '6', + label: 'informational', + }, + { + value: '7', + label: 'debug', + }, + ] \ No newline at end of file diff --git a/web/src/vite-env.d.ts b/web/src/vite-env.d.ts new file mode 100644 index 0000000..0630e85 --- /dev/null +++ b/web/src/vite-env.d.ts @@ -0,0 +1,8 @@ +/* + * Copyright (c) KylinSoft Co., Ltd. 2024.All rights reserved. + * PilotGo-plugin-logs licensed under the Mulan Permissive Software License, Version 2. + * See LICENSE file for more details. + * Author: Wangjunqi123 + * Date: Mon Dec 16 08:43:58 2024 +0800 + */ +/// diff --git a/web/tsconfig.json b/web/tsconfig.json new file mode 100644 index 0000000..9e03e60 --- /dev/null +++ b/web/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/web/tsconfig.node.json b/web/tsconfig.node.json new file mode 100644 index 0000000..97ede7e --- /dev/null +++ b/web/tsconfig.node.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "strict": true + }, + "include": ["vite.config.ts"] +} diff --git a/web/vite.config.ts b/web/vite.config.ts new file mode 100644 index 0000000..b013b55 --- /dev/null +++ b/web/vite.config.ts @@ -0,0 +1,26 @@ +/* + * Copyright (c) KylinSoft Co., Ltd. 2024.All rights reserved. + * PilotGo-plugin-logs licensed under the Mulan Permissive Software License, Version 2. + * See LICENSE file for more details. + * Author: Wangjunqi123 + * Date: Mon Dec 16 08:43:58 2024 +0800 + */ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +// https://vitejs.dev/config/ +export default defineConfig({ + base: "/plugin/logs", + plugins: [vue()], + server: { + host:'localhost', + proxy: { + '/plugin/logs/api': { + target: 'https://10.41.107.29:9994', + secure:false, + changeOrigin: true, + rewrite: path => path.replace(/^\//, '') + }, + }, + } +}) diff --git a/web/yarn.lock b/web/yarn.lock new file mode 100644 index 0000000..165fa51 --- /dev/null +++ b/web/yarn.lock @@ -0,0 +1,930 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/parser@^7.24.7": + version "7.24.7" + resolved "https://registry.npmmirror.com/@babel/parser/-/parser-7.24.7.tgz" + integrity sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw== + +"@ctrl/tinycolor@^3.4.1": + version "3.6.1" + resolved "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz#b6c75a56a1947cc916ea058772d666a2c8932f31" + integrity sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA== + +"@element-plus/icons-vue@^2.3.1": + version "2.3.1" + resolved "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.3.1.tgz#1f635ad5fdd5c85ed936481525570e82b5a8307a" + integrity sha512-XxVUZv48RZAd87ucGS48jPf6pKu0yV5UCg9f4FFwtrYxXOwWuVJo6wOvSLKEoMQKjv8GsX/mhP6UsC1lRwbUWg== + +"@esbuild/aix-ppc64@0.21.5": + version "0.21.5" + resolved "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz#c7184a326533fcdf1b8ee0733e21c713b975575f" + integrity sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ== + +"@esbuild/android-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz#09d9b4357780da9ea3a7dfb833a1f1ff439b4052" + integrity sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A== + +"@esbuild/android-arm@0.21.5": + version "0.21.5" + resolved "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz#9b04384fb771926dfa6d7ad04324ecb2ab9b2e28" + integrity sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg== + +"@esbuild/android-x64@0.21.5": + version "0.21.5" + resolved "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz#29918ec2db754cedcb6c1b04de8cd6547af6461e" + integrity sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA== + +"@esbuild/darwin-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz#e495b539660e51690f3928af50a76fb0a6ccff2a" + integrity sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ== + +"@esbuild/darwin-x64@0.21.5": + version "0.21.5" + resolved "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz#c13838fa57372839abdddc91d71542ceea2e1e22" + integrity sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw== + +"@esbuild/freebsd-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz#646b989aa20bf89fd071dd5dbfad69a3542e550e" + integrity sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g== + +"@esbuild/freebsd-x64@0.21.5": + version "0.21.5" + resolved "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz#aa615cfc80af954d3458906e38ca22c18cf5c261" + integrity sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ== + +"@esbuild/linux-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz" + integrity sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q== + +"@esbuild/linux-arm@0.21.5": + version "0.21.5" + resolved "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz#fc6fd11a8aca56c1f6f3894f2bea0479f8f626b9" + integrity sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA== + +"@esbuild/linux-ia32@0.21.5": + version "0.21.5" + resolved "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz#3271f53b3f93e3d093d518d1649d6d68d346ede2" + integrity sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg== + +"@esbuild/linux-loong64@0.21.5": + version "0.21.5" + resolved "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz#ed62e04238c57026aea831c5a130b73c0f9f26df" + integrity sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg== + +"@esbuild/linux-mips64el@0.21.5": + version "0.21.5" + resolved "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz#e79b8eb48bf3b106fadec1ac8240fb97b4e64cbe" + integrity sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg== + +"@esbuild/linux-ppc64@0.21.5": + version "0.21.5" + resolved "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz#5f2203860a143b9919d383ef7573521fb154c3e4" + integrity sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w== + +"@esbuild/linux-riscv64@0.21.5": + version "0.21.5" + resolved "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz#07bcafd99322d5af62f618cb9e6a9b7f4bb825dc" + integrity sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA== + +"@esbuild/linux-s390x@0.21.5": + version "0.21.5" + resolved "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz#b7ccf686751d6a3e44b8627ababc8be3ef62d8de" + integrity sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A== + +"@esbuild/linux-x64@0.21.5": + version "0.21.5" + resolved "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz#6d8f0c768e070e64309af8004bb94e68ab2bb3b0" + integrity sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ== + +"@esbuild/netbsd-x64@0.21.5": + version "0.21.5" + resolved "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz#bbe430f60d378ecb88decb219c602667387a6047" + integrity sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg== + +"@esbuild/openbsd-x64@0.21.5": + version "0.21.5" + resolved "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz#99d1cf2937279560d2104821f5ccce220cb2af70" + integrity sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow== + +"@esbuild/sunos-x64@0.21.5": + version "0.21.5" + resolved "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz#08741512c10d529566baba837b4fe052c8f3487b" + integrity sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg== + +"@esbuild/win32-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz#675b7385398411240735016144ab2e99a60fc75d" + integrity sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A== + +"@esbuild/win32-ia32@0.21.5": + version "0.21.5" + resolved "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz#1bfc3ce98aa6ca9a0969e4d2af72144c59c1193b" + integrity sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA== + +"@esbuild/win32-x64@0.21.5": + version "0.21.5" + resolved "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz#acad351d582d157bb145535db2a6ff53dd514b5c" + integrity sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw== + +"@floating-ui/core@^1.6.0": + version "1.6.8" + resolved "https://registry.npmmirror.com/@floating-ui/core/-/core-1.6.8.tgz#aa43561be075815879305965020f492cdb43da12" + integrity sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA== + dependencies: + "@floating-ui/utils" "^0.2.8" + +"@floating-ui/dom@^1.0.1": + version "1.6.12" + resolved "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.6.12.tgz#6333dcb5a8ead3b2bf82f33d6bc410e95f54e556" + integrity sha512-NP83c0HjokcGVEMeoStg317VD9W7eDlGK7457dMBANbKA6GJZdc7rjujdgqzTaz93jkGgc5P/jeWbaCHnMNc+w== + dependencies: + "@floating-ui/core" "^1.6.0" + "@floating-ui/utils" "^0.2.8" + +"@floating-ui/utils@^0.2.8": + version "0.2.8" + resolved "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.8.tgz#21a907684723bbbaa5f0974cf7730bd797eb8e62" + integrity sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig== + +"@jridgewell/sourcemap-codec@^1.4.15": + version "1.4.15" + resolved "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + +"@parcel/watcher-android-arm64@2.5.0": + version "2.5.0" + resolved "https://registry.npmmirror.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.0.tgz#e32d3dda6647791ee930556aee206fcd5ea0fb7a" + integrity sha512-qlX4eS28bUcQCdribHkg/herLe+0A9RyYC+mm2PXpncit8z5b3nSqGVzMNR3CmtAOgRutiZ02eIJJgP/b1iEFQ== + +"@parcel/watcher-darwin-arm64@2.5.0": + version "2.5.0" + resolved "https://registry.npmmirror.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.0.tgz#0d9e680b7e9ec1c8f54944f1b945aa8755afb12f" + integrity sha512-hyZ3TANnzGfLpRA2s/4U1kbw2ZI4qGxaRJbBH2DCSREFfubMswheh8TeiC1sGZ3z2jUf3s37P0BBlrD3sjVTUw== + +"@parcel/watcher-darwin-x64@2.5.0": + version "2.5.0" + resolved "https://registry.npmmirror.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.0.tgz#f9f1d5ce9d5878d344f14ef1856b7a830c59d1bb" + integrity sha512-9rhlwd78saKf18fT869/poydQK8YqlU26TMiNg7AIu7eBp9adqbJZqmdFOsbZ5cnLp5XvRo9wcFmNHgHdWaGYA== + +"@parcel/watcher-freebsd-x64@2.5.0": + version "2.5.0" + resolved "https://registry.npmmirror.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.0.tgz#2b77f0c82d19e84ff4c21de6da7f7d096b1a7e82" + integrity sha512-syvfhZzyM8kErg3VF0xpV8dixJ+RzbUaaGaeb7uDuz0D3FK97/mZ5AJQ3XNnDsXX7KkFNtyQyFrXZzQIcN49Tw== + +"@parcel/watcher-linux-arm-glibc@2.5.0": + version "2.5.0" + resolved "https://registry.npmmirror.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.0.tgz#92ed322c56dbafa3d2545dcf2803334aee131e42" + integrity sha512-0VQY1K35DQET3dVYWpOaPFecqOT9dbuCfzjxoQyif1Wc574t3kOSkKevULddcR9znz1TcklCE7Ht6NIxjvTqLA== + +"@parcel/watcher-linux-arm-musl@2.5.0": + version "2.5.0" + resolved "https://registry.npmmirror.com/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.0.tgz#cd48e9bfde0cdbbd2ecd9accfc52967e22f849a4" + integrity sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA== + +"@parcel/watcher-linux-arm64-glibc@2.5.0": + version "2.5.0" + resolved "https://registry.npmmirror.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.0.tgz#7b81f6d5a442bb89fbabaf6c13573e94a46feb03" + integrity sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA== + +"@parcel/watcher-linux-arm64-musl@2.5.0": + version "2.5.0" + resolved "https://registry.npmmirror.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.0.tgz#dcb8ff01077cdf59a18d9e0a4dff7a0cfe5fd732" + integrity sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q== + +"@parcel/watcher-linux-x64-glibc@2.5.0": + version "2.5.0" + resolved "https://registry.npmmirror.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.0.tgz#2e254600fda4e32d83942384d1106e1eed84494d" + integrity sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw== + +"@parcel/watcher-linux-x64-musl@2.5.0": + version "2.5.0" + resolved "https://registry.npmmirror.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.0.tgz#01fcea60fedbb3225af808d3f0a7b11229792eef" + integrity sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA== + +"@parcel/watcher-win32-arm64@2.5.0": + version "2.5.0" + resolved "https://registry.npmmirror.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.0.tgz#87cdb16e0783e770197e52fb1dc027bb0c847154" + integrity sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig== + +"@parcel/watcher-win32-ia32@2.5.0": + version "2.5.0" + resolved "https://registry.npmmirror.com/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.0.tgz#778c39b56da33e045ba21c678c31a9f9d7c6b220" + integrity sha512-+rgpsNRKwo8A53elqbbHXdOMtY/tAtTzManTWShB5Kk54N8Q9mzNWV7tV+IbGueCbcj826MfWGU3mprWtuf1TA== + +"@parcel/watcher-win32-x64@2.5.0": + version "2.5.0" + resolved "https://registry.npmmirror.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.0.tgz#33873876d0bbc588aacce38e90d1d7480ce81cb7" + integrity sha512-lPrxve92zEHdgeff3aiu4gDOIt4u7sJYha6wbdEZDCDUhtjTsOMiaJzG5lMY4GkWH8p0fMmO2Ppq5G5XXG+DQw== + +"@parcel/watcher@^2.4.1": + version "2.5.0" + resolved "https://registry.npmmirror.com/@parcel/watcher/-/watcher-2.5.0.tgz#5c88818b12b8de4307a9d3e6dc3e28eba0dfbd10" + integrity sha512-i0GV1yJnm2n3Yq1qw6QrUrd/LI9bE8WEBOTtOkpCXHHdyN3TAGgqAK/DAT05z4fq2x04cARXt2pDmjWjL92iTQ== + dependencies: + detect-libc "^1.0.3" + is-glob "^4.0.3" + micromatch "^4.0.5" + node-addon-api "^7.0.0" + optionalDependencies: + "@parcel/watcher-android-arm64" "2.5.0" + "@parcel/watcher-darwin-arm64" "2.5.0" + "@parcel/watcher-darwin-x64" "2.5.0" + "@parcel/watcher-freebsd-x64" "2.5.0" + "@parcel/watcher-linux-arm-glibc" "2.5.0" + "@parcel/watcher-linux-arm-musl" "2.5.0" + "@parcel/watcher-linux-arm64-glibc" "2.5.0" + "@parcel/watcher-linux-arm64-musl" "2.5.0" + "@parcel/watcher-linux-x64-glibc" "2.5.0" + "@parcel/watcher-linux-x64-musl" "2.5.0" + "@parcel/watcher-win32-arm64" "2.5.0" + "@parcel/watcher-win32-ia32" "2.5.0" + "@parcel/watcher-win32-x64" "2.5.0" + +"@popperjs/core@npm:@sxzz/popperjs-es@^2.11.7": + version "2.11.7" + resolved "https://registry.npmmirror.com/@sxzz/popperjs-es/-/popperjs-es-2.11.7.tgz#a7f69e3665d3da9b115f9e71671dae1b97e13671" + integrity sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ== + +"@rollup/rollup-android-arm-eabi@4.18.0": + version "4.18.0" + resolved "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz#bbd0e616b2078cd2d68afc9824d1fadb2f2ffd27" + integrity sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ== + +"@rollup/rollup-android-arm64@4.18.0": + version "4.18.0" + resolved "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.0.tgz#97255ef6384c5f73f4800c0de91f5f6518e21203" + integrity sha512-avCea0RAP03lTsDhEyfy+hpfr85KfyTctMADqHVhLAF3MlIkq83CP8UfAHUssgXTYd+6er6PaAhx/QGv4L1EiA== + +"@rollup/rollup-darwin-arm64@4.18.0": + version "4.18.0" + resolved "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.0.tgz#b6dd74e117510dfe94541646067b0545b42ff096" + integrity sha512-IWfdwU7KDSm07Ty0PuA/W2JYoZ4iTj3TUQjkVsO/6U+4I1jN5lcR71ZEvRh52sDOERdnNhhHU57UITXz5jC1/w== + +"@rollup/rollup-darwin-x64@4.18.0": + version "4.18.0" + resolved "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.0.tgz#e07d76de1cec987673e7f3d48ccb8e106d42c05c" + integrity sha512-n2LMsUz7Ynu7DoQrSQkBf8iNrjOGyPLrdSg802vk6XT3FtsgX6JbE8IHRvposskFm9SNxzkLYGSq9QdpLYpRNA== + +"@rollup/rollup-linux-arm-gnueabihf@4.18.0": + version "4.18.0" + resolved "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.0.tgz#9f1a6d218b560c9d75185af4b8bb42f9f24736b8" + integrity sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA== + +"@rollup/rollup-linux-arm-musleabihf@4.18.0": + version "4.18.0" + resolved "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.0.tgz#53618b92e6ffb642c7b620e6e528446511330549" + integrity sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A== + +"@rollup/rollup-linux-arm64-gnu@4.18.0": + version "4.18.0" + resolved "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.0.tgz" + integrity sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw== + +"@rollup/rollup-linux-arm64-musl@4.18.0": + version "4.18.0" + resolved "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.0.tgz" + integrity sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ== + +"@rollup/rollup-linux-powerpc64le-gnu@4.18.0": + version "4.18.0" + resolved "https://registry.npmmirror.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.0.tgz#cbb0837408fe081ce3435cf3730e090febafc9bf" + integrity sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA== + +"@rollup/rollup-linux-riscv64-gnu@4.18.0": + version "4.18.0" + resolved "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.0.tgz#8ed09c1d1262ada4c38d791a28ae0fea28b80cc9" + integrity sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg== + +"@rollup/rollup-linux-s390x-gnu@4.18.0": + version "4.18.0" + resolved "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.0.tgz#938138d3c8e0c96f022252a28441dcfb17afd7ec" + integrity sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg== + +"@rollup/rollup-linux-x64-gnu@4.18.0": + version "4.18.0" + resolved "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz#1a7481137a54740bee1ded4ae5752450f155d942" + integrity sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w== + +"@rollup/rollup-linux-x64-musl@4.18.0": + version "4.18.0" + resolved "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.0.tgz#f1186afc601ac4f4fc25fac4ca15ecbee3a1874d" + integrity sha512-LKaqQL9osY/ir2geuLVvRRs+utWUNilzdE90TpyoX0eNqPzWjRm14oMEE+YLve4k/NAqCdPkGYDaDF5Sw+xBfg== + +"@rollup/rollup-win32-arm64-msvc@4.18.0": + version "4.18.0" + resolved "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.0.tgz#ed6603e93636a96203c6915be4117245c1bd2daf" + integrity sha512-7J6TkZQFGo9qBKH0pk2cEVSRhJbL6MtfWxth7Y5YmZs57Pi+4x6c2dStAUvaQkHQLnEQv1jzBUW43GvZW8OFqA== + +"@rollup/rollup-win32-ia32-msvc@4.18.0": + version "4.18.0" + resolved "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.0.tgz#14e0b404b1c25ebe6157a15edb9c46959ba74c54" + integrity sha512-Txjh+IxBPbkUB9+SXZMpv+b/vnTEtFyfWZgJ6iyCmt2tdx0OF5WhFowLmnh8ENGNpfUlUZkdI//4IEmhwPieNg== + +"@rollup/rollup-win32-x64-msvc@4.18.0": + version "4.18.0" + resolved "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.0.tgz#5d694d345ce36b6ecf657349e03eb87297e68da4" + integrity sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g== + +"@types/estree@1.0.5": + version "1.0.5" + resolved "https://registry.npmmirror.com/@types/estree/-/estree-1.0.5.tgz" + integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== + +"@types/lodash-es@^4.17.6": + version "4.17.12" + resolved "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz#65f6d1e5f80539aa7cfbfc962de5def0cf4f341b" + integrity sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ== + dependencies: + "@types/lodash" "*" + +"@types/lodash@*", "@types/lodash@^4.14.182": + version "4.17.13" + resolved "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.13.tgz#786e2d67cfd95e32862143abe7463a7f90c300eb" + integrity sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg== + +"@types/web-bluetooth@^0.0.16": + version "0.0.16" + resolved "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz#1d12873a8e49567371f2a75fe3e7f7edca6662d8" + integrity sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ== + +"@vitejs/plugin-vue@^5.0.4": + version "5.0.5" + resolved "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.0.5.tgz" + integrity sha512-LOjm7XeIimLBZyzinBQ6OSm3UBCNVCpLkxGC0oWmm2YPzVZoxMsdvNVimLTBzpAnR9hl/yn1SHGuRfe6/Td9rQ== + +"@volar/language-core@2.3.0", "@volar/language-core@~2.3.0-alpha.15": + version "2.3.0" + resolved "https://registry.npmmirror.com/@volar/language-core/-/language-core-2.3.0.tgz" + integrity sha512-pvhL24WUh3VDnv7Yw5N1sjhPtdx7q9g+Wl3tggmnkMcyK8GcCNElF2zHiKznryn0DiUGk+eez/p2qQhz+puuHw== + dependencies: + "@volar/source-map" "2.3.0" + +"@volar/source-map@2.3.0": + version "2.3.0" + resolved "https://registry.npmmirror.com/@volar/source-map/-/source-map-2.3.0.tgz" + integrity sha512-G/228aZjAOGhDjhlyZ++nDbKrS9uk+5DMaEstjvzglaAw7nqtDyhnQAsYzUg6BMP9BtwZ59RIw5HGePrutn00Q== + dependencies: + muggle-string "^0.4.0" + +"@volar/typescript@~2.3.0-alpha.15": + version "2.3.0" + resolved "https://registry.npmmirror.com/@volar/typescript/-/typescript-2.3.0.tgz" + integrity sha512-PtUwMM87WsKVeLJN33GSTUjBexlKfKgouWlOUIv7pjrOnTwhXHZNSmpc312xgXdTjQPpToK6KXSIcKu9sBQ5LQ== + dependencies: + "@volar/language-core" "2.3.0" + path-browserify "^1.0.1" + vscode-uri "^3.0.8" + +"@vue/compiler-core@3.4.29": + version "3.4.29" + resolved "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.4.29.tgz" + integrity sha512-TFKiRkKKsRCKvg/jTSSKK7mYLJEQdUiUfykbG49rubC9SfDyvT2JrzTReopWlz2MxqeLyxh9UZhvxEIBgAhtrg== + dependencies: + "@babel/parser" "^7.24.7" + "@vue/shared" "3.4.29" + entities "^4.5.0" + estree-walker "^2.0.2" + source-map-js "^1.2.0" + +"@vue/compiler-dom@3.4.29", "@vue/compiler-dom@^3.4.0": + version "3.4.29" + resolved "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.4.29.tgz" + integrity sha512-A6+iZ2fKIEGnfPJejdB7b1FlJzgiD+Y/sxxKwJWg1EbJu6ZPgzaPQQ51ESGNv0CP6jm6Z7/pO6Ia8Ze6IKrX7w== + dependencies: + "@vue/compiler-core" "3.4.29" + "@vue/shared" "3.4.29" + +"@vue/compiler-sfc@3.4.29": + version "3.4.29" + resolved "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.4.29.tgz" + integrity sha512-zygDcEtn8ZimDlrEQyLUovoWgKQic6aEQqRXce2WXBvSeHbEbcAsXyCk9oG33ZkyWH4sl9D3tkYc1idoOkdqZQ== + dependencies: + "@babel/parser" "^7.24.7" + "@vue/compiler-core" "3.4.29" + "@vue/compiler-dom" "3.4.29" + "@vue/compiler-ssr" "3.4.29" + "@vue/shared" "3.4.29" + estree-walker "^2.0.2" + magic-string "^0.30.10" + postcss "^8.4.38" + source-map-js "^1.2.0" + +"@vue/compiler-ssr@3.4.29": + version "3.4.29" + resolved "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.4.29.tgz" + integrity sha512-rFbwCmxJ16tDp3N8XCx5xSQzjhidYjXllvEcqX/lopkoznlNPz3jyy0WGJCyhAaVQK677WWFt3YO/WUEkMMUFQ== + dependencies: + "@vue/compiler-dom" "3.4.29" + "@vue/shared" "3.4.29" + +"@vue/devtools-api@^6.6.3": + version "6.6.4" + resolved "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz#cbe97fe0162b365edc1dba80e173f90492535343" + integrity sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g== + +"@vue/language-core@2.0.21": + version "2.0.21" + resolved "https://registry.npmmirror.com/@vue/language-core/-/language-core-2.0.21.tgz" + integrity sha512-vjs6KwnCK++kIXT+eI63BGpJHfHNVJcUCr3RnvJsccT3vbJnZV5IhHR2puEkoOkIbDdp0Gqi1wEnv3hEd3WsxQ== + dependencies: + "@volar/language-core" "~2.3.0-alpha.15" + "@vue/compiler-dom" "^3.4.0" + "@vue/shared" "^3.4.0" + computeds "^0.0.1" + minimatch "^9.0.3" + path-browserify "^1.0.1" + vue-template-compiler "^2.7.14" + +"@vue/reactivity@3.4.29": + version "3.4.29" + resolved "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.4.29.tgz" + integrity sha512-w8+KV+mb1a8ornnGQitnMdLfE0kXmteaxLdccm2XwdFxXst4q/Z7SEboCV5SqJNpZbKFeaRBBJBhW24aJyGINg== + dependencies: + "@vue/shared" "3.4.29" + +"@vue/runtime-core@3.4.29": + version "3.4.29" + resolved "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.4.29.tgz" + integrity sha512-s8fmX3YVR/Rk5ig0ic0NuzTNjK2M7iLuVSZyMmCzN/+Mjuqqif1JasCtEtmtoJWF32pAtUjyuT2ljNKNLeOmnQ== + dependencies: + "@vue/reactivity" "3.4.29" + "@vue/shared" "3.4.29" + +"@vue/runtime-dom@3.4.29": + version "3.4.29" + resolved "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.4.29.tgz" + integrity sha512-gI10atCrtOLf/2MPPMM+dpz3NGulo9ZZR9d1dWo4fYvm+xkfvRrw1ZmJ7mkWtiJVXSsdmPbcK1p5dZzOCKDN0g== + dependencies: + "@vue/reactivity" "3.4.29" + "@vue/runtime-core" "3.4.29" + "@vue/shared" "3.4.29" + csstype "^3.1.3" + +"@vue/server-renderer@3.4.29": + version "3.4.29" + resolved "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.4.29.tgz" + integrity sha512-HMLCmPI2j/k8PVkSBysrA2RxcxC5DgBiCdj7n7H2QtR8bQQPqKAe8qoaxLcInzouBmzwJ+J0x20ygN/B5mYBng== + dependencies: + "@vue/compiler-ssr" "3.4.29" + "@vue/shared" "3.4.29" + +"@vue/shared@3.4.29", "@vue/shared@^3.4.0": + version "3.4.29" + resolved "https://registry.npmmirror.com/@vue/shared/-/shared-3.4.29.tgz" + integrity sha512-hQ2gAQcBO/CDpC82DCrinJNgOHI2v+FA7BDW4lMSPeBpQ7sRe2OLHWe5cph1s7D8DUQAwRt18dBDfJJ220APEA== + +"@vueuse/core@^9.1.0": + version "9.13.0" + resolved "https://registry.npmmirror.com/@vueuse/core/-/core-9.13.0.tgz#2f69e66d1905c1e4eebc249a01759cf88ea00cf4" + integrity sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw== + dependencies: + "@types/web-bluetooth" "^0.0.16" + "@vueuse/metadata" "9.13.0" + "@vueuse/shared" "9.13.0" + vue-demi "*" + +"@vueuse/metadata@9.13.0": + version "9.13.0" + resolved "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-9.13.0.tgz#bc25a6cdad1b1a93c36ce30191124da6520539ff" + integrity sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ== + +"@vueuse/shared@9.13.0": + version "9.13.0" + resolved "https://registry.npmmirror.com/@vueuse/shared/-/shared-9.13.0.tgz#089ff4cc4e2e7a4015e57a8f32e4b39d096353b9" + integrity sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw== + dependencies: + vue-demi "*" + +async-validator@^4.2.5: + version "4.2.5" + resolved "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz#c96ea3332a521699d0afaaceed510a54656c6339" + integrity sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg== + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +axios@^1.7.2: + version "1.7.2" + resolved "https://registry.npmmirror.com/axios/-/axios-1.7.2.tgz" + integrity sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw== + dependencies: + follow-redirects "^1.15.6" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.1.tgz" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + +braces@^3.0.3: + version "3.0.3" + resolved "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + +chokidar@^4.0.0: + version "4.0.1" + resolved "https://registry.npmmirror.com/chokidar/-/chokidar-4.0.1.tgz#4a6dff66798fb0f72a94f616abbd7e1a19f31d41" + integrity sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA== + dependencies: + readdirp "^4.0.1" + +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +computeds@^0.0.1: + version "0.0.1" + resolved "https://registry.npmmirror.com/computeds/-/computeds-0.0.1.tgz" + integrity sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q== + +csstype@^3.1.3: + version "3.1.3" + resolved "https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz" + integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== + +dayjs@^1.11.13: + version "1.11.13" + resolved "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.13.tgz#92430b0139055c3ebb60150aa13e860a4b5a366c" + integrity sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg== + +de-indent@^1.0.2: + version "1.0.2" + resolved "https://registry.npmmirror.com/de-indent/-/de-indent-1.0.2.tgz" + integrity sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg== + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + +detect-libc@^1.0.3: + version "1.0.3" + resolved "https://registry.npmmirror.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" + integrity sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg== + +element-plus@^2.9.0: + version "2.9.0" + resolved "https://registry.npmmirror.com/element-plus/-/element-plus-2.9.0.tgz#76a16566ab6dbadb555a40704bde870a02c306bc" + integrity sha512-ccOFXKsauo2dtokAr4OX7gZsb7TuAoVxA2zGRZo5o2yyDDBLBaZxOoFQPoxITSLcHbBfQuNDGK5Iag5hnyKkZA== + dependencies: + "@ctrl/tinycolor" "^3.4.1" + "@element-plus/icons-vue" "^2.3.1" + "@floating-ui/dom" "^1.0.1" + "@popperjs/core" "npm:@sxzz/popperjs-es@^2.11.7" + "@types/lodash" "^4.14.182" + "@types/lodash-es" "^4.17.6" + "@vueuse/core" "^9.1.0" + async-validator "^4.2.5" + dayjs "^1.11.13" + escape-html "^1.0.3" + lodash "^4.17.21" + lodash-es "^4.17.21" + lodash-unified "^1.0.2" + memoize-one "^6.0.0" + normalize-wheel-es "^1.2.0" + +entities@^4.5.0: + version "4.5.0" + resolved "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz" + integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== + +esbuild@^0.21.3: + version "0.21.5" + resolved "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz" + integrity sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw== + optionalDependencies: + "@esbuild/aix-ppc64" "0.21.5" + "@esbuild/android-arm" "0.21.5" + "@esbuild/android-arm64" "0.21.5" + "@esbuild/android-x64" "0.21.5" + "@esbuild/darwin-arm64" "0.21.5" + "@esbuild/darwin-x64" "0.21.5" + "@esbuild/freebsd-arm64" "0.21.5" + "@esbuild/freebsd-x64" "0.21.5" + "@esbuild/linux-arm" "0.21.5" + "@esbuild/linux-arm64" "0.21.5" + "@esbuild/linux-ia32" "0.21.5" + "@esbuild/linux-loong64" "0.21.5" + "@esbuild/linux-mips64el" "0.21.5" + "@esbuild/linux-ppc64" "0.21.5" + "@esbuild/linux-riscv64" "0.21.5" + "@esbuild/linux-s390x" "0.21.5" + "@esbuild/linux-x64" "0.21.5" + "@esbuild/netbsd-x64" "0.21.5" + "@esbuild/openbsd-x64" "0.21.5" + "@esbuild/sunos-x64" "0.21.5" + "@esbuild/win32-arm64" "0.21.5" + "@esbuild/win32-ia32" "0.21.5" + "@esbuild/win32-x64" "0.21.5" + +escape-html@^1.0.3: + version "1.0.3" + resolved "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +estree-walker@^2.0.2: + version "2.0.2" + resolved "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz" + integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +follow-redirects@^1.15.6: + version "1.15.6" + resolved "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.6.tgz" + integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== + +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.npmmirror.com/form-data/-/form-data-4.0.0.tgz" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +fsevents@~2.3.2, fsevents@~2.3.3: + version "2.3.3" + resolved "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +he@^1.2.0: + version "1.2.0" + resolved "https://registry.npmmirror.com/he/-/he-1.2.0.tgz" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + +immutable@^5.0.2: + version "5.0.3" + resolved "https://registry.npmmirror.com/immutable/-/immutable-5.0.3.tgz#aa037e2313ea7b5d400cd9298fa14e404c933db1" + integrity sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw== + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-glob@^4.0.3: + version "4.0.3" + resolved "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +lodash-es@^4.17.21: + version "4.17.21" + resolved "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" + integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== + +lodash-unified@^1.0.2: + version "1.0.3" + resolved "https://registry.npmmirror.com/lodash-unified/-/lodash-unified-1.0.3.tgz#80b1eac10ed2eb02ed189f08614a29c27d07c894" + integrity sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ== + +lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +magic-string@^0.30.10: + version "0.30.10" + resolved "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.10.tgz" + integrity sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ== + dependencies: + "@jridgewell/sourcemap-codec" "^1.4.15" + +memoize-one@^6.0.0: + version "6.0.0" + resolved "https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045" + integrity sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw== + +micromatch@^4.0.5: + version "4.0.8" + resolved "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== + dependencies: + braces "^3.0.3" + picomatch "^2.3.1" + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12: + version "2.1.35" + resolved "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +minimatch@^9.0.3: + version "9.0.4" + resolved "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.4.tgz" + integrity sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw== + dependencies: + brace-expansion "^2.0.1" + +muggle-string@^0.4.0: + version "0.4.1" + resolved "https://registry.npmmirror.com/muggle-string/-/muggle-string-0.4.1.tgz" + integrity sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ== + +nanoid@^3.3.7: + version "3.3.7" + resolved "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.7.tgz" + integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== + +node-addon-api@^7.0.0: + version "7.1.1" + resolved "https://registry.npmmirror.com/node-addon-api/-/node-addon-api-7.1.1.tgz#1aba6693b0f255258a049d621329329322aad558" + integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ== + +normalize-wheel-es@^1.2.0: + version "1.2.0" + resolved "https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz#0fa2593d619f7245a541652619105ab076acf09e" + integrity sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw== + +path-browserify@^1.0.1: + version "1.0.1" + resolved "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz" + integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g== + +picocolors@^1.0.0: + version "1.0.1" + resolved "https://registry.npmmirror.com/picocolors/-/picocolors-1.0.1.tgz" + integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew== + +picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pinia@^2.2.8: + version "2.2.8" + resolved "https://registry.npmmirror.com/pinia/-/pinia-2.2.8.tgz#78bccede31f39e0119188fdaf705c31a93da6d5f" + integrity sha512-NRTYy2g+kju5tBRe0oNlriZIbMNvma8ZJrpHsp3qudyiMEA8jMmPPKQ2QMHg0Oc4BkUyQYWagACabrwriCK9HQ== + dependencies: + "@vue/devtools-api" "^6.6.3" + vue-demi "^0.14.10" + +postcss@^8.4.38: + version "8.4.38" + resolved "https://registry.npmmirror.com/postcss/-/postcss-8.4.38.tgz" + integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A== + dependencies: + nanoid "^3.3.7" + picocolors "^1.0.0" + source-map-js "^1.2.0" + +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + +readdirp@^4.0.1: + version "4.0.2" + resolved "https://registry.npmmirror.com/readdirp/-/readdirp-4.0.2.tgz#388fccb8b75665da3abffe2d8f8ed59fe74c230a" + integrity sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA== + +rollup@^4.13.0: + version "4.18.0" + resolved "https://registry.npmmirror.com/rollup/-/rollup-4.18.0.tgz" + integrity sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg== + dependencies: + "@types/estree" "1.0.5" + optionalDependencies: + "@rollup/rollup-android-arm-eabi" "4.18.0" + "@rollup/rollup-android-arm64" "4.18.0" + "@rollup/rollup-darwin-arm64" "4.18.0" + "@rollup/rollup-darwin-x64" "4.18.0" + "@rollup/rollup-linux-arm-gnueabihf" "4.18.0" + "@rollup/rollup-linux-arm-musleabihf" "4.18.0" + "@rollup/rollup-linux-arm64-gnu" "4.18.0" + "@rollup/rollup-linux-arm64-musl" "4.18.0" + "@rollup/rollup-linux-powerpc64le-gnu" "4.18.0" + "@rollup/rollup-linux-riscv64-gnu" "4.18.0" + "@rollup/rollup-linux-s390x-gnu" "4.18.0" + "@rollup/rollup-linux-x64-gnu" "4.18.0" + "@rollup/rollup-linux-x64-musl" "4.18.0" + "@rollup/rollup-win32-arm64-msvc" "4.18.0" + "@rollup/rollup-win32-ia32-msvc" "4.18.0" + "@rollup/rollup-win32-x64-msvc" "4.18.0" + fsevents "~2.3.2" + +sass@^1.82.0: + version "1.82.0" + resolved "https://registry.npmmirror.com/sass/-/sass-1.82.0.tgz#30da277af3d0fa6042e9ceabd0d984ed6d07df70" + integrity sha512-j4GMCTa8elGyN9A7x7bEglx0VgSpNUG4W4wNedQ33wSMdnkqQCT8HTwOaVSV4e6yQovcu/3Oc4coJP/l0xhL2Q== + dependencies: + chokidar "^4.0.0" + immutable "^5.0.2" + source-map-js ">=0.6.2 <2.0.0" + optionalDependencies: + "@parcel/watcher" "^2.4.1" + +semver@^7.5.4: + version "7.6.2" + resolved "https://registry.npmmirror.com/semver/-/semver-7.6.2.tgz" + integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w== + +"source-map-js@>=0.6.2 <2.0.0": + version "1.2.1" + resolved "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" + integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== + +source-map-js@^1.2.0: + version "1.2.0" + resolved "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.0.tgz" + integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +typescript@^5.2.2: + version "5.4.5" + resolved "https://registry.npmmirror.com/typescript/-/typescript-5.4.5.tgz" + integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ== + +vite@^5.2.0: + version "5.3.1" + resolved "https://registry.npmmirror.com/vite/-/vite-5.3.1.tgz" + integrity sha512-XBmSKRLXLxiaPYamLv3/hnP/KXDai1NDexN0FpkTaZXTfycHvkRHoenpgl/fvuK/kPbB6xAgoyiryAhQNxYmAQ== + dependencies: + esbuild "^0.21.3" + postcss "^8.4.38" + rollup "^4.13.0" + optionalDependencies: + fsevents "~2.3.3" + +vscode-uri@^3.0.8: + version "3.0.8" + resolved "https://registry.npmmirror.com/vscode-uri/-/vscode-uri-3.0.8.tgz" + integrity sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw== + +vue-demi@*, vue-demi@^0.14.10: + version "0.14.10" + resolved "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz#afc78de3d6f9e11bf78c55e8510ee12814522f04" + integrity sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg== + +vue-template-compiler@^2.7.14: + version "2.7.16" + resolved "https://registry.npmmirror.com/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz" + integrity sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ== + dependencies: + de-indent "^1.0.2" + he "^1.2.0" + +vue-tsc@^2.0.6: + version "2.0.21" + resolved "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-2.0.21.tgz" + integrity sha512-E6x1p1HaHES6Doy8pqtm7kQern79zRtIewkf9fiv7Y43Zo4AFDS5hKi+iHi2RwEhqRmuiwliB1LCEFEGwvxQnw== + dependencies: + "@volar/typescript" "~2.3.0-alpha.15" + "@vue/language-core" "2.0.21" + semver "^7.5.4" + +vue@^3.4.21: + version "3.4.29" + resolved "https://registry.npmmirror.com/vue/-/vue-3.4.29.tgz" + integrity sha512-8QUYfRcYzNlYuzKPfge1UWC6nF9ym0lx7mpGVPJYNhddxEf3DD0+kU07NTL0sXuiT2HuJuKr/iEO8WvXvT0RSQ== + dependencies: + "@vue/compiler-dom" "3.4.29" + "@vue/compiler-sfc" "3.4.29" + "@vue/runtime-dom" "3.4.29" + "@vue/server-renderer" "3.4.29" + "@vue/shared" "3.4.29" -- Gitee