import os import time import json import argparse from typing import List from vs_types import GroupConfig from gco_wrapper import GCOWrapper from logger_setup import setup_app_logger from booker_builtin import BuiltinBookerGCO from booker_order import OrderBookerGCO from toolkit.vs_cloud_api import VSCloudApi def load_config(path): with open(path, "r", encoding="utf-8") as f: return json.load(f) def save_config(path: str, new_config): new_config_str = json.dumps(new_config, indent=2) with open(path, "w", encoding="utf-8") as f: f.write(new_config_str) def main(): # ===== 1️⃣ 命令行参数 ===== parser = argparse.ArgumentParser(description="Booker Runner") parser.add_argument( "-c", "--config", type=str, required=False, default="config/config_booker.json", help="Path to booker config.json" ) args = parser.parse_args() config_path = args.config CONF_NAME = 'COORDINATOR_BOOKER' # ===== 2️⃣ 日志 ===== app_logger = setup_app_logger("Booker") app_logger.info("Booker Logger is ready!") # ===== 3️⃣ 加载配置 ===== cfg_data = load_config(config_path) current_version = cfg_data.get('version', 0) redis_conf = cfg_data.get('redis') groups_conf = cfg_data.get('group_list', []) wrappers: List[GCOWrapper] = [] # ===== 4️⃣ 启动逻辑 ===== for item in groups_conf: cfg = GroupConfig.from_json(item) if not cfg.enable: continue if cfg.booker.account_source == "order": gco_class = OrderBookerGCO app_logger.info(f"[{cfg.identifier}] Mode: ORDER (Bound)") else: gco_class = BuiltinBookerGCO app_logger.info(f"[{cfg.identifier}] Mode: BUILT-IN (Unbound)") wrapper = GCOWrapper( gco_class=gco_class, gco_cfg=cfg, redis_conf=redis_conf ) wrapper.load() wrapper.start() wrappers.append(wrapper) app_logger.info(f"Started {len(wrappers)} Booker groups. Press Ctrl+C to stop.") # ===== 5️⃣ 保持运行 & 热更新监听 ===== try: while True: time.sleep(30) # 每 3 秒检查一次文件状态 if not os.path.exists(config_path): continue try: new_cfg_data = VSCloudApi.Instance().get_dynamic_config(config_name=CONF_NAME) new_version = new_cfg_data.get('version') # 如果 API 返回的没有 version 字段,或者版本号没变,则跳过 if new_version == current_version: continue app_logger.info("Config file modification detected! Reloading Booker configurations...") save_config(config_path, new_cfg_data) current_version = new_version redis_conf = new_cfg_data.get('redis') new_groups_conf = new_cfg_data.get('group_list', []) # 转换新配置为字典格式 {identifier: config_dict} new_cfg_dict = { item.get("identifier"): item for item in new_groups_conf if item.get("identifier") } # ---------------- A. 处理【参数更新】、【删除/禁用】、【模式切换】 ---------------- surviving_wrappers = [] for wrapper in wrappers: current_id = wrapper.m_cfg.identifier if current_id in new_cfg_dict: new_group_cfg = GroupConfig.from_json(new_cfg_dict[current_id]) # 1. 后端禁用了该组 if not new_group_cfg.enable: app_logger.info(f"Group [{current_id}] disabled by backend. Stopping...") wrapper.stop() # 2. 账号来源模式发生了改变 (例如 built-in 变 order) -> 需要更换底层类,必须重启该组 elif wrapper.m_cfg.booker.account_source != new_group_cfg.booker.account_source: app_logger.info(f"Group [{current_id}] Mode changed ({wrapper.m_cfg.booker.account_source} -> {new_group_cfg.booker.account_source}). Stopping old instance to recreate...") wrapper.stop() # 不加入 surviving_wrappers,让下面的步骤 B 重新创建它 # 3. 模式没变,依然启用 -> 热更新参数 else: wrapper.update_config(new_group_cfg) surviving_wrappers.append(wrapper) else: # 4. 该组被从 JSON 中删除了 app_logger.info(f"Group [{current_id}] deleted from config. Stopping...") wrapper.stop() wrappers = surviving_wrappers # ---------------- B. 处理【新增组】、【重新启用组】、【模式切换后的重建】 ---------------- existing_ids = {w.m_cfg.identifier for w in wrappers} for new_id, item_data in new_cfg_dict.items(): if new_id not in existing_ids: cfg = GroupConfig.from_json(item_data) if not cfg.enable: continue # 判断采用哪个底层类 if cfg.booker.account_source == "order": gco_class = OrderBookerGCO mode_str = "ORDER (Bound)" else: gco_class = BuiltinBookerGCO mode_str = "BUILT-IN (Unbound)" app_logger.info(f"Dynamically starting NEW/RECREATED Booker group [{cfg.identifier}] Mode: {mode_str}...") new_wrapper = GCOWrapper( gco_class=gco_class, gco_cfg=cfg, redis_conf=redis_conf ) try: new_wrapper.load() new_wrapper.start() wrappers.append(new_wrapper) except Exception as e: app_logger.error(f"Failed to dynamically start Booker group [{cfg.identifier}]: {e}") except json.JSONDecodeError: app_logger.warning("Config file is currently invalid JSON (maybe still writing), skipping this reload.") except Exception as e: app_logger.error(f"Error while hot-reloading config: {e}") except KeyboardInterrupt: app_logger.info("Shutting down Bookers...") for wrapper in wrappers: wrapper.stop() if __name__ == "__main__": main()