feat: 添加 Windows 本地开发服务器控制脚本

- 新增 `serve.py` 脚本,支持启动、停止和查看开发服务器状态
- 更新 README,提供 Windows 用户使用开发服务器的说明
- 脚本检查项目文件完整性,并处理端口占用情况
This commit is contained in:
eddy
2026-08-01 14:52:42 +08:00
parent dadffae5cf
commit 9a20add58a
2 changed files with 202 additions and 0 deletions
+25
View File
@@ -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
+177
View File
@@ -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()