From 9a20add58a3a402340f132f45c485ab393c4d576 Mon Sep 17 00:00:00 2001 From: eddy Date: Sat, 1 Aug 2026 02:31:20 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20Windows=20?= =?UTF-8?q?=E6=9C=AC=E5=9C=B0=E5=BC=80=E5=8F=91=E6=9C=8D=E5=8A=A1=E5=99=A8?= =?UTF-8?q?=E6=8E=A7=E5=88=B6=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 `serve.py` 脚本,支持启动、停止和查看开发服务器状态 - 更新 README,提供 Windows 用户使用开发服务器的说明 - 脚本检查项目文件完整性,并处理端口占用情况 --- README.md | 25 ++++++++ serve.py | 177 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 202 insertions(+) create mode 100644 serve.py diff --git a/README.md b/README.md index a033268..7f02f12 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/serve.py b/serve.py new file mode 100644 index 0000000..611d63c --- /dev/null +++ b/serve.py @@ -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()