Repository navigation
Expand file tree
/
Copy pathstart.sh
More file actions
executable file
·231 lines (209 loc) · 7.48 KB
/
Copy pathstart.sh
File metadata and controls
executable file
·231 lines (209 loc) · 7.48 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
#!/usr/bin/env bash
# ============================================================
# 🐘 PHP 编程教学平台 · 一键启动器
#
# 用法:
# ./start.sh 启动服务并自动打开浏览器 (Ctrl+C 停止)
# ./start.sh stop 停止已在运行的服务
# SKIP_BROWSER=1 ./start.sh 只启动服务,不打开浏览器
#
# 说明:
# - 项目自带 runtime/php (静态 PHP 8.3,无需安装任何东西)
# - 若 runtime 缺失,会自动尝试系统 php,都没有则联网自动下载
# - 数据(学习进度/测验成绩)保存在 data/database.sqlite
# ============================================================
# 文件管理器双击时可能用 sh 执行,强制切回 bash
if [ -z "$BASH_VERSION" ]; then exec bash "$0" "$@"; fi
APP_NAME="PHP编程教学平台"
APP_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$APP_DIR" || exit 1
PID_FILE="$APP_DIR/data/.server.pid"
LOG_FILE="$APP_DIR/data/server.log"
SKIP_BROWSER="${SKIP_BROWSER:-0}"
# ---------- 颜色 ----------
if [ -t 1 ]; then
C_G=$'\033[1;32m'; C_C=$'\033[1;36m'; C_Y=$'\033[1;33m'; C_R=$'\033[1;31m'; C_0=$'\033[0m'
else
C_G=""; C_C=""; C_Y=""; C_R=""; C_0=""
fi
say_ok() { echo "${C_G}[✓]${C_0} $1"; }
say_info() { echo "${C_C}[·]${C_0} $1"; }
say_warn() { echo "${C_Y}[!]${C_0} $1"; }
say_err() { echo "${C_R}[✗]${C_0} $1"; }
# ---------- 工具函数 ----------
port_in_use() {
(exec 3<>/dev/tcp/127.0.0.1/"$1") 2>/dev/null && { exec 3>&- 3<&-; return 0; }
return 1
}
# 探测某端口是否为本应用(识别首页标题里的「编程教学」)
is_our_app() {
command -v curl >/dev/null 2>&1 || return 1
curl -sf --max-time 2 "http://127.0.0.1:$1/" 2>/dev/null | grep -q "编程教学"
}
find_php() {
# 1) 项目内置运行时(离线可用)
[ -x "$APP_DIR/runtime/php" ] && { echo "$APP_DIR/runtime/php"; return 0; }
# 2) 同级 .tools/php(开发环境)
[ -x "$APP_DIR/../.tools/php" ] && { echo "$APP_DIR/../.tools/php"; return 0; }
# 3) 系统 php
if command -v php >/dev/null 2>&1; then command -v php; return 0; fi
return 1
}
download_php() {
local arch a url
arch="$(uname -m)"
case "$arch" in
x86_64) a="x86_64" ;;
aarch64|arm64) a="aarch64" ;;
*) return 1 ;;
esac
url="https://dl.static-php.dev/static-php-cli/common/php-8.3.13-cli-linux-${a}.tar.gz"
say_info "正在自动下载 PHP 运行时(约 8MB)……"
mkdir -p "$APP_DIR/runtime"
curl -L --retry 3 --connect-timeout 15 -o "$APP_DIR/runtime/php.tar.gz" "$url" || return 1
tar -xzf "$APP_DIR/runtime/php.tar.gz" -C "$APP_DIR/runtime" || return 1
rm -f "$APP_DIR/runtime/php.tar.gz"
chmod +x "$APP_DIR/runtime/php" 2>/dev/null
[ -x "$APP_DIR/runtime/php" ]
}
open_browser() {
local b
for b in xdg-open x-www-browser sensible-browser firefox firefox-esr chromium chromium-browser google-chrome; do
if command -v "$b" >/dev/null 2>&1; then
nohup "$b" "$1" >/dev/null 2>&1 &
return 0
fi
done
return 1
}
# ---------- 停止命令 ----------
do_stop() {
if [ -f "$PID_FILE" ]; then
local pid
pid="$(cat "$PID_FILE" 2>/dev/null)"
rm -f "$PID_FILE"
if [ -n "$pid" ] && kill "$pid" 2>/dev/null; then
say_ok "服务已停止 (PID $pid)"
return 0
fi
say_warn "PID 文件已过期,尝试按端口查找……"
fi
# 兜底1:按启动特征匹配本项目进程
if pkill -f -- "-S 127.0.0.1:[0-9]* -t $APP_DIR" 2>/dev/null; then
say_ok "服务已停止"
return 0
fi
# 兜底2:按端口找监听进程(lsof / fuser)
if command -v lsof >/dev/null 2>&1; then
pids="$(lsof -t -i TCP:8090-8099 -s TCP:LISTEN 2>/dev/null)"
elif command -v fuser >/dev/null 2>&1; then
pids=""
for p in $(seq 8090 8099); do
fp="$(fuser "$p"/tcp 2>/dev/null | tr -d ' ')"
[ -n "$fp" ] && pids="$pids $fp"
done
else
pids=""
fi
if [ -n "${pids:-}" ]; then
kill $pids 2>/dev/null && say_ok "服务已停止"
return 0
fi
say_info "没有正在运行的服务"
}
# ---------- 主流程 ----------
case "${1:-}" in
stop) do_stop; exit 0 ;;
-h|--help)
sed -n '2,14p' "$0" | sed 's/^# \{0,1\}//'
exit 0 ;;
esac
echo ""
echo "${C_C}======================================================${C_0}"
echo "${C_C} 🐘 $APP_NAME${C_0}"
echo "${C_C}======================================================${C_0}"
echo ""
# 1) 定位 PHP
PHP_BIN="$(find_php)"
if [ -z "$PHP_BIN" ]; then
download_php || { say_err "未找到 PHP 且自动下载失败,请安装 PHP 8 后重试"; exit 1; }
PHP_BIN="$APP_DIR/runtime/php"
fi
say_ok "PHP 运行时: $PHP_BIN ($("$PHP_BIN" -r 'echo PHP_VERSION;' 2>/dev/null))"
# 2) 检查必需扩展
MISS_EXT=""
"$PHP_BIN" -m 2>/dev/null | grep -qi '^sqlite3$' || MISS_EXT="$MISS_EXT sqlite3"
"$PHP_BIN" -m 2>/dev/null | grep -qi '^pdo_sqlite$' || MISS_EXT="$MISS_EXT pdo_sqlite"
"$PHP_BIN" -m 2>/dev/null | grep -qi '^session$' || MISS_EXT="$MISS_EXT session"
if [ -n "$MISS_EXT" ]; then
say_err "当前 PHP 缺少扩展:$MISS_EXT"
say_info "项目内置的 runtime/php 自带全部扩展;或使用 --with-sqlite3 --with-pdo-sqlite 编译的系统 PHP"
exit 1
fi
say_ok "扩展检查通过 (sqlite3 / pdo_sqlite / session)"
# 3) 已有实例在运行?→ 直接打开,不重复启动
if command -v curl >/dev/null 2>&1; then
for p in $(seq 8090 8099); do
if is_our_app "$p"; then
echo ""
say_warn "服务已在运行 → http://127.0.0.1:$p"
if [ "$SKIP_BROWSER" = "1" ]; then
say_info "SKIP_BROWSER=1,跳过打开浏览器"
else
open_browser "http://127.0.0.1:$p" || say_info "请手动在浏览器打开上面地址"
fi
exit 0
fi
done
fi
# 4) 选择空闲端口并启动
PORT="" SPID=""
mkdir -p "$APP_DIR/data"
: > "$LOG_FILE"
for p in $(seq 8090 8099); do
port_in_use "$p" && continue
if [ -f "$APP_DIR/router.php" ]; then
"$PHP_BIN" -S 127.0.0.1:"$p" -t "$APP_DIR" "$APP_DIR/router.php" >>"$LOG_FILE" 2>&1 &
else
"$PHP_BIN" -S 127.0.0.1:"$p" -t "$APP_DIR" >>"$LOG_FILE" 2>&1 &
fi
SPID=$!
sleep 0.7
if kill -0 "$SPID" 2>/dev/null; then PORT="$p"; break; fi
wait "$SPID" 2>/dev/null # 绑定失败,尝试下一个端口
done
if [ -z "$PORT" ]; then
say_err "8090~8099 端口均被占用,无法启动"
tail -5 "$LOG_FILE" 2>/dev/null
exit 1
fi
echo "$SPID" > "$PID_FILE"
# 5) 等待服务就绪
READY=0
for _ in $(seq 1 25); do
port_in_use "$PORT" && { READY=1; break; }
sleep 0.2
done
URL="http://127.0.0.1:$PORT"
if [ "$READY" != "1" ]; then
say_err "服务启动失败,日志如下:"
tail -10 "$LOG_FILE"
kill "$SPID" 2>/dev/null; rm -f "$PID_FILE"
exit 1
fi
# Ctrl+C / 退出时自动清理服务进程
trap 'kill "$SPID" 2>/dev/null; rm -f "$PID_FILE"' EXIT INT TERM
echo ""
say_ok "服务已启动: ${C_G}$URL${C_0} (PID $SPID)"
say_info "停止方式: 按 Ctrl+C,或执行 ./start.sh stop"
echo ""
# 6) 自动打开浏览器
if [ "$SKIP_BROWSER" != "1" ]; then
if open_browser "$URL"; then
say_ok "正在打开浏览器……"
else
say_warn "未能自动打开浏览器,请手动访问: $URL"
fi
fi
# 前台驻留(保持窗口开启 = 服务运行中)
wait "$SPID" 2>/dev/null