Template
feat: add prefixed service initializer and packaged deployment
This commit is contained in:
@@ -1,4 +1,7 @@
|
||||
name: 'Build & Deploy'
|
||||
env:
|
||||
DEPLOY_DIR: /root
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
@@ -17,15 +20,14 @@ jobs:
|
||||
go build -ldflags="-s -w" -o server ./cmd/main_program
|
||||
go build -ldflags="-s -w" -o cli ./cmd/cli_control
|
||||
go build -ldflags="-s -w" -o scheduler ./cmd/scheduler
|
||||
go build -ldflags="-s -w" -o initializer .
|
||||
tar -czf deployment.tar.gz server cli scheduler initializer
|
||||
|
||||
- name: Save artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: build-artifacts
|
||||
path: |
|
||||
./server
|
||||
./cli
|
||||
./scheduler
|
||||
path: ./deployment.tar.gz
|
||||
|
||||
deploy:
|
||||
needs: build
|
||||
@@ -53,11 +55,9 @@ jobs:
|
||||
while read -r ip; do
|
||||
if [ -n "$ip" ]; then
|
||||
echo "Deploying to $ip..."
|
||||
scp -o StrictHostKeyChecking=no server cli scheduler ${{ vars.ROOT_USER_NAME }}@"$ip":/root/
|
||||
ssh -n ${{ vars.ROOT_USER_NAME }}@"$ip" << 'ENDSSH'
|
||||
chmod +x /root/server /root/cli /root/scheduler
|
||||
systemctl restart server cli scheduler
|
||||
ENDSSH
|
||||
scp -o StrictHostKeyChecking=no deployment.tar.gz ${{ vars.ROOT_USER_NAME }}@"$ip":"$DEPLOY_DIR/deployment.tar.gz"
|
||||
ssh -n ${{ vars.ROOT_USER_NAME }}@"$ip" \
|
||||
"set -e; bash '$DEPLOY_DIR/stop.sh'; tar -xzf '$DEPLOY_DIR/deployment.tar.gz' -C '$DEPLOY_DIR'; chmod +x '$DEPLOY_DIR/server' '$DEPLOY_DIR/cli' '$DEPLOY_DIR/scheduler' '$DEPLOY_DIR/initializer'; bash '$DEPLOY_DIR/start.sh'"
|
||||
echo "Deployment to $ip completed"
|
||||
fi
|
||||
done < server_list.txt
|
||||
|
||||
@@ -11,6 +11,11 @@
|
||||
/vendor/
|
||||
/logs/
|
||||
/tmp/
|
||||
/.service-prefix
|
||||
/server
|
||||
/cli
|
||||
/scheduler
|
||||
/initializer
|
||||
|
||||
__debug_bin*
|
||||
.idea/
|
||||
|
||||
@@ -52,6 +52,7 @@ go run ./cmd/scheduler # 定时任务调度器
|
||||
go build -ldflags="-s -w" -o server ./cmd/main_program
|
||||
go build -ldflags="-s -w" -o cli ./cmd/cli_control
|
||||
go build -ldflags="-s -w" -o scheduler ./cmd/scheduler
|
||||
go build -ldflags="-s -w" -o initializer .
|
||||
```
|
||||
|
||||
## 模板使用步骤
|
||||
@@ -269,9 +270,20 @@ go build -ldflags="-s -w" -o scheduler ./cmd/scheduler
|
||||
|
||||
### 首次安装(systemd 服务注册)
|
||||
|
||||
`initializer` 已内置 `.env.example`、`install.sh`、启停脚本和 `deploy/*.service`。将它与三个业务二进制放到部署目录后执行:
|
||||
|
||||
```bash
|
||||
chmod +x install.sh start.sh stop.sh restart.sh server cli scheduler
|
||||
sudo bash install.sh
|
||||
chmod +x initializer server cli scheduler
|
||||
sudo ./initializer
|
||||
# 请输入项目前缀(例如 myapp)
|
||||
```
|
||||
|
||||
初始化工具会自动释放部署文件、添加执行权限、把前缀保存到 `.service-prefix`,并在 `.env` 不存在时根据 `.env.example` 自动创建它,随后执行 `install.sh`。已有 `.env` 会被保留,不会覆盖其中的密钥和环境配置。注册出的服务名为 `myapp-server`、`myapp-cli` 和 `myapp-scheduler`。
|
||||
|
||||
无人值守初始化时可直接传入前缀:
|
||||
|
||||
```bash
|
||||
sudo ./initializer -prefix myapp
|
||||
```
|
||||
|
||||
### 日常运维
|
||||
@@ -285,11 +297,11 @@ bash restart.sh # 重启全部
|
||||
单独管理:
|
||||
|
||||
```bash
|
||||
systemctl status server # 主服务状态
|
||||
systemctl status scheduler # 调度器状态
|
||||
systemctl restart scheduler # 只重启调度器
|
||||
journalctl -u server -f # 主服务实时日志
|
||||
journalctl -u scheduler -f # 调度器实时日志
|
||||
systemctl status myapp-server # 主服务状态
|
||||
systemctl status myapp-scheduler # 调度器状态
|
||||
systemctl restart myapp-scheduler # 只重启调度器
|
||||
journalctl -u myapp-server -f # 主服务实时日志
|
||||
journalctl -u myapp-scheduler -f # 调度器实时日志
|
||||
```
|
||||
|
||||
### systemd 服务特性
|
||||
@@ -309,7 +321,9 @@ journalctl -u scheduler -f # 调度器实时日志
|
||||
├── server # HTTP 主服务
|
||||
├── cli # httplog 上报
|
||||
├── scheduler # 定时任务调度器
|
||||
├── initializer # 首次安装工具(内置部署资源)
|
||||
├── .env # 环境配置
|
||||
├── .service-prefix # 初始化时保存的 systemd 服务名前缀
|
||||
├── deploy/
|
||||
│ ├── server.service
|
||||
│ ├── cli.service
|
||||
@@ -322,6 +336,8 @@ journalctl -u scheduler -f # 调度器实时日志
|
||||
└── cron_*.log # 定时任务专用日志
|
||||
```
|
||||
|
||||
CI 会将 `server`、`cli`、`scheduler` 和 `initializer` 打包为 `deployment.tar.gz` 后上传。部署目录由工作流顶部的 `DEPLOY_DIR` 统一配置(默认 `/root`)。压缩包上传完成后,服务器依次执行 `stop.sh`、覆盖解压、添加执行权限和 `start.sh`;启停脚本会读取 `.service-prefix` 操作带项目前缀的三个服务。首次部署前需要先运行一次 `initializer`。
|
||||
|
||||
## 内置功能清单
|
||||
|
||||
- Hertz HTTP 框架 + 路由分组
|
||||
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"embed"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// deploymentAssets contains everything needed to register and manage the services.
|
||||
//
|
||||
//go:embed .env.example install.sh start.sh stop.sh restart.sh deploy/*.service
|
||||
var deploymentAssets embed.FS
|
||||
|
||||
var validServicePrefix = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9_.-]*$`)
|
||||
|
||||
var assetModes = map[string]os.FileMode{
|
||||
"install.sh": 0o755,
|
||||
"start.sh": 0o755,
|
||||
"stop.sh": 0o755,
|
||||
"restart.sh": 0o755,
|
||||
".env.example": 0o644,
|
||||
"deploy/server.service": 0o644,
|
||||
"deploy/cli.service": 0o644,
|
||||
"deploy/scheduler.service": 0o644,
|
||||
}
|
||||
|
||||
func main() {
|
||||
prefixFlag := flag.String("prefix", "", "systemd 服务名前缀;未指定时交互输入")
|
||||
flag.Parse()
|
||||
|
||||
if runtime.GOOS != "linux" {
|
||||
fatalf("初始化工具只能在 Linux 服务器上运行")
|
||||
}
|
||||
if err := requireRoot(); err != nil {
|
||||
fatalf("%v", err)
|
||||
}
|
||||
|
||||
appDir, err := executableDir()
|
||||
if err != nil {
|
||||
fatalf("获取程序目录失败: %v", err)
|
||||
}
|
||||
|
||||
prefix, err := resolvePrefix(*prefixFlag, os.Stdin, os.Stdout)
|
||||
if err != nil {
|
||||
fatalf("读取项目前缀失败: %v", err)
|
||||
}
|
||||
|
||||
if err := extractAssets(appDir); err != nil {
|
||||
fatalf("释放部署文件失败: %v", err)
|
||||
}
|
||||
envCreated, err := initializeEnv(appDir)
|
||||
if err != nil {
|
||||
fatalf("初始化 .env 失败: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(appDir, ".service-prefix"), []byte(prefix+"\n"), 0o644); err != nil {
|
||||
fatalf("保存项目前缀失败: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("部署文件已释放到 %s\n", appDir)
|
||||
if envCreated {
|
||||
fmt.Println("已根据 .env.example 初始化 .env,请按实际环境修改配置")
|
||||
} else {
|
||||
fmt.Println("检测到已有 .env,已保留现有配置")
|
||||
}
|
||||
fmt.Printf("正在注册服务: %s-server, %s-cli, %s-scheduler\n", prefix, prefix, prefix)
|
||||
|
||||
install := exec.Command("bash", filepath.Join(appDir, "install.sh"))
|
||||
install.Dir = appDir
|
||||
install.Stdin = os.Stdin
|
||||
install.Stdout = os.Stdout
|
||||
install.Stderr = os.Stderr
|
||||
if err := install.Run(); err != nil {
|
||||
fatalf("install.sh 执行失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func requireRoot() error {
|
||||
uid, err := exec.Command("id", "-u").Output()
|
||||
if err != nil {
|
||||
return fmt.Errorf("无法检查当前用户权限: %w", err)
|
||||
}
|
||||
if strings.TrimSpace(string(uid)) != "0" {
|
||||
return errors.New("请使用 root 权限运行: sudo ./initializer")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func executableDir() (string, error) {
|
||||
filename, err := os.Executable()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
filename, err = filepath.Abs(filename)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Dir(filename), nil
|
||||
}
|
||||
|
||||
func resolvePrefix(value string, in io.Reader, out io.Writer) (string, error) {
|
||||
prefix := strings.TrimSpace(value)
|
||||
if prefix == "" {
|
||||
if _, err := fmt.Fprint(out, "请输入项目前缀(例如 myapp): "); err != nil {
|
||||
return "", err
|
||||
}
|
||||
line, err := bufio.NewReader(in).ReadString('\n')
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
return "", err
|
||||
}
|
||||
prefix = strings.TrimSpace(line)
|
||||
}
|
||||
|
||||
if !validServicePrefix.MatchString(prefix) {
|
||||
return "", fmt.Errorf("%q 不是有效前缀,只允许字母、数字、点、下划线和连字符,且必须以字母或数字开头", prefix)
|
||||
}
|
||||
return prefix, nil
|
||||
}
|
||||
|
||||
func extractAssets(appDir string) error {
|
||||
for name, mode := range assetModes {
|
||||
content, err := deploymentAssets.ReadFile(name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取 %s: %w", name, err)
|
||||
}
|
||||
target := filepath.Join(appDir, filepath.FromSlash(name))
|
||||
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
|
||||
return fmt.Errorf("创建 %s: %w", filepath.Dir(target), err)
|
||||
}
|
||||
if err := os.WriteFile(target, content, mode); err != nil {
|
||||
return fmt.Errorf("写入 %s: %w", target, err)
|
||||
}
|
||||
if err := os.Chmod(target, mode); err != nil {
|
||||
return fmt.Errorf("设置 %s 权限: %w", target, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func initializeEnv(appDir string) (bool, error) {
|
||||
target := filepath.Join(appDir, ".env")
|
||||
if _, err := os.Stat(target); err == nil {
|
||||
return false, nil
|
||||
} else if !errors.Is(err, os.ErrNotExist) {
|
||||
return false, err
|
||||
}
|
||||
|
||||
content, err := deploymentAssets.ReadFile(".env.example")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := os.WriteFile(target, content, 0o600); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func fatalf(format string, args ...any) {
|
||||
fmt.Fprintf(os.Stderr, "错误: "+format+"\n", args...)
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestResolvePrefixFromFlag(t *testing.T) {
|
||||
prefix, err := resolvePrefix(" demo-api ", strings.NewReader(""), &bytes.Buffer{})
|
||||
if err != nil {
|
||||
t.Fatalf("resolvePrefix() error = %v", err)
|
||||
}
|
||||
if prefix != "demo-api" {
|
||||
t.Fatalf("resolvePrefix() = %q, want %q", prefix, "demo-api")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitializeEnvCreatesAndPreservesConfig(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
created, err := initializeEnv(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("initializeEnv() error = %v", err)
|
||||
}
|
||||
if !created {
|
||||
t.Fatal("initializeEnv() created = false, want true")
|
||||
}
|
||||
|
||||
target := filepath.Join(dir, ".env")
|
||||
want, err := deploymentAssets.ReadFile(".env.example")
|
||||
if err != nil {
|
||||
t.Fatalf("read embedded .env.example: %v", err)
|
||||
}
|
||||
got, err := os.ReadFile(target)
|
||||
if err != nil {
|
||||
t.Fatalf("read generated .env: %v", err)
|
||||
}
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Fatal("generated .env does not match embedded .env.example")
|
||||
}
|
||||
|
||||
const custom = "SECRET=keep-me\n"
|
||||
if err := os.WriteFile(target, []byte(custom), 0o600); err != nil {
|
||||
t.Fatalf("write existing .env: %v", err)
|
||||
}
|
||||
created, err = initializeEnv(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("second initializeEnv() error = %v", err)
|
||||
}
|
||||
if created {
|
||||
t.Fatal("second initializeEnv() created = true, want false")
|
||||
}
|
||||
got, err = os.ReadFile(target)
|
||||
if err != nil {
|
||||
t.Fatalf("read preserved .env: %v", err)
|
||||
}
|
||||
if string(got) != custom {
|
||||
t.Fatalf("existing .env was overwritten: got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolvePrefixFromInput(t *testing.T) {
|
||||
var output bytes.Buffer
|
||||
prefix, err := resolvePrefix("", strings.NewReader("template_1\n"), &output)
|
||||
if err != nil {
|
||||
t.Fatalf("resolvePrefix() error = %v", err)
|
||||
}
|
||||
if prefix != "template_1" {
|
||||
t.Fatalf("resolvePrefix() = %q, want %q", prefix, "template_1")
|
||||
}
|
||||
if !strings.Contains(output.String(), "请输入项目前缀") {
|
||||
t.Fatalf("prompt = %q, want prefix prompt", output.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolvePrefixRejectsUnsafeValues(t *testing.T) {
|
||||
values := []string{"", "-demo", "demo api", "demo;reboot", "项目"}
|
||||
for _, value := range values {
|
||||
t.Run(value, func(t *testing.T) {
|
||||
if _, err := resolvePrefix(value, strings.NewReader(""), &bytes.Buffer{}); err == nil {
|
||||
t.Fatalf("resolvePrefix(%q) succeeded, want error", value)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+33
-16
@@ -1,11 +1,23 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
set -euo pipefail
|
||||
|
||||
APP_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
DEPLOY_DIR="$APP_DIR/deploy"
|
||||
SERVICE_DIR="/etc/systemd/system"
|
||||
LOG_DIR="$APP_DIR/logs"
|
||||
SERVICES="server cli scheduler"
|
||||
PREFIX_FILE="$APP_DIR/.service-prefix"
|
||||
SERVICE_SUFFIXES=(server cli scheduler)
|
||||
|
||||
if [ ! -f "$PREFIX_FILE" ]; then
|
||||
echo "缺少 $PREFIX_FILE,请先运行 initializer 完成初始化"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SERVICE_PREFIX="$(tr -d '\r\n' < "$PREFIX_FILE")"
|
||||
if [[ ! "$SERVICE_PREFIX" =~ ^[A-Za-z0-9][A-Za-z0-9_.-]*$ ]]; then
|
||||
echo "无效的服务前缀: $SERVICE_PREFIX"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "请使用 root 权限运行: sudo bash install.sh"
|
||||
@@ -16,27 +28,32 @@ mkdir -p "$LOG_DIR"
|
||||
|
||||
echo "=== 安装 systemd 服务 ==="
|
||||
|
||||
for svc in $SERVICES; do
|
||||
for svc in "${SERVICE_SUFFIXES[@]}"; do
|
||||
src="$DEPLOY_DIR/${svc}.service"
|
||||
service_name="${SERVICE_PREFIX}-${svc}"
|
||||
if [ ! -f "$src" ]; then
|
||||
echo "[${svc}] service 文件不存在: $src"
|
||||
continue
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sed "s|WorkingDirectory=/root|WorkingDirectory=$APP_DIR|g; \
|
||||
s|ExecStart=/root/|ExecStart=$APP_DIR/|g; \
|
||||
s|EnvironmentFile=-/root/.env|EnvironmentFile=-$APP_DIR/.env|g; \
|
||||
s|/root/logs/|$LOG_DIR/|g" \
|
||||
"$src" > "$SERVICE_DIR/${svc}.service"
|
||||
escaped_app_dir="$(printf '%s' "$APP_DIR" | sed 's/[&|\\]/\\&/g')"
|
||||
sed "s|/root|$escaped_app_dir|g" "$src" > "$SERVICE_DIR/${service_name}.service"
|
||||
|
||||
echo "[${svc}] 已安装到 $SERVICE_DIR/${svc}.service"
|
||||
echo "[${service_name}] 已安装到 $SERVICE_DIR/${service_name}.service"
|
||||
done
|
||||
|
||||
systemctl daemon-reload
|
||||
|
||||
for svc in $SERVICES; do
|
||||
systemctl enable "$svc"
|
||||
echo "[${svc}] 已设置开机自启"
|
||||
for svc in "${SERVICE_SUFFIXES[@]}"; do
|
||||
service_name="${SERVICE_PREFIX}-${svc}"
|
||||
systemctl enable "$service_name"
|
||||
echo "[${service_name}] 已设置开机自启"
|
||||
done
|
||||
|
||||
for binary in server cli scheduler; do
|
||||
if [ -f "$APP_DIR/$binary" ]; then
|
||||
chmod +x "$APP_DIR/$binary"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
@@ -45,6 +62,6 @@ echo "使用方式:"
|
||||
echo " bash start.sh # 启动全部"
|
||||
echo " bash stop.sh # 停止全部"
|
||||
echo " bash restart.sh # 重启全部"
|
||||
echo " systemctl status server # 查看主服务状态"
|
||||
echo " systemctl status scheduler # 查看调度器状态"
|
||||
echo " journalctl -u scheduler -f # 查看调度器实时日志"
|
||||
echo " systemctl status ${SERVICE_PREFIX}-server # 查看主服务状态"
|
||||
echo " systemctl status ${SERVICE_PREFIX}-scheduler # 查看调度器状态"
|
||||
echo " journalctl -u ${SERVICE_PREFIX}-scheduler -f # 查看调度器实时日志"
|
||||
|
||||
+10
-3
@@ -1,10 +1,17 @@
|
||||
#!/bin/bash
|
||||
SERVICES="server cli scheduler"
|
||||
set -euo pipefail
|
||||
|
||||
for svc in $SERVICES; do
|
||||
APP_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PREFIX_FILE="$APP_DIR/.service-prefix"
|
||||
[ -f "$PREFIX_FILE" ] || { echo "缺少 $PREFIX_FILE,请先运行 initializer"; exit 1; }
|
||||
SERVICE_PREFIX="$(tr -d '\r\n' < "$PREFIX_FILE")"
|
||||
[[ "$SERVICE_PREFIX" =~ ^[A-Za-z0-9][A-Za-z0-9_.-]*$ ]] || { echo "无效的服务前缀: $SERVICE_PREFIX"; exit 1; }
|
||||
SERVICES=("${SERVICE_PREFIX}-server" "${SERVICE_PREFIX}-cli" "${SERVICE_PREFIX}-scheduler")
|
||||
|
||||
for svc in "${SERVICES[@]}"; do
|
||||
systemctl restart "$svc"
|
||||
echo "[$svc] 已重启"
|
||||
done
|
||||
|
||||
echo ""
|
||||
systemctl status $SERVICES --no-pager -l
|
||||
systemctl status "${SERVICES[@]}" --no-pager -l
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
#!/bin/bash
|
||||
SERVICES="server cli scheduler"
|
||||
set -euo pipefail
|
||||
|
||||
for svc in $SERVICES; do
|
||||
APP_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PREFIX_FILE="$APP_DIR/.service-prefix"
|
||||
[ -f "$PREFIX_FILE" ] || { echo "缺少 $PREFIX_FILE,请先运行 initializer"; exit 1; }
|
||||
SERVICE_PREFIX="$(tr -d '\r\n' < "$PREFIX_FILE")"
|
||||
[[ "$SERVICE_PREFIX" =~ ^[A-Za-z0-9][A-Za-z0-9_.-]*$ ]] || { echo "无效的服务前缀: $SERVICE_PREFIX"; exit 1; }
|
||||
SERVICES=("${SERVICE_PREFIX}-server" "${SERVICE_PREFIX}-cli" "${SERVICE_PREFIX}-scheduler")
|
||||
|
||||
for svc in "${SERVICES[@]}"; do
|
||||
if systemctl is-active --quiet "$svc"; then
|
||||
echo "[$svc] 已在运行"
|
||||
else
|
||||
@@ -11,4 +18,4 @@ for svc in $SERVICES; do
|
||||
done
|
||||
|
||||
echo ""
|
||||
systemctl status $SERVICES --no-pager -l
|
||||
systemctl status "${SERVICES[@]}" --no-pager -l
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
#!/bin/bash
|
||||
SERVICES="server cli scheduler"
|
||||
set -euo pipefail
|
||||
|
||||
for svc in $SERVICES; do
|
||||
APP_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PREFIX_FILE="$APP_DIR/.service-prefix"
|
||||
[ -f "$PREFIX_FILE" ] || { echo "缺少 $PREFIX_FILE,请先运行 initializer"; exit 1; }
|
||||
SERVICE_PREFIX="$(tr -d '\r\n' < "$PREFIX_FILE")"
|
||||
[[ "$SERVICE_PREFIX" =~ ^[A-Za-z0-9][A-Za-z0-9_.-]*$ ]] || { echo "无效的服务前缀: $SERVICE_PREFIX"; exit 1; }
|
||||
SERVICES=("${SERVICE_PREFIX}-server" "${SERVICE_PREFIX}-cli" "${SERVICE_PREFIX}-scheduler")
|
||||
|
||||
for svc in "${SERVICES[@]}"; do
|
||||
if systemctl is-active --quiet "$svc"; then
|
||||
systemctl stop "$svc"
|
||||
echo "[$svc] 已停止"
|
||||
|
||||
Reference in New Issue
Block a user