#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 身体平衡评估系统 - 主启动脚本 这个脚本负责启动整个应用程序,包括后端服务和前端界面。 支持开发模式和生产模式。 """ import os import sys import time import signal import subprocess import threading import webbrowser import logging from pathlib import Path # 添加项目根目录到Python路径 project_root = Path(__file__).parent.parent sys.path.insert(0, str(project_root)) sys.path.insert(0, str(project_root / 'backend')) from utils import Config, Logger class ApplicationLauncher: """应用程序启动器""" def __init__(self): self.config = Config() # 设置日志 Logger.setup_logging('INFO', 'logs/app.log') self.logger = logging.getLogger('main') self.backend_process = None self.frontend_process = None self.running = False # 设置信号处理 signal.signal(signal.SIGINT, self._signal_handler) signal.signal(signal.SIGTERM, self._signal_handler) def _signal_handler(self, signum, frame): """信号处理器""" self.logger.info(f"接收到信号 {signum},正在关闭应用程序...") self.stop() def check_dependencies(self): """检查依赖项""" self.logger.info("检查系统依赖项...") # 检查Python版本 if sys.version_info < (3, 8): self.logger.error("需要Python 3.8或更高版本") return False # 检查必要的Python包 required_packages = [ 'flask', 'flask_cors', 'flask_socketio', 'numpy', 'pandas', 'opencv-python', 'sqlite3' ] missing_packages = [] for package in required_packages: try: if package == 'sqlite3': import sqlite3 elif package == 'opencv-python': import cv2 elif package == 'flask_cors': import flask_cors elif package == 'flask_socketio': import flask_socketio else: __import__(package) except ImportError: missing_packages.append(package) if missing_packages: self.logger.error(f"缺少必要的Python包: {', '.join(missing_packages)}") self.logger.info("请运行: pip install -r backend/requirements.txt") return False # 检查Node.js和npm(用于前端开发) if self.config.get('APP', 'mode', 'development') == 'development': try: result = subprocess.run(['node', '--version'], capture_output=True, text=True) if result.returncode != 0: self.logger.warning("未找到Node.js,将跳过前端开发服务器") else: self.logger.info(f"Node.js版本: {result.stdout.strip()}") except FileNotFoundError: self.logger.warning("未找到Node.js,将跳过前端开发服务器") self.logger.info("依赖项检查完成") return True def setup_directories(self): """设置必要的目录""" self.logger.info("设置应用程序目录...") directories = [ 'data', 'data/patients', 'data/sessions', 'data/exports', 'data/backups', 'logs', 'temp' ] for directory in directories: dir_path = project_root / directory dir_path.mkdir(parents=True, exist_ok=True) self.logger.debug(f"创建目录: {dir_path}") self.logger.info("目录设置完成") def start_backend(self): """启动后端服务""" self.logger.info("启动后端服务...") backend_script = project_root / 'backend' / 'app.py' if not backend_script.exists(): self.logger.error(f"后端脚本不存在: {backend_script}") return False try: # 设置环境变量 env = os.environ.copy() env['PYTHONPATH'] = str(project_root) env['FLASK_APP'] = str(backend_script) if self.config.get('APP', 'mode', 'development') == 'development': env['FLASK_ENV'] = 'development' env['FLASK_DEBUG'] = '1' else: env['FLASK_ENV'] = 'production' env['FLASK_DEBUG'] = '0' # 启动后端进程 cmd = [ sys.executable, str(backend_script), '--host', self.config.get('SERVER', 'host', '127.0.0.1'), '--port', self.config.get('SERVER', 'port', '5000') ] self.backend_process = subprocess.Popen( cmd, env=env, cwd=str(project_root), stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True ) self.logger.info(f"后端服务已启动 (PID: {self.backend_process.pid})") # 等待后端服务启动 self._wait_for_backend() return True except Exception as e: self.logger.error(f"启动后端服务失败: {e}") return False def _wait_for_backend(self, timeout=30): """等待后端服务启动""" import requests backend_url = f"http://{self.config.get('SERVER', 'host', '127.0.0.1')}:{self.config.get('SERVER', 'port', '5000')}" health_url = f"{backend_url}/api/health" self.logger.info("等待后端服务启动...") start_time = time.time() while time.time() - start_time < timeout: try: response = requests.get(health_url, timeout=2) if response.status_code == 200: self.logger.info("后端服务已就绪") return True except requests.exceptions.RequestException: pass time.sleep(1) self.logger.warning("后端服务启动超时") return False def start_frontend_dev(self): """启动前端开发服务器""" if self.config.get('APP', 'mode', 'development') != 'development': return True self.logger.info("启动前端开发服务器...") frontend_dir = project_root / 'frontend' / 'src' / 'renderer' if not frontend_dir.exists(): self.logger.warning("前端目录不存在,跳过前端开发服务器") return True package_json = frontend_dir / 'package.json' if not package_json.exists(): self.logger.warning("package.json不存在,跳过前端开发服务器") return True try: # 检查是否已安装依赖 node_modules = frontend_dir / 'node_modules' if not node_modules.exists(): self.logger.info("安装前端依赖...") install_process = subprocess.run( ['npm', 'install'], cwd=str(frontend_dir), capture_output=True, text=True, shell=True ) if install_process.returncode != 0: self.logger.error(f"安装前端依赖失败: {install_process.stderr}") return False # 启动开发服务器 self.frontend_process = subprocess.Popen( ['npm', 'run', 'dev'], cwd=str(frontend_dir), stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, shell=True ) self.logger.info(f"前端开发服务器已启动 (PID: {self.frontend_process.pid})") return True except Exception as e: self.logger.error(f"启动前端开发服务器失败: {e}") return False def open_browser(self): """打开浏览器""" if not self.config.getboolean('APP', 'auto_open_browser', True): return if self.config.get('APP', 'mode', 'development') == 'development': # 开发模式下打开前端开发服务器 url = "http://localhost:3000" # Vite配置端口 else: # 生产模式下打开后端服务 url = f"http://{self.config.get('SERVER', 'host', '127.0.0.1')}:{self.config.get('SERVER', 'port', '5000')}" def delayed_open(): time.sleep(3) # 等待服务启动 try: webbrowser.open(url) self.logger.info(f"已打开浏览器: {url}") except Exception as e: self.logger.warning(f"打开浏览器失败: {e}") threading.Thread(target=delayed_open, daemon=True).start() def monitor_processes(self): """监控子进程""" while self.running: try: # 检查后端进程 if self.backend_process and self.backend_process.poll() is not None: self.logger.error("后端进程意外退出") if self.backend_process.returncode != 0: stderr = self.backend_process.stderr.read() if stderr: self.logger.error(f"后端错误: {stderr}") self.running = False break # 检查前端进程 if (self.frontend_process and self.frontend_process.poll() is not None and self.config.get('APP', 'mode', 'development') == 'development'): self.logger.warning("前端开发服务器意外退出") time.sleep(5) except Exception as e: self.logger.error(f"进程监控错误: {e}") break def start(self): """启动应用程序""" self.logger.info("=" * 50) self.logger.info("身体平衡评估系统启动中...") self.logger.info(f"模式: {self.config.get('APP', 'mode', 'development')}") self.logger.info("=" * 50) # 检查依赖项 if not self.check_dependencies(): return False # 设置目录 self.setup_directories() # 启动后端服务 if not self.start_backend(): return False # 启动前端开发服务器(仅开发模式) if not self.start_frontend_dev(): self.logger.warning("前端开发服务器启动失败,但继续运行") self.running = True # 打开浏览器 self.open_browser() # 启动进程监控 monitor_thread = threading.Thread(target=self.monitor_processes, daemon=True) monitor_thread.start() self.logger.info("应用程序启动完成") self.logger.info(f"后端服务: http://{self.config.get('SERVER', 'host', '127.0.0.1')}:{self.config.get('SERVER', 'port', '5000')}") if self.config.get('APP', 'mode', 'development') == 'development': self.logger.info("前端开发服务器: http://localhost:3000") self.logger.info("按 Ctrl+C 退出应用程序") # 主循环 try: while self.running: time.sleep(1) except KeyboardInterrupt: self.logger.info("接收到中断信号") self.stop() return True def stop(self): """停止应用程序""" if not self.running: return self.logger.info("正在停止应用程序...") self.running = False # 停止前端进程 if self.frontend_process: try: self.frontend_process.terminate() self.frontend_process.wait(timeout=5) self.logger.info("前端开发服务器已停止") except subprocess.TimeoutExpired: self.frontend_process.kill() self.logger.warning("强制终止前端开发服务器") except Exception as e: self.logger.error(f"停止前端服务器失败: {e}") # 停止后端进程 if self.backend_process: try: self.backend_process.terminate() self.backend_process.wait(timeout=10) self.logger.info("后端服务已停止") except subprocess.TimeoutExpired: self.backend_process.kill() self.logger.warning("强制终止后端服务") except Exception as e: self.logger.error(f"停止后端服务失败: {e}") self.logger.info("应用程序已停止") def main(): """主函数""" import argparse parser = argparse.ArgumentParser(description='身体平衡评估系统') parser.add_argument('--mode', choices=['development', 'production'], default='development', help='运行模式') parser.add_argument('--host', default='127.0.0.1', help='服务器主机') parser.add_argument('--port', type=int, default=5000, help='服务器端口') parser.add_argument('--no-browser', action='store_true', help='不自动打开浏览器') parser.add_argument('--log-level', choices=['DEBUG', 'INFO', 'WARNING', 'ERROR'], default='INFO', help='日志级别') args = parser.parse_args() # 更新配置 config = Config() config.set('APP', 'mode', args.mode) config.set('APP', 'auto_open_browser', str(not args.no_browser)) config.set('SERVER', 'host', args.host) config.set('SERVER', 'port', str(args.port)) # 设置日志级别 import logging logging.getLogger().setLevel(getattr(logging, args.log_level)) # 启动应用程序 launcher = ApplicationLauncher() try: success = launcher.start() sys.exit(0 if success else 1) except Exception as e: print(f"启动失败: {e}") sys.exit(1) if __name__ == '__main__': main()