mirror of
https://github.com/Cccc-owo/CheckInApp.git
synced 2026-06-17 05:56:29 +00:00
refactor(structure): reorganize app layout
BREAKING CHANGE: root backend/frontend directories and old run/manage entrypoints were removed. Use apps/backend, apps/frontend, and python main.py commands instead.
This commit is contained in:
@@ -0,0 +1,293 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Cross-platform project manager for CheckIn App."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent
|
||||
APPS_DIR = REPO_ROOT / "apps"
|
||||
BACKEND_DIR = APPS_DIR / "backend"
|
||||
FRONTEND_DIR = APPS_DIR / "frontend"
|
||||
VENV_DIR = REPO_ROOT / "venv"
|
||||
PYTHON_BIN = VENV_DIR / ("Scripts/python.exe" if os.name == "nt" else "bin/python")
|
||||
BACKEND_PID = REPO_ROOT / "backend.pid"
|
||||
FRONTEND_PID = REPO_ROOT / "frontend.pid"
|
||||
LOGS_DIR = REPO_ROOT / "logs"
|
||||
BACKEND_LOG = LOGS_DIR / "backend.log"
|
||||
FRONTEND_LOG = LOGS_DIR / "frontend.log"
|
||||
|
||||
BACKEND_PORT = 8000
|
||||
FRONTEND_PORT = 3000
|
||||
|
||||
|
||||
def ensure_import_path() -> None:
|
||||
apps_path = str(APPS_DIR)
|
||||
if apps_path not in sys.path:
|
||||
sys.path.insert(0, apps_path)
|
||||
os.chdir(REPO_ROOT)
|
||||
|
||||
|
||||
def ensure_runtime_dirs() -> None:
|
||||
for path in (REPO_ROOT / "data", LOGS_DIR, REPO_ROOT / "sessions"):
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
def get_python() -> str:
|
||||
if PYTHON_BIN.exists():
|
||||
return str(PYTHON_BIN)
|
||||
return sys.executable
|
||||
|
||||
|
||||
def read_pid(path: Path) -> int | None:
|
||||
try:
|
||||
return int(path.read_text(encoding="utf-8").strip())
|
||||
except (FileNotFoundError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def is_process_alive(pid: int) -> bool:
|
||||
try:
|
||||
os.kill(pid, 0)
|
||||
except OSError:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def stop_pid_file(path: Path, name: str) -> bool:
|
||||
pid = read_pid(path)
|
||||
if pid is None:
|
||||
print(f"{name}: not running")
|
||||
return False
|
||||
if not is_process_alive(pid):
|
||||
path.unlink(missing_ok=True)
|
||||
print(f"{name}: stale pid removed")
|
||||
return False
|
||||
sig = signal.CTRL_BREAK_EVENT if os.name == "nt" else signal.SIGTERM
|
||||
os.kill(pid, sig)
|
||||
path.unlink(missing_ok=True)
|
||||
print(f"{name}: stopped pid {pid}")
|
||||
return True
|
||||
|
||||
|
||||
def backend_env() -> dict[str, str]:
|
||||
env = os.environ.copy()
|
||||
existing = env.get("PYTHONPATH")
|
||||
apps_path = str(APPS_DIR)
|
||||
env["PYTHONPATH"] = apps_path if not existing else os.pathsep.join([apps_path, existing])
|
||||
return env
|
||||
|
||||
|
||||
def run_backend(args: argparse.Namespace) -> int:
|
||||
ensure_import_path()
|
||||
ensure_runtime_dirs()
|
||||
if args.check:
|
||||
try:
|
||||
import backend.main # noqa: F401
|
||||
except ModuleNotFoundError as exc:
|
||||
print(f"backend import path OK; missing dependency: {exc.name}", file=sys.stderr)
|
||||
print(f"install dependencies with: {get_python()} -m pip install -r apps/backend/requirements.txt", file=sys.stderr)
|
||||
return 2
|
||||
print("backend.main:app import OK")
|
||||
return 0
|
||||
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run(
|
||||
"backend.main:app",
|
||||
host=args.host,
|
||||
port=args.port,
|
||||
reload=args.reload,
|
||||
reload_dirs=[str(BACKEND_DIR)] if args.reload else None,
|
||||
log_level=args.log_level,
|
||||
access_log=True,
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def start_backend_daemon(args: argparse.Namespace) -> int:
|
||||
ensure_runtime_dirs()
|
||||
if BACKEND_PID.exists():
|
||||
pid = read_pid(BACKEND_PID)
|
||||
if pid is not None and is_process_alive(pid):
|
||||
print(f"backend: already running pid {pid}")
|
||||
return 0
|
||||
BACKEND_PID.unlink(missing_ok=True)
|
||||
|
||||
cmd = [
|
||||
get_python(),
|
||||
str(REPO_ROOT / "main.py"),
|
||||
"backend",
|
||||
"--host",
|
||||
args.host,
|
||||
"--port",
|
||||
str(args.port),
|
||||
"--no-reload",
|
||||
"--log-level",
|
||||
args.log_level,
|
||||
]
|
||||
BACKEND_LOG.parent.mkdir(parents=True, exist_ok=True)
|
||||
log_file = BACKEND_LOG.open("a", encoding="utf-8")
|
||||
proc = subprocess.Popen(
|
||||
cmd,
|
||||
cwd=REPO_ROOT,
|
||||
env=backend_env(),
|
||||
stdout=log_file,
|
||||
stderr=subprocess.STDOUT,
|
||||
start_new_session=os.name != "nt",
|
||||
)
|
||||
BACKEND_PID.write_text(str(proc.pid), encoding="utf-8")
|
||||
print(f"backend: started pid {proc.pid}")
|
||||
print(f"log: {BACKEND_LOG}")
|
||||
return 0
|
||||
|
||||
|
||||
def frontend_env() -> dict[str, str]:
|
||||
return os.environ.copy()
|
||||
|
||||
|
||||
def run_frontend(args: argparse.Namespace) -> int:
|
||||
if not FRONTEND_DIR.exists():
|
||||
print(f"frontend directory not found: {FRONTEND_DIR}", file=sys.stderr)
|
||||
return 1
|
||||
cmd = ["npm", "run", "dev", "--", "--host", args.host, "--port", str(args.port)]
|
||||
return subprocess.call(cmd, cwd=FRONTEND_DIR, env=frontend_env())
|
||||
|
||||
|
||||
def start_frontend_daemon(args: argparse.Namespace) -> int:
|
||||
if FRONTEND_PID.exists():
|
||||
pid = read_pid(FRONTEND_PID)
|
||||
if pid is not None and is_process_alive(pid):
|
||||
print(f"frontend: already running pid {pid}")
|
||||
return 0
|
||||
FRONTEND_PID.unlink(missing_ok=True)
|
||||
|
||||
LOGS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
log_file = FRONTEND_LOG.open("a", encoding="utf-8")
|
||||
cmd = [get_python(), str(REPO_ROOT / "main.py"), "frontend", "--host", args.host, "--port", str(args.port)]
|
||||
proc = subprocess.Popen(
|
||||
cmd,
|
||||
cwd=REPO_ROOT,
|
||||
stdout=log_file,
|
||||
stderr=subprocess.STDOUT,
|
||||
start_new_session=os.name != "nt",
|
||||
)
|
||||
FRONTEND_PID.write_text(str(proc.pid), encoding="utf-8")
|
||||
print(f"frontend: started pid {proc.pid}")
|
||||
print(f"log: {FRONTEND_LOG}")
|
||||
return 0
|
||||
|
||||
|
||||
def build_frontend(args: argparse.Namespace) -> int:
|
||||
if not FRONTEND_DIR.exists():
|
||||
print(f"frontend directory not found: {FRONTEND_DIR}", file=sys.stderr)
|
||||
return 1
|
||||
if args.install and not (FRONTEND_DIR / "node_modules").exists():
|
||||
result = subprocess.call(["npm", "install"], cwd=FRONTEND_DIR)
|
||||
if result != 0:
|
||||
return result
|
||||
return subprocess.call(["npm", "run", "build"], cwd=FRONTEND_DIR)
|
||||
|
||||
|
||||
def deploy_frontend(args: argparse.Namespace) -> int:
|
||||
dist = FRONTEND_DIR / "dist"
|
||||
if not dist.exists():
|
||||
print(f"build output not found: {dist}", file=sys.stderr)
|
||||
print("run: python main.py frontend-build", file=sys.stderr)
|
||||
return 1
|
||||
print(f"build output ready: {dist}")
|
||||
print("copy this directory to the web server root configured by nginx")
|
||||
return 0
|
||||
|
||||
|
||||
def status(_: argparse.Namespace) -> int:
|
||||
for name, pid_file, log_file in (
|
||||
("backend", BACKEND_PID, BACKEND_LOG),
|
||||
("frontend", FRONTEND_PID, FRONTEND_LOG),
|
||||
):
|
||||
pid = read_pid(pid_file)
|
||||
running = pid is not None and is_process_alive(pid)
|
||||
state = "RUNNING" if running else "NOT RUNNING"
|
||||
print(f"{name}: {state}")
|
||||
if pid is not None:
|
||||
print(f" pid: {pid}")
|
||||
print(f" log: {log_file}")
|
||||
return 0
|
||||
|
||||
|
||||
def stop(args: argparse.Namespace) -> int:
|
||||
stopped = False
|
||||
targets = ("backend", "frontend") if args.target == "all" else (args.target,)
|
||||
for target in targets:
|
||||
if target == "backend":
|
||||
stopped = stop_pid_file(BACKEND_PID, "backend") or stopped
|
||||
elif target == "frontend":
|
||||
stopped = stop_pid_file(FRONTEND_PID, "frontend") or stopped
|
||||
return 0 if stopped or args.target in {"backend", "frontend", "all"} else 1
|
||||
|
||||
|
||||
def add_backend_args(parser: argparse.ArgumentParser) -> None:
|
||||
parser.add_argument("--host", default="0.0.0.0")
|
||||
parser.add_argument("--port", type=int, default=BACKEND_PORT)
|
||||
parser.add_argument("--reload", dest="reload", action="store_true", default=True)
|
||||
parser.add_argument("--no-reload", dest="reload", action="store_false")
|
||||
parser.add_argument("--log-level", default="info")
|
||||
parser.add_argument("--check", action="store_true", help="only verify backend.main:app imports")
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(description="CheckIn App project manager")
|
||||
sub = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
backend = sub.add_parser("backend", help="run backend in the foreground")
|
||||
add_backend_args(backend)
|
||||
backend.set_defaults(func=run_backend)
|
||||
|
||||
backend_daemon = sub.add_parser("backend-daemon", help="start backend in the background")
|
||||
backend_daemon.add_argument("--host", default="0.0.0.0")
|
||||
backend_daemon.add_argument("--port", type=int, default=BACKEND_PORT)
|
||||
backend_daemon.add_argument("--log-level", default="info")
|
||||
backend_daemon.set_defaults(func=start_backend_daemon)
|
||||
|
||||
frontend = sub.add_parser("frontend", help="run frontend dev server in the foreground")
|
||||
frontend.add_argument("--host", default="0.0.0.0")
|
||||
frontend.add_argument("--port", type=int, default=FRONTEND_PORT)
|
||||
frontend.set_defaults(func=run_frontend)
|
||||
|
||||
frontend_daemon = sub.add_parser("frontend-daemon", help="start frontend dev server in the background")
|
||||
frontend_daemon.add_argument("--host", default="0.0.0.0")
|
||||
frontend_daemon.add_argument("--port", type=int, default=FRONTEND_PORT)
|
||||
frontend_daemon.set_defaults(func=start_frontend_daemon)
|
||||
|
||||
frontend_build = sub.add_parser("frontend-build", help="build current frontend")
|
||||
frontend_build.add_argument("--install", action="store_true", help="run npm install if node_modules is missing")
|
||||
frontend_build.set_defaults(func=build_frontend)
|
||||
|
||||
deploy = sub.add_parser("frontend-deploy", help="show frontend deployment output path")
|
||||
deploy.set_defaults(func=deploy_frontend)
|
||||
|
||||
service_status = sub.add_parser("status", help="show managed process status")
|
||||
service_status.set_defaults(func=status)
|
||||
|
||||
service_stop = sub.add_parser("stop", help="stop managed daemon processes")
|
||||
service_stop.add_argument("target", choices=["backend", "frontend", "all"], nargs="?", default="all")
|
||||
service_stop.set_defaults(func=stop)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = build_parser()
|
||||
args = parser.parse_args()
|
||||
return args.func(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user