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
+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()