613 lines
15 KiB
Bash
Executable File
613 lines
15 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
|
||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||
COMPOSE_FILE="${ROOT_DIR}/deploy/docker-compose.dev.yml"
|
||
BACKEND_ENV="${ROOT_DIR}/backend/.env"
|
||
BACKEND_ENV_EXAMPLE="${ROOT_DIR}/backend/.env.example"
|
||
FRONTEND_DIR="${ROOT_DIR}/frontend"
|
||
FRONTEND_PACKAGE_JSON="${FRONTEND_DIR}/package.json"
|
||
FRONTEND_PACKAGE_LOCK="${FRONTEND_DIR}/package-lock.json"
|
||
FRONTEND_NODE_MODULES="${FRONTEND_DIR}/node_modules"
|
||
FRONTEND_NODE_MODULES_LOCK="${FRONTEND_NODE_MODULES}/.package-lock.json"
|
||
MIGRATIONS_DIR="${ROOT_DIR}/backend/migrations"
|
||
MYSQL_DATABASE="${DEV_MYSQL_DATABASE:-hfb_sys}"
|
||
BACKEND_PORT="${DEV_BACKEND_PORT:-8080}"
|
||
FRONTEND_PORT="${DEV_FRONTEND_PORT:-5173}"
|
||
BACKEND_HEALTH_URL="${DEV_BACKEND_HEALTH_URL:-http://127.0.0.1:${BACKEND_PORT}/health}"
|
||
FRONTEND_HEALTH_URL="${DEV_FRONTEND_HEALTH_URL:-http://127.0.0.1:${FRONTEND_PORT}/}"
|
||
HTTP_READY_TIMEOUT="${DEV_HTTP_READY_TIMEOUT:-120}"
|
||
AUTO_KILL_PORTS="${DEV_AUTO_KILL_PORTS:-1}"
|
||
FORCE_NPM_CI="${DEV_FORCE_NPM_CI:-0}"
|
||
GOOSE_VERSION="${GOOSE_VERSION:-v3.27.1}"
|
||
GOOSE_BIN="${GOOSE_BIN:-}"
|
||
|
||
BACKEND_PID=""
|
||
FRONTEND_PID=""
|
||
CLEANED_UP=0
|
||
|
||
# 颜色定义
|
||
RED='\033[0;31m'
|
||
GREEN='\033[0;32m'
|
||
YELLOW='\033[1;33m'
|
||
BLUE='\033[1;34m'
|
||
NC='\033[0m'
|
||
|
||
log() {
|
||
printf "${BLUE}[dev]${NC} %s\n" "$*"
|
||
}
|
||
|
||
log_success() {
|
||
printf "${GREEN}[dev]${NC} %s\n" "$*"
|
||
}
|
||
|
||
log_warn() {
|
||
printf "${YELLOW}[dev]${NC} %s\n" "$*"
|
||
}
|
||
|
||
log_error() {
|
||
printf "${RED}[dev]${NC} %s\n" "$*" >&2
|
||
}
|
||
|
||
need_cmd() {
|
||
if ! command -v "$1" >/dev/null 2>&1; then
|
||
log_error "缺少命令:$1"
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
show_help() {
|
||
cat << EOF
|
||
用法: ./scripts/dev.sh [选项]
|
||
|
||
选项:
|
||
--reset-db 重置数据库(删除并重建)
|
||
--seed-only 仅重新同步基础数据
|
||
--no-migrate 不执行数据库迁移
|
||
--no-frontend 不启动前端
|
||
--no-backend 不启动后端
|
||
-h, --help 显示帮助信息
|
||
|
||
环境变量:
|
||
DEV_RESET_DB=1 等同于 --reset-db
|
||
DEV_MYSQL_DATABASE=hfb_sys
|
||
开发数据库名
|
||
DEV_BACKEND_PORT=8080
|
||
后端启动端口
|
||
DEV_FRONTEND_PORT=5173
|
||
前端启动端口
|
||
DEV_BACKEND_HEALTH_URL=http://127.0.0.1:8080/health
|
||
后端健康检查地址
|
||
DEV_FRONTEND_HEALTH_URL=http://127.0.0.1:5173/
|
||
前端健康检查地址
|
||
DEV_HTTP_READY_TIMEOUT=120
|
||
HTTP 服务就绪等待秒数
|
||
DEV_AUTO_KILL_PORTS=1
|
||
前后端端口被占用时自动停止占用的 Docker 容器或本机进程
|
||
DEV_FORCE_NPM_CI=0
|
||
强制重新执行前端 npm ci
|
||
|
||
示例:
|
||
./scripts/dev.sh # 正常启动
|
||
./scripts/dev.sh --reset-db # 重置数据库后启动(推荐迁移后使用)
|
||
./scripts/dev.sh --seed-only # 重新同步基础数据
|
||
EOF
|
||
exit 0
|
||
}
|
||
|
||
# 参数解析
|
||
RESET_DB="${DEV_RESET_DB:-0}"
|
||
SEED_ONLY=0
|
||
NO_MIGRATE=0
|
||
NO_FRONTEND=0
|
||
NO_BACKEND=0
|
||
|
||
while [[ $# -gt 0 ]]; do
|
||
case "$1" in
|
||
--reset-db)
|
||
RESET_DB=1
|
||
shift
|
||
;;
|
||
--seed-only)
|
||
SEED_ONLY=1
|
||
shift
|
||
;;
|
||
--no-migrate)
|
||
NO_MIGRATE=1
|
||
shift
|
||
;;
|
||
--no-frontend)
|
||
NO_FRONTEND=1
|
||
shift
|
||
;;
|
||
--no-backend)
|
||
NO_BACKEND=1
|
||
shift
|
||
;;
|
||
-h|--help)
|
||
show_help
|
||
;;
|
||
*)
|
||
log_error "未知选项: $1"
|
||
show_help
|
||
;;
|
||
esac
|
||
done
|
||
|
||
cleanup() {
|
||
if [[ "${CLEANED_UP}" == "1" ]]; then
|
||
return
|
||
fi
|
||
CLEANED_UP=1
|
||
|
||
if [[ -z "${FRONTEND_PID}" && -z "${BACKEND_PID}" ]]; then
|
||
return
|
||
fi
|
||
|
||
log "正在停止前后端开发进程..."
|
||
if [[ -n "${FRONTEND_PID}" ]] && kill -0 "${FRONTEND_PID}" >/dev/null 2>&1; then
|
||
kill "${FRONTEND_PID}" >/dev/null 2>&1 || true
|
||
fi
|
||
if [[ -n "${BACKEND_PID}" ]] && kill -0 "${BACKEND_PID}" >/dev/null 2>&1; then
|
||
kill "${BACKEND_PID}" >/dev/null 2>&1 || true
|
||
fi
|
||
}
|
||
|
||
wait_container_healthy() {
|
||
local container_name="$1"
|
||
local label="$2"
|
||
local attempt
|
||
|
||
log "等待 ${label} 就绪..."
|
||
for attempt in {1..60}; do
|
||
local status
|
||
status="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "${container_name}" 2>/dev/null || true)"
|
||
if [[ "${status}" == "healthy" || "${status}" == "running" ]]; then
|
||
log "${label} 已就绪"
|
||
return 0
|
||
fi
|
||
sleep 2
|
||
done
|
||
|
||
printf "%s 启动超时,请检查:docker logs %s\n" "${label}" "${container_name}" >&2
|
||
exit 1
|
||
}
|
||
|
||
wait_http_ready() {
|
||
local url="$1"
|
||
local label="$2"
|
||
local pid="${3:-}"
|
||
local attempt
|
||
|
||
log "等待 ${label} HTTP 服务就绪..."
|
||
for ((attempt = 1; attempt <= HTTP_READY_TIMEOUT; attempt++)); do
|
||
if [[ -n "${pid}" ]] && ! kill -0 "${pid}" >/dev/null 2>&1; then
|
||
wait "${pid}" || true
|
||
log_error "${label} 进程已退出,请检查上方日志"
|
||
exit 1
|
||
fi
|
||
|
||
if curl -fsS --max-time 2 "${url}" >/dev/null 2>&1; then
|
||
log_success "${label} 已就绪:${url}"
|
||
return 0
|
||
fi
|
||
|
||
sleep 1
|
||
done
|
||
|
||
log_error "${label} HTTP 服务启动超时:${url}"
|
||
exit 1
|
||
}
|
||
|
||
port_in_use() {
|
||
local port="$1"
|
||
|
||
if command -v lsof >/dev/null 2>&1; then
|
||
lsof -nP -iTCP:"${port}" -sTCP:LISTEN >/dev/null 2>&1
|
||
return $?
|
||
fi
|
||
|
||
if command -v nc >/dev/null 2>&1; then
|
||
nc -z 127.0.0.1 "${port}" >/dev/null 2>&1
|
||
return $?
|
||
fi
|
||
|
||
return 1
|
||
}
|
||
|
||
wait_port_released() {
|
||
local port="$1"
|
||
local timeout="${2:-5}"
|
||
local attempt
|
||
|
||
for ((attempt = 1; attempt <= timeout; attempt++)); do
|
||
if ! port_in_use "${port}"; then
|
||
return 0
|
||
fi
|
||
sleep 1
|
||
done
|
||
|
||
return 1
|
||
}
|
||
|
||
is_allowed_container() {
|
||
local container_name="$1"
|
||
shift
|
||
|
||
local allowed_name
|
||
for allowed_name in "$@"; do
|
||
if [[ "${container_name}" == "${allowed_name}" ]]; then
|
||
return 0
|
||
fi
|
||
done
|
||
|
||
return 1
|
||
}
|
||
|
||
docker_containers_on_host_port() {
|
||
local port="$1"
|
||
|
||
docker ps --format '{{.ID}}\t{{.Names}}\t{{.Ports}}' 2>/dev/null |
|
||
awk -F '\t' -v port="${port}" 'index($3, ":" port "->") || $3 ~ ("(^|[ ,])" port "->") { print $1 "\t" $2 }'
|
||
}
|
||
|
||
stop_docker_containers_on_port() {
|
||
local port="$1"
|
||
local label="$2"
|
||
shift 2
|
||
|
||
local containers container_id container_name allowed_found=0 stopped_found=0
|
||
containers="$(docker_containers_on_host_port "${port}" || true)"
|
||
if [[ -z "${containers}" ]]; then
|
||
return 1
|
||
fi
|
||
|
||
while IFS=$'\t' read -r container_id container_name; do
|
||
[[ -z "${container_id}" ]] && continue
|
||
|
||
if is_allowed_container "${container_name}" "$@"; then
|
||
allowed_found=1
|
||
log "${label}端口 ${port} 已由本项目容器 ${container_name} 使用,保留运行"
|
||
continue
|
||
fi
|
||
|
||
if [[ "${AUTO_KILL_PORTS}" != "1" ]]; then
|
||
log_error "${label}端口 ${port} 被 Docker 容器 ${container_name} 占用"
|
||
exit 1
|
||
fi
|
||
|
||
log_warn "${label}端口 ${port} 被 Docker 容器 ${container_name} 占用,正在停止..."
|
||
docker stop "${container_id}" >/dev/null
|
||
stopped_found=1
|
||
log_success "已停止容器 ${container_name}"
|
||
done <<< "${containers}"
|
||
|
||
if [[ "${allowed_found}" == "1" ]]; then
|
||
return 0
|
||
fi
|
||
|
||
if [[ "${stopped_found}" == "1" ]]; then
|
||
wait_port_released "${port}" 5 || true
|
||
fi
|
||
|
||
return 1
|
||
}
|
||
|
||
process_args() {
|
||
local pid="$1"
|
||
ps -p "${pid}" -o args= 2>/dev/null || true
|
||
}
|
||
|
||
is_docker_proxy_process() {
|
||
local args="$1"
|
||
|
||
[[ "${args}" == *"com.docker"* || "${args}" == *"Docker Desktop"* || "${args}" == *"vpnkit"* || "${args}" == *"OrbStack"* ]]
|
||
}
|
||
|
||
ensure_port_free() {
|
||
local port="$1"
|
||
local label="$2"
|
||
shift 2
|
||
|
||
if stop_docker_containers_on_port "${port}" "${label}" "$@"; then
|
||
return 0
|
||
fi
|
||
|
||
if command -v lsof >/dev/null 2>&1; then
|
||
local pids pid args
|
||
pids="$(lsof -nP -tiTCP:"${port}" -sTCP:LISTEN 2>/dev/null | sort -u || true)"
|
||
if [[ -z "${pids}" ]]; then
|
||
return 0
|
||
fi
|
||
|
||
if [[ "${AUTO_KILL_PORTS}" != "1" ]]; then
|
||
log_error "${label}端口 ${port} 已被占用,请先停止旧进程或调整端口"
|
||
exit 1
|
||
fi
|
||
|
||
log_warn "${label}端口 ${port} 已被占用,正在停止占用进程..."
|
||
while IFS= read -r pid; do
|
||
[[ -z "${pid}" ]] && continue
|
||
args="$(process_args "${pid}")"
|
||
|
||
if is_docker_proxy_process "${args}"; then
|
||
log_error "${label}端口 ${port} 仍由 Docker 代理进程占用,但未找到可自动停止的容器,请执行 docker ps 检查"
|
||
exit 1
|
||
fi
|
||
|
||
log_warn "停止 PID ${pid}: ${args:-未知进程}"
|
||
kill "${pid}" >/dev/null 2>&1 || true
|
||
done <<< "${pids}"
|
||
|
||
if ! wait_port_released "${port}" 5; then
|
||
log_warn "${label}端口 ${port} 未及时释放,尝试强制停止..."
|
||
pids="$(lsof -nP -tiTCP:"${port}" -sTCP:LISTEN 2>/dev/null | sort -u || true)"
|
||
while IFS= read -r pid; do
|
||
[[ -z "${pid}" ]] && continue
|
||
args="$(process_args "${pid}")"
|
||
|
||
if is_docker_proxy_process "${args}"; then
|
||
log_error "${label}端口 ${port} 仍由 Docker 代理进程占用,但未找到可自动停止的容器,请执行 docker ps 检查"
|
||
exit 1
|
||
fi
|
||
|
||
log_warn "强制停止 PID ${pid}: ${args:-未知进程}"
|
||
kill -9 "${pid}" >/dev/null 2>&1 || true
|
||
done <<< "${pids}"
|
||
|
||
if ! wait_port_released "${port}" 5; then
|
||
log_error "${label}端口 ${port} 释放失败,请手动检查占用进程"
|
||
exit 1
|
||
fi
|
||
fi
|
||
|
||
log_success "${label}端口 ${port} 已释放"
|
||
return 0
|
||
fi
|
||
|
||
if command -v nc >/dev/null 2>&1; then
|
||
if nc -z 127.0.0.1 "${port}" >/dev/null 2>&1; then
|
||
log_error "${label}端口 ${port} 已被占用,但未找到 lsof,无法自动定位并停止占用进程"
|
||
exit 1
|
||
fi
|
||
return 0
|
||
fi
|
||
|
||
log_warn "未找到 lsof 或 nc,跳过 ${label}端口 ${port} 占用检查"
|
||
}
|
||
|
||
mysql_root() {
|
||
docker exec -i -e MYSQL_PWD=rootsecret hfb-mysql mysql -uroot "$@"
|
||
}
|
||
|
||
validate_database_name() {
|
||
if [[ ! "${MYSQL_DATABASE}" =~ ^[A-Za-z0-9_]+$ ]]; then
|
||
log_error "DEV_MYSQL_DATABASE 只能包含字母、数字和下划线,当前值:${MYSQL_DATABASE}"
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
reset_database() {
|
||
if [[ "${RESET_DB}" != "1" ]]; then
|
||
return 0
|
||
fi
|
||
|
||
log_warn "重置数据库 ${MYSQL_DATABASE}..."
|
||
mysql_root -e \
|
||
"DROP DATABASE IF EXISTS \`${MYSQL_DATABASE}\`; CREATE DATABASE \`${MYSQL_DATABASE}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
|
||
log_success "数据库已重建"
|
||
}
|
||
|
||
ensure_goose() {
|
||
if [[ -z "${GOOSE_BIN}" ]]; then
|
||
GOOSE_BIN="$(command -v goose || true)"
|
||
fi
|
||
|
||
if [[ -z "${GOOSE_BIN}" ]]; then
|
||
need_cmd go
|
||
log "未找到 goose,正在安装 ${GOOSE_VERSION}..."
|
||
(cd "${ROOT_DIR}/backend" && go install "github.com/pressly/goose/v3/cmd/goose@${GOOSE_VERSION}")
|
||
GOOSE_BIN="$(go env GOPATH)/bin/goose"
|
||
fi
|
||
|
||
if [[ ! -x "${GOOSE_BIN}" ]]; then
|
||
log_error "goose 不可执行:${GOOSE_BIN}"
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
goose_dsn() {
|
||
local dsn="$1"
|
||
if [[ "${dsn}" != *"multiStatements="* ]]; then
|
||
if [[ "${dsn}" == *"?"* ]]; then
|
||
dsn="${dsn}&multiStatements=true"
|
||
else
|
||
dsn="${dsn}?multiStatements=true"
|
||
fi
|
||
fi
|
||
printf "%s" "${dsn}"
|
||
}
|
||
|
||
dev_goose_dsn() {
|
||
goose_dsn "hfb:secret@tcp(127.0.0.1:3306)/${MYSQL_DATABASE}?charset=utf8mb4&parseTime=True&loc=Local"
|
||
}
|
||
|
||
goose_cmd() {
|
||
"${GOOSE_BIN}" -dir "${MIGRATIONS_DIR}" mysql "$(dev_goose_dsn)" "$@"
|
||
}
|
||
|
||
run_migrations() {
|
||
if [[ "${NO_MIGRATE}" == "1" ]]; then
|
||
log_warn "已跳过数据库迁移"
|
||
return 0
|
||
fi
|
||
|
||
ensure_goose
|
||
log "使用 goose 执行数据库迁移..."
|
||
goose_cmd up
|
||
log_success "数据库迁移完成"
|
||
}
|
||
|
||
load_env_file() {
|
||
local env_file="$1"
|
||
local line
|
||
|
||
while IFS= read -r line || [[ -n "${line}" ]]; do
|
||
[[ -z "${line}" || "${line}" =~ ^[[:space:]]*# ]] && continue
|
||
line="${line#export }"
|
||
export "${line}"
|
||
done < "${env_file}"
|
||
}
|
||
|
||
start_backend() {
|
||
if [[ ! -f "${BACKEND_ENV}" ]]; then
|
||
log "创建 backend/.env"
|
||
cp "${BACKEND_ENV_EXAMPLE}" "${BACKEND_ENV}"
|
||
fi
|
||
|
||
log "启动后端:http://localhost:8080"
|
||
(
|
||
cd "${ROOT_DIR}/backend"
|
||
load_env_file "${BACKEND_ENV}"
|
||
go run ./cmd/api
|
||
) &
|
||
BACKEND_PID="$!"
|
||
}
|
||
|
||
frontend_dependencies_need_ci() {
|
||
if [[ "${FORCE_NPM_CI}" == "1" ]]; then
|
||
return 0
|
||
fi
|
||
|
||
if [[ ! -d "${FRONTEND_NODE_MODULES}" ]]; then
|
||
return 0
|
||
fi
|
||
|
||
if [[ ! -f "${FRONTEND_NODE_MODULES_LOCK}" ]]; then
|
||
return 0
|
||
fi
|
||
|
||
if [[ "${FRONTEND_PACKAGE_JSON}" -nt "${FRONTEND_NODE_MODULES_LOCK}" || "${FRONTEND_PACKAGE_LOCK}" -nt "${FRONTEND_NODE_MODULES_LOCK}" ]]; then
|
||
return 0
|
||
fi
|
||
|
||
(
|
||
cd "${FRONTEND_DIR}"
|
||
npm ls --depth=0 >/dev/null 2>&1
|
||
) || return 0
|
||
|
||
return 1
|
||
}
|
||
|
||
ensure_frontend_dependencies() {
|
||
if [[ ! -f "${FRONTEND_PACKAGE_LOCK}" ]]; then
|
||
log_error "缺少 frontend/package-lock.json,无法执行 npm ci"
|
||
exit 1
|
||
fi
|
||
|
||
if frontend_dependencies_need_ci; then
|
||
log "同步前端依赖(npm ci)..."
|
||
(
|
||
cd "${FRONTEND_DIR}"
|
||
npm ci
|
||
)
|
||
log_success "前端依赖已同步"
|
||
else
|
||
log "前端依赖已同步,跳过 npm ci"
|
||
fi
|
||
}
|
||
|
||
start_frontend() {
|
||
ensure_frontend_dependencies
|
||
|
||
log "启动前端:http://localhost:5173"
|
||
(
|
||
cd "${FRONTEND_DIR}"
|
||
npm run dev
|
||
) &
|
||
FRONTEND_PID="$!"
|
||
}
|
||
|
||
watch_processes() {
|
||
while true; do
|
||
if [[ -n "${BACKEND_PID}" ]] && ! kill -0 "${BACKEND_PID}" >/dev/null 2>&1; then
|
||
wait "${BACKEND_PID}" || true
|
||
printf "后端进程已退出\n" >&2
|
||
exit 1
|
||
fi
|
||
if [[ -n "${FRONTEND_PID}" ]] && ! kill -0 "${FRONTEND_PID}" >/dev/null 2>&1; then
|
||
wait "${FRONTEND_PID}" || true
|
||
printf "前端进程已退出\n" >&2
|
||
exit 1
|
||
fi
|
||
sleep 2
|
||
done
|
||
}
|
||
|
||
main() {
|
||
need_cmd docker
|
||
if [[ "${SEED_ONLY}" != "1" && "${NO_BACKEND}" == "0" ]]; then
|
||
need_cmd go
|
||
fi
|
||
if [[ "${SEED_ONLY}" != "1" && "${NO_FRONTEND}" == "0" ]]; then
|
||
need_cmd npm
|
||
fi
|
||
if [[ "${SEED_ONLY}" != "1" && ( "${NO_BACKEND}" == "0" || "${NO_FRONTEND}" == "0" ) ]]; then
|
||
need_cmd curl
|
||
fi
|
||
|
||
trap cleanup EXIT
|
||
trap 'cleanup; exit 130' INT TERM
|
||
|
||
validate_database_name
|
||
if [[ "${RESET_DB}" == "1" && "${NO_MIGRATE}" == "1" ]]; then
|
||
log_error "--reset-db 会清空数据库,不能和 --no-migrate 同时使用"
|
||
exit 1
|
||
fi
|
||
|
||
log "启动开发依赖:MySQL、Redis、MinIO"
|
||
docker compose -f "${COMPOSE_FILE}" up -d
|
||
|
||
wait_container_healthy hfb-mysql "MySQL"
|
||
wait_container_healthy hfb-redis "Redis"
|
||
wait_container_healthy hfb-minio "MinIO"
|
||
|
||
reset_database
|
||
run_migrations
|
||
|
||
# 仅同步基础数据模式
|
||
if [[ "${SEED_ONLY}" == "1" ]]; then
|
||
log_success "基础数据同步完成,退出"
|
||
exit 0
|
||
fi
|
||
|
||
if [[ "${NO_BACKEND}" == "0" ]]; then
|
||
ensure_port_free "${BACKEND_PORT}" "后端"
|
||
start_backend
|
||
wait_http_ready "${BACKEND_HEALTH_URL}" "后端" "${BACKEND_PID}"
|
||
fi
|
||
|
||
if [[ "${NO_FRONTEND}" == "0" ]]; then
|
||
ensure_port_free "${FRONTEND_PORT}" "前端"
|
||
start_frontend
|
||
wait_http_ready "${FRONTEND_HEALTH_URL}" "前端" "${FRONTEND_PID}"
|
||
fi
|
||
|
||
if [[ -z "${BACKEND_PID}" && -z "${FRONTEND_PID}" ]]; then
|
||
log "仅启动 Docker 开发依赖,前后端未启动"
|
||
return 0
|
||
fi
|
||
|
||
log_success "开发环境已启动"
|
||
if [[ "${NO_FRONTEND}" == "0" ]]; then
|
||
log "前台:http://localhost:5173"
|
||
log "后台:http://localhost:5173/admin/login(admin / admin123456)"
|
||
fi
|
||
|
||
log "按 Ctrl+C 停止前后端;Docker 依赖会保留运行"
|
||
|
||
watch_processes
|
||
}
|
||
|
||
main "$@"
|