first commit
This commit is contained in:
262
INSTALL.md
Normal file
262
INSTALL.md
Normal file
@@ -0,0 +1,262 @@
|
||||
# LinkMaster 节点端安装指南
|
||||
|
||||
## 一句话安装
|
||||
|
||||
### 从 GitHub 安装(推荐)
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/yourbask/linkmaster-node/main/install.sh | bash -s -- http://your-backend-server:8080
|
||||
```
|
||||
|
||||
**替换说明:**
|
||||
- `yourbask/linkmaster-node` - 独立的 node 项目 GitHub 仓库地址
|
||||
- `http://your-backend-server:8080` - 替换为实际的后端服务器地址
|
||||
|
||||
**重要提示:**
|
||||
- ⚠️ 节点端需要直接连接后端服务器(端口 8080),不是前端地址
|
||||
- 前端通过 `/api` 路径代理到后端,但节点端不使用前端代理
|
||||
- 如果节点和后端在同一服务器:使用 `http://localhost:8080`
|
||||
- 如果节点和后端在不同服务器:使用 `http://backend-ip:8080` 或 `http://backend-domain:8080`
|
||||
- **本项目是独立的 GitHub 仓库**,与前后端项目分离
|
||||
|
||||
**示例:**
|
||||
```bash
|
||||
# 如果后端服务器在 192.168.1.100:8080
|
||||
curl -fsSL https://raw.githubusercontent.com/yourbask/linkmaster-node/main/install.sh | bash -s -- http://192.168.1.100:8080
|
||||
```
|
||||
|
||||
### 指定分支安装
|
||||
|
||||
```bash
|
||||
GITHUB_BRANCH=develop curl -fsSL https://raw.githubusercontent.com/yourbask/linkmaster-node/main/install.sh | bash -s -- http://your-backend-server:8080
|
||||
```
|
||||
|
||||
## 安装步骤说明
|
||||
|
||||
安装脚本会自动完成以下步骤:
|
||||
|
||||
1. **检测系统** - 自动识别 Linux 发行版和 CPU 架构
|
||||
2. **安装依赖** - 自动安装 Git、Go、ping、traceroute、dnsutils 等工具
|
||||
3. **克隆源码** - 从 GitHub 克隆 node 项目源码到 `/opt/linkmaster-node`
|
||||
4. **编译安装** - 自动编译源码并安装二进制文件
|
||||
5. **创建服务** - 自动创建 systemd 服务文件(使用 run.sh 启动)
|
||||
6. **启动服务** - 自动启动并设置开机自启
|
||||
7. **验证安装** - 检查服务状态和健康检查
|
||||
|
||||
**注意:** 每次服务启动时会自动拉取最新代码并重新编译,确保使用最新版本。
|
||||
|
||||
## 安装后管理
|
||||
|
||||
### 查看服务状态
|
||||
|
||||
```bash
|
||||
sudo systemctl status linkmaster-node
|
||||
```
|
||||
|
||||
### 查看日志
|
||||
|
||||
```bash
|
||||
# 实时日志
|
||||
sudo journalctl -u linkmaster-node -f
|
||||
|
||||
# 最近50行日志
|
||||
sudo journalctl -u linkmaster-node -n 50
|
||||
```
|
||||
|
||||
### 重启服务
|
||||
|
||||
```bash
|
||||
sudo systemctl restart linkmaster-node
|
||||
```
|
||||
|
||||
### 停止服务
|
||||
|
||||
```bash
|
||||
sudo systemctl stop linkmaster-node
|
||||
```
|
||||
|
||||
### 禁用开机自启
|
||||
|
||||
```bash
|
||||
sudo systemctl disable linkmaster-node
|
||||
```
|
||||
|
||||
## 验证安装
|
||||
|
||||
### 检查进程
|
||||
|
||||
```bash
|
||||
ps aux | grep linkmaster-node
|
||||
```
|
||||
|
||||
### 检查端口
|
||||
|
||||
```bash
|
||||
netstat -tlnp | grep 2200
|
||||
# 或
|
||||
ss -tlnp | grep 2200
|
||||
```
|
||||
|
||||
### 健康检查
|
||||
|
||||
```bash
|
||||
curl http://localhost:2200/api/health
|
||||
```
|
||||
|
||||
应该返回:`{"status":"ok"}`
|
||||
|
||||
## 手动安装(不使用脚本)
|
||||
|
||||
如果无法使用一键安装脚本,可以手动安装:
|
||||
|
||||
### 1. 克隆源码并编译
|
||||
|
||||
```bash
|
||||
# 克隆仓库
|
||||
git clone https://github.com/yourbask/linkmaster-node.git /opt/linkmaster-node
|
||||
cd /opt/linkmaster-node
|
||||
|
||||
# 安装 Go 环境(如果未安装)
|
||||
# Ubuntu/Debian
|
||||
sudo apt-get install -y golang-go
|
||||
|
||||
# CentOS/RHEL
|
||||
sudo yum install -y golang
|
||||
|
||||
# 编译
|
||||
go build -o agent ./cmd/agent
|
||||
|
||||
# 安装到系统目录
|
||||
sudo cp agent /usr/local/bin/linkmaster-node
|
||||
sudo chmod +x /usr/local/bin/linkmaster-node
|
||||
```
|
||||
|
||||
### 2. 安装系统依赖
|
||||
|
||||
```bash
|
||||
# Ubuntu/Debian
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y ping traceroute dnsutils curl
|
||||
|
||||
# CentOS/RHEL
|
||||
sudo yum install -y iputils traceroute bind-utils curl
|
||||
```
|
||||
|
||||
### 3. 创建 systemd 服务
|
||||
|
||||
```bash
|
||||
# 确保 run.sh 有执行权限
|
||||
sudo chmod +x /opt/linkmaster-node/run.sh
|
||||
|
||||
sudo tee /etc/systemd/system/linkmaster-node.service > /dev/null <<EOF
|
||||
[Unit]
|
||||
Description=LinkMaster Node Service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=/opt/linkmaster-node
|
||||
ExecStart=/opt/linkmaster-node/run.sh start
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
Environment="BACKEND_URL=http://your-backend-server:8080"
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
```
|
||||
|
||||
**注意:** 使用 `run.sh` 启动的好处是每次启动会自动拉取最新代码并重新编译。
|
||||
|
||||
### 4. 启动服务
|
||||
|
||||
```bash
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable linkmaster-node
|
||||
sudo systemctl start linkmaster-node
|
||||
```
|
||||
|
||||
**注意:** 确保 `BACKEND_URL` 环境变量指向后端服务器的实际地址和端口(默认 8080),不是前端地址。
|
||||
|
||||
## 防火墙配置
|
||||
|
||||
确保开放端口 2200:
|
||||
|
||||
```bash
|
||||
# Ubuntu/Debian (ufw)
|
||||
sudo ufw allow 2200/tcp
|
||||
|
||||
# CentOS/RHEL (firewalld)
|
||||
sudo firewall-cmd --permanent --add-port=2200/tcp
|
||||
sudo firewall-cmd --reload
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 1. 克隆或编译失败
|
||||
|
||||
**问题:** 无法从 GitHub 克隆源码或编译失败
|
||||
|
||||
**解决:**
|
||||
- 检查网络连接
|
||||
- 确认 GitHub 仓库地址正确(独立的 node 项目仓库)
|
||||
- 确认已安装 Git 和 Go 环境
|
||||
- 手动克隆并编译:`git clone https://github.com/yourbask/linkmaster-node.git && cd linkmaster-node && go build -o agent ./cmd/agent`
|
||||
|
||||
### 2. 服务启动失败
|
||||
|
||||
**问题:** systemctl status 显示服务失败
|
||||
|
||||
**解决:**
|
||||
```bash
|
||||
# 查看详细日志
|
||||
sudo journalctl -u linkmaster-node -n 100
|
||||
|
||||
# 检查后端地址是否正确
|
||||
sudo systemctl cat linkmaster-node | grep BACKEND_URL
|
||||
|
||||
# 手动测试后端连接
|
||||
curl http://your-backend-server:8080/api/public/nodes/online
|
||||
```
|
||||
|
||||
### 3. 端口被占用
|
||||
|
||||
**问题:** 端口 2200 已被占用
|
||||
|
||||
**解决:**
|
||||
```bash
|
||||
# 查找占用进程
|
||||
sudo lsof -i :2200
|
||||
|
||||
# 停止占用进程或修改配置
|
||||
```
|
||||
|
||||
### 4. 无法连接后端
|
||||
|
||||
**问题:** 节点无法连接到后端服务器
|
||||
|
||||
**解决:**
|
||||
- 检查后端地址是否正确(应该是 `http://backend-server:8080`,不是前端地址)
|
||||
- 检查网络连通性:`ping your-backend-server`
|
||||
- 检查端口是否开放:`telnet your-backend-server 8080` 或 `nc -zv your-backend-server 8080`
|
||||
- 检查防火墙规则(确保后端服务器的 8080 端口开放)
|
||||
- 检查后端服务是否运行:`curl http://your-backend-server:8080/api/public/nodes/online`
|
||||
- 如果使用前端代理,节点端仍需要直接连接后端,不能使用前端地址
|
||||
|
||||
## 卸载
|
||||
|
||||
```bash
|
||||
# 停止服务
|
||||
sudo systemctl stop linkmaster-node
|
||||
sudo systemctl disable linkmaster-node
|
||||
|
||||
# 删除服务文件
|
||||
sudo rm /etc/systemd/system/linkmaster-node.service
|
||||
sudo systemctl daemon-reload
|
||||
|
||||
# 删除二进制文件和源码目录
|
||||
sudo rm /usr/local/bin/linkmaster-node
|
||||
sudo rm -rf /opt/linkmaster-node
|
||||
```
|
||||
|
||||
11
Makefile
Normal file
11
Makefile
Normal file
@@ -0,0 +1,11 @@
|
||||
.PHONY: build build-linux clean
|
||||
|
||||
build:
|
||||
go build -o bin/linkmaster-node ./cmd/agent
|
||||
|
||||
build-linux:
|
||||
GOOS=linux GOARCH=amd64 go build -o bin/linkmaster-node-linux ./cmd/agent
|
||||
|
||||
clean:
|
||||
rm -rf bin/
|
||||
|
||||
@@ -181,3 +181,4 @@ BACKEND_URL=http://192.168.1.100:8080 ./run.sh start
|
||||
|
||||
健康检查
|
||||
# linkmaster-node
|
||||
# linkmaster-node
|
||||
|
||||
75
cmd/agent/main.go
Normal file
75
cmd/agent/main.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"linkmaster-node/internal/config"
|
||||
"linkmaster-node/internal/heartbeat"
|
||||
"linkmaster-node/internal/recovery"
|
||||
"linkmaster-node/internal/server"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 加载配置
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
fmt.Printf("加载配置失败: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// 初始化日志
|
||||
logger, err := initLogger(cfg)
|
||||
if err != nil {
|
||||
fmt.Printf("初始化日志失败: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer logger.Sync()
|
||||
|
||||
logger.Info("节点服务启动", zap.String("version", "1.0.0"))
|
||||
|
||||
// 初始化错误恢复
|
||||
recovery.Init()
|
||||
|
||||
// 启动心跳上报
|
||||
heartbeatReporter := heartbeat.NewReporter(cfg)
|
||||
go heartbeatReporter.Start(context.Background())
|
||||
|
||||
// 启动HTTP服务器
|
||||
httpServer := server.NewHTTPServer(cfg)
|
||||
go func() {
|
||||
if err := httpServer.Start(); err != nil {
|
||||
logger.Fatal("HTTP服务器启动失败", zap.Error(err))
|
||||
}
|
||||
}()
|
||||
|
||||
// 等待中断信号
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
|
||||
<-sigChan
|
||||
|
||||
logger.Info("收到停止信号,正在关闭服务...")
|
||||
|
||||
// 优雅关闭
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
httpServer.Shutdown(ctx)
|
||||
heartbeatReporter.Stop()
|
||||
|
||||
logger.Info("服务已关闭")
|
||||
}
|
||||
|
||||
func initLogger(cfg *config.Config) (*zap.Logger, error) {
|
||||
if cfg.Debug {
|
||||
return zap.NewDevelopment()
|
||||
}
|
||||
return zap.NewProduction()
|
||||
}
|
||||
|
||||
36
go.mod
Normal file
36
go.mod
Normal file
@@ -0,0 +1,36 @@
|
||||
module linkmaster-node
|
||||
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
go.uber.org/zap v1.26.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
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.0 // 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/leodido/go-urn v1.2.4 // 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.0.8 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
golang.org/x/crypto v0.14.0 // indirect
|
||||
golang.org/x/net v0.17.0 // indirect
|
||||
golang.org/x/sys v0.13.0 // indirect
|
||||
golang.org/x/text v0.13.0 // indirect
|
||||
google.golang.org/protobuf v1.30.0 // indirect
|
||||
)
|
||||
92
go.sum
Normal file
92
go.sum
Normal file
@@ -0,0 +1,92 @@
|
||||
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/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/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
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.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
|
||||
github.com/go-playground/validator/v10 v10.14.0/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/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
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/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/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.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
|
||||
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/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.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
|
||||
github.com/stretchr/testify v1.8.3/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=
|
||||
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
|
||||
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
|
||||
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
|
||||
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
|
||||
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.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
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=
|
||||
323
install.sh
Executable file
323
install.sh
Executable file
@@ -0,0 +1,323 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ============================================
|
||||
# LinkMaster 节点端一键安装脚本
|
||||
# 使用方法: curl -fsSL https://raw.githubusercontent.com/yourbask/linkmaster-node/main/install.sh | bash -s -- <后端地址>
|
||||
# 示例: curl -fsSL https://raw.githubusercontent.com/yourbask/linkmaster-node/main/install.sh | bash -s -- http://192.168.1.100:8080
|
||||
# ============================================
|
||||
|
||||
set -e
|
||||
|
||||
# 颜色输出
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
# 配置
|
||||
# 尝试从脚本URL自动提取仓库信息(如果通过curl下载)
|
||||
SCRIPT_URL="${SCRIPT_URL:-}"
|
||||
if [ -z "$SCRIPT_URL" ] && [ -n "${BASH_SOURCE[0]}" ]; then
|
||||
# 如果脚本是通过 curl 下载的,尝试从环境变量获取
|
||||
SCRIPT_URL="${SCRIPT_URL:-}"
|
||||
fi
|
||||
|
||||
# 默认配置(如果无法自动提取,使用这些默认值)
|
||||
GITHUB_REPO="${GITHUB_REPO:-yourbask/linkmaster-node}" # 默认仓库(独立的 node 项目)
|
||||
GITHUB_BRANCH="${GITHUB_BRANCH:-main}" # 默认分支
|
||||
SOURCE_DIR="/opt/linkmaster-node" # 源码目录
|
||||
BINARY_NAME="linkmaster-node"
|
||||
INSTALL_DIR="/usr/local/bin"
|
||||
SERVICE_NAME="linkmaster-node"
|
||||
|
||||
# 获取后端地址参数
|
||||
BACKEND_URL="${1:-}"
|
||||
if [ -z "$BACKEND_URL" ]; then
|
||||
echo -e "${RED}错误: 请提供后端服务器地址${NC}"
|
||||
echo -e "${YELLOW}使用方法:${NC}"
|
||||
echo " curl -fsSL https://raw.githubusercontent.com/${GITHUB_REPO}/${GITHUB_BRANCH}/install.sh | bash -s -- http://your-backend-server:8080"
|
||||
echo ""
|
||||
echo -e "${YELLOW}注意:${NC}"
|
||||
echo " - 节点端需要直接连接后端服务器,不是前端地址"
|
||||
echo " - 后端默认端口: 8080"
|
||||
echo " - 如果节点和后端在同一服务器: http://localhost:8080"
|
||||
echo " - 如果节点和后端在不同服务器: http://backend-ip:8080 或 http://backend-domain:8080"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检测系统类型和架构
|
||||
detect_system() {
|
||||
if [ -f /etc/os-release ]; then
|
||||
. /etc/os-release
|
||||
OS=$ID
|
||||
OS_VERSION=$VERSION_ID
|
||||
else
|
||||
echo -e "${RED}无法检测系统类型${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ARCH=$(uname -m)
|
||||
case $ARCH in
|
||||
x86_64)
|
||||
ARCH="amd64"
|
||||
;;
|
||||
aarch64|arm64)
|
||||
ARCH="arm64"
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}不支持的架构: $ARCH${NC}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo -e "${BLUE}检测到系统: $OS $OS_VERSION ($ARCH)${NC}"
|
||||
}
|
||||
|
||||
# 安装系统依赖
|
||||
install_dependencies() {
|
||||
echo -e "${BLUE}安装系统依赖...${NC}"
|
||||
|
||||
if [ "$OS" = "ubuntu" ] || [ "$OS" = "debian" ]; then
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y -qq curl wget ping traceroute dnsutils git > /dev/null 2>&1
|
||||
elif [ "$OS" = "centos" ] || [ "$OS" = "rhel" ] || [ "$OS" = "rocky" ] || [ "$OS" = "almalinux" ]; then
|
||||
sudo yum install -y -q curl wget iputils traceroute bind-utils git > /dev/null 2>&1
|
||||
else
|
||||
echo -e "${YELLOW}警告: 未知系统类型,跳过依赖安装${NC}"
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}✓ 依赖安装完成${NC}"
|
||||
}
|
||||
|
||||
# 安装 Go 环境
|
||||
install_go() {
|
||||
echo -e "${BLUE}安装 Go 环境...${NC}"
|
||||
|
||||
if [ "$OS" = "ubuntu" ] || [ "$OS" = "debian" ]; then
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y -qq golang-go > /dev/null 2>&1
|
||||
elif [ "$OS" = "centos" ] || [ "$OS" = "rhel" ] || [ "$OS" = "rocky" ] || [ "$OS" = "almalinux" ]; then
|
||||
sudo yum install -y -q golang > /dev/null 2>&1
|
||||
else
|
||||
echo -e "${YELLOW}无法自动安装 Go,请手动安装: https://golang.org/dl/${NC}"
|
||||
show_build_alternatives
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if command -v go > /dev/null 2>&1; then
|
||||
GO_VERSION=$(go version 2>/dev/null | head -1)
|
||||
echo -e "${GREEN}✓ Go 安装完成: ${GO_VERSION}${NC}"
|
||||
else
|
||||
echo -e "${RED}Go 安装失败${NC}"
|
||||
show_build_alternatives
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 显示替代方案
|
||||
show_build_alternatives() {
|
||||
echo ""
|
||||
echo -e "${YELLOW}═══════════════════════════════════════════════════════════${NC}"
|
||||
echo -e "${YELLOW} 安装失败,请使用以下替代方案:${NC}"
|
||||
echo -e "${YELLOW}═══════════════════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
echo -e "${GREEN}手动编译安装:${NC}"
|
||||
echo " git clone https://github.com/${GITHUB_REPO}.git ${SOURCE_DIR}"
|
||||
echo " cd ${SOURCE_DIR}"
|
||||
echo " go build -o agent ./cmd/agent"
|
||||
echo " sudo cp agent /usr/local/bin/linkmaster-node"
|
||||
echo " sudo chmod +x /usr/local/bin/linkmaster-node"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# 从源码编译安装
|
||||
build_from_source() {
|
||||
echo -e "${BLUE}从源码编译安装节点端...${NC}"
|
||||
|
||||
# 检查 Go 环境
|
||||
if ! command -v go > /dev/null 2>&1; then
|
||||
echo -e "${BLUE}未检测到 Go 环境,开始安装...${NC}"
|
||||
install_go
|
||||
fi
|
||||
|
||||
# 检查 Go 版本
|
||||
GO_VERSION=$(go version 2>/dev/null | head -1 || echo "")
|
||||
if [ -z "$GO_VERSION" ]; then
|
||||
echo -e "${RED}无法获取 Go 版本信息${NC}"
|
||||
show_build_alternatives
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${BLUE}检测到 Go 版本: ${GO_VERSION}${NC}"
|
||||
|
||||
# 如果源码目录已存在,先备份或删除
|
||||
if [ -d "$SOURCE_DIR" ]; then
|
||||
echo -e "${YELLOW}源码目录已存在,将更新代码...${NC}"
|
||||
sudo rm -rf "$SOURCE_DIR"
|
||||
fi
|
||||
|
||||
# 克隆仓库到源码目录
|
||||
echo -e "${BLUE}克隆仓库到 ${SOURCE_DIR}...${NC}"
|
||||
if ! sudo git clone --branch "${GITHUB_BRANCH}" "https://github.com/${GITHUB_REPO}.git" "$SOURCE_DIR" 2>&1; then
|
||||
echo -e "${RED}克隆仓库失败,请检查网络连接和仓库地址${NC}"
|
||||
echo -e "${YELLOW}仓库地址: https://github.com/${GITHUB_REPO}.git${NC}"
|
||||
show_build_alternatives
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 设置目录权限
|
||||
sudo chown -R $USER:$USER "$SOURCE_DIR" 2>/dev/null || true
|
||||
|
||||
cd "$SOURCE_DIR"
|
||||
|
||||
# 下载依赖
|
||||
echo -e "${BLUE}下载 Go 依赖...${NC}"
|
||||
if ! go mod download 2>&1; then
|
||||
echo -e "${RED}下载依赖失败${NC}"
|
||||
show_build_alternatives
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 编译
|
||||
echo -e "${BLUE}编译二进制文件...${NC}"
|
||||
BINARY_PATH="$SOURCE_DIR/agent"
|
||||
if GOOS=linux GOARCH=${ARCH} CGO_ENABLED=0 go build -ldflags="-w -s" -o "$BINARY_PATH" ./cmd/agent 2>&1; then
|
||||
if [ -f "$BINARY_PATH" ] && [ -s "$BINARY_PATH" ]; then
|
||||
echo -e "${GREEN}✓ 编译成功${NC}"
|
||||
else
|
||||
echo -e "${RED}编译失败:未生成二进制文件${NC}"
|
||||
show_build_alternatives
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo -e "${RED}编译失败${NC}"
|
||||
show_build_alternatives
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 复制到安装目录(可选,保留在源码目录供 run.sh 使用)
|
||||
sudo mkdir -p "$INSTALL_DIR"
|
||||
sudo cp "$BINARY_PATH" "$INSTALL_DIR/$BINARY_NAME"
|
||||
sudo chmod +x "$INSTALL_DIR/$BINARY_NAME"
|
||||
|
||||
echo -e "${GREEN}✓ 编译安装完成${NC}"
|
||||
echo -e "${BLUE}源码目录: ${SOURCE_DIR}${NC}"
|
||||
echo -e "${BLUE}二进制文件: ${INSTALL_DIR}/${BINARY_NAME}${NC}"
|
||||
}
|
||||
|
||||
# 创建 systemd 服务
|
||||
create_service() {
|
||||
echo -e "${BLUE}创建 systemd 服务...${NC}"
|
||||
|
||||
# 确保 run.sh 有执行权限
|
||||
sudo chmod +x "$SOURCE_DIR/run.sh"
|
||||
|
||||
sudo tee /etc/systemd/system/${SERVICE_NAME}.service > /dev/null <<EOF
|
||||
[Unit]
|
||||
Description=LinkMaster Node Service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=$SOURCE_DIR
|
||||
ExecStart=$SOURCE_DIR/run.sh start
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
Environment="BACKEND_URL=$BACKEND_URL"
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
sudo systemctl daemon-reload
|
||||
echo -e "${GREEN}✓ 服务创建完成${NC}"
|
||||
}
|
||||
|
||||
# 启动服务
|
||||
start_service() {
|
||||
echo -e "${BLUE}启动服务...${NC}"
|
||||
|
||||
sudo systemctl enable ${SERVICE_NAME} > /dev/null 2>&1
|
||||
sudo systemctl restart ${SERVICE_NAME}
|
||||
|
||||
# 等待服务启动
|
||||
sleep 3
|
||||
|
||||
# 检查服务状态
|
||||
if sudo systemctl is-active --quiet ${SERVICE_NAME}; then
|
||||
echo -e "${GREEN}✓ 服务启动成功${NC}"
|
||||
else
|
||||
echo -e "${RED}✗ 服务启动失败${NC}"
|
||||
echo -e "${YELLOW}查看日志: sudo journalctl -u ${SERVICE_NAME} -n 50${NC}"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 验证安装
|
||||
verify_installation() {
|
||||
echo -e "${BLUE}验证安装...${NC}"
|
||||
|
||||
# 检查进程
|
||||
if pgrep -f "$BINARY_NAME" > /dev/null; then
|
||||
echo -e "${GREEN}✓ 进程运行中${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}⚠ 进程未运行${NC}"
|
||||
fi
|
||||
|
||||
# 检查端口
|
||||
if command -v netstat > /dev/null 2>&1; then
|
||||
if netstat -tlnp 2>/dev/null | grep -q ":2200"; then
|
||||
echo -e "${GREEN}✓ 端口 2200 已监听${NC}"
|
||||
fi
|
||||
elif command -v ss > /dev/null 2>&1; then
|
||||
if ss -tlnp 2>/dev/null | grep -q ":2200"; then
|
||||
echo -e "${GREEN}✓ 端口 2200 已监听${NC}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 健康检查
|
||||
sleep 2
|
||||
if curl -sf http://localhost:2200/api/health > /dev/null; then
|
||||
echo -e "${GREEN}✓ 健康检查通过${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}⚠ 健康检查未通过,请稍后重试${NC}"
|
||||
fi
|
||||
}
|
||||
|
||||
# 主安装流程
|
||||
main() {
|
||||
echo -e "${GREEN}========================================${NC}"
|
||||
echo -e "${GREEN} LinkMaster 节点端安装程序${NC}"
|
||||
echo -e "${GREEN}========================================${NC}"
|
||||
echo ""
|
||||
|
||||
detect_system
|
||||
install_dependencies
|
||||
build_from_source
|
||||
create_service
|
||||
start_service
|
||||
verify_installation
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}========================================${NC}"
|
||||
echo -e "${GREEN} 安装完成!${NC}"
|
||||
echo -e "${GREEN}========================================${NC}"
|
||||
echo ""
|
||||
echo -e "${BLUE}服务管理命令:${NC}"
|
||||
echo " 查看状态: sudo systemctl status ${SERVICE_NAME}"
|
||||
echo " 查看日志: sudo journalctl -u ${SERVICE_NAME} -f"
|
||||
echo " 重启服务: sudo systemctl restart ${SERVICE_NAME}"
|
||||
echo " 停止服务: sudo systemctl stop ${SERVICE_NAME}"
|
||||
echo ""
|
||||
echo -e "${BLUE}后端地址: ${BACKEND_URL}${NC}"
|
||||
echo -e "${BLUE}节点端口: 2200${NC}"
|
||||
echo ""
|
||||
echo -e "${YELLOW}重要提示:${NC}"
|
||||
echo " - 节点端直接连接后端服务器,不使用前端代理"
|
||||
echo " - 确保后端地址可访问: curl ${BACKEND_URL}/api/public/nodes/online"
|
||||
}
|
||||
|
||||
# 执行安装
|
||||
main
|
||||
60
internal/config/config.go
Normal file
60
internal/config/config.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Server struct {
|
||||
Port int `yaml:"port"`
|
||||
} `yaml:"server"`
|
||||
|
||||
Backend struct {
|
||||
URL string `yaml:"url"`
|
||||
} `yaml:"backend"`
|
||||
|
||||
Heartbeat struct {
|
||||
Interval int `yaml:"interval"` // 心跳间隔(秒)
|
||||
} `yaml:"heartbeat"`
|
||||
|
||||
Debug bool `yaml:"debug"`
|
||||
}
|
||||
|
||||
func Load() (*Config, error) {
|
||||
cfg := &Config{}
|
||||
|
||||
// 默认配置
|
||||
cfg.Server.Port = 2200
|
||||
cfg.Heartbeat.Interval = 60
|
||||
cfg.Debug = false
|
||||
|
||||
// 从环境变量读取后端URL
|
||||
backendURL := os.Getenv("BACKEND_URL")
|
||||
if backendURL == "" {
|
||||
backendURL = "http://localhost:8080"
|
||||
}
|
||||
cfg.Backend.URL = backendURL
|
||||
|
||||
// 尝试从配置文件读取
|
||||
configPath := os.Getenv("CONFIG_PATH")
|
||||
if configPath == "" {
|
||||
configPath = "config.yaml"
|
||||
}
|
||||
|
||||
if _, err := os.Stat(configPath); err == nil {
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取配置文件失败: %w", err)
|
||||
}
|
||||
|
||||
if err := yaml.Unmarshal(data, cfg); err != nil {
|
||||
return nil, fmt.Errorf("解析配置文件失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
143
internal/continuous/ping.go
Normal file
143
internal/continuous/ping.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package continuous
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type PingTask struct {
|
||||
TaskID string
|
||||
Target string
|
||||
Interval time.Duration
|
||||
MaxDuration time.Duration
|
||||
StartTime time.Time
|
||||
LastRequest time.Time
|
||||
StopCh chan struct{}
|
||||
IsRunning bool
|
||||
mu sync.RWMutex
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
func NewPingTask(taskID, target string, interval, maxDuration time.Duration) *PingTask {
|
||||
logger, _ := zap.NewProduction()
|
||||
return &PingTask{
|
||||
TaskID: taskID,
|
||||
Target: target,
|
||||
Interval: interval,
|
||||
MaxDuration: maxDuration,
|
||||
StartTime: time.Now(),
|
||||
LastRequest: time.Now(),
|
||||
StopCh: make(chan struct{}),
|
||||
IsRunning: true,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *PingTask) Start(ctx context.Context, resultCallback func(result map[string]interface{})) {
|
||||
ticker := time.NewTicker(t.Interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-t.StopCh:
|
||||
return
|
||||
case <-ticker.C:
|
||||
// 检查是否超过最大运行时长
|
||||
t.mu.RLock()
|
||||
if time.Since(t.StartTime) > t.MaxDuration {
|
||||
t.mu.RUnlock()
|
||||
t.Stop()
|
||||
return
|
||||
}
|
||||
t.mu.RUnlock()
|
||||
|
||||
// 执行ping测试
|
||||
result := t.executePing()
|
||||
if resultCallback != nil {
|
||||
resultCallback(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *PingTask) Stop() {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
if t.IsRunning {
|
||||
t.IsRunning = false
|
||||
close(t.StopCh)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *PingTask) UpdateLastRequest() {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
t.LastRequest = time.Now()
|
||||
}
|
||||
|
||||
func (t *PingTask) executePing() map[string]interface{} {
|
||||
cmd := exec.Command("ping", "-c", "4", t.Target)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return map[string]interface{}{
|
||||
"timestamp": time.Now().Unix(),
|
||||
"latency": -1,
|
||||
"success": false,
|
||||
"packet_loss": true,
|
||||
"error": err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
// 解析ping输出
|
||||
result := parsePingOutput(string(output))
|
||||
result["timestamp"] = time.Now().Unix()
|
||||
return result
|
||||
}
|
||||
|
||||
func parsePingOutput(output string) map[string]interface{} {
|
||||
result := map[string]interface{}{
|
||||
"latency": 0.0,
|
||||
"success": true,
|
||||
"packet_loss": false,
|
||||
}
|
||||
|
||||
lines := strings.Split(output, "\n")
|
||||
for _, line := range lines {
|
||||
if strings.Contains(line, "packets transmitted") {
|
||||
// 解析丢包率
|
||||
parts := strings.Fields(line)
|
||||
for i, part := range parts {
|
||||
if part == "packet" && i+2 < len(parts) {
|
||||
if loss, err := strconv.ParseFloat(strings.Trim(parts[i+1], "%"), 64); err == nil {
|
||||
result["packet_loss"] = loss > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if strings.Contains(line, "min/avg/max") {
|
||||
// 解析平均延迟
|
||||
parts := strings.Fields(line)
|
||||
for _, part := range parts {
|
||||
if strings.Contains(part, "/") {
|
||||
times := strings.Split(part, "/")
|
||||
if len(times) >= 2 {
|
||||
if avg, err := strconv.ParseFloat(times[1], 64); err == nil {
|
||||
result["latency"] = avg
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
126
internal/continuous/tcping.go
Normal file
126
internal/continuous/tcping.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package continuous
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type TCPingTask struct {
|
||||
TaskID string
|
||||
Target string
|
||||
Host string
|
||||
Port int
|
||||
Interval time.Duration
|
||||
MaxDuration time.Duration
|
||||
StartTime time.Time
|
||||
LastRequest time.Time
|
||||
StopCh chan struct{}
|
||||
IsRunning bool
|
||||
mu sync.RWMutex
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
func NewTCPingTask(taskID, target string, interval, maxDuration time.Duration) (*TCPingTask, error) {
|
||||
// 解析host:port
|
||||
parts := strings.Split(target, ":")
|
||||
if len(parts) != 2 {
|
||||
return nil, fmt.Errorf("无效的target格式,需要 host:port")
|
||||
}
|
||||
|
||||
host := parts[0]
|
||||
port, err := strconv.Atoi(parts[1])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("无效的端口: %v", err)
|
||||
}
|
||||
|
||||
logger, _ := zap.NewProduction()
|
||||
return &TCPingTask{
|
||||
TaskID: taskID,
|
||||
Target: target,
|
||||
Host: host,
|
||||
Port: port,
|
||||
Interval: interval,
|
||||
MaxDuration: maxDuration,
|
||||
StartTime: time.Now(),
|
||||
LastRequest: time.Now(),
|
||||
StopCh: make(chan struct{}),
|
||||
IsRunning: true,
|
||||
logger: logger,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *TCPingTask) Start(ctx context.Context, resultCallback func(result map[string]interface{})) {
|
||||
ticker := time.NewTicker(t.Interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-t.StopCh:
|
||||
return
|
||||
case <-ticker.C:
|
||||
// 检查是否超过最大运行时长
|
||||
t.mu.RLock()
|
||||
if time.Since(t.StartTime) > t.MaxDuration {
|
||||
t.mu.RUnlock()
|
||||
t.Stop()
|
||||
return
|
||||
}
|
||||
t.mu.RUnlock()
|
||||
|
||||
// 执行tcping测试
|
||||
result := t.executeTCPing()
|
||||
if resultCallback != nil {
|
||||
resultCallback(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TCPingTask) Stop() {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
if t.IsRunning {
|
||||
t.IsRunning = false
|
||||
close(t.StopCh)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TCPingTask) UpdateLastRequest() {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
t.LastRequest = time.Now()
|
||||
}
|
||||
|
||||
func (t *TCPingTask) executeTCPing() map[string]interface{} {
|
||||
start := time.Now()
|
||||
conn, err := net.DialTimeout("tcp", net.JoinHostPort(t.Host, strconv.Itoa(t.Port)), 5*time.Second)
|
||||
latency := time.Since(start).Milliseconds()
|
||||
|
||||
if err != nil {
|
||||
return map[string]interface{}{
|
||||
"timestamp": time.Now().Unix(),
|
||||
"latency": -1,
|
||||
"success": false,
|
||||
"packet_loss": true,
|
||||
"error": err.Error(),
|
||||
}
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
return map[string]interface{}{
|
||||
"timestamp": time.Now().Unix(),
|
||||
"latency": float64(latency),
|
||||
"success": true,
|
||||
"packet_loss": false,
|
||||
}
|
||||
}
|
||||
|
||||
315
internal/handler/continuous.go
Normal file
315
internal/handler/continuous.go
Normal file
@@ -0,0 +1,315 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"linkmaster-node/internal/config"
|
||||
"linkmaster-node/internal/continuous"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
var continuousTasks = make(map[string]*ContinuousTask)
|
||||
var taskMutex sync.RWMutex
|
||||
var backendURL string
|
||||
var logger *zap.Logger
|
||||
|
||||
func InitContinuousHandler(cfg *config.Config) {
|
||||
backendURL = cfg.Backend.URL
|
||||
logger, _ = zap.NewProduction()
|
||||
}
|
||||
|
||||
type ContinuousTask struct {
|
||||
TaskID string
|
||||
Type string
|
||||
Target string
|
||||
Interval time.Duration
|
||||
MaxDuration time.Duration
|
||||
StartTime time.Time
|
||||
LastRequest time.Time
|
||||
StopCh chan struct{}
|
||||
IsRunning bool
|
||||
pingTask *continuous.PingTask
|
||||
tcpingTask *continuous.TCPingTask
|
||||
}
|
||||
|
||||
func HandleContinuousStart(c *gin.Context) {
|
||||
var req struct {
|
||||
Type string `json:"type" binding:"required"`
|
||||
Target string `json:"target" binding:"required"`
|
||||
Interval int `json:"interval"` // 秒
|
||||
MaxDuration int `json:"max_duration"` // 分钟
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 生成任务ID
|
||||
taskID := generateTaskID()
|
||||
|
||||
// 设置默认值
|
||||
interval := 10 * time.Second
|
||||
if req.Interval > 0 {
|
||||
interval = time.Duration(req.Interval) * time.Second
|
||||
}
|
||||
|
||||
maxDuration := 60 * time.Minute
|
||||
if req.MaxDuration > 0 {
|
||||
maxDuration = time.Duration(req.MaxDuration) * time.Minute
|
||||
}
|
||||
|
||||
// 创建任务
|
||||
task := &ContinuousTask{
|
||||
TaskID: taskID,
|
||||
Type: req.Type,
|
||||
Target: req.Target,
|
||||
Interval: interval,
|
||||
MaxDuration: maxDuration,
|
||||
StartTime: time.Now(),
|
||||
LastRequest: time.Now(),
|
||||
StopCh: make(chan struct{}),
|
||||
IsRunning: true,
|
||||
}
|
||||
|
||||
// 根据类型创建对应的任务
|
||||
if req.Type == "ping" {
|
||||
pingTask := continuous.NewPingTask(taskID, req.Target, interval, maxDuration)
|
||||
task.pingTask = pingTask
|
||||
} else if req.Type == "tcping" {
|
||||
tcpingTask, err := continuous.NewTCPingTask(taskID, req.Target, interval, maxDuration)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
task.tcpingTask = tcpingTask
|
||||
} else {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "不支持的持续测试类型"})
|
||||
return
|
||||
}
|
||||
|
||||
taskMutex.Lock()
|
||||
continuousTasks[taskID] = task
|
||||
taskMutex.Unlock()
|
||||
|
||||
// 启动持续测试goroutine
|
||||
ctx := context.Background()
|
||||
if task.pingTask != nil {
|
||||
go task.pingTask.Start(ctx, func(result map[string]interface{}) {
|
||||
pushResultToBackend(taskID, result)
|
||||
})
|
||||
} else if task.tcpingTask != nil {
|
||||
go task.tcpingTask.Start(ctx, func(result map[string]interface{}) {
|
||||
pushResultToBackend(taskID, result)
|
||||
})
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"task_id": taskID,
|
||||
})
|
||||
}
|
||||
|
||||
func HandleContinuousStop(c *gin.Context) {
|
||||
var req struct {
|
||||
TaskID string `json:"task_id" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
taskMutex.Lock()
|
||||
task, exists := continuousTasks[req.TaskID]
|
||||
if exists {
|
||||
task.IsRunning = false
|
||||
if task.pingTask != nil {
|
||||
task.pingTask.Stop()
|
||||
}
|
||||
if task.tcpingTask != nil {
|
||||
task.tcpingTask.Stop()
|
||||
}
|
||||
close(task.StopCh)
|
||||
delete(continuousTasks, req.TaskID)
|
||||
}
|
||||
taskMutex.Unlock()
|
||||
|
||||
if !exists {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "任务不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "任务已停止"})
|
||||
}
|
||||
|
||||
func HandleContinuousStatus(c *gin.Context) {
|
||||
taskID := c.Query("task_id")
|
||||
if taskID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "task_id参数缺失"})
|
||||
return
|
||||
}
|
||||
|
||||
taskMutex.RLock()
|
||||
task, exists := continuousTasks[taskID]
|
||||
if exists {
|
||||
// 更新LastRequest时间
|
||||
task.LastRequest = time.Now()
|
||||
if task.pingTask != nil {
|
||||
task.pingTask.UpdateLastRequest()
|
||||
}
|
||||
if task.tcpingTask != nil {
|
||||
task.tcpingTask.UpdateLastRequest()
|
||||
}
|
||||
}
|
||||
taskMutex.RUnlock()
|
||||
|
||||
if !exists {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "任务不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"task_id": task.TaskID,
|
||||
"is_running": task.IsRunning,
|
||||
"start_time": task.StartTime,
|
||||
"last_request": task.LastRequest,
|
||||
})
|
||||
}
|
||||
|
||||
func pushResultToBackend(taskID string, result map[string]interface{}) {
|
||||
// 推送结果到后端
|
||||
url := fmt.Sprintf("%s/api/public/node/continuous/result", backendURL)
|
||||
|
||||
// 获取本机IP
|
||||
nodeIP := getLocalIP()
|
||||
|
||||
data := map[string]interface{}{
|
||||
"task_id": taskID,
|
||||
"node_ip": nodeIP,
|
||||
"result": result,
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
logger.Error("序列化结果失败", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
logger.Error("创建请求失败", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{Timeout: 5 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
logger.Warn("推送结果失败", zap.Error(err))
|
||||
// 如果推送失败,停止任务
|
||||
taskMutex.Lock()
|
||||
if task, exists := continuousTasks[taskID]; exists {
|
||||
task.IsRunning = false
|
||||
if task.pingTask != nil {
|
||||
task.pingTask.Stop()
|
||||
}
|
||||
if task.tcpingTask != nil {
|
||||
task.tcpingTask.Stop()
|
||||
}
|
||||
delete(continuousTasks, taskID)
|
||||
}
|
||||
taskMutex.Unlock()
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
logger.Warn("推送结果失败", zap.Int("status", resp.StatusCode))
|
||||
// 如果推送失败,停止任务
|
||||
taskMutex.Lock()
|
||||
if task, exists := continuousTasks[taskID]; exists {
|
||||
task.IsRunning = false
|
||||
if task.pingTask != nil {
|
||||
task.pingTask.Stop()
|
||||
}
|
||||
if task.tcpingTask != nil {
|
||||
task.tcpingTask.Stop()
|
||||
}
|
||||
delete(continuousTasks, taskID)
|
||||
}
|
||||
taskMutex.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func getLocalIP() string {
|
||||
// 简化实现:返回第一个非回环IP
|
||||
// 实际应该获取外网IP
|
||||
addrs, err := net.InterfaceAddrs()
|
||||
if err != nil {
|
||||
return "127.0.0.1"
|
||||
}
|
||||
|
||||
for _, addr := range addrs {
|
||||
if ipNet, ok := addr.(*net.IPNet); ok && !ipNet.IP.IsLoopback() {
|
||||
if ipNet.IP.To4() != nil {
|
||||
return ipNet.IP.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "127.0.0.1"
|
||||
}
|
||||
|
||||
func generateTaskID() string {
|
||||
return fmt.Sprintf("task_%d", time.Now().UnixNano())
|
||||
}
|
||||
|
||||
// 定期清理超时任务
|
||||
func StartTaskCleanup() {
|
||||
ticker := time.NewTicker(1 * time.Minute)
|
||||
go func() {
|
||||
for range ticker.C {
|
||||
now := time.Now()
|
||||
taskMutex.Lock()
|
||||
for taskID, task := range continuousTasks {
|
||||
// 检查最大运行时长
|
||||
if now.Sub(task.StartTime) > task.MaxDuration {
|
||||
logger.Info("任务达到最大运行时长,自动停止", zap.String("task_id", taskID))
|
||||
task.IsRunning = false
|
||||
if task.pingTask != nil {
|
||||
task.pingTask.Stop()
|
||||
}
|
||||
if task.tcpingTask != nil {
|
||||
task.tcpingTask.Stop()
|
||||
}
|
||||
delete(continuousTasks, taskID)
|
||||
continue
|
||||
}
|
||||
// 检查无客户端连接(30分钟无请求)
|
||||
if now.Sub(task.LastRequest) > 30*time.Minute {
|
||||
logger.Info("任务无客户端连接,自动停止", zap.String("task_id", taskID))
|
||||
task.IsRunning = false
|
||||
if task.pingTask != nil {
|
||||
task.pingTask.Stop()
|
||||
}
|
||||
if task.tcpingTask != nil {
|
||||
task.tcpingTask.Stop()
|
||||
}
|
||||
delete(continuousTasks, taskID)
|
||||
}
|
||||
}
|
||||
taskMutex.Unlock()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
45
internal/handler/dns.go
Normal file
45
internal/handler/dns.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func handleDns(c *gin.Context, url string, params map[string]interface{}) {
|
||||
// 执行DNS查询
|
||||
start := time.Now()
|
||||
ips, err := net.LookupIP(url)
|
||||
lookupTime := time.Since(start).Milliseconds()
|
||||
|
||||
if err != nil {
|
||||
c.JSON(200, gin.H{
|
||||
"type": "ceDns",
|
||||
"url": url,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 格式化IP列表
|
||||
ipList := make([]map[string]interface{}, 0)
|
||||
for _, ip := range ips {
|
||||
ipType := "A"
|
||||
if ip.To4() == nil {
|
||||
ipType = "AAAA"
|
||||
}
|
||||
ipList = append(ipList, map[string]interface{}{
|
||||
"type": ipType,
|
||||
"ip": ip.String(),
|
||||
})
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
"type": "ceDns",
|
||||
"url": url,
|
||||
"ips": ipList,
|
||||
"lookup_time": lookupTime,
|
||||
})
|
||||
}
|
||||
|
||||
84
internal/handler/findping.go
Normal file
84
internal/handler/findping.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os/exec"
|
||||
"sync"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func handleFindPing(c *gin.Context, url string, params map[string]interface{}) {
|
||||
// url应该是CIDR格式,如 8.8.8.0/24
|
||||
cidr := url
|
||||
if cidrParam, ok := params["cidr"].(string); ok && cidrParam != "" {
|
||||
cidr = cidrParam
|
||||
}
|
||||
|
||||
// 解析CIDR
|
||||
_, ipNet, err := net.ParseCIDR(cidr)
|
||||
if err != nil {
|
||||
c.JSON(200, gin.H{
|
||||
"type": "ceFindPing",
|
||||
"error": "无效的CIDR格式",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 生成IP列表
|
||||
var ipList []string
|
||||
for ip := ipNet.IP.Mask(ipNet.Mask); ipNet.Contains(ip); incIP(ip) {
|
||||
ipList = append(ipList, ip.String())
|
||||
}
|
||||
|
||||
// 移除网络地址和广播地址
|
||||
if len(ipList) > 2 {
|
||||
ipList = ipList[1 : len(ipList)-1]
|
||||
}
|
||||
|
||||
// 并发ping测试
|
||||
var wg sync.WaitGroup
|
||||
var mu sync.Mutex
|
||||
aliveIPs := make([]string, 0)
|
||||
|
||||
// 限制并发数
|
||||
semaphore := make(chan struct{}, 50)
|
||||
|
||||
for _, ip := range ipList {
|
||||
wg.Add(1)
|
||||
semaphore <- struct{}{}
|
||||
go func(ipAddr string) {
|
||||
defer wg.Done()
|
||||
defer func() { <-semaphore }()
|
||||
|
||||
// 执行ping(只ping一次,快速检测)
|
||||
cmd := exec.Command("ping", "-c", "1", "-W", "1", ipAddr)
|
||||
err := cmd.Run()
|
||||
if err == nil {
|
||||
mu.Lock()
|
||||
aliveIPs = append(aliveIPs, ipAddr)
|
||||
mu.Unlock()
|
||||
}
|
||||
}(ip)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
"type": "ceFindPing",
|
||||
"cidr": cidr,
|
||||
"alive_ips": aliveIPs,
|
||||
"alive_count": len(aliveIPs),
|
||||
"total_ips": len(ipList),
|
||||
})
|
||||
}
|
||||
|
||||
func incIP(ip net.IP) {
|
||||
for j := len(ip) - 1; j >= 0; j-- {
|
||||
ip[j]++
|
||||
if ip[j] > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
32
internal/handler/get.go
Normal file
32
internal/handler/get.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func handleGet(c *gin.Context, url string, params map[string]interface{}) {
|
||||
// TODO: 实现HTTP GET测试
|
||||
// 这里先返回一个简单的响应
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"type": "ceGet",
|
||||
"url": url,
|
||||
"statuscode": 200,
|
||||
"totaltime": time.Since(time.Now()).Milliseconds(),
|
||||
"response": "OK",
|
||||
})
|
||||
}
|
||||
|
||||
func handlePost(c *gin.Context, url string, params map[string]interface{}) {
|
||||
// TODO: 实现HTTP POST测试
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"type": "cePost",
|
||||
"url": url,
|
||||
"statuscode": 200,
|
||||
"totaltime": time.Since(time.Now()).Milliseconds(),
|
||||
"response": "OK",
|
||||
})
|
||||
}
|
||||
|
||||
85
internal/handler/ping.go
Normal file
85
internal/handler/ping.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func handlePing(c *gin.Context, url string, params map[string]interface{}) {
|
||||
// 执行ping命令
|
||||
cmd := exec.Command("ping", "-c", "4", url)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
c.JSON(200, gin.H{
|
||||
"type": "cePing",
|
||||
"url": url,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 解析ping输出
|
||||
result := parsePingOutput(string(output), url)
|
||||
c.JSON(200, result)
|
||||
}
|
||||
|
||||
func parsePingOutput(output, url string) map[string]interface{} {
|
||||
result := map[string]interface{}{
|
||||
"type": "cePing",
|
||||
"url": url,
|
||||
"ip": "",
|
||||
}
|
||||
|
||||
// 解析IP地址
|
||||
lines := strings.Split(output, "\n")
|
||||
for _, line := range lines {
|
||||
if strings.Contains(line, "PING") {
|
||||
// 提取IP地址
|
||||
parts := strings.Fields(line)
|
||||
for _, part := range parts {
|
||||
if net.ParseIP(part) != nil {
|
||||
result["ip"] = part
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if strings.Contains(line, "packets transmitted") {
|
||||
// 解析丢包率
|
||||
parts := strings.Fields(line)
|
||||
for i, part := range parts {
|
||||
if part == "packet" && i+2 < len(parts) {
|
||||
if loss, err := strconv.ParseFloat(strings.Trim(parts[i+1], "%"), 64); err == nil {
|
||||
result["packets_losrat"] = loss
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if strings.Contains(line, "min/avg/max") {
|
||||
// 解析延迟统计
|
||||
parts := strings.Fields(line)
|
||||
for _, part := range parts {
|
||||
if strings.Contains(part, "/") {
|
||||
times := strings.Split(part, "/")
|
||||
if len(times) >= 3 {
|
||||
if min, err := strconv.ParseFloat(times[0], 64); err == nil {
|
||||
result["time_min"] = min
|
||||
}
|
||||
if avg, err := strconv.ParseFloat(times[1], 64); err == nil {
|
||||
result["time_avg"] = avg
|
||||
}
|
||||
if max, err := strconv.ParseFloat(times[2], 64); err == nil {
|
||||
result["time_max"] = max
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
59
internal/handler/socket.go
Normal file
59
internal/handler/socket.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func handleSocket(c *gin.Context, url string, params map[string]interface{}) {
|
||||
// 解析host:port格式
|
||||
parts := strings.Split(url, ":")
|
||||
if len(parts) != 2 {
|
||||
c.JSON(200, gin.H{
|
||||
"type": "ceSocket",
|
||||
"url": url,
|
||||
"error": "格式错误,需要 host:port",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
host := parts[0]
|
||||
portStr := parts[1]
|
||||
port, err := strconv.Atoi(portStr)
|
||||
if err != nil {
|
||||
c.JSON(200, gin.H{
|
||||
"type": "ceSocket",
|
||||
"url": url,
|
||||
"error": "端口格式错误",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 执行TCP连接测试
|
||||
conn, err := net.DialTimeout("tcp", net.JoinHostPort(host, portStr), 5*time.Second)
|
||||
if err != nil {
|
||||
c.JSON(200, gin.H{
|
||||
"type": "ceSocket",
|
||||
"url": url,
|
||||
"host": host,
|
||||
"port": port,
|
||||
"result": "false",
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
"type": "ceSocket",
|
||||
"url": url,
|
||||
"host": host,
|
||||
"port": port,
|
||||
"result": "true",
|
||||
})
|
||||
}
|
||||
|
||||
63
internal/handler/tcping.go
Normal file
63
internal/handler/tcping.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func handleTCPing(c *gin.Context, url string, params map[string]interface{}) {
|
||||
// 解析host:port格式
|
||||
parts := strings.Split(url, ":")
|
||||
if len(parts) != 2 {
|
||||
c.JSON(200, gin.H{
|
||||
"type": "ceTCPing",
|
||||
"url": url,
|
||||
"error": "格式错误,需要 host:port",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
host := parts[0]
|
||||
portStr := parts[1]
|
||||
port, err := strconv.Atoi(portStr)
|
||||
if err != nil {
|
||||
c.JSON(200, gin.H{
|
||||
"type": "ceTCPing",
|
||||
"url": url,
|
||||
"error": "端口格式错误",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 执行TCP连接测试
|
||||
start := time.Now()
|
||||
conn, err := net.DialTimeout("tcp", net.JoinHostPort(host, portStr), 5*time.Second)
|
||||
latency := time.Since(start).Milliseconds()
|
||||
|
||||
if err != nil {
|
||||
c.JSON(200, gin.H{
|
||||
"type": "ceTCPing",
|
||||
"url": url,
|
||||
"host": host,
|
||||
"port": port,
|
||||
"latency": -1,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
"type": "ceTCPing",
|
||||
"url": url,
|
||||
"host": host,
|
||||
"port": port,
|
||||
"latency": latency,
|
||||
"success": true,
|
||||
})
|
||||
}
|
||||
|
||||
49
internal/handler/test.go
Normal file
49
internal/handler/test.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// HandleTest 统一测试接口
|
||||
func HandleTest(c *gin.Context) {
|
||||
var req struct {
|
||||
Type string `json:"type" binding:"required"`
|
||||
URL string `json:"url" binding:"required"`
|
||||
Params map[string]interface{} `json:"params"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 根据类型分发到不同的处理器
|
||||
switch req.Type {
|
||||
case "ceGet":
|
||||
handleGet(c, req.URL, req.Params)
|
||||
case "cePost":
|
||||
handlePost(c, req.URL, req.Params)
|
||||
case "cePing":
|
||||
handlePing(c, req.URL, req.Params)
|
||||
case "ceDns":
|
||||
handleDns(c, req.URL, req.Params)
|
||||
case "ceTrace":
|
||||
handleTrace(c, req.URL, req.Params)
|
||||
case "ceSocket":
|
||||
handleSocket(c, req.URL, req.Params)
|
||||
case "ceTCPing":
|
||||
handleTCPing(c, req.URL, req.Params)
|
||||
case "ceFindPing":
|
||||
handleFindPing(c, req.URL, req.Params)
|
||||
default:
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "不支持的测试类型"})
|
||||
}
|
||||
}
|
||||
|
||||
// HandleHealth 健康检查
|
||||
func HandleHealth(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||||
}
|
||||
|
||||
39
internal/handler/trace.go
Normal file
39
internal/handler/trace.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func handleTrace(c *gin.Context, url string, params map[string]interface{}) {
|
||||
// 执行traceroute命令
|
||||
cmd := exec.Command("traceroute", url)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
c.JSON(200, gin.H{
|
||||
"type": "ceTrace",
|
||||
"url": url,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 解析输出
|
||||
lines := strings.Split(string(output), "\n")
|
||||
traceResult := make([]string, 0)
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line != "" {
|
||||
traceResult = append(traceResult, line)
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
"type": "ceTrace",
|
||||
"url": url,
|
||||
"trace_result": traceResult,
|
||||
})
|
||||
}
|
||||
|
||||
103
internal/heartbeat/reporter.go
Normal file
103
internal/heartbeat/reporter.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package heartbeat
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"linkmaster-node/internal/config"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type Reporter struct {
|
||||
cfg *config.Config
|
||||
client *http.Client
|
||||
logger *zap.Logger
|
||||
stopCh chan struct{}
|
||||
}
|
||||
|
||||
func NewReporter(cfg *config.Config) *Reporter {
|
||||
logger, _ := zap.NewProduction()
|
||||
return &Reporter{
|
||||
cfg: cfg,
|
||||
client: &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
},
|
||||
logger: logger,
|
||||
stopCh: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Reporter) Start(ctx context.Context) {
|
||||
ticker := time.NewTicker(time.Duration(r.cfg.Heartbeat.Interval) * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
// 立即发送一次心跳
|
||||
r.sendHeartbeat()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-r.stopCh:
|
||||
return
|
||||
case <-ticker.C:
|
||||
r.sendHeartbeat()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Reporter) Stop() {
|
||||
close(r.stopCh)
|
||||
}
|
||||
|
||||
func (r *Reporter) sendHeartbeat() {
|
||||
// 获取本机IP
|
||||
localIP := getLocalIP()
|
||||
|
||||
// 发送心跳(使用Form格式,兼容旧接口)
|
||||
url := fmt.Sprintf("%s/api/node/heartbeat", r.cfg.Backend.URL)
|
||||
req, err := http.NewRequest("POST", url, bytes.NewBufferString(fmt.Sprintf("realNodeIP=%s&type=pingServer", localIP)))
|
||||
if err != nil {
|
||||
r.logger.Error("创建心跳请求失败", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
resp, err := r.client.Do(req)
|
||||
if err != nil {
|
||||
r.logger.Warn("发送心跳失败", zap.Error(err))
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
r.logger.Debug("心跳发送成功", zap.String("ip", localIP))
|
||||
} else {
|
||||
r.logger.Warn("心跳发送失败", zap.Int("status", resp.StatusCode))
|
||||
}
|
||||
}
|
||||
|
||||
func getLocalIP() string {
|
||||
// 获取第一个非回环IP
|
||||
addrs, err := net.InterfaceAddrs()
|
||||
if err != nil {
|
||||
return "127.0.0.1"
|
||||
}
|
||||
|
||||
for _, addr := range addrs {
|
||||
if ipNet, ok := addr.(*net.IPNet); ok && !ipNet.IP.IsLoopback() {
|
||||
if ipNet.IP.To4() != nil {
|
||||
return ipNet.IP.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "127.0.0.1"
|
||||
}
|
||||
|
||||
25
internal/recovery/recovery.go
Normal file
25
internal/recovery/recovery.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package recovery
|
||||
|
||||
import (
|
||||
"runtime/debug"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
var logger *zap.Logger
|
||||
|
||||
func Init() {
|
||||
// 初始化logger(这里简化处理,实际应该从外部传入)
|
||||
logger, _ = zap.NewProduction()
|
||||
}
|
||||
|
||||
// Recover 恢复panic
|
||||
func Recover() {
|
||||
if r := recover(); r != nil {
|
||||
logger.Error("发生panic",
|
||||
zap.Any("panic", r),
|
||||
zap.String("stack", string(debug.Stack())),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
72
internal/server/http.go
Normal file
72
internal/server/http.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"linkmaster-node/internal/config"
|
||||
"linkmaster-node/internal/handler"
|
||||
"linkmaster-node/internal/recovery"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type HTTPServer struct {
|
||||
server *http.Server
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
func NewHTTPServer(cfg *config.Config) *HTTPServer {
|
||||
if !cfg.Debug {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
}
|
||||
|
||||
router := gin.New()
|
||||
router.Use(gin.Recovery())
|
||||
router.Use(recoveryMiddleware)
|
||||
|
||||
// 初始化持续测试处理器
|
||||
handler.InitContinuousHandler(cfg)
|
||||
|
||||
// 启动任务清理goroutine
|
||||
handler.StartTaskCleanup()
|
||||
|
||||
// 注册路由
|
||||
api := router.Group("/api")
|
||||
{
|
||||
api.POST("/test", handler.HandleTest)
|
||||
api.POST("/continuous/start", handler.HandleContinuousStart)
|
||||
api.POST("/continuous/stop", handler.HandleContinuousStop)
|
||||
api.GET("/continuous/status", handler.HandleContinuousStatus)
|
||||
api.GET("/health", handler.HandleHealth)
|
||||
}
|
||||
|
||||
server := &http.Server{
|
||||
Addr: fmt.Sprintf(":%d", cfg.Server.Port),
|
||||
Handler: router,
|
||||
}
|
||||
|
||||
logger, _ := zap.NewProduction()
|
||||
|
||||
return &HTTPServer{
|
||||
server: server,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *HTTPServer) Start() error {
|
||||
s.logger.Info("HTTP服务器启动", zap.String("addr", s.server.Addr))
|
||||
return s.server.ListenAndServe()
|
||||
}
|
||||
|
||||
func (s *HTTPServer) Shutdown(ctx context.Context) error {
|
||||
return s.server.Shutdown(ctx)
|
||||
}
|
||||
|
||||
func recoveryMiddleware(c *gin.Context) {
|
||||
defer recovery.Recover()
|
||||
c.Next()
|
||||
}
|
||||
|
||||
5
node.log
Normal file
5
node.log
Normal file
@@ -0,0 +1,5 @@
|
||||
{"level":"info","ts":1763025207.120181,"caller":"agent/main.go:35","msg":"节点服务启动","version":"1.0.0"}
|
||||
{"level":"info","ts":1763025207.1208231,"caller":"server/http.go:60","msg":"HTTP服务器启动","addr":":2200"}
|
||||
{"level":"info","ts":1763653448.720011,"caller":"agent/main.go:57","msg":"收到停止信号,正在关闭服务..."}
|
||||
{"level":"fatal","ts":1763653448.720453,"caller":"agent/main.go:48","msg":"HTTP服务器启动失败","error":"http: Server closed","stacktrace":"main.main.func1\n\t/Users/yoyo/Desktop/newLinkMaster/node/cmd/agent/main.go:48"}
|
||||
{"level":"info","ts":1763653448.720591,"caller":"agent/main.go:66","msg":"服务已关闭"}
|
||||
339
run.sh
Executable file
339
run.sh
Executable file
@@ -0,0 +1,339 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ============================================
|
||||
# LinkMaster 节点端运行脚本
|
||||
# 用途:启动、停止、重启节点端服务
|
||||
# ============================================
|
||||
|
||||
set -e
|
||||
|
||||
# 颜色输出
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# 脚本目录
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
# 配置
|
||||
BINARY_NAME="agent"
|
||||
LOG_FILE="node.log"
|
||||
PID_FILE="node.pid"
|
||||
BACKEND_URL="${BACKEND_URL:-http://localhost:8080}"
|
||||
|
||||
# 获取PID
|
||||
get_pid() {
|
||||
if [ -f "$PID_FILE" ]; then
|
||||
PID=$(cat "$PID_FILE")
|
||||
if ps -p "$PID" > /dev/null 2>&1; then
|
||||
echo "$PID"
|
||||
else
|
||||
rm -f "$PID_FILE"
|
||||
echo ""
|
||||
fi
|
||||
else
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
# 拉取最新源码并编译
|
||||
update_and_build() {
|
||||
echo -e "${BLUE}拉取最新源码...${NC}"
|
||||
|
||||
# 检查是否在 Git 仓库中
|
||||
if [ ! -d ".git" ]; then
|
||||
echo -e "${YELLOW}警告: 当前目录不是 Git 仓库,跳过代码更新${NC}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# 拉取最新代码
|
||||
if git pull 2>&1; then
|
||||
echo -e "${GREEN}✓ 代码更新完成${NC}"
|
||||
else
|
||||
PULL_EXIT_CODE=$?
|
||||
echo -e "${YELLOW}警告: Git 拉取失败(退出码: $PULL_EXIT_CODE),将使用当前代码继续${NC}"
|
||||
echo -e "${YELLOW}可能原因: 网络问题、权限问题或本地有未提交的更改${NC}"
|
||||
fi
|
||||
|
||||
# 检查 Go 环境
|
||||
if ! command -v go > /dev/null 2>&1; then
|
||||
echo -e "${RED}错误: 未找到 Go 环境,无法编译${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 更新依赖
|
||||
echo -e "${BLUE}更新 Go 依赖...${NC}"
|
||||
if ! go mod download 2>&1; then
|
||||
echo -e "${YELLOW}警告: 依赖更新失败,尝试继续编译${NC}"
|
||||
else
|
||||
echo -e "${GREEN}✓ 依赖更新完成${NC}"
|
||||
fi
|
||||
|
||||
# 编译
|
||||
echo -e "${BLUE}编译二进制文件...${NC}"
|
||||
ARCH=$(uname -m)
|
||||
case $ARCH in
|
||||
x86_64)
|
||||
ARCH="amd64"
|
||||
;;
|
||||
aarch64|arm64)
|
||||
ARCH="arm64"
|
||||
;;
|
||||
*)
|
||||
ARCH="amd64"
|
||||
;;
|
||||
esac
|
||||
|
||||
if GOOS=linux GOARCH=${ARCH} CGO_ENABLED=0 go build -ldflags="-w -s" -o "$BINARY_NAME" ./cmd/agent 2>&1; then
|
||||
if [ -f "$BINARY_NAME" ] && [ -s "$BINARY_NAME" ]; then
|
||||
chmod +x "$BINARY_NAME"
|
||||
echo -e "${GREEN}✓ 编译成功${NC}"
|
||||
else
|
||||
echo -e "${RED}错误: 编译失败,未生成二进制文件${NC}"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo -e "${RED}错误: 编译失败${NC}"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 检查二进制文件
|
||||
check_binary() {
|
||||
if [ ! -f "$BINARY_NAME" ]; then
|
||||
echo -e "${RED}错误: 找不到二进制文件 $BINARY_NAME${NC}"
|
||||
echo -e "${YELLOW}尝试编译...${NC}"
|
||||
update_and_build
|
||||
return
|
||||
fi
|
||||
|
||||
if [ ! -x "$BINARY_NAME" ]; then
|
||||
chmod +x "$BINARY_NAME"
|
||||
fi
|
||||
}
|
||||
|
||||
# 检查端口占用
|
||||
check_port() {
|
||||
if command -v lsof > /dev/null 2>&1; then
|
||||
PORT_PID=$(lsof -ti :2200 2>/dev/null || echo "")
|
||||
if [ -n "$PORT_PID" ]; then
|
||||
# 检查是否是我们的进程
|
||||
if [ -f "$PID_FILE" ] && [ "$PORT_PID" = "$(cat "$PID_FILE")" ]; then
|
||||
return 0
|
||||
fi
|
||||
echo -e "${YELLOW}警告: 端口2200已被占用 (PID: $PORT_PID)${NC}"
|
||||
echo -e "${YELLOW}是否要停止该进程? (y/n)${NC}"
|
||||
read -r answer
|
||||
if [ "$answer" = "y" ] || [ "$answer" = "Y" ]; then
|
||||
kill "$PORT_PID" 2>/dev/null || true
|
||||
sleep 1
|
||||
else
|
||||
echo -e "${RED}取消启动${NC}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# 启动服务
|
||||
start() {
|
||||
PID=$(get_pid)
|
||||
if [ -n "$PID" ]; then
|
||||
echo -e "${YELLOW}节点端已在运行 (PID: $PID)${NC}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# 拉取最新源码并编译
|
||||
update_and_build
|
||||
|
||||
check_port
|
||||
|
||||
echo -e "${BLUE}启动节点端服务...${NC}"
|
||||
echo -e "${BLUE}后端地址: $BACKEND_URL${NC}"
|
||||
|
||||
# 设置环境变量
|
||||
export BACKEND_URL="$BACKEND_URL"
|
||||
|
||||
# 后台运行
|
||||
nohup ./"$BINARY_NAME" > "$LOG_FILE" 2>&1 &
|
||||
NEW_PID=$!
|
||||
|
||||
# 保存PID
|
||||
echo "$NEW_PID" > "$PID_FILE"
|
||||
|
||||
# 等待启动
|
||||
sleep 2
|
||||
|
||||
# 检查是否启动成功
|
||||
if ps -p "$NEW_PID" > /dev/null 2>&1; then
|
||||
# 再次检查健康状态
|
||||
sleep 1
|
||||
if curl -s http://localhost:2200/api/health > /dev/null 2>&1; then
|
||||
echo -e "${GREEN}✓ 节点端已启动 (PID: $NEW_PID)${NC}"
|
||||
echo -e "${BLUE}日志文件: $LOG_FILE${NC}"
|
||||
echo -e "${BLUE}查看日志: tail -f $LOG_FILE${NC}"
|
||||
else
|
||||
echo -e "${GREEN}✓ 节点端进程已启动 (PID: $NEW_PID)${NC}"
|
||||
echo -e "${YELLOW}⚠ 健康检查未通过,请稍后查看日志${NC}"
|
||||
fi
|
||||
else
|
||||
echo -e "${RED}✗ 节点端启动失败${NC}"
|
||||
echo -e "${YELLOW}请查看日志: cat $LOG_FILE${NC}"
|
||||
rm -f "$PID_FILE"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 停止服务
|
||||
stop() {
|
||||
PID=$(get_pid)
|
||||
|
||||
# 如果没有PID文件,尝试通过端口查找
|
||||
if [ -z "$PID" ]; then
|
||||
if command -v lsof > /dev/null 2>&1; then
|
||||
PORT_PID=$(lsof -ti :2200 2>/dev/null || echo "")
|
||||
if [ -n "$PORT_PID" ]; then
|
||||
PID="$PORT_PID"
|
||||
echo -e "${YELLOW}通过端口找到进程 (PID: $PID)${NC}"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$PID" ]; then
|
||||
echo -e "${YELLOW}节点端未运行${NC}"
|
||||
rm -f "$PID_FILE"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo -e "${BLUE}停止节点端服务 (PID: $PID)...${NC}"
|
||||
|
||||
# 发送TERM信号
|
||||
kill "$PID" 2>/dev/null || true
|
||||
|
||||
# 等待进程结束
|
||||
for i in {1..10}; do
|
||||
if ! ps -p "$PID" > /dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# 如果还在运行,强制杀死
|
||||
if ps -p "$PID" > /dev/null 2>&1; then
|
||||
echo -e "${YELLOW}强制停止节点端...${NC}"
|
||||
kill -9 "$PID" 2>/dev/null || true
|
||||
sleep 1
|
||||
fi
|
||||
|
||||
rm -f "$PID_FILE"
|
||||
echo -e "${GREEN}✓ 节点端已停止${NC}"
|
||||
}
|
||||
|
||||
# 重启服务
|
||||
restart() {
|
||||
echo -e "${BLUE}重启节点端服务...${NC}"
|
||||
stop
|
||||
sleep 1
|
||||
start
|
||||
}
|
||||
|
||||
# 查看状态
|
||||
status() {
|
||||
PID=$(get_pid)
|
||||
if [ -n "$PID" ]; then
|
||||
echo -e "${GREEN}节点端运行中 (PID: $PID)${NC}"
|
||||
|
||||
# 检查健康状态
|
||||
if command -v curl > /dev/null 2>&1; then
|
||||
HEALTH=$(curl -s http://localhost:2200/api/health 2>/dev/null || echo "failed")
|
||||
if [ "$HEALTH" = '{"status":"ok"}' ]; then
|
||||
echo -e "${GREEN}✓ 健康检查: 正常${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}⚠ 健康检查: 异常${NC}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 显示进程信息
|
||||
ps -p "$PID" -o pid,ppid,cmd,%mem,%cpu,etime 2>/dev/null || true
|
||||
else
|
||||
echo -e "${RED}节点端未运行${NC}"
|
||||
fi
|
||||
}
|
||||
|
||||
# 查看日志
|
||||
logs() {
|
||||
if [ -f "$LOG_FILE" ]; then
|
||||
tail -f "$LOG_FILE"
|
||||
else
|
||||
echo -e "${YELLOW}日志文件不存在: $LOG_FILE${NC}"
|
||||
fi
|
||||
}
|
||||
|
||||
# 查看完整日志
|
||||
logs_all() {
|
||||
if [ -f "$LOG_FILE" ]; then
|
||||
cat "$LOG_FILE"
|
||||
else
|
||||
echo -e "${YELLOW}日志文件不存在: $LOG_FILE${NC}"
|
||||
fi
|
||||
}
|
||||
|
||||
# 显示帮助
|
||||
help() {
|
||||
echo "LinkMaster 节点端运行脚本"
|
||||
echo ""
|
||||
echo "使用方法:"
|
||||
echo " $0 {start|stop|restart|status|logs|logs-all|help}"
|
||||
echo ""
|
||||
echo "命令说明:"
|
||||
echo " start - 启动节点端服务(会自动拉取最新代码并编译)"
|
||||
echo " stop - 停止节点端服务"
|
||||
echo " restart - 重启节点端服务"
|
||||
echo " status - 查看运行状态"
|
||||
echo " logs - 实时查看日志"
|
||||
echo " logs-all - 查看完整日志"
|
||||
echo " help - 显示帮助信息"
|
||||
echo ""
|
||||
echo "环境变量:"
|
||||
echo " BACKEND_URL - 后端服务地址 (默认: http://localhost:8080)"
|
||||
echo ""
|
||||
echo "示例:"
|
||||
echo " BACKEND_URL=http://192.168.1.100:8080 $0 start"
|
||||
echo " $0 status"
|
||||
echo " $0 logs"
|
||||
}
|
||||
|
||||
# 主逻辑
|
||||
case "${1:-help}" in
|
||||
start)
|
||||
start
|
||||
;;
|
||||
stop)
|
||||
stop
|
||||
;;
|
||||
restart)
|
||||
restart
|
||||
;;
|
||||
status)
|
||||
status
|
||||
;;
|
||||
logs)
|
||||
logs
|
||||
;;
|
||||
logs-all)
|
||||
logs_all
|
||||
;;
|
||||
help|--help|-h)
|
||||
help
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}未知命令: $1${NC}"
|
||||
echo ""
|
||||
help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
Reference in New Issue
Block a user