Skip to main content

futu_opend/startup/
phase4.rs

1//! v1.4.110 Layer 3 A: startup Phase 4 — surface server (WS/REST/gRPC/Telnet)
2//! spawn / card_num expand / SIGHUP unified handler / TCP fail-closed gate /
3//! 主 `tokio::select!` 循环 + 清理. 抽自原 `mod.rs::run_daemon` 533..1075 行段.
4//!
5//! Phase 4 取走 `Phase1Out` 所有权 (保证 `_audit_guard` 直到 fn 返回才 drop),
6//! 同时取 `bridge: Arc<GatewayBridge>` 与 `Phase3Out` 启动各 surface server.
7
8use anyhow::{Context, Result};
9use std::sync::Arc;
10
11use futu_gateway_core::bridge::GatewayBridge;
12use futu_server::ws_listener::{WsServer, WsServerDeps};
13
14use crate::config::RuntimeConfig;
15use crate::startup::phase1::Phase1Out;
16use crate::startup::phase3::Phase3Out;
17
18mod network_exposure;
19mod shutdown;
20mod summary;
21#[cfg(test)]
22mod tests;
23
24#[cfg(test)]
25use network_exposure::legacy_network_exposure_warnings;
26use network_exposure::warn_legacy_network_exposure;
27use shutdown::{
28    BackgroundJoinHandle, SHUTDOWN_GRACE_PERIOD, SurfaceJoinHandle, await_background_shutdown,
29    await_surface_shutdown, log_surface_task_result, request_surface_shutdown,
30    surface_task_result_or_pending,
31};
32#[cfg(test)]
33use summary::http_host_for_bind;
34use summary::{log_startup_summary, merge_push_health_snapshots_for_rest};
35
36async fn wait_for_sigterm() {
37    #[cfg(unix)]
38    {
39        use tokio::signal::unix::{SignalKind, signal};
40
41        let mut sigterm = match signal(SignalKind::terminate()) {
42            Ok(signal) => signal,
43            Err(error) => {
44                tracing::error!(
45                    error = %error,
46                    "failed to install SIGTERM handler; graceful SIGTERM shutdown unavailable"
47                );
48                std::future::pending::<()>().await;
49                return;
50            }
51        };
52        let _ = sigterm.recv().await;
53    }
54
55    #[cfg(not(unix))]
56    {
57        std::future::pending::<()>().await;
58    }
59}
60
61pub(super) async fn run_phase4(
62    config: &RuntimeConfig,
63    phase1: Phase1Out,
64    bridge: Arc<GatewayBridge>,
65    phase3: Phase3Out,
66    shutdown_tx: tokio::sync::watch::Sender<bool>,
67    mut shutdown_rx: tokio::sync::watch::Receiver<bool>,
68) -> Result<()> {
69    let Phase1Out {
70        _audit_guard,
71        shared_counters,
72        listen_addr,
73        rest_keys_file,
74        ws_keys_file,
75        grpc_keys_file,
76        allow_tcp_unauthenticated,
77    } = phase1;
78    let Phase3Out {
79        server,
80        server_config,
81        ws_broadcaster,
82        grpc_broadcaster,
83    } = phase3;
84
85    // v1.4.103 (B10): 把 3 个 server 的 key_store Option 提升到外层作用域,
86    // 让后面的 expand_allowed_card_nums spawn task 能跨 server 块捕获.
87    // 各 server 块仍各自加载 (允许独立 keys.json), 这里只共享 Arc clone.
88    let mut ws_key_store_holder: Option<std::sync::Arc<futu_auth::KeyStore>> = None;
89    let mut rest_key_store_holder: Option<std::sync::Arc<futu_auth::KeyStore>> = None;
90    let mut grpc_key_store_holder: Option<std::sync::Arc<futu_auth::KeyStore>> = None;
91    let any_keys_configured =
92        rest_keys_file.is_some() || grpc_keys_file.is_some() || ws_keys_file.is_some();
93    warn_legacy_network_exposure(config, any_keys_configured);
94
95    // 7. 启动 WebSocket 服务(可选)
96    let mut ws_handle = if let Some(ws_port) = config.websocket_port {
97        let ws_addr = format!("{}:{}", config.ip, ws_port);
98        // v1.0:WS 握手鉴权 —— 复用 REST 的 key store 设计(`--ws-keys-file` 独立
99        // 指定,不指定时 legacy 放行)
100        // v1.4.102 BUG-007 fix (P1, leaf v1.4.100 报告): keys-file load 失败
101        // 必须 fail-closed (abort daemon), 不再 silent fallback to legacy mode.
102        // 历史: 用户传 `--ws-keys-file` 表示明确意图启用 auth, 文件 typo / parse
103        // 错时 daemon 只 log error 然后继续无 auth → 用户以为有门锁, 实际门锁
104        // 没装上 (security misconfig 比纯 legacy 更危险).
105        let ws_key_store = match &ws_keys_file {
106            Some(path) => match futu_auth::KeyStore::load(path) {
107                Ok(ks) => {
108                    tracing::info!(
109                        path = %path.display(),
110                        keys_loaded = ks.len(),
111                        "WS keys file loaded (Bearer/?token auth enabled)"
112                    );
113                    Some(std::sync::Arc::new(ks))
114                }
115                Err(e) => {
116                    // v1.4.102 BUG-007: fail-closed
117                    tracing::error!(
118                        error = %e,
119                        path = %path.display(),
120                        "failed to load WS keys file (--ws-keys-file 明确指定 → fail-closed). \
121                         daemon abort. fix the keys file then restart. \
122                         不再 fallback to legacy unauth (v1.4.102 BUG-007 fix)."
123                    );
124                    return Err(anyhow::anyhow!(
125                        "failed to load WS keys file {}: {e}. \
126                         --ws-keys-file 明确指定 → fail-closed (BUG-007).",
127                        path.display()
128                    ));
129                }
130            },
131            None => None,
132        };
133        let ws_counters = std::sync::Arc::clone(&shared_counters);
134        // v1.4.103 (B10): clone Arc 给外层 holder 持有, 同时让 ws_server 仍接管原 Arc.
135        ws_key_store_holder = ws_key_store.as_ref().map(std::sync::Arc::clone);
136        let ws_server = WsServer::with_auth(
137            ws_addr.clone(),
138            server_config.clone(),
139            WsServerDeps::new(
140                std::sync::Arc::clone(server.connections()),
141                std::sync::Arc::clone(server.router()),
142                Some(bridge.subscription_runtime().manager()),
143            ),
144            ws_key_store,
145            Some(ws_counters),
146        )
147        .with_server_time_offset_secs(bridge.server_clock().offset_secs());
148        tracing::info!(addr = %ws_addr, "starting WebSocket server");
149        let ws_shutdown_rx = shutdown_rx.clone();
150        Some(tokio::spawn(async move {
151            ws_server
152                .run_until_shutdown(ws_shutdown_rx)
153                .await
154                .context("WebSocket server error")
155        }))
156    } else {
157        None
158    };
159
160    // 8. 启动 REST API 服务(可选,含 WebSocket 推送)
161    let mut rest_handle = if let Some(rest_port) = config.rest_port {
162        let rest_addr = format!("{}:{}", config.ip, rest_port);
163        let router = std::sync::Arc::clone(server.router());
164        let broadcaster = std::sync::Arc::clone(&ws_broadcaster);
165        // v1.4.102 BUG-007 fix: 同 WS 路径 (fail-closed when keys-file 显式指定).
166        let rest_key_store = match &rest_keys_file {
167            Some(path) => match futu_auth::KeyStore::load(path) {
168                Ok(ks) => {
169                    tracing::info!(
170                        path = %path.display(),
171                        keys_loaded = ks.len(),
172                        "REST keys file loaded (Bearer auth enabled)"
173                    );
174                    std::sync::Arc::new(ks)
175                }
176                Err(e) => {
177                    tracing::error!(
178                        error = %e,
179                        path = %path.display(),
180                        "failed to load REST keys file (--rest-keys-file 明确指定 → fail-closed). \
181                         daemon abort. fix the keys file then restart. \
182                         不再 fallback to legacy unauth (v1.4.102 BUG-007 fix)."
183                    );
184                    return Err(anyhow::anyhow!(
185                        "failed to load REST keys file {}: {e}. \
186                         --rest-keys-file 明确指定 → fail-closed (BUG-007).",
187                        path.display()
188                    ));
189                }
190            },
191            None => std::sync::Arc::new(futu_auth::KeyStore::empty()),
192        };
193        tracing::info!(addr = %rest_addr, "starting REST API server (WebSocket: /ws)");
194
195        // v1.4.103 (B10): clone Arc 给外层 holder 持有 (跨 server 块共享给
196        // expand_allowed_card_nums spawn task). 仅在 keys 实际配置时填充
197        // (empty store 不需要 expand).
198        if rest_key_store.is_configured() {
199            rest_key_store_holder = Some(std::sync::Arc::clone(&rest_key_store));
200        }
201
202        // v1.4.103 codex F3.1 (P1) round 3: REST 单独的 SIGHUP reload listener
203        // 已**移除** — 防与 card_num expand 间的 race (多 listener 并发顺序乱
204        // 可能让 reload 覆盖 expand 写入的 sentinel, 受限 key 在 reload 窗口
205        // 内 silent unrestricted). 现在 reload + expand 由文件末尾的 unified
206        // SIGHUP handler 顺序操作 (调 card_num_reload_and_expand_fn(true)).
207
208        let rest_counters = std::sync::Arc::clone(&shared_counters);
209        // v1.4.32+ admin snapshot provider:closure 捕获 `Arc<GatewayBridge>`
210        // 每次被 admin_status handler 调用时返回实时 StatusSnapshot JSON。
211        // bridge 已经在上面 Arc 化,这里只做一次 clone。
212        let bridge_for_status = std::sync::Arc::clone(&bridge);
213        let admin_status_provider: futu_rest::adapter::AdminStatusProvider =
214            std::sync::Arc::new(move || {
215                serde_json::to_value(bridge_for_status.snapshot_status())
216                    .unwrap_or_else(|_| serde_json::json!({"error": "snapshot serialize failed"}))
217            });
218        let rest_shutdown_tx = shutdown_tx.clone();
219        let admin_shutdown_handler: futu_rest::adapter::AdminShutdownHandler =
220            std::sync::Arc::new(move || {
221                rest_shutdown_tx
222                    .send(true)
223                    .map_err(|e| format!("shutdown receiver dropped: {e}"))
224            });
225        // v1.4.32+ admin reload handler:closure 调 Bridge::reload() 清 cipher cache
226        // v1.4.34: reload 升级为 async(内部刷 credentials 走网络 I/O),
227        // handler 返 Future,axum admin_reload async handler await 之
228        // v1.4.106 codex 0554 F3 [P2]: reload 拆两阶段后变 sync (sync clear +
229        // tokio::spawn refresh). closure 仍返 Future 类型不变 (向后兼容
230        // AdminReloadHandler API), 但内部不 await — sync 阶段已完成 + spawn
231        // 已派发, ReloadReport 立即可用.
232        let bridge_for_reload = std::sync::Arc::clone(&bridge);
233        let admin_reload_handler: futu_rest::adapter::AdminReloadHandler =
234            std::sync::Arc::new(move || {
235                let bridge = std::sync::Arc::clone(&bridge_for_reload);
236                Box::pin(async move {
237                    serde_json::to_value(bridge.reload())
238                        .unwrap_or_else(|_| serde_json::json!({"error": "reload serialize failed"}))
239                })
240            });
241        // v1.4.83 §9 Phase 2 F5: push health snapshot provider —— closure
242        // 捕获 bridge.push_runtime().push_health Arc, 每次
243        // `/api/push-subscriber-info` 调用时返当前真实 snapshot.
244        // v1.4.91 P1-D wiring: closure 同时捕获 bridge.push_runtime().qot_login_health,
245        // 在返 push_health 同时多带一个 qot_login_health 字段供 ops 看
246        // qot_logined self-heal counter (修 P1-D non-deterministic gap).
247        let bridge_for_push_health = std::sync::Arc::clone(&bridge);
248        let push_health_snapshot_provider: futu_rest::adapter::PushHealthSnapshotProvider =
249            std::sync::Arc::new(move || {
250                let push = serde_json::to_value(
251                    bridge_for_push_health
252                        .push_runtime()
253                        .push_health()
254                        .snapshot(),
255                )
256                .unwrap_or_else(
257                    |_| serde_json::json!({"error": "push_health snapshot serialize failed"}),
258                );
259                let qot_login = serde_json::to_value(
260                    bridge_for_push_health
261                        .push_runtime()
262                        .qot_login_health()
263                        .snapshot(),
264                )
265                .unwrap_or_else(
266                    |_| serde_json::json!({"error": "qot_login_health snapshot serialize failed"}),
267                );
268                let backend_connected =
269                    bridge_for_push_health.broker_runtime().platform_connected();
270                merge_push_health_snapshots_for_rest(push, qot_login, backend_connected)
271            });
272        // v1.4.105 D12 (Phase 2): 注入 card_num resolver 让 REST trade
273        // handler (place_order / modify_order / cancel_all_order) 能解析
274        // user 传 `card_num` 字段 → acc_id (覆盖 c2s.header.acc_id).
275        // closure 捕获 bridge.caches().trd_cache, 调
276        // `find_acc_ids_by_card_num(input) -> Vec<u64>`.
277        let bridge_for_card_num = std::sync::Arc::clone(&bridge);
278        let card_num_resolver: futu_rest::adapter::CardNumResolver =
279            std::sync::Arc::new(move |cn: &str| {
280                bridge_for_card_num
281                    .caches()
282                    .trd_cache
283                    .find_acc_ids_by_card_num(cn)
284            });
285        let rest_shutdown_rx = shutdown_rx.clone();
286        Some(tokio::spawn(async move {
287            futu_rest::server::start_with_auth_full_admin_until_shutdown(
288                &rest_addr,
289                router,
290                broadcaster,
291                rest_key_store,
292                rest_counters,
293                futu_rest::server::RestAdminHooks {
294                    admin_status_provider: Some(admin_status_provider),
295                    admin_shutdown_handler: Some(admin_shutdown_handler),
296                    admin_reload_handler: Some(admin_reload_handler),
297                    push_health_snapshot_provider: Some(push_health_snapshot_provider),
298                    card_num_resolver: Some(card_num_resolver),
299                },
300                rest_shutdown_rx,
301            )
302            .await
303            .context("REST API server error")
304        }))
305    } else {
306        None
307    };
308
309    // 9. 启动 gRPC 服务(可选,含流式推送)
310    let mut grpc_handle = if let Some(grpc_port) = config.grpc_port {
311        let grpc_addr = format!("{}:{}", config.ip, grpc_port);
312        let router = std::sync::Arc::clone(server.router());
313        let broadcaster = std::sync::Arc::clone(&grpc_broadcaster);
314        // v1.4.102 BUG-007 fix: 同 WS / REST 路径 (fail-closed when keys-file 显式指定).
315        let grpc_key_store = match &grpc_keys_file {
316            Some(path) => match futu_auth::KeyStore::load(path) {
317                Ok(ks) => {
318                    tracing::info!(
319                        path = %path.display(),
320                        keys_loaded = ks.len(),
321                        "gRPC keys file loaded (Bearer auth enabled)"
322                    );
323                    std::sync::Arc::new(ks)
324                }
325                Err(e) => {
326                    tracing::error!(
327                        error = %e,
328                        path = %path.display(),
329                        "failed to load gRPC keys file (--grpc-keys-file 明确指定 → fail-closed). \
330                         daemon abort. fix the keys file then restart. \
331                         不再 fallback to legacy unauth (v1.4.102 BUG-007 fix)."
332                    );
333                    return Err(anyhow::anyhow!(
334                        "failed to load gRPC keys file {}: {e}. \
335                         --grpc-keys-file 明确指定 → fail-closed (BUG-007).",
336                        path.display()
337                    ));
338                }
339            },
340            None => std::sync::Arc::new(futu_auth::KeyStore::empty()),
341        };
342        tracing::info!(addr = %grpc_addr, "starting gRPC server (SubscribePush: streaming)");
343
344        // v1.4.103 (B10): clone Arc 给外层 holder 持有.
345        if grpc_key_store.is_configured() {
346            grpc_key_store_holder = Some(std::sync::Arc::clone(&grpc_key_store));
347        }
348
349        // v1.4.103 codex F3.1 (P1) round 3: gRPC 单独的 SIGHUP reload listener
350        // 已**移除** — 同 REST 块, 由文件末尾的 unified SIGHUP handler 顺序
351        // reload + expand 防 race.
352
353        let grpc_counters = std::sync::Arc::clone(&shared_counters);
354        let grpc_shutdown_rx = shutdown_rx.clone();
355        Some(tokio::spawn(async move {
356            futu_grpc::server::start_with_auth_until_shutdown(
357                &grpc_addr,
358                router,
359                broadcaster,
360                grpc_key_store,
361                grpc_counters,
362                grpc_shutdown_rx,
363            )
364            .await
365            .map_err(|e| anyhow::anyhow!("gRPC server error: {e}"))
366        }))
367    } else {
368        None
369    };
370
371    // 10. 启动 Telnet 管理服务(可选)
372    let mut telnet_handle = if let Some(telnet_port) = config.telnet_port {
373        let telnet_addr = format!("{}:{}", config.telnet_ip, telnet_port);
374        // v1.4.97 P1-D-F: relogin callback closes over bridge to clear
375        // login_cache; next 30s P1-D health tick triggers relogin.
376        // Aligns with C++ GTWCmd_ReLogin (FTGateway/FTGTW_Define_Key.h:5).
377        let bridge_for_relogin = std::sync::Arc::clone(&bridge);
378        let relogin_fn: futu_server::telnet::ReloginFn = std::sync::Arc::new(move || {
379            tracing::warn!(
380                "v1.4.97 P1-D-F: telnet relogin clearing login_cache; \
381                     next P1-D tick will trigger AuthRefresher relogin"
382            );
383            bridge_for_relogin.caches().login_cache.clear();
384        });
385        let telnet_server = futu_server::telnet::TelnetServer::new(
386            telnet_addr.clone(),
387            std::sync::Arc::clone(server.connections()),
388            Some(bridge.subscription_runtime().manager()),
389            Some(std::sync::Arc::clone(server.metrics())),
390            shutdown_tx.clone(),
391        )
392        .with_relogin_fn(relogin_fn);
393        tracing::info!(addr = %telnet_addr, "starting Telnet server");
394        let telnet_shutdown_rx = shutdown_rx.clone();
395        Some(tokio::spawn(async move {
396            telnet_server
397                .run_until_shutdown(telnet_shutdown_rx)
398                .await
399                .context("Telnet server error")
400        }))
401    } else {
402        None
403    };
404
405    log_startup_summary(config, &listen_addr, &bridge);
406    tracing::info!("gateway ready, accepting connections on {listen_addr}");
407    tracing::info!("press Ctrl+C to exit");
408
409    // v1.4.103 (B10) + codex F2 (P1): 立即跑首次 expand + background retry
410    // + SIGHUP reload 钩子 (fail-closed sentinel 即时生效, 无 startup window).
411    //
412    // 行为:
413    // 1. **立即跑首次 expand** — 即便 cache 空, fail-closed sentinel (codex F1)
414    //    也写进 allowed_acc_ids, 短路 startup window 的 silent unrestricted.
415    // 2. **background retry**: 每 10s 检查 cache, 加载后 re-expand 真 acc_id
416    //    覆盖 sentinel. 60s 上限.
417    // 3. **SIGHUP reload 钩子**: 重载 keys.json 后再 expand 一次 (codex F2 reload window).
418    // v1.4.103 codex F3.1 (P1) round 3: 合并 reload + expand 为单一 ordered op,
419    // 防 race. 当 SIGHUP 触发时, 此 fn 同步顺序: (1) reload 所有 store (从
420    // keys.json 读 raw allowed_card_nums + ArcSwap.store) (2) expand (resolve
421    // card_num → acc_ids + ArcSwap.store with sentinel/resolved). 因为是单线
422    // 调用, 不存在多 SIGHUP listener 间的乱序 race.
423    //
424    // `do_reload` 控制是否先 reload (启动时第一次 / 60s retry 不需 reload,
425    // 直接 expand; SIGHUP 路径需要先 reload).
426    let card_num_reload_and_expand_fn: std::sync::Arc<dyn Fn(bool) + Send + Sync> = {
427        let bridge_for_expand = std::sync::Arc::clone(&bridge);
428        let ws_ks = ws_key_store_holder.clone();
429        let rest_ks = rest_key_store_holder.clone();
430        let grpc_ks = grpc_key_store_holder.clone();
431        std::sync::Arc::new(move |do_reload: bool| {
432            // (1) Reload phase — 仅 SIGHUP 路径调
433            if do_reload {
434                for (ks_name, ks_opt) in [("ws", &ws_ks), ("rest", &rest_ks), ("grpc", &grpc_ks)] {
435                    let Some(ks) = ks_opt.as_ref() else { continue };
436                    match ks.reload() {
437                        Ok(()) => tracing::warn!(
438                            ks = ks_name,
439                            keys_loaded = ks.len(),
440                            "v1.4.103 F3.1: keys reloaded on SIGHUP (before card_num expand)"
441                        ),
442                        Err(e) => tracing::error!(
443                            ks = ks_name,
444                            error = %e,
445                            "v1.4.103 F3.1: keys reload failed (skipping expand for this store)"
446                        ),
447                    }
448                }
449            }
450            // (2) Expand phase
451            let trd_cache = std::sync::Arc::clone(&bridge_for_expand.caches().trd_cache);
452            let resolver = {
453                let cache_clone = std::sync::Arc::clone(&trd_cache);
454                move |cn: &str| cache_clone.find_acc_ids_by_card_num(cn)
455            };
456            for (ks_name, ks_opt) in [("ws", &ws_ks), ("rest", &rest_ks), ("grpc", &grpc_ks)] {
457                let Some(ks) = ks_opt.as_ref() else { continue };
458                let (resolved, unresolved, ambiguous) = ks.expand_allowed_card_nums(
459                    &resolver,
460                    |key_id, cn| {
461                        tracing::warn!(
462                            key_id = %key_id,
463                            card_num = %cn,
464                            "v1.4.103 B10/F1 fail-closed: card_num not found in trd_cache; \
465                             writing sentinel acc_id=0 to enforce restrictive denylist \
466                             (limits.contains check 永远 false → reject 真账户)"
467                        );
468                    },
469                    |key_id, cn, candidates| {
470                        tracing::warn!(
471                            key_id = %key_id,
472                            card_num = %cn,
473                            candidates = ?candidates,
474                            "v1.4.103 B10/F1 fail-closed: ambiguous card_num suffix \
475                             matched multiple accounts (skipped, write 完整 16 位 / specific 4 位)"
476                        );
477                    },
478                );
479                tracing::info!(
480                    ks = ks_name,
481                    resolved,
482                    unresolved,
483                    ambiguous,
484                    "v1.4.103 B10: expanded allowed_card_nums into allowed_acc_ids"
485                );
486            }
487        })
488    };
489    // 老 alias (不 reload, 仅 expand) 用于启动 + retry 路径
490    let card_num_expand_fn: std::sync::Arc<dyn Fn() + Send + Sync> = {
491        let inner = std::sync::Arc::clone(&card_num_reload_and_expand_fn);
492        std::sync::Arc::new(move || (inner)(false))
493    };
494
495    // codex F2 (P1) 立即跑首次: fail-closed sentinel 即时生效, 不留 startup window
496    (card_num_expand_fn)();
497
498    // background retry loop: cache 加载后 re-expand 覆盖 sentinel
499    let card_num_retry_handle: BackgroundJoinHandle = {
500        let card_num_expand_fn_loop = std::sync::Arc::clone(&card_num_expand_fn);
501        let bridge_for_check = std::sync::Arc::clone(&bridge);
502        let mut card_num_retry_shutdown_rx = shutdown_rx.clone();
503        tokio::spawn(async move {
504            let trd_cache = std::sync::Arc::clone(&bridge_for_check.caches().trd_cache);
505            let mut attempts = 0u32;
506            let max_attempts = 6u32; // 6 × 10s = 60s
507            loop {
508                tokio::select! {
509                    changed = card_num_retry_shutdown_rx.changed() => {
510                        if changed.is_err() || *card_num_retry_shutdown_rx.borrow() {
511                            tracing::debug!(
512                                "v1.4.111: card_num retry loop received shutdown signal"
513                            );
514                            return;
515                        }
516                    }
517                    _ = tokio::time::sleep(std::time::Duration::from_secs(10)) => {}
518                }
519                attempts += 1;
520                let accounts = trd_cache.get_accounts();
521                if accounts.is_empty() {
522                    if attempts >= max_attempts {
523                        tracing::warn!(
524                            "v1.4.103 B10: trd_cache 仍空 (after {max_attempts} × 10s); \
525                             受限 key 仍走 fail-closed sentinel reject 直到 SIGHUP / cache 加载."
526                        );
527                        return;
528                    }
529                    continue;
530                }
531                (card_num_expand_fn_loop)();
532                return;
533            }
534        })
535    };
536
537    // codex F2 (P1) + F3.1 (P1) round 3: 单一 SIGHUP 钩子 — reload + expand
538    // 顺序操作避免 race. 之前每 server (REST/gRPC) 各自 SIGHUP 监听 reload,
539    // 加上 card_num expand 单独监听 — 多 SIGHUP listener 并发执行无序, 可能
540    // expand 先跑 (写 sentinel) 然后 reload 后跑 (overwrite sentinel 用 raw
541    // allowed_card_nums) → 受限 key 在 reload window 内 silent unrestricted.
542    //
543    // 现在: **唯一 SIGHUP listener**, 顺序 reload → expand. 删除 REST/gRPC 各
544    // 自的 SIGHUP reload listener (上面 server 块内已注释).
545    #[cfg(unix)]
546    let sighup_handle: Option<BackgroundJoinHandle> = {
547        let unified_sighup_fn = std::sync::Arc::clone(&card_num_reload_and_expand_fn);
548        let mut sighup_shutdown_rx = shutdown_rx.clone();
549        Some(tokio::spawn(async move {
550            use tokio::signal::unix::{SignalKind, signal};
551            let mut sig = match signal(SignalKind::hangup()) {
552                Ok(s) => s,
553                Err(e) => {
554                    tracing::error!(error = %e, "SIGHUP install failed (unified reload+expand)");
555                    return;
556                }
557            };
558            tracing::info!(
559                "v1.4.103 F3.1: unified SIGHUP handler installed (reload all keys + expand card_num)"
560            );
561            loop {
562                tokio::select! {
563                    signal = sig.recv() => {
564                        if signal.is_none() {
565                            return;
566                        }
567                        tracing::info!(
568                            "v1.4.103 F3.1: SIGHUP received — running reload_all_stores + \
569                             expand_allowed_card_nums (single ordered op, no race)"
570                        );
571                        (unified_sighup_fn)(true); // do_reload=true
572                    }
573                    changed = sighup_shutdown_rx.changed() => {
574                        if changed.is_err() || *sighup_shutdown_rx.borrow() {
575                            tracing::debug!(
576                                "v1.4.111: unified SIGHUP handler received shutdown signal"
577                            );
578                            return;
579                        }
580                    }
581                }
582            }
583        }))
584    };
585    #[cfg(not(unix))]
586    let sighup_handle: Option<BackgroundJoinHandle> = None;
587
588    // 11. v1.4.104 external reviewer S-001 (P0): native TCP keystore guard.
589    //
590    // 配任一 keys file (rest / grpc / ws) → 用户**意图**启用 scope mode.
591    // 但 TCP FTAPI 协议 InitConnect 没有 Bearer 字段, 无法做 caller-specific
592    // scope check (S-001). 默认 fail-closed: 不启 TCP listener, 用户需要
593    // 通过 REST/gRPC/WS 与 daemon 交互 (这些 surface 都已加 pipeline auth).
594    //
595    // 显式 `--allow-tcp-unauthenticated` opt-in → 启 TCP, 但 daemon 启动
596    // loud warn 该端口完全无 auth, agent skill 等 local process 可任意调用.
597    // 用之前 captured 的 local 变量, args 已被 merge_config 消费
598    let tcp_disabled = any_keys_configured && !allow_tcp_unauthenticated;
599
600    if tcp_disabled {
601        tracing::warn!(
602            listen_addr = %listen_addr,
603            "v1.4.104 external report S-001 (P0) fix: TCP listener (port {}) NOT started — \
604             keys file configured but --allow-tcp-unauthenticated not set. \
605             native TCP FTAPI protocol has no Bearer field, cannot enforce \
606             caller-specific scope check; defaulting to fail-closed (skip TCP). \
607             Use REST/gRPC/WS endpoints for authenticated access. \
608             To restore TCP (legacy Python SDK clients) add --allow-tcp-unauthenticated, \
609             but be aware that port {} will accept ANY local connection without \
610             scope check (跨账户 leak risk).",
611            config.port,
612            config.port,
613        );
614        eprintln!(
615            "⚠️  TCP listener (port {}) DISABLED (v1.4.104 external report S-001 fix): \
616             keys file configured + no --allow-tcp-unauthenticated. \
617             Pass --allow-tcp-unauthenticated to restore (with security warning).",
618            config.port,
619        );
620    } else if any_keys_configured && allow_tcp_unauthenticated {
621        tracing::warn!(
622            listen_addr = %listen_addr,
623            "⚠️  v1.4.104: TCP listener running WITHOUT scope check despite keys configured \
624             (--allow-tcp-unauthenticated set). Port {} accepts ANY local connection — \
625             跨账户 leak risk. Use REST/gRPC/WS for authenticated clients; reserve \
626             TCP only for legacy Python SDK / C++ OpenD where Bearer not feasible.",
627            config.port,
628        );
629        eprintln!(
630            "⚠️  TCP port {} ACCEPTS UNAUTHENTICATED connections (--allow-tcp-unauthenticated). \
631             受限 keys 不在该 surface 强制. 推荐改用 REST/gRPC/WS.",
632            config.port,
633        );
634    }
635
636    let tcp_shutdown_rx = shutdown_rx.clone();
637    let mut tcp_handle: Option<SurfaceJoinHandle> = if tcp_disabled {
638        None
639    } else {
640        Some(tokio::spawn(async move {
641            server
642                .run_until_shutdown(tcp_shutdown_rx)
643                .await
644                .context("API server error")
645        }))
646    };
647
648    // 11. 等待任一 surface 退出、Ctrl+C 或 telnet shutdown
649    let phase4_result: anyhow::Result<()> = tokio::select! {
650        result = surface_task_result_or_pending(&mut tcp_handle) => {
651            tcp_handle = None;
652            log_surface_task_result("API server", result)
653        }
654        result = surface_task_result_or_pending(&mut ws_handle) => {
655            ws_handle = None;
656            log_surface_task_result("WebSocket", result)
657        }
658        result = surface_task_result_or_pending(&mut rest_handle) => {
659            rest_handle = None;
660            log_surface_task_result("REST API", result)
661        }
662        result = surface_task_result_or_pending(&mut grpc_handle) => {
663            grpc_handle = None;
664            log_surface_task_result("gRPC", result)
665        }
666        result = surface_task_result_or_pending(&mut telnet_handle) => {
667            telnet_handle = None;
668            log_surface_task_result("Telnet", result)
669        }
670        _ = tokio::signal::ctrl_c() => {
671            tracing::info!("received Ctrl+C, shutting down gracefully...");
672            Ok(())
673        }
674        _ = wait_for_sigterm() => {
675            tracing::info!("received SIGTERM, shutting down gracefully...");
676            Ok(())
677        }
678        _ = async {
679            while shutdown_rx.changed().await.is_ok() {
680                if *shutdown_rx.borrow() {
681                    break;
682                }
683            }
684        } => {
685            tracing::info!("shutdown requested via telnet");
686            Ok(())
687        }
688    };
689
690    // 清理后台任务
691    request_surface_shutdown(&shutdown_tx, "phase4 exit");
692    await_background_shutdown(
693        "card_num retry",
694        card_num_retry_handle,
695        SHUTDOWN_GRACE_PERIOD,
696    )
697    .await;
698    if let Some(handle) = sighup_handle {
699        await_background_shutdown("SIGHUP reload", handle, SHUTDOWN_GRACE_PERIOD).await;
700    }
701    if let Some(handle) = tcp_handle {
702        await_surface_shutdown("API server", handle, SHUTDOWN_GRACE_PERIOD).await;
703    }
704    if let Some(handle) = ws_handle {
705        await_surface_shutdown("WebSocket", handle, SHUTDOWN_GRACE_PERIOD).await;
706    }
707    if let Some(handle) = rest_handle {
708        await_surface_shutdown("REST API", handle, SHUTDOWN_GRACE_PERIOD).await;
709    }
710    if let Some(handle) = grpc_handle {
711        await_surface_shutdown("gRPC", handle, SHUTDOWN_GRACE_PERIOD).await;
712    }
713    if let Some(handle) = telnet_handle {
714        await_surface_shutdown("Telnet", handle, SHUTDOWN_GRACE_PERIOD).await;
715    }
716
717    phase4_result?;
718    tracing::info!("gateway stopped");
719    Ok(())
720}