feat: 添加 Windows 本地开发服务器控制脚本
- 新增 `serve.py` 脚本,支持启动、停止和查看开发服务器状态 - 更新 README,提供 Windows 用户使用开发服务器的说明 - 脚本检查项目文件完整性,并处理端口占用情况
This commit is contained in:
@@ -49,6 +49,31 @@ npm start
|
||||
|
||||
打开 `serve` 输出的地址即可使用。由于应用采用原生 ES Modules,必须通过 HTTP 服务器访问,不支持使用 `file://` 直接打开 `src/index.html`。
|
||||
|
||||
#### Windows 开发服务器脚本
|
||||
|
||||
Windows 用户可以使用项目根目录下的 `serve.py` 控制本地开发服务器。服务器默认在 `http://localhost:8000/` 提供 `src` 中的静态文件。
|
||||
|
||||
```bash
|
||||
# 启动服务器
|
||||
python serve.py start
|
||||
|
||||
# 查看运行状态
|
||||
python serve.py status
|
||||
|
||||
# 停止服务器
|
||||
python serve.py stop
|
||||
```
|
||||
|
||||
如需使用其他端口,通过 `--port` 为每条命令指定相同端口:
|
||||
|
||||
```bash
|
||||
python serve.py start --port 8080
|
||||
python serve.py status --port 8080
|
||||
python serve.py stop --port 8080
|
||||
```
|
||||
|
||||
启动时,脚本会检查 `src/index.html`、`src/css` 和 `src/js` 是否存在。如果端口已被其他程序占用,脚本会显示占用进程,并在启动操作中询问是否将其关闭。
|
||||
|
||||
也可以使用其他静态服务器:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
"""Tasks 项目的 Windows 本地开发服务器控制脚本。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
SOURCE_DIR = ROOT / "src"
|
||||
DEFAULT_PORT = 8000
|
||||
HOST = "127.0.0.1"
|
||||
REQUIRED_PATHS = (
|
||||
SOURCE_DIR / "index.html",
|
||||
SOURCE_DIR / "css",
|
||||
SOURCE_DIR / "js",
|
||||
)
|
||||
|
||||
|
||||
def listener_pid(port: int) -> int | None:
|
||||
command = (
|
||||
f"Get-NetTCPConnection -LocalPort {port} -State Listen "
|
||||
"-ErrorAction SilentlyContinue | "
|
||||
"Select-Object -First 1 -ExpandProperty OwningProcess"
|
||||
)
|
||||
result = subprocess.run(
|
||||
["powershell", "-NoProfile", "-Command", command],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
try:
|
||||
return int(result.stdout.strip())
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def command_line(pid: int) -> str:
|
||||
command = f"(Get-CimInstance Win32_Process -Filter 'ProcessId = {pid}').CommandLine"
|
||||
result = subprocess.run(
|
||||
["powershell", "-NoProfile", "-Command", command],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
def is_tasks_server(pid: int, port: int) -> bool:
|
||||
command = command_line(pid).lower()
|
||||
return (
|
||||
"-m http.server" in command
|
||||
and str(SOURCE_DIR).lower() in command
|
||||
and str(port) in command
|
||||
)
|
||||
|
||||
|
||||
def terminate_process(pid: int) -> bool:
|
||||
result = subprocess.run(
|
||||
["taskkill", "/PID", str(pid), "/T", "/F"],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
check=False,
|
||||
)
|
||||
return result.returncode == 0
|
||||
|
||||
|
||||
def validate_project() -> bool:
|
||||
missing = [path.relative_to(ROOT) for path in REQUIRED_PATHS if not path.exists()]
|
||||
if not missing:
|
||||
return True
|
||||
|
||||
print("Tasks 项目文件不完整,缺少:")
|
||||
for path in missing:
|
||||
print(f" - {path}")
|
||||
return False
|
||||
|
||||
|
||||
def server_url(port: int) -> str:
|
||||
return f"http://localhost:{port}/"
|
||||
|
||||
|
||||
def start(port: int) -> None:
|
||||
if not validate_project():
|
||||
return
|
||||
|
||||
listener = listener_pid(port)
|
||||
if listener:
|
||||
if is_tasks_server(listener, port):
|
||||
print(f"Tasks 开发服务器已运行:{server_url(port)}(PID {listener})")
|
||||
return
|
||||
|
||||
answer = input(
|
||||
f"端口 {port} 已被其他程序使用(PID {listener})。"
|
||||
"是否关闭该进程并强制启动?[y/N]: "
|
||||
).strip().lower()
|
||||
if answer not in ("y", "yes"):
|
||||
print("已取消启动。")
|
||||
return
|
||||
if not terminate_process(listener):
|
||||
print(f"无法关闭 PID {listener},请以管理员身份运行终端后重试。")
|
||||
return
|
||||
time.sleep(0.3)
|
||||
if listener_pid(port):
|
||||
print(f"端口 {port} 仍被占用,无法启动服务。")
|
||||
return
|
||||
print(f"已关闭 PID {listener},正在启动 Tasks 开发服务器。")
|
||||
|
||||
process = subprocess.Popen(
|
||||
[
|
||||
sys.executable,
|
||||
"-m",
|
||||
"http.server",
|
||||
str(port),
|
||||
"--bind",
|
||||
HOST,
|
||||
"--directory",
|
||||
str(SOURCE_DIR),
|
||||
],
|
||||
cwd=ROOT,
|
||||
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.DETACHED_PROCESS,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
time.sleep(0.5)
|
||||
if listener_pid(port) == process.pid:
|
||||
print(f"Tasks 开发服务器已启动:{server_url(port)}(PID {process.pid})")
|
||||
else:
|
||||
print("服务启动失败,请确认 Python 可用且端口未被占用。")
|
||||
|
||||
|
||||
def stop(port: int) -> None:
|
||||
listener = listener_pid(port)
|
||||
if not listener or not is_tasks_server(listener, port):
|
||||
print(f"端口 {port} 上没有运行 Tasks 开发服务器。")
|
||||
return
|
||||
|
||||
if terminate_process(listener):
|
||||
print("Tasks 开发服务器已停止。")
|
||||
else:
|
||||
print(f"无法停止 PID {listener},请以管理员身份运行终端后重试。")
|
||||
|
||||
|
||||
def status(port: int) -> None:
|
||||
listener = listener_pid(port)
|
||||
if listener and is_tasks_server(listener, port):
|
||||
print(f"Tasks 开发服务器运行中:{server_url(port)}(PID {listener})")
|
||||
elif listener:
|
||||
print(f"端口 {port} 已被其他程序使用(PID {listener})。")
|
||||
else:
|
||||
print(f"端口 {port} 上没有运行 Tasks 开发服务器。")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="控制 Tasks 项目的本地开发服务器。")
|
||||
parser.add_argument(
|
||||
"command",
|
||||
choices=("start", "stop", "status"),
|
||||
help="启动、停止或查看服务器状态",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--port",
|
||||
type=int,
|
||||
default=DEFAULT_PORT,
|
||||
help=f"HTTP 端口(默认:{DEFAULT_PORT})",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
if not 1 <= args.port <= 65535:
|
||||
parser.error("端口必须在 1 到 65535 之间")
|
||||
|
||||
{"start": start, "stop": stop, "status": status}[args.command](args.port)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user