futu_gateway_core/bridge/
reload.rs1use super::*;
2
3fn now_ms() -> u64 {
8 crate::bridge::clock::wall_clock_since_unix_epoch_or_zero("admin reload refresh").as_millis()
9 as u64
10}
11
12async fn run_reload_refresh_task(reload_state: Option<ReloadState>) -> ReloadRefreshStatus {
18 let Some(state) = reload_state else {
19 let finished_ms = now_ms();
21 let reason = "no reload_state (daemon started in offline mode); cipher \
22 cache cleared but tgtgt not refreshable"
23 .to_string();
24 tracing::warn!(reason, "admin reload (async phase): refresh skipped");
25 return ReloadRefreshStatus::Skipped {
26 finished_ms,
27 reason,
28 };
29 };
30
31 match auth::refresh_credentials_on_disk(
32 &state.http,
33 &state.account,
34 &state.device_id,
35 state.client_type,
36 state.region_code.as_deref(),
37 state.attribution,
38 )
39 .await
40 {
41 Ok(report) => {
42 let finished_ms = now_ms();
43 if report.credentials_refreshed {
44 tracing::info!(
45 "admin reload (async phase): credentials refreshed on disk \
46 (new tgtgt picked up by next Platform reconnect)"
47 );
48 ReloadRefreshStatus::Succeeded {
49 finished_ms,
50 refreshed: true,
51 }
52 } else {
53 tracing::info!(
55 "admin reload (async phase): credentials already fresh, \
56 no refresh needed"
57 );
58 ReloadRefreshStatus::NotApplicable { finished_ms }
59 }
60 }
61 Err(e) => {
62 let finished_ms = now_ms();
63 let err_str = format!("{e}");
66 let hinted = if err_str.contains("no cached credentials") {
67 format!(
68 "{err_str}\n\
69 【hint】如果这是 moomoo 账户,credentials 文件可能没被首登流程 \
70 save (v1.4.42 已知 bug,根因待 raw response 分析)。workaround:\n\
71 1. ./futu-opend --reset-device --setup-only \
72 --login-account <account> --login-pwd <pwd> --platform moomoo\n\
73 2. 走完 SMS 流程,daemon 早退出 (凭据已写盘)\n\
74 3. 正式启 daemon; admin/reload 后应可正常 refresh"
75 )
76 } else {
77 err_str
78 };
79 tracing::warn!(
80 error = %hinted,
81 "admin reload (async phase): credentials refresh failed \
82 (cipher cache still cleared; shutdown + restart opend if \
83 Platform conn is stuck)"
84 );
85 ReloadRefreshStatus::Failed {
86 finished_ms,
87 error: hinted,
88 }
89 }
90 }
91}
92
93impl GatewayBridge {
94 pub fn snapshot_status(&self) -> StatusSnapshot {
106 let auth_result = self.auth_runtime().auth_result();
107 let login_state = self.caches().login_cache.get_login_state();
108 let credential = login_state
109 .as_ref()
110 .and_then(|state| auth::credential_ticket_status(&state.login_account))
111 .map(|status| LoginCredentialStatus {
112 saved_at: status.saved_at,
113 age_days: status.age_days,
114 expires_in_days: status.expires_in_days,
115 expiry_warning: status.expiry_warning,
116 });
117 let auth_refresh = self.auth_runtime().client_sig_refresh_status();
118 let login = match &auth_result {
119 Some(auth) => LoginStatus {
120 online: true,
121 user_id: Some(auth.user_id),
122 attribution: Some(format!("{:?}", auth.user_attribution)),
125 credential,
126 auth_refresh: auth_refresh.clone(),
127 },
128 None => LoginStatus {
129 online: false,
130 user_id: None,
131 attribution: None,
132 credential: None,
133 auth_refresh,
134 },
135 };
136
137 let platform_connected = self.broker_runtime().platform_connected();
138 let platform = PlatformStatus {
139 connected: platform_connected,
140 };
141
142 let brokers = self.broker_runtime().broker_statuses();
145
146 let cipher_count = self.caches().trd_cache.ciphers.len();
147 let account_count = self.caches().trd_cache.get_accounts().len();
148 let stock_list_status = self.caches().static_cache.stock_list_sync_status();
149 let security_count = self.caches().static_cache.security_info_count();
150 let cache = CacheStats {
151 trade_unlocked: cipher_count > 0,
152 cached_cipher_count: cipher_count,
153 cached_account_count: account_count,
154 static_data: StaticCacheStats {
155 security_count,
156 stock_list_readiness: stock_list_status
157 .readiness_for_security_count(security_count)
158 .as_str(),
159 stock_list_first_sync_done: stock_list_status.first_sync_done,
160 stock_list_converged: stock_list_status.converged,
161 stock_list_sync_started_total: stock_list_status.started_total,
162 stock_list_sync_finished_total: stock_list_status.finished_total,
163 stock_list_sync_failed_total: stock_list_status.failed_total,
164 stock_list_sync_recoverable_retry_total: stock_list_status.recoverable_retry_total,
165 stock_list_zero_delta_total: stock_list_status.zero_delta_total,
166 stock_list_last_version: stock_list_status.last_version,
167 stock_list_last_total_stocks: stock_list_status.last_total_stocks,
168 stock_list_last_cached_count: stock_list_status.last_cached_count,
169 stock_list_last_finished_ms: stock_list_status.last_finished_ms,
170 },
171 };
172
173 let last_reload_refresh = self.auth_runtime().last_reload_refresh().read().clone();
176
177 StatusSnapshot {
178 version: env!("CARGO_PKG_VERSION"),
179 login,
180 platform,
181 brokers,
182 cache,
183 last_reload_refresh,
184 }
185 }
186
187 pub fn reload(&self) -> ReloadReport {
215 let (ciphers_dropped, bumped_versions) = self
227 .caches()
228 .trd_cache
229 .clear_all_ciphers_and_bump_versions();
230 tracing::warn!(
231 ciphers_dropped,
232 ?bumped_versions,
233 "admin reload (sync phase): trade cipher cache cleared + \
234 cipher_state_version bumped (BUG-008 idem cache invalidation); \
235 clients must re-unlock"
236 );
237
238 let started_ms = now_ms();
241 let last_status = self.auth_runtime().last_reload_refresh();
242 *last_status.write() = ReloadRefreshStatus::Running { started_ms };
243
244 let reload_state = self.auth_runtime().reload_state();
245 self.background_runtime().track(tokio::spawn(async move {
246 let final_status = run_reload_refresh_task(reload_state).await;
247 *last_status.write() = final_status;
248 }));
249
250 let message = if ciphers_dropped == 0 {
252 "no ciphers to drop; credentials refresh dispatched in background \
253 (see /api/admin/status → last_reload_refresh)"
254 .to_string()
255 } else {
256 format!(
257 "dropped {ciphers_dropped} trade cipher(s); credentials refresh \
258 dispatched in background (see /api/admin/status → \
259 last_reload_refresh); call /api/unlock-trade again to re-enable \
260 order placement"
261 )
262 };
263
264 ReloadReport {
265 ok: true,
266 ciphers_dropped,
267 credentials_refreshed: false,
272 credentials_refresh_error: Some(
273 "refresh dispatched in background; query /api/admin/status \
274 (last_reload_refresh) for outcome"
275 .to_string(),
276 ),
277 message,
278 }
279 }
280}
281
282#[cfg(test)]
283mod reload_tests;