配置全自动docker版本部署
This commit is contained in:
+33
@@ -0,0 +1,33 @@
|
|||||||
|
# ============================================================
|
||||||
|
# frpc-console .gitignore
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
# ---------- 构建产物 ----------
|
||||||
|
/build/
|
||||||
|
frpc-console
|
||||||
|
frpc-console.exe
|
||||||
|
*.exe
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# ---------- 运行时生成 ----------
|
||||||
|
*.db
|
||||||
|
*.db-journal
|
||||||
|
frpc.log
|
||||||
|
frpc.pid
|
||||||
|
frpc.toml
|
||||||
|
|
||||||
|
# ---------- IDE ----------
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# ---------- OS 垃圾 ----------
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
desktop.ini
|
||||||
|
|
||||||
|
# ---------- 临时文件 ----------
|
||||||
|
/tmp/
|
||||||
|
*.tmp
|
||||||
+47
@@ -0,0 +1,47 @@
|
|||||||
|
# ============================================================
|
||||||
|
# 构建阶段
|
||||||
|
# ============================================================
|
||||||
|
FROM golang:1.21-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 复制依赖文件
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
# 复制源码(确保 bin/ 和 static/ 目录存在)
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# 编译 Linux 版(CGO_ENABLED=0 保证静态编译)
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux go build \
|
||||||
|
-ldflags="-s -w" \
|
||||||
|
-o frpc-console .
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 运行阶段
|
||||||
|
# ============================================================
|
||||||
|
FROM alpine:latest
|
||||||
|
|
||||||
|
# 安装 ca-certificates 和 tzdata(确保 HTTPS 和时区正常)
|
||||||
|
RUN apk --no-cache add ca-certificates tzdata
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 从构建阶段复制编译好的二进制
|
||||||
|
COPY --from=builder /app/frpc-console /app/frpc-console
|
||||||
|
|
||||||
|
# 创建数据目录(用于持久化 db 和日志)
|
||||||
|
RUN mkdir -p /app/data
|
||||||
|
|
||||||
|
# 暴露端口(默认 9300)
|
||||||
|
EXPOSE 9300
|
||||||
|
|
||||||
|
# 环境变量
|
||||||
|
ENV PORT=9300
|
||||||
|
ENV TZ=Asia/Shanghai
|
||||||
|
|
||||||
|
# 数据卷(持久化数据库、配置、日志)
|
||||||
|
VOLUME ["/app/data"]
|
||||||
|
|
||||||
|
# 启动
|
||||||
|
ENTRYPOINT ["/app/frpc-console"]
|
||||||
@@ -20,11 +20,10 @@ func SetupRouter() *gin.Engine {
|
|||||||
r := gin.Default()
|
r := gin.Default()
|
||||||
|
|
||||||
// 从 embed 读取前端静态文件
|
// 从 embed 读取前端静态文件
|
||||||
// 注意:embed.FS 不支持直接作为 http.FileSystem 传参,需要用 http.FS 转换
|
|
||||||
staticSubFS, _ := fs.Sub(staticFS, "static")
|
staticSubFS, _ := fs.Sub(staticFS, "static")
|
||||||
r.StaticFS("/static", http.FS(staticSubFS))
|
r.StaticFS("/static", http.FS(staticSubFS))
|
||||||
|
|
||||||
// 根路由:直接返回 index.html
|
// 根路由
|
||||||
r.GET("/", func(c *gin.Context) {
|
r.GET("/", func(c *gin.Context) {
|
||||||
content, err := staticFS.ReadFile("static/index.html")
|
content, err := staticFS.ReadFile("static/index.html")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -39,40 +38,32 @@ func SetupRouter() *gin.Engine {
|
|||||||
c.String(200, "frpc-console 后端已启动 🎉")
|
c.String(200, "frpc-console 后端已启动 🎉")
|
||||||
})
|
})
|
||||||
|
|
||||||
// API 路由
|
|
||||||
api := r.Group("/api")
|
api := r.Group("/api")
|
||||||
{
|
{
|
||||||
// 登录(无需鉴权)
|
|
||||||
api.GET("/check/users", checkUsersHandler)
|
api.GET("/check/users", checkUsersHandler)
|
||||||
api.POST("/register", registerHandler)
|
api.POST("/register", registerHandler)
|
||||||
api.POST("/login", loginHandler)
|
api.POST("/login", loginHandler)
|
||||||
|
|
||||||
// 需要鉴权的路由
|
|
||||||
auth := api.Group("/")
|
auth := api.Group("/")
|
||||||
auth.Use(AuthMiddleware())
|
auth.Use(AuthMiddleware())
|
||||||
{
|
{
|
||||||
// 全局配置
|
|
||||||
auth.GET("/config", getConfigHandler)
|
auth.GET("/config", getConfigHandler)
|
||||||
auth.PUT("/config", updateConfigHandler)
|
auth.PUT("/config", updateConfigHandler)
|
||||||
|
|
||||||
// 隧道管理
|
|
||||||
auth.GET("/proxies", getProxiesHandler)
|
auth.GET("/proxies", getProxiesHandler)
|
||||||
auth.GET("/proxy/:id", getProxyHandler)
|
auth.GET("/proxy/:id", getProxyHandler)
|
||||||
auth.POST("/proxy", createProxyHandler)
|
auth.POST("/proxy", createProxyHandler)
|
||||||
auth.PUT("/proxy/:id", updateProxyHandler)
|
auth.PUT("/proxy/:id", updateProxyHandler)
|
||||||
auth.DELETE("/proxy/:id", deleteProxyHandler)
|
auth.DELETE("/proxy/:id", deleteProxyHandler)
|
||||||
|
|
||||||
// frpc 控制
|
|
||||||
auth.POST("/frpc/reload", reloadFrpcHandler)
|
auth.POST("/frpc/reload", reloadFrpcHandler)
|
||||||
auth.POST("/frpc/start", startFrpcHandler)
|
auth.POST("/frpc/start", startFrpcHandler)
|
||||||
auth.POST("/frpc/stop", stopFrpcHandler)
|
auth.POST("/frpc/stop", stopFrpcHandler)
|
||||||
auth.GET("/frpc/status", getFrpcStatusHandler)
|
auth.GET("/frpc/status", getFrpcStatusHandler)
|
||||||
|
|
||||||
// 导入/导出 TOML
|
|
||||||
auth.POST("/import/toml", importTomlHandler)
|
auth.POST("/import/toml", importTomlHandler)
|
||||||
auth.GET("/export/toml", ExportTomlHandler)
|
auth.GET("/export/toml", ExportTomlHandler)
|
||||||
|
|
||||||
// 修改密码
|
|
||||||
auth.PUT("/user/password", changePasswordHandler)
|
auth.PUT("/user/password", changePasswordHandler)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,7 +71,8 @@ func SetupRouter() *gin.Engine {
|
|||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 检查是否有用户 ==========
|
// ========== 所有 Handler ==========
|
||||||
|
|
||||||
func checkUsersHandler(c *gin.Context) {
|
func checkUsersHandler(c *gin.Context) {
|
||||||
count, err := CountUsers()
|
count, err := CountUsers()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -89,14 +81,10 @@ func checkUsersHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"code": 0,
|
"code": 0,
|
||||||
"data": gin.H{
|
"data": gin.H{"hasUsers": count > 0, "count": count},
|
||||||
"hasUsers": count > 0,
|
|
||||||
"count": count,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 注册(仅首次启动可用) ==========
|
|
||||||
func registerHandler(c *gin.Context) {
|
func registerHandler(c *gin.Context) {
|
||||||
var req struct {
|
var req struct {
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
@@ -123,10 +111,7 @@ func registerHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !ValidatePassword(req.Password) {
|
if !ValidatePassword(req.Password) {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{
|
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "密码至少 8 位,需包含大小写字母、数字和特殊字符"})
|
||||||
"code": 1,
|
|
||||||
"msg": "密码至少 8 位,需包含大小写字母、数字和特殊字符",
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,7 +139,6 @@ func registerHandler(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 登录 ==========
|
|
||||||
func loginHandler(c *gin.Context) {
|
func loginHandler(c *gin.Context) {
|
||||||
var req struct {
|
var req struct {
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
@@ -189,7 +173,6 @@ func loginHandler(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 修改密码 ==========
|
|
||||||
func changePasswordHandler(c *gin.Context) {
|
func changePasswordHandler(c *gin.Context) {
|
||||||
var req struct {
|
var req struct {
|
||||||
OldPassword string `json:"oldPassword"`
|
OldPassword string `json:"oldPassword"`
|
||||||
@@ -218,10 +201,7 @@ func changePasswordHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !ValidatePassword(req.NewPassword) {
|
if !ValidatePassword(req.NewPassword) {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{
|
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "密码至少 8 位,需包含大小写字母、数字和特殊字符"})
|
||||||
"code": 1,
|
|
||||||
"msg": "密码至少 8 位,需包含大小写字母、数字和特殊字符",
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,7 +219,6 @@ func changePasswordHandler(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "密码修改成功"})
|
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "密码修改成功"})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 全局配置 ==========
|
|
||||||
func getConfigHandler(c *gin.Context) {
|
func getConfigHandler(c *gin.Context) {
|
||||||
cfg, err := GetGlobalConfig()
|
cfg, err := GetGlobalConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -275,7 +254,6 @@ func updateConfigHandler(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "配置更新成功"})
|
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "配置更新成功"})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 隧道管理 ==========
|
|
||||||
func getProxiesHandler(c *gin.Context) {
|
func getProxiesHandler(c *gin.Context) {
|
||||||
proxies, err := GetProxies()
|
proxies, err := GetProxies()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -366,7 +344,6 @@ func deleteProxyHandler(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "隧道删除成功"})
|
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "隧道删除成功"})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== frpc 控制 ==========
|
|
||||||
func reloadFrpcHandler(c *gin.Context) {
|
func reloadFrpcHandler(c *gin.Context) {
|
||||||
if err := GenerateFrpcConfig(); err != nil {
|
if err := GenerateFrpcConfig(); err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "生成配置失败: " + err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "生成配置失败: " + err.Error()})
|
||||||
@@ -404,7 +381,6 @@ func getFrpcStatusHandler(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, gin.H{"code": 0, "data": gin.H{"running": running}})
|
c.JSON(http.StatusOK, gin.H{"code": 0, "data": gin.H{"running": running}})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 导入 TOML ==========
|
|
||||||
func importTomlHandler(c *gin.Context) {
|
func importTomlHandler(c *gin.Context) {
|
||||||
file, err := c.FormFile("file")
|
file, err := c.FormFile("file")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -455,6 +431,7 @@ func importTomlHandler(c *gin.Context) {
|
|||||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "生成配置失败: " + err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "生成配置失败: " + err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ReloadFrpc(); err != nil {
|
if err := ReloadFrpc(); err != nil {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"code": 0,
|
"code": 0,
|
||||||
@@ -469,7 +446,6 @@ func importTomlHandler(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 导出 TOML ==========
|
|
||||||
func ExportTomlHandler(c *gin.Context) {
|
func ExportTomlHandler(c *gin.Context) {
|
||||||
cfg, err := GetGlobalConfig()
|
cfg, err := GetGlobalConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -515,7 +491,6 @@ func ExportTomlHandler(c *gin.Context) {
|
|||||||
c.String(http.StatusOK, buf.String())
|
c.String(http.StatusOK, buf.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 辅助 ==========
|
|
||||||
func generateAndReload() error {
|
func generateAndReload() error {
|
||||||
if err := GenerateFrpcConfig(); err != nil {
|
if err := GenerateFrpcConfig(); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -0,0 +1,330 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# frpc-console 一键部署脚本
|
||||||
|
# 支持:Linux x86_64 / ARM64 / ARMv7
|
||||||
|
# 自动安装:git / curl / wget / Go / Docker
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# ---------- 颜色输出 ----------
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
# ---------- 配置 ----------
|
||||||
|
REPO_URL="https://git.whitetop.xyz/lxh2875931338/frpc-console.git"
|
||||||
|
BRANCH="main"
|
||||||
|
WORK_DIR="/tmp/frpc-console-build"
|
||||||
|
DEPLOY_DIR="/opt/frpc-console"
|
||||||
|
DATA_DIR="${DEPLOY_DIR}/data"
|
||||||
|
DEFAULT_PORT=9300
|
||||||
|
IMAGE_NAME="frpc-console"
|
||||||
|
GO_VERSION="1.21.5"
|
||||||
|
|
||||||
|
# ---------- 打印函数 ----------
|
||||||
|
print_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
|
||||||
|
print_success() { echo -e "${GREEN}[✓]${NC} $1"; }
|
||||||
|
print_warn() { echo -e "${YELLOW}[⚠]${NC} $1"; }
|
||||||
|
print_error() { echo -e "${RED}[✗]${NC} $1"; }
|
||||||
|
print_step() { echo -e "\n${CYAN}▶${NC} $1"; }
|
||||||
|
|
||||||
|
# ---------- 检查 root ----------
|
||||||
|
if [ "$EUID" -ne 0 ]; then
|
||||||
|
print_error "请使用 root 权限运行此脚本"
|
||||||
|
echo " sudo bash deploy.sh"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------- 1. 检测操作系统 ----------
|
||||||
|
print_step "检测操作系统..."
|
||||||
|
if [ -f /etc/os-release ]; then
|
||||||
|
. /etc/os-release
|
||||||
|
OS=$ID
|
||||||
|
OS_VERSION=$VERSION_ID
|
||||||
|
else
|
||||||
|
print_error "无法识别操作系统"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
print_info "系统: $OS $OS_VERSION"
|
||||||
|
|
||||||
|
# ---------- 2. 检测 CPU 架构 ----------
|
||||||
|
print_step "检测 CPU 架构..."
|
||||||
|
ARCH=$(uname -m)
|
||||||
|
case $ARCH in
|
||||||
|
x86_64|amd64)
|
||||||
|
GO_ARCH="amd64"
|
||||||
|
;;
|
||||||
|
aarch64|arm64)
|
||||||
|
GO_ARCH="arm64"
|
||||||
|
;;
|
||||||
|
armv7l|armhf)
|
||||||
|
GO_ARCH="armv6l"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
print_error "不支持的 CPU 架构: $ARCH"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
print_success "CPU 架构: $ARCH → Go 架构: $GO_ARCH"
|
||||||
|
|
||||||
|
# ---------- 3. 安装必要工具 ----------
|
||||||
|
print_step "检查必要工具..."
|
||||||
|
|
||||||
|
# git
|
||||||
|
if ! command -v git &> /dev/null; then
|
||||||
|
print_warn "git 未安装,正在安装..."
|
||||||
|
case $OS in
|
||||||
|
opensuse*|suse*|opensuse-tumbleweed|opensuse-slowroll)
|
||||||
|
zypper install -y git
|
||||||
|
;;
|
||||||
|
ubuntu|debian|linuxmint)
|
||||||
|
apt update -qq && apt install -y git
|
||||||
|
;;
|
||||||
|
centos|rhel|fedora|rocky|almalinux)
|
||||||
|
yum install -y git
|
||||||
|
;;
|
||||||
|
alpine)
|
||||||
|
apk add git
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
print_error "无法识别包管理器,请手动安装 git"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
print_success "git 安装完成"
|
||||||
|
else
|
||||||
|
print_success "git 已安装: $(git --version | awk '{print $3}')"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# curl
|
||||||
|
if ! command -v curl &> /dev/null; then
|
||||||
|
print_warn "curl 未安装,正在安装..."
|
||||||
|
case $OS in
|
||||||
|
opensuse*|suse*|opensuse-tumbleweed|opensuse-slowroll) zypper install -y curl ;;
|
||||||
|
ubuntu|debian|linuxmint) apt install -y curl ;;
|
||||||
|
centos|rhel|fedora|rocky|almalinux) yum install -y curl ;;
|
||||||
|
alpine) apk add curl ;;
|
||||||
|
*) print_error "无法安装 curl"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
print_success "curl 安装完成"
|
||||||
|
else
|
||||||
|
print_success "curl 已安装"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# wget(备用)
|
||||||
|
if ! command -v wget &> /dev/null; then
|
||||||
|
print_warn "wget 未安装,正在安装..."
|
||||||
|
case $OS in
|
||||||
|
opensuse*|suse*|opensuse-tumbleweed|opensuse-slowroll) zypper install -y wget ;;
|
||||||
|
ubuntu|debian|linuxmint) apt install -y wget ;;
|
||||||
|
centos|rhel|fedora|rocky|almalinux) yum install -y wget ;;
|
||||||
|
alpine) apk add wget ;;
|
||||||
|
*) print_warn "wget 未安装,但不影响主要功能" ;;
|
||||||
|
esac
|
||||||
|
else
|
||||||
|
print_success "wget 已安装"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------- 4. 安装 Go ----------
|
||||||
|
print_step "检查 Go 环境..."
|
||||||
|
if ! command -v go &> /dev/null; then
|
||||||
|
print_warn "Go 未安装,正在下载 Go ${GO_VERSION} (${GO_ARCH})..."
|
||||||
|
|
||||||
|
GO_TMP="/tmp/go${GO_VERSION}.linux-${GO_ARCH}.tar.gz"
|
||||||
|
|
||||||
|
if [ -f "$GO_TMP" ]; then
|
||||||
|
print_info "使用缓存: $GO_TMP"
|
||||||
|
else
|
||||||
|
print_info "正在下载..."
|
||||||
|
MIRRORS=(
|
||||||
|
"https://mirrors.aliyun.com/golang/go${GO_VERSION}.linux-${GO_ARCH}.tar.gz"
|
||||||
|
"https://mirrors.tuna.tsinghua.edu.cn/golang/go${GO_VERSION}.linux-${GO_ARCH}.tar.gz"
|
||||||
|
"https://golang.google.cn/dl/go${GO_VERSION}.linux-${GO_ARCH}.tar.gz"
|
||||||
|
"https://dl.google.com/go/go${GO_VERSION}.linux-${GO_ARCH}.tar.gz"
|
||||||
|
)
|
||||||
|
|
||||||
|
DOWNLOADED=false
|
||||||
|
for MIRROR in "${MIRRORS[@]}"; do
|
||||||
|
print_info "尝试: $MIRROR"
|
||||||
|
if curl -# -f -L -o "$GO_TMP" "$MIRROR"; then
|
||||||
|
print_success "下载成功"
|
||||||
|
DOWNLOADED=true
|
||||||
|
break
|
||||||
|
else
|
||||||
|
print_warn "失败,尝试下一个镜像..."
|
||||||
|
rm -f "$GO_TMP"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$DOWNLOADED" = false ]; then
|
||||||
|
print_error "所有镜像源均下载失败"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 安装 Go
|
||||||
|
rm -rf /usr/local/go
|
||||||
|
tar -C /usr/local -xzf "$GO_TMP"
|
||||||
|
export PATH=$PATH:/usr/local/go/bin
|
||||||
|
echo 'export PATH=$PATH:/usr/local/go/bin' > /etc/profile.d/go.sh
|
||||||
|
|
||||||
|
# 配置 Go 代理
|
||||||
|
/usr/local/go/bin/go env -w GOPROXY=https://goproxy.cn,direct
|
||||||
|
/usr/local/go/bin/go env -w GOPRIVATE=git.whitetop.xyz
|
||||||
|
|
||||||
|
print_success "Go ${GO_VERSION} (${GO_ARCH}) 安装完成"
|
||||||
|
else
|
||||||
|
print_success "Go 已安装: $(go version | awk '{print $3}') (架构: $(go env GOARCH))"
|
||||||
|
go env -w GOPROXY=https://goproxy.cn,direct 2>/dev/null || true
|
||||||
|
go env -w GOPRIVATE=git.whitetop.xyz 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------- 5. 检查 Docker ----------
|
||||||
|
print_step "检查 Docker 环境..."
|
||||||
|
if ! command -v docker &> /dev/null; then
|
||||||
|
print_error "Docker 未安装,请先安装 Docker"
|
||||||
|
echo ""
|
||||||
|
echo " 快速安装:"
|
||||||
|
echo " curl -fsSL https://get.docker.com | bash"
|
||||||
|
echo " systemctl enable --now docker"
|
||||||
|
echo ""
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
print_success "Docker 已安装: $(docker --version | awk '{print $3}' | tr -d ',')"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------- 6. 交互式配置 ----------
|
||||||
|
print_step "部署配置"
|
||||||
|
read -p "监听端口 [${DEFAULT_PORT}]: " PORT
|
||||||
|
PORT=${PORT:-$DEFAULT_PORT}
|
||||||
|
|
||||||
|
read -p "部署目录 [${DEPLOY_DIR}]: " DEPLOY_DIR_INPUT
|
||||||
|
DEPLOY_DIR=${DEPLOY_DIR_INPUT:-$DEPLOY_DIR}
|
||||||
|
DATA_DIR="${DEPLOY_DIR}/data"
|
||||||
|
|
||||||
|
print_info "端口: $PORT"
|
||||||
|
print_info "部署目录: $DEPLOY_DIR"
|
||||||
|
print_info "数据目录: $DATA_DIR"
|
||||||
|
|
||||||
|
# ---------- 7. 拉取代码 ----------
|
||||||
|
print_step "拉取代码..."
|
||||||
|
rm -rf "$WORK_DIR"
|
||||||
|
git clone --depth 1 --branch "$BRANCH" "$REPO_URL" "$WORK_DIR"
|
||||||
|
cd "$WORK_DIR"
|
||||||
|
|
||||||
|
# 确保目录存在
|
||||||
|
mkdir -p bin static
|
||||||
|
|
||||||
|
print_success "代码拉取完成"
|
||||||
|
|
||||||
|
# ---------- 8. 修复 go.mod 版本(防止 toolchain 下载) ----------
|
||||||
|
print_step "检查 go.mod..."
|
||||||
|
if grep -q "go 1.26" go.mod 2>/dev/null; then
|
||||||
|
print_warn "检测到 go.mod 版本过高,自动降级到 go 1.21"
|
||||||
|
sed -i 's/go 1.26.*/go 1.21/' go.mod
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------- 9. 下载依赖 ----------
|
||||||
|
print_step "下载 Go 依赖..."
|
||||||
|
go mod download
|
||||||
|
print_success "依赖下载完成"
|
||||||
|
|
||||||
|
# ---------- 10. 编译 ----------
|
||||||
|
print_step "编译 frpc-console..."
|
||||||
|
CGO_ENABLED=0 GOOS=linux go build \
|
||||||
|
-ldflags="-s -w" \
|
||||||
|
-o frpc-console .
|
||||||
|
|
||||||
|
if [ -f "frpc-console" ]; then
|
||||||
|
SIZE=$(du -h frpc-console | cut -f1)
|
||||||
|
print_success "编译完成 ($SIZE)"
|
||||||
|
else
|
||||||
|
print_error "编译失败"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------- 11. 准备部署目录 ----------
|
||||||
|
print_step "准备部署目录..."
|
||||||
|
mkdir -p "$DEPLOY_DIR"
|
||||||
|
mkdir -p "$DATA_DIR"
|
||||||
|
|
||||||
|
# 如果已有数据,保留;否则创建空占位
|
||||||
|
if [ ! -f "$DATA_DIR/frpc-console.db" ]; then
|
||||||
|
touch "$DATA_DIR/frpc-console.db"
|
||||||
|
print_info "新数据目录已创建"
|
||||||
|
else
|
||||||
|
print_info "已有数据目录,保留现有数据"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cp frpc-console "$DEPLOY_DIR/"
|
||||||
|
print_success "二进制已复制到 $DEPLOY_DIR"
|
||||||
|
|
||||||
|
# ---------- 12. 构建 Docker 镜像 ----------
|
||||||
|
print_step "构建 Docker 镜像..."
|
||||||
|
docker build -t "${IMAGE_NAME}:latest" .
|
||||||
|
print_success "Docker 镜像构建完成: ${IMAGE_NAME}:latest"
|
||||||
|
|
||||||
|
# ---------- 13. 停止旧容器 ----------
|
||||||
|
print_step "检查旧容器..."
|
||||||
|
if docker ps -a --format '{{.Names}}' | grep -q "^frpc-console$"; then
|
||||||
|
print_warn "发现旧容器,正在停止并删除..."
|
||||||
|
docker stop frpc-console 2>/dev/null || true
|
||||||
|
docker rm frpc-console 2>/dev/null || true
|
||||||
|
print_success "旧容器已清理"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------- 14. 启动新容器 ----------
|
||||||
|
print_step "启动 frpc-console 容器..."
|
||||||
|
docker run -d \
|
||||||
|
--name frpc-console \
|
||||||
|
--restart=always \
|
||||||
|
-p ${PORT}:9300 \
|
||||||
|
-v ${DATA_DIR}:/app/data \
|
||||||
|
-e PORT=9300 \
|
||||||
|
-e TZ=Asia/Shanghai \
|
||||||
|
${IMAGE_NAME}:latest
|
||||||
|
|
||||||
|
if docker ps | grep -q frpc-console; then
|
||||||
|
print_success "容器启动成功!"
|
||||||
|
else
|
||||||
|
print_error "容器启动失败,请检查日志: docker logs frpc-console"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------- 15. 检查 frpc 子进程 ----------
|
||||||
|
print_step "检查 frpc 状态..."
|
||||||
|
sleep 3
|
||||||
|
if docker exec frpc-console ps aux 2>/dev/null | grep -q "[f]rpc -c"; then
|
||||||
|
print_success "frpc 进程运行正常"
|
||||||
|
else
|
||||||
|
print_warn "frpc 进程未运行(可能配置为空,请在 WebUI 中导入 TOML)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------- 16. 清理临时文件 ----------
|
||||||
|
print_step "清理临时文件..."
|
||||||
|
rm -rf "$WORK_DIR"
|
||||||
|
print_success "临时文件已清理"
|
||||||
|
|
||||||
|
# ---------- 17. 输出信息 ----------
|
||||||
|
echo ""
|
||||||
|
echo "=========================================="
|
||||||
|
echo -e "${GREEN}✅ frpc-console 部署完成!${NC}"
|
||||||
|
echo "=========================================="
|
||||||
|
echo ""
|
||||||
|
echo "📍 访问地址: http://$(hostname -I | awk '{print $1}'):${PORT}"
|
||||||
|
echo "📂 数据目录: ${DATA_DIR}"
|
||||||
|
echo "🐳 容器名称: frpc-console"
|
||||||
|
echo ""
|
||||||
|
echo "常用命令:"
|
||||||
|
echo " docker logs frpc-console # 查看日志"
|
||||||
|
echo " docker restart frpc-console # 重启服务"
|
||||||
|
echo " docker stop frpc-console # 停止服务"
|
||||||
|
echo " docker start frpc-console # 启动服务"
|
||||||
|
echo ""
|
||||||
|
echo "首次访问需要注册管理员账户"
|
||||||
|
echo "=========================================="
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
//go:build linux || darwin || freebsd || netbsd || openbsd || solaris
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/exec"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
// setSysProcAttr 为 Unix 系统设置 Setsid,让 frpc 进程脱离父进程独立运行
|
||||||
|
func setSysProcAttr(cmd *exec.Cmd) {
|
||||||
|
if cmd.SysProcAttr == nil {
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{}
|
||||||
|
}
|
||||||
|
cmd.SysProcAttr.Setsid = true
|
||||||
|
}
|
||||||
@@ -4,17 +4,16 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"embed"
|
"embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"syscall"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
"github.com/creack/pty"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed bin/*
|
//go:embed bin/*
|
||||||
@@ -36,7 +35,6 @@ func getFrpcPath() (string, error) {
|
|||||||
frpcPathMutex.Lock()
|
frpcPathMutex.Lock()
|
||||||
defer frpcPathMutex.Unlock()
|
defer frpcPathMutex.Unlock()
|
||||||
|
|
||||||
// 如果缓存有效,直接返回
|
|
||||||
if cachedFrpcPath != "" {
|
if cachedFrpcPath != "" {
|
||||||
if _, err := os.Stat(cachedFrpcPath); err == nil {
|
if _, err := os.Stat(cachedFrpcPath); err == nil {
|
||||||
return cachedFrpcPath, nil
|
return cachedFrpcPath, nil
|
||||||
@@ -44,7 +42,6 @@ func getFrpcPath() (string, error) {
|
|||||||
cachedFrpcPath = ""
|
cachedFrpcPath = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确定当前平台的文件名
|
|
||||||
var fileName string
|
var fileName string
|
||||||
switch {
|
switch {
|
||||||
case runtime.GOOS == "windows" && runtime.GOARCH == "amd64":
|
case runtime.GOOS == "windows" && runtime.GOARCH == "amd64":
|
||||||
@@ -58,7 +55,6 @@ func getFrpcPath() (string, error) {
|
|||||||
case runtime.GOOS == "linux" && runtime.GOARCH == "arm":
|
case runtime.GOOS == "linux" && runtime.GOARCH == "arm":
|
||||||
fileName = "frpc_linux_armv7"
|
fileName = "frpc_linux_armv7"
|
||||||
default:
|
default:
|
||||||
// 不支持的平台,尝试从 PATH 找
|
|
||||||
path, err := exec.LookPath("frpc")
|
path, err := exec.LookPath("frpc")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
cachedFrpcPath = path
|
cachedFrpcPath = path
|
||||||
@@ -67,14 +63,14 @@ func getFrpcPath() (string, error) {
|
|||||||
return "", fmt.Errorf("不支持的平台: %s/%s", runtime.GOOS, runtime.GOARCH)
|
return "", fmt.Errorf("不支持的平台: %s/%s", runtime.GOOS, runtime.GOARCH)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== 优先级 1:从当前目录 ./bin/ 查找 =====
|
// 优先级1:本地 ./bin/
|
||||||
localPath := filepath.Join(".", "bin", fileName)
|
localPath := filepath.Join(".", "bin", fileName)
|
||||||
if _, err := os.Stat(localPath); err == nil {
|
if _, err := os.Stat(localPath); err == nil {
|
||||||
cachedFrpcPath = localPath
|
cachedFrpcPath = localPath
|
||||||
return localPath, nil
|
return localPath, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== 优先级 2:从 embed 读取并解压到临时目录 =====
|
// 优先级2:embed 解压到临时目录
|
||||||
data, err := embeddedFrpc.ReadFile("bin/" + fileName)
|
data, err := embeddedFrpc.ReadFile("bin/" + fileName)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
tmpPath := filepath.Join(os.TempDir(), "frpc")
|
tmpPath := filepath.Join(os.TempDir(), "frpc")
|
||||||
@@ -85,14 +81,13 @@ func getFrpcPath() (string, error) {
|
|||||||
cachedFrpcPath = tmpPath
|
cachedFrpcPath = tmpPath
|
||||||
return tmpPath, nil
|
return tmpPath, nil
|
||||||
}
|
}
|
||||||
// 写入失败时,检查是否已有可用的临时文件
|
|
||||||
if _, statErr := os.Stat(tmpPath); statErr == nil {
|
if _, statErr := os.Stat(tmpPath); statErr == nil {
|
||||||
cachedFrpcPath = tmpPath
|
cachedFrpcPath = tmpPath
|
||||||
return tmpPath, nil
|
return tmpPath, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== 优先级 3:降级到系统 PATH =====
|
// 优先级3:系统 PATH
|
||||||
path, err := exec.LookPath("frpc")
|
path, err := exec.LookPath("frpc")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
cachedFrpcPath = path
|
cachedFrpcPath = path
|
||||||
@@ -102,7 +97,7 @@ func getFrpcPath() (string, error) {
|
|||||||
return "", fmt.Errorf("未找到 frpc 文件 (本地 bin/、embed、PATH 均无)")
|
return "", fmt.Errorf("未找到 frpc 文件 (本地 bin/、embed、PATH 均无)")
|
||||||
}
|
}
|
||||||
|
|
||||||
// GenerateFrpcConfig 生成 frpc.toml 配置文件
|
// GenerateFrpcConfig 生成 frpc.toml
|
||||||
func GenerateFrpcConfig() error {
|
func GenerateFrpcConfig() error {
|
||||||
cfg, err := GetGlobalConfig()
|
cfg, err := GetGlobalConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -129,7 +124,6 @@ func GenerateFrpcConfig() error {
|
|||||||
Proxies: activeProxies,
|
Proxies: activeProxies,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 优先使用外部模板文件,不存在则用 embed
|
|
||||||
var tmplContent string
|
var tmplContent string
|
||||||
if _, err := os.Stat("frpc.tmpl"); err == nil {
|
if _, err := os.Stat("frpc.tmpl"); err == nil {
|
||||||
content, readErr := os.ReadFile("frpc.tmpl")
|
content, readErr := os.ReadFile("frpc.tmpl")
|
||||||
@@ -159,8 +153,36 @@ func GenerateFrpcConfig() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// StartFrpc 启动 frpc
|
// isFrpcRunning 通过 PID 文件检查 frpc 是否在运行
|
||||||
|
func isFrpcRunning() bool {
|
||||||
|
pidData, err := os.ReadFile("./frpc.pid")
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
pid, err := strconv.Atoi(strings.TrimSpace(string(pidData)))
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
// Windows 用 tasklist 检查
|
||||||
|
cmd := exec.Command("tasklist", "/FI", "PID eq", strconv.Itoa(pid))
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return strings.Contains(string(output), strconv.Itoa(pid))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Linux/Unix: 用 kill 0 检查进程是否存在
|
||||||
|
process, err := os.FindProcess(pid)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return process.Signal(syscall.Signal(0)) == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartFrpc 启动 frpc(独立进程,脱离父进程)
|
||||||
func StartFrpc() error {
|
func StartFrpc() error {
|
||||||
frpcPath, err := getFrpcPath()
|
frpcPath, err := getFrpcPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -173,37 +195,84 @@ func StartFrpc() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
running, _ := GetFrpcStatus()
|
if isFrpcRunning() {
|
||||||
if running {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 清理旧的 PID 文件
|
||||||
|
os.Remove("./frpc.pid")
|
||||||
|
|
||||||
cmd := exec.Command(frpcPath, "-c", "./frpc.toml")
|
cmd := exec.Command(frpcPath, "-c", "./frpc.toml")
|
||||||
setWindowHide(cmd)
|
setWindowHide(cmd)
|
||||||
|
|
||||||
// ✅ Linux 下分配伪终端,让 frpc 认为自己在真实终端中运行
|
// ✅ 设置进程属性(Unix: Setsid 脱离父进程,Windows: 不处理)
|
||||||
if runtime.GOOS != "windows" {
|
setSysProcAttr(cmd)
|
||||||
f, err := pty.Start(cmd)
|
|
||||||
if err != nil {
|
// 把输出重定向到日志文件
|
||||||
return fmt.Errorf("启动伪终端失败: %w", err)
|
logFile, err := os.OpenFile("./frpc.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
||||||
}
|
if err != nil {
|
||||||
// 可选:丢弃 pty 输出,或写入日志文件
|
return fmt.Errorf("打开日志文件失败: %w", err)
|
||||||
go io.Copy(io.Discard, f)
|
}
|
||||||
} else {
|
cmd.Stdout = logFile
|
||||||
if err := cmd.Start(); err != nil {
|
cmd.Stderr = logFile
|
||||||
return fmt.Errorf("启动 frpc 失败: %w", err)
|
|
||||||
}
|
if err := cmd.Start(); err != nil {
|
||||||
|
return fmt.Errorf("启动 frpc 失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 保存 PID 到文件
|
||||||
|
if err := os.WriteFile("./frpc.pid", []byte(fmt.Sprintf("%d", cmd.Process.Pid)), 0644); err != nil {
|
||||||
|
return fmt.Errorf("保存 PID 失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StopFrpc 停止 frpc(通过 PID 文件精准杀进程)
|
||||||
|
func StopFrpc() error {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
cmd := exec.Command("taskkill", "/F", "/IM", "frpc.exe")
|
||||||
|
if err := cmd.Run(); err != nil && !strings.Contains(err.Error(), "not found") {
|
||||||
|
return fmt.Errorf("停止 frpc 失败: %w", err)
|
||||||
|
}
|
||||||
|
os.Remove("./frpc.pid")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Linux: 读取 PID 文件精准杀进程
|
||||||
|
pidData, err := os.ReadFile("./frpc.pid")
|
||||||
|
if err != nil {
|
||||||
|
// 降级到 pkill
|
||||||
|
cmd := exec.Command("pkill", "-f", "frpc")
|
||||||
|
if err := cmd.Run(); err != nil && !strings.Contains(err.Error(), "no process") {
|
||||||
|
return fmt.Errorf("停止 frpc 失败: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pid, _ := strconv.Atoi(strings.TrimSpace(string(pidData)))
|
||||||
|
process, err := os.FindProcess(pid)
|
||||||
|
if err != nil {
|
||||||
|
os.Remove("./frpc.pid")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := process.Kill(); err != nil {
|
||||||
|
return fmt.Errorf("杀死进程失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Remove("./frpc.pid")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFrpcStatus 获取 frpc 运行状态(通过 PID 文件)
|
||||||
|
func GetFrpcStatus() (bool, error) {
|
||||||
|
return isFrpcRunning(), nil
|
||||||
|
}
|
||||||
|
|
||||||
// ReloadFrpc 热加载,失败时自动降级为重启
|
// ReloadFrpc 热加载,失败时自动降级为重启
|
||||||
func ReloadFrpc() error {
|
func ReloadFrpc() error {
|
||||||
running, err := GetFrpcStatus()
|
running := isFrpcRunning()
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("检查 frpc 状态失败: %w", err)
|
|
||||||
}
|
|
||||||
if !running {
|
if !running {
|
||||||
return StartFrpc()
|
return StartFrpc()
|
||||||
}
|
}
|
||||||
@@ -227,37 +296,3 @@ func ReloadFrpc() error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// StopFrpc 停止 frpc
|
|
||||||
func StopFrpc() error {
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
cmd := exec.Command("taskkill", "/F", "/IM", "frpc.exe")
|
|
||||||
if err := cmd.Run(); err != nil && !strings.Contains(err.Error(), "not found") {
|
|
||||||
return fmt.Errorf("停止 frpc 失败: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
cmd := exec.Command("pkill", "-f", "frpc")
|
|
||||||
if err := cmd.Run(); err != nil && !strings.Contains(err.Error(), "no process") {
|
|
||||||
return fmt.Errorf("停止 frpc 失败: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetFrpcStatus 检查 frpc 是否在运行
|
|
||||||
func GetFrpcStatus() (bool, error) {
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
cmd := exec.Command("tasklist", "/FI", "IMAGENAME eq frpc.exe")
|
|
||||||
output, err := cmd.CombinedOutput()
|
|
||||||
if err != nil {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
return strings.Contains(string(output), "frpc.exe"), nil
|
|
||||||
}
|
|
||||||
cmd := exec.Command("pgrep", "-f", "frpc")
|
|
||||||
output, err := cmd.Output()
|
|
||||||
if err != nil {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
return len(output) > 0, nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// frp_other.go
|
|
||||||
//go:build !windows
|
//go:build !windows
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|||||||
+6
-1
@@ -7,10 +7,15 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
)
|
)
|
||||||
|
|
||||||
// setWindowHide 在 Windows 上隐藏 frpc 命令行窗口
|
// setWindowHide Windows 隐藏窗口
|
||||||
func setWindowHide(cmd *exec.Cmd) {
|
func setWindowHide(cmd *exec.Cmd) {
|
||||||
if cmd.SysProcAttr == nil {
|
if cmd.SysProcAttr == nil {
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{}
|
cmd.SysProcAttr = &syscall.SysProcAttr{}
|
||||||
}
|
}
|
||||||
cmd.SysProcAttr.HideWindow = true
|
cmd.SysProcAttr.HideWindow = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// setSysProcAttr Windows 不需要 Setsid,空操作
|
||||||
|
func setSysProcAttr(cmd *exec.Cmd) {
|
||||||
|
// Windows 不需要 Setsid,什么都不做
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ require (
|
|||||||
github.com/bytedance/sonic v1.15.0 // indirect
|
github.com/bytedance/sonic v1.15.0 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
github.com/creack/pty v1.1.24 // indirect
|
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
||||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiD
|
|||||||
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||||
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
|
||||||
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -9,10 +10,7 @@ func main() {
|
|||||||
log.Fatal("❌ 数据库初始化失败:", err)
|
log.Fatal("❌ 数据库初始化失败:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ Web 注册已取代 CLI 初始化,不再需要终端交互
|
// 启动时生成配置并启动 frpc
|
||||||
// 前端会自动检测是否首次启动并显示注册页
|
|
||||||
// InitAdminUser()
|
|
||||||
|
|
||||||
if err := GenerateFrpcConfig(); err != nil {
|
if err := GenerateFrpcConfig(); err != nil {
|
||||||
log.Println("⚠️ 生成配置文件失败:", err)
|
log.Println("⚠️ 生成配置文件失败:", err)
|
||||||
}
|
}
|
||||||
@@ -21,12 +19,32 @@ func main() {
|
|||||||
log.Println("⚠️ 启动 frpc 失败:", err)
|
log.Println("⚠️ 启动 frpc 失败:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ✅ 启动 Watchdog:每 30 秒检查一次 frpc 状态,挂了自动重启
|
||||||
|
go startWatchdog()
|
||||||
|
|
||||||
r := SetupRouter()
|
r := SetupRouter()
|
||||||
log.Println("🚀 frpc-console 启动成功!")
|
log.Println("🚀 frpc-console 启动成功!")
|
||||||
log.Println("📍 访问地址: http://localhost:8080")
|
log.Println("📍 访问地址: http://localhost:8080")
|
||||||
log.Println("📍 API 地址: http://localhost:8080/api")
|
log.Println("📍 API 地址: http://localhost:8080/api")
|
||||||
|
|
||||||
if err := r.Run(":8080"); err != nil {
|
if err := r.Run(":9300"); err != nil {
|
||||||
log.Fatal("❌ 服务启动失败:", err)
|
log.Fatal("❌ 服务启动失败:", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Watchdog 协程:定期检查 frpc 是否存活
|
||||||
|
func startWatchdog() {
|
||||||
|
ticker := time.NewTicker(30 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for range ticker.C {
|
||||||
|
if !isFrpcRunning() {
|
||||||
|
log.Println("⚠️ frpc 进程已停止,自动重启...")
|
||||||
|
if err := StartFrpc(); err != nil {
|
||||||
|
log.Printf("❌ 自动重启 frpc 失败: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Println("✅ frpc 已自动重启")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+9
-20
@@ -1,6 +1,5 @@
|
|||||||
//go:build ignore
|
//go:build ignore
|
||||||
|
|
||||||
// build.go
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -15,8 +14,8 @@ import (
|
|||||||
type Platform struct {
|
type Platform struct {
|
||||||
OS string
|
OS string
|
||||||
Arch string
|
Arch string
|
||||||
Name string // 显示名称
|
Name string
|
||||||
Ext string // 文件扩展名(Windows 需要 .exe)
|
Ext string
|
||||||
}
|
}
|
||||||
|
|
||||||
var platforms = []Platform{
|
var platforms = []Platform{
|
||||||
@@ -34,22 +33,19 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// 解析命令行参数
|
|
||||||
if len(os.Args) > 1 {
|
if len(os.Args) > 1 {
|
||||||
target := os.Args[1]
|
target := os.Args[1]
|
||||||
if target == "all" {
|
switch target {
|
||||||
|
case "all":
|
||||||
buildAll()
|
buildAll()
|
||||||
return
|
return
|
||||||
}
|
case "clean":
|
||||||
if target == "clean" {
|
|
||||||
clean()
|
clean()
|
||||||
return
|
return
|
||||||
}
|
case "list":
|
||||||
if target == "list" {
|
|
||||||
listPlatforms()
|
listPlatforms()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 尝试匹配平台
|
|
||||||
for _, p := range platforms {
|
for _, p := range platforms {
|
||||||
if target == p.OS+"/"+p.Arch {
|
if target == p.OS+"/"+p.Arch {
|
||||||
build(p, false)
|
build(p, false)
|
||||||
@@ -61,8 +57,6 @@ func main() {
|
|||||||
fmt.Println(" 或: all, list, clean")
|
fmt.Println(" 或: all, list, clean")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 交互式菜单
|
|
||||||
interactiveMenu()
|
interactiveMenu()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,7 +98,6 @@ func interactiveMenu() {
|
|||||||
buildAll()
|
buildAll()
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
// 尝试匹配数字
|
|
||||||
var idx int
|
var idx int
|
||||||
if n, err := fmt.Sscanf(input, "%d", &idx); n == 1 && err == nil && idx >= 1 && idx <= len(platforms) {
|
if n, err := fmt.Sscanf(input, "%d", &idx); n == 1 && err == nil && idx >= 1 && idx <= len(platforms) {
|
||||||
build(platforms[idx-1], false)
|
build(platforms[idx-1], false)
|
||||||
@@ -136,8 +129,8 @@ func build(p Platform, silent bool) {
|
|||||||
outName := Binary + "-" + p.OS + "-" + p.Arch + p.Ext
|
outName := Binary + "-" + p.OS + "-" + p.Arch + p.Ext
|
||||||
outPath := filepath.Join(outDir, outName)
|
outPath := filepath.Join(outDir, outName)
|
||||||
|
|
||||||
// 构建命令
|
// 用 "go" 而不是硬编码路径,让系统去 PATH 里找
|
||||||
cmd := exec.Command("go.exe", "build",
|
cmd := exec.Command("go", "build",
|
||||||
"-ldflags=-s -w -X main.version="+Version,
|
"-ldflags=-s -w -X main.version="+Version,
|
||||||
"-o", outPath,
|
"-o", outPath,
|
||||||
".",
|
".",
|
||||||
@@ -145,7 +138,7 @@ func build(p Platform, silent bool) {
|
|||||||
cmd.Env = append(os.Environ(),
|
cmd.Env = append(os.Environ(),
|
||||||
"GOOS="+p.OS,
|
"GOOS="+p.OS,
|
||||||
"GOARCH="+p.Arch,
|
"GOARCH="+p.Arch,
|
||||||
"CGO_ENABLED=0", // 禁用 CGO,保证静态编译
|
"CGO_ENABLED=0",
|
||||||
)
|
)
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
cmd.Stderr = os.Stderr
|
cmd.Stderr = os.Stderr
|
||||||
@@ -157,14 +150,12 @@ func build(p Platform, silent bool) {
|
|||||||
|
|
||||||
if !silent {
|
if !silent {
|
||||||
fmt.Printf("✅ 构建完成: %s\n", outPath)
|
fmt.Printf("✅ 构建完成: %s\n", outPath)
|
||||||
// 显示文件大小
|
|
||||||
if info, err := os.Stat(outPath); err == nil {
|
if info, err := os.Stat(outPath); err == nil {
|
||||||
size := float64(info.Size()) / 1024 / 1024
|
size := float64(info.Size()) / 1024 / 1024
|
||||||
fmt.Printf(" 📦 %.2f MB\n", size)
|
fmt.Printf(" 📦 %.2f MB\n", size)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 打包(仅单平台构建时自动打包)
|
|
||||||
if !silent {
|
if !silent {
|
||||||
packageBinary(outPath, p)
|
packageBinary(outPath, p)
|
||||||
}
|
}
|
||||||
@@ -175,14 +166,12 @@ func packageBinary(binPath string, p Platform) {
|
|||||||
dir := filepath.Dir(binPath)
|
dir := filepath.Dir(binPath)
|
||||||
|
|
||||||
if p.Ext == ".exe" {
|
if p.Ext == ".exe" {
|
||||||
// Windows: zip
|
|
||||||
zipName := filepath.Join(dir, base+".zip")
|
zipName := filepath.Join(dir, base+".zip")
|
||||||
cmd := exec.Command("zip", "-j", zipName, binPath)
|
cmd := exec.Command("zip", "-j", zipName, binPath)
|
||||||
if err := cmd.Run(); err == nil {
|
if err := cmd.Run(); err == nil {
|
||||||
fmt.Printf(" 📦 已打包: %s\n", zipName)
|
fmt.Printf(" 📦 已打包: %s\n", zipName)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Linux/macOS: tar.gz
|
|
||||||
tarName := filepath.Join(dir, base+".tar.gz")
|
tarName := filepath.Join(dir, base+".tar.gz")
|
||||||
cmd := exec.Command("tar", "-czf", tarName, "-C", dir, filepath.Base(binPath))
|
cmd := exec.Command("tar", "-czf", tarName, "-C", dir, filepath.Base(binPath))
|
||||||
if err := cmd.Run(); err == nil {
|
if err := cmd.Run(); err == nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user