298 lines
8.6 KiB
Python
298 lines
8.6 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
PANJIT Document Translator - 統一啟動入口
|
||
適用於 1Panel 環境部署
|
||
|
||
此腳本會:
|
||
1. 啟動 Flask Web 服務
|
||
2. 啟動 Celery Worker(翻譯任務處理)
|
||
3. 啟動 Celery Beat(定時任務)
|
||
|
||
Author: PANJIT IT Team
|
||
Created: 2025-10-03
|
||
"""
|
||
|
||
import os
|
||
import sys
|
||
import signal
|
||
import subprocess
|
||
import time
|
||
from pathlib import Path
|
||
from multiprocessing import Process
|
||
|
||
# 添加專案根目錄到 Python 路徑
|
||
project_root = Path(__file__).parent
|
||
sys.path.insert(0, str(project_root))
|
||
|
||
# ANSI 顏色碼
|
||
class Colors:
|
||
BLUE = '\033[94m'
|
||
GREEN = '\033[92m'
|
||
YELLOW = '\033[93m'
|
||
RED = '\033[91m'
|
||
ENDC = '\033[0m'
|
||
BOLD = '\033[1m'
|
||
|
||
# 全域進程列表
|
||
processes = []
|
||
|
||
def log_info(message):
|
||
"""顯示資訊日誌"""
|
||
print(f"{Colors.BLUE}[INFO]{Colors.ENDC} {message}")
|
||
|
||
def log_success(message):
|
||
"""顯示成功日誌"""
|
||
print(f"{Colors.GREEN}[SUCCESS]{Colors.ENDC} {message}")
|
||
|
||
def log_warning(message):
|
||
"""顯示警告日誌"""
|
||
print(f"{Colors.YELLOW}[WARNING]{Colors.ENDC} {message}")
|
||
|
||
def log_error(message):
|
||
"""顯示錯誤日誌"""
|
||
print(f"{Colors.RED}[ERROR]{Colors.ENDC} {message}")
|
||
|
||
def show_banner():
|
||
"""顯示啟動橫幅"""
|
||
print(f"""
|
||
{Colors.BOLD}╔═══════════════════════════════════════════════════════════╗
|
||
║ PANJIT Document Translator V2 ║
|
||
║ 正在啟動服務... ║
|
||
╚═══════════════════════════════════════════════════════════╝{Colors.ENDC}
|
||
""")
|
||
|
||
def check_environment():
|
||
"""檢查環境配置"""
|
||
log_info("檢查環境配置...")
|
||
|
||
# 檢查 Python 版本
|
||
if sys.version_info < (3, 10):
|
||
log_error(f"Python 版本過低: {sys.version}")
|
||
log_error("需要 Python 3.10 或更高版本")
|
||
sys.exit(1)
|
||
|
||
log_success(f"Python 版本: {sys.version.split()[0]} ✓")
|
||
|
||
# 檢查必要檔案
|
||
required_files = ['app.py', 'celery_app.py', 'requirements.txt']
|
||
for file in required_files:
|
||
if not Path(file).exists():
|
||
log_error(f"找不到必要檔案: {file}")
|
||
sys.exit(1)
|
||
|
||
log_success("必要檔案檢查完成 ✓")
|
||
|
||
# 檢查環境變數
|
||
if not Path('.env').exists():
|
||
log_warning("找不到 .env 檔案,將使用預設配置")
|
||
else:
|
||
log_success("找到 .env 配置檔案 ✓")
|
||
|
||
# 檢查必要目錄
|
||
directories = ['uploads', 'logs', 'static']
|
||
for directory in directories:
|
||
Path(directory).mkdir(parents=True, exist_ok=True)
|
||
|
||
log_success("目錄結構檢查完成 ✓")
|
||
print()
|
||
|
||
def start_flask_app():
|
||
"""啟動 Flask Web 服務"""
|
||
log_info("啟動 Flask Web 服務...")
|
||
|
||
# 從環境變數讀取配置
|
||
host = os.environ.get('HOST', '0.0.0.0')
|
||
port = int(os.environ.get('PORT', 12010))
|
||
|
||
# 使用 gunicorn 啟動(生產環境)
|
||
if os.environ.get('FLASK_ENV') == 'production':
|
||
workers = int(os.environ.get('GUNICORN_WORKERS', 4))
|
||
cmd = [
|
||
'gunicorn',
|
||
'--bind', f'{host}:{port}',
|
||
'--workers', str(workers),
|
||
'--timeout', '300',
|
||
'--access-logfile', 'logs/access.log',
|
||
'--error-logfile', 'logs/error.log',
|
||
'--log-level', 'info',
|
||
'app:app' # 直接使用 app.py 中的 app 物件
|
||
]
|
||
else:
|
||
# 開發環境使用 Flask 內建伺服器
|
||
cmd = ['python3', 'app.py']
|
||
|
||
try:
|
||
process = subprocess.Popen(
|
||
cmd,
|
||
stdout=subprocess.PIPE,
|
||
stderr=subprocess.STDOUT,
|
||
universal_newlines=True,
|
||
bufsize=1
|
||
)
|
||
processes.append(('Flask App', process))
|
||
log_success(f"Flask 服務已啟動 (PID: {process.pid}) ✓")
|
||
log_info(f"服務地址: http://{host}:{port}")
|
||
return process
|
||
except Exception as e:
|
||
log_error(f"Flask 服務啟動失敗: {e}")
|
||
sys.exit(1)
|
||
|
||
def start_celery_worker():
|
||
"""啟動 Celery Worker"""
|
||
log_info("啟動 Celery Worker...")
|
||
|
||
# 檢查 Redis 連線
|
||
redis_url = os.environ.get('REDIS_URL', 'redis://localhost:6379/0')
|
||
log_info(f"Redis URL: {redis_url}")
|
||
|
||
# Celery Worker 命令
|
||
cmd = [
|
||
'celery',
|
||
'-A', 'celery_app.celery',
|
||
'worker',
|
||
'--loglevel=info',
|
||
'--concurrency=2',
|
||
'--logfile=logs/celery_worker.log'
|
||
]
|
||
|
||
try:
|
||
process = subprocess.Popen(
|
||
cmd,
|
||
stdout=subprocess.PIPE,
|
||
stderr=subprocess.STDOUT,
|
||
universal_newlines=True,
|
||
bufsize=1
|
||
)
|
||
processes.append(('Celery Worker', process))
|
||
log_success(f"Celery Worker 已啟動 (PID: {process.pid}) ✓")
|
||
return process
|
||
except Exception as e:
|
||
log_error(f"Celery Worker 啟動失敗: {e}")
|
||
log_warning("如果沒有 Redis 服務,翻譯功能將無法使用")
|
||
return None
|
||
|
||
def start_celery_beat():
|
||
"""啟動 Celery Beat(定時任務)"""
|
||
log_info("啟動 Celery Beat...")
|
||
|
||
cmd = [
|
||
'celery',
|
||
'-A', 'celery_app.celery',
|
||
'beat',
|
||
'--loglevel=info',
|
||
'--logfile=logs/celery_beat.log'
|
||
]
|
||
|
||
try:
|
||
process = subprocess.Popen(
|
||
cmd,
|
||
stdout=subprocess.PIPE,
|
||
stderr=subprocess.STDOUT,
|
||
universal_newlines=True,
|
||
bufsize=1
|
||
)
|
||
processes.append(('Celery Beat', process))
|
||
log_success(f"Celery Beat 已啟動 (PID: {process.pid}) ✓")
|
||
return process
|
||
except Exception as e:
|
||
log_error(f"Celery Beat 啟動失敗: {e}")
|
||
log_warning("定時任務將無法執行")
|
||
return None
|
||
|
||
def signal_handler(signum, frame):
|
||
"""處理終止信號"""
|
||
print()
|
||
log_warning("收到終止信號,正在關閉所有服務...")
|
||
|
||
for name, process in processes:
|
||
if process and process.poll() is None:
|
||
log_info(f"停止 {name} (PID: {process.pid})...")
|
||
try:
|
||
process.terminate()
|
||
process.wait(timeout=5)
|
||
log_success(f"{name} 已停止 ✓")
|
||
except subprocess.TimeoutExpired:
|
||
log_warning(f"{name} 未響應,強制終止...")
|
||
process.kill()
|
||
log_success(f"{name} 已強制終止 ✓")
|
||
except Exception as e:
|
||
log_error(f"停止 {name} 時發生錯誤: {e}")
|
||
|
||
log_success("\n所有服務已停止")
|
||
sys.exit(0)
|
||
|
||
def monitor_processes():
|
||
"""監控進程狀態"""
|
||
log_info("開始監控服務狀態...")
|
||
print()
|
||
print("=" * 60)
|
||
print(f"{Colors.BOLD}服務狀態:{Colors.ENDC}")
|
||
for name, process in processes:
|
||
if process:
|
||
status = "運行中 ✓" if process.poll() is None else "已停止 ✗"
|
||
print(f" • {name:20s} PID: {process.pid:6d} {status}")
|
||
print("=" * 60)
|
||
print()
|
||
log_success("所有服務已啟動完成!")
|
||
print()
|
||
log_info("按 Ctrl+C 停止所有服務")
|
||
print()
|
||
|
||
try:
|
||
while True:
|
||
time.sleep(5)
|
||
|
||
# 檢查進程是否異常退出
|
||
for name, process in processes:
|
||
if process and process.poll() is not None:
|
||
log_error(f"{name} 異常退出 (退出碼: {process.returncode})")
|
||
log_warning("正在停止其他服務...")
|
||
signal_handler(signal.SIGTERM, None)
|
||
sys.exit(1)
|
||
|
||
except KeyboardInterrupt:
|
||
signal_handler(signal.SIGINT, None)
|
||
|
||
def main():
|
||
"""主函數"""
|
||
# 註冊信號處理
|
||
signal.signal(signal.SIGINT, signal_handler)
|
||
signal.signal(signal.SIGTERM, signal_handler)
|
||
|
||
# 顯示橫幅
|
||
show_banner()
|
||
|
||
# 檢查環境
|
||
check_environment()
|
||
|
||
# 啟動服務
|
||
log_info("正在啟動所有服務...")
|
||
print()
|
||
|
||
# 1. 啟動 Flask Web 服務
|
||
flask_process = start_flask_app()
|
||
time.sleep(2) # 等待 Flask 啟動
|
||
|
||
# 2. 啟動 Celery Worker
|
||
worker_process = start_celery_worker()
|
||
time.sleep(2) # 等待 Worker 啟動
|
||
|
||
# 3. 啟動 Celery Beat
|
||
beat_process = start_celery_beat()
|
||
time.sleep(2) # 等待 Beat 啟動
|
||
|
||
print()
|
||
|
||
# 監控進程
|
||
monitor_processes()
|
||
|
||
if __name__ == '__main__':
|
||
try:
|
||
main()
|
||
except Exception as e:
|
||
log_error(f"啟動失敗: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
sys.exit(1)
|