1use async_trait::async_trait;
8use bytes::Bytes;
9use futu_command_runtime::{
10 CommandAuthorizationEvidence, CommandRequest, CommandResponse, CommandTransport,
11};
12use futu_command_spec::trade::{TradeBackendChannel, trade_account_discovery_command};
13use futu_command_spec::{
14 AuthRequirement, BackendChannelKind, BrokerDiscoveryOperation, CommandSpecId,
15 ConnectionDiscoveryOperation, HeartbeatOperation, backend_extension_command,
16 broker_discovery_command, command_spec_by_id, connection_discovery_command, heartbeat_command,
17 indicator_command, static_data_read_command, system_read_command, user_cloud_command,
18};
19pub use futu_command_spec::{
20 BackendExtensionOperation, CMD_ALGO_ORDER_LOGS, CMD_BATCH_CLOSE_POSITIONS,
21 CMD_BFF_ORDER_CANCEL, CMD_BFF_ORDER_CONFIRM, CMD_BFF_ORDER_DELETE, CMD_BFF_ORDER_DETAIL,
22 CMD_BFF_ORDER_NEW_V2, CMD_BFF_ORDER_REPLACE, CMD_CLEAR_FUTURES_POSITIONS, CMD_CUSTOMER_BCAN,
23 CMD_CUSTOMER_COMPANY_INFO, CMD_GET_BIZ_GROUP, CMD_GET_BOND_ANSWER_STATE,
24 CMD_GET_BOND_POSITION_LIST, CMD_GET_BOND_SINGLE_ASSET, CMD_GET_BOND_TOTAL_ASSET,
25 CMD_GET_BOND_TRADE_REMINDER, CMD_GET_CASH_DETAIL, CMD_GET_CASH_LOG, CMD_GET_CN_AH_MARGIN_INFO,
26 CMD_GET_HK_MARGIN_INFO, CMD_GET_US_MARGIN_INFO, CMD_POSITION_ACTION_RESULT,
27 CMD_POSITION_CORPORATE_ACTION_DERIVATIVE, CMD_POSITION_CORPORATE_ACTION_STOCK,
28 CMD_PULL_ACCOUNT_FLAG, CMD_REVERSE_POSITION, CMD_ROLL_POSITION,
29 IndicatorOperation as IndicatorCommandOperation, MarketEventOperation, QotReadOperation,
30 QotWriteOperation, StaticDataReadOperation, SystemReadOperation,
31 TradeAccountDiscoveryOperation, TradeAuthOperation, TradeQueryEnvironment, TradeQueryOperation,
32 UserCloudOperation,
33 trade::{CryptoTradeOperation, TradeWriteOperation},
34};
35use futu_core::error::{FutuError, Result as FutuResult};
36
37use crate::conn::{
38 BackendConn, BackendWriterAdmission, RequestTimeoutPolicy, WriterAdmissionObserver,
39};
40use crate::quote_sub::{
41 DispatchAccepted, NormalSubscriptionDispatchHooks, QotSubscriptionWriterAdmission,
42};
43
44#[path = "command_runtime_policy.rs"]
45mod policy;
46
47#[path = "command_runtime_qot.rs"]
48mod qot;
49
50pub(crate) use qot::execute_qot_subscription_set_with_dispatch;
51pub use qot::{
52 execute_market_event, execute_qot_read, execute_qot_read_with_reserved,
53 execute_qot_subscription_set,
54};
55
56use policy::{BACKEND_COMMAND_TIMEOUT, BackendCommandPolicyPorts};
57
58#[cfg(test)]
59#[path = "command_runtime_tests.rs"]
60mod tests;
61
62#[cfg(test)]
63#[path = "command_runtime_crypto_tests.rs"]
64mod crypto_tests;
65
66#[cfg(test)]
67#[path = "command_runtime_ordinary_tests.rs"]
68mod ordinary_tests;
69
70#[cfg(test)]
71pub(crate) use crate::command_runtime_test_support::{
72 decode_internal_test_request, decode_internal_test_request_body, encode_internal_test_response,
73 execute_internal_test_command,
74};
75
76pub use futu_command_runtime::{
77 ChannelId, ChannelLifecycleState, ChannelRuntimeAction, CommandExecution,
78 CommandExecutionContext, CommandExecutionOutcome, CommandOutcome, CommandRuntime,
79 CommandRuntimeAction, CommandRuntimeDecision, CommandRuntimeError, decide_command_outcome,
80};
81
82#[derive(Clone)]
83pub(crate) struct SubscriptionDispatchContext {
84 pub(crate) quote_market_type: u8,
85 pub(crate) exact_normal_wire: Vec<u8>,
86 pub(crate) hooks: NormalSubscriptionDispatchHooks,
87 pub(crate) accepted: std::sync::Arc<parking_lot::Mutex<Option<DispatchAccepted>>>,
88}
89
90pub type QotWritePublishTerminal = dyn Fn(u64, &mut dyn FnMut()) -> bool + Send + Sync;
91
92pub struct ChannelBoundBackendTransport<'a> {
93 backend: &'a BackendConn,
94 channel: BackendChannelKind,
95 request_timeout: std::time::Duration,
96 timeout_policy: RequestTimeoutPolicy,
97 subscription_dispatch: Option<SubscriptionDispatchContext>,
98 writer_admission_guard: Option<std::sync::Arc<dyn Fn(u64) -> bool + Send + Sync>>,
99 writer_admission_publish_terminal: Option<std::sync::Arc<QotWritePublishTerminal>>,
100 writer_admission_accepted: Option<std::sync::Arc<dyn Fn(u64) + Send + Sync>>,
101}
102
103impl<'a> ChannelBoundBackendTransport<'a> {
104 #[must_use]
105 pub fn new(backend: &'a BackendConn, channel: BackendChannelKind) -> Self {
106 Self {
107 backend,
108 channel,
109 request_timeout: BACKEND_COMMAND_TIMEOUT,
110 timeout_policy: RequestTimeoutPolicy::Disconnect,
111 subscription_dispatch: None,
112 writer_admission_guard: None,
113 writer_admission_publish_terminal: None,
114 writer_admission_accepted: None,
115 }
116 }
117
118 fn keep_connection_on_timeout(mut self) -> Self {
119 self.timeout_policy = RequestTimeoutPolicy::KeepConnection;
120 self
121 }
122
123 fn with_subscription_dispatch(mut self, context: SubscriptionDispatchContext) -> Self {
124 self.subscription_dispatch = Some(context);
125 self
126 }
127
128 fn with_writer_admission_guard(
129 mut self,
130 guard: std::sync::Arc<dyn Fn(u64) -> bool + Send + Sync>,
131 ) -> Self {
132 self.writer_admission_guard = Some(guard);
133 self
134 }
135
136 fn with_writer_admission_accepted(
137 mut self,
138 accepted: std::sync::Arc<dyn Fn(u64) + Send + Sync>,
139 ) -> Self {
140 self.writer_admission_accepted = Some(accepted);
141 self
142 }
143
144 fn with_writer_admission_publish_terminal(
145 mut self,
146 terminal: std::sync::Arc<QotWritePublishTerminal>,
147 ) -> Self {
148 self.writer_admission_publish_terminal = Some(terminal);
149 self
150 }
151}
152
153#[async_trait]
154impl CommandTransport for ChannelBoundBackendTransport<'_> {
155 fn channel_kind(&self) -> Option<BackendChannelKind> {
156 Some(self.channel)
157 }
158
159 async fn execute(&self, request: CommandRequest) -> FutuResult<CommandResponse> {
160 let context = self.subscription_dispatch.as_ref();
161 let observer = match request.spec_id {
162 CommandSpecId::QotSubscriptionSet => context.map(|context| {
163 let quote_market_type = context.quote_market_type;
164 let exact_normal_wire = context.exact_normal_wire.clone();
165 let hooks = context.hooks.clone();
166 let admission_hooks = hooks.clone();
167 let accepted = std::sync::Arc::clone(&context.accepted);
168 WriterAdmissionObserver {
169 try_admit: std::sync::Arc::new(move |admission: BackendWriterAdmission| {
170 (admission_hooks.try_admit)(QotSubscriptionWriterAdmission {
171 connection_generation: admission.connection_generation,
172 serial_no: admission.serial_no,
173 })
174 }),
175 publish_terminal: None,
176 on_accepted: std::sync::Arc::new(move |admission: BackendWriterAdmission| {
177 let receipt = (hooks.on_accepted)(
178 QotSubscriptionWriterAdmission {
179 connection_generation: admission.connection_generation,
180 serial_no: admission.serial_no,
181 },
182 quote_market_type,
183 exact_normal_wire.clone(),
184 );
185 *accepted.lock() = Some(receipt);
186 }),
187 }
188 }),
189 _ if context.is_some() => {
190 return Err(FutuError::Codec(
191 "subscription dispatch context used by non-subscription command".into(),
192 ));
193 }
194 _ if self.writer_admission_guard.is_some()
195 || self.writer_admission_publish_terminal.is_some() =>
196 {
197 let guard = self.writer_admission_guard.clone();
198 let terminal = self.writer_admission_publish_terminal.clone();
199 let accepted = self.writer_admission_accepted.clone();
200 Some(WriterAdmissionObserver {
201 try_admit: std::sync::Arc::new(move |admission| {
202 guard
203 .as_ref()
204 .is_none_or(|guard| guard(admission.connection_generation))
205 }),
206 publish_terminal: terminal.map(|terminal| {
207 std::sync::Arc::new(
208 move |admission: BackendWriterAdmission, publish: &mut dyn FnMut()| {
209 terminal(admission.connection_generation, publish)
210 },
211 )
212 as std::sync::Arc<
213 dyn Fn(BackendWriterAdmission, &mut dyn FnMut()) -> bool
214 + Send
215 + Sync,
216 >
217 }),
218 on_accepted: std::sync::Arc::new(move |admission| {
219 if let Some(accepted) = accepted.as_ref() {
220 accepted(admission.connection_generation);
221 }
222 }),
223 })
224 }
225 _ => None,
226 };
227 execute_on_backend(
228 self.backend,
229 request,
230 self.request_timeout,
231 self.timeout_policy,
232 observer,
233 )
234 .await
235 }
236}
237
238fn runtime_with_transport(
239 transport: ChannelBoundBackendTransport<'_>,
240 authorization: CommandAuthorizationEvidence,
241) -> CommandRuntime<ChannelBoundBackendTransport<'_>, BackendCommandPolicyPorts<'_>> {
242 let policy = BackendCommandPolicyPorts::new(transport.backend, authorization);
243 CommandRuntime::with_policy(transport, policy)
244}
245
246fn login_runtime_with_transport(
247 transport: ChannelBoundBackendTransport<'_>,
248) -> CommandRuntime<ChannelBoundBackendTransport<'_>, BackendCommandPolicyPorts<'_>> {
249 runtime_with_transport(transport, CommandAuthorizationEvidence::LoginSession)
250}
251
252fn authorization_for_command(
253 spec_id: CommandSpecId,
254 trade_cipher: Option<&[u8]>,
255) -> FutuResult<CommandAuthorizationEvidence> {
256 let spec = command_spec_by_id(spec_id)
257 .ok_or_else(|| FutuError::Codec(format!("unknown command spec id: {spec_id:?}")))?;
258 match spec.auth {
259 AuthRequirement::TradeUnlocked => trade_cipher
260 .ok_or_else(|| {
261 FutuError::Codec(
262 "command authorization evidence: trade cipher was not provided".to_owned(),
263 )
264 })
265 .and_then(|cipher| {
266 CommandAuthorizationEvidence::from_trade_cipher(cipher).map_err(|error| {
267 FutuError::Codec(format!("command authorization evidence: {error}"))
268 })
269 }),
270 AuthRequirement::None | AuthRequirement::Login => {
271 Ok(CommandAuthorizationEvidence::LoginSession)
272 }
273 }
274}
275
276pub async fn execute_trade_read(
277 backend: &BackendConn,
278 operation: TradeQueryOperation,
279 environment: TradeQueryEnvironment,
280 trade_cipher: Option<&[u8]>,
281 body: Bytes,
282) -> FutuResult<CommandResponse> {
283 let channel = match environment {
284 TradeQueryEnvironment::Real => BackendChannelKind::Broker,
285 TradeQueryEnvironment::Sim => BackendChannelKind::Platform,
286 };
287 let spec_id = CommandSpecId::TradeRead {
288 operation,
289 environment,
290 };
291 let runtime = runtime_with_transport(
292 ChannelBoundBackendTransport::new(backend, channel),
293 authorization_for_command(spec_id, trade_cipher)?,
294 );
295 let execution = runtime
296 .execute(CommandExecutionContext::new(spec_id, body))
297 .await
298 .map_err(|error| FutuError::Codec(format!("trade-read command spec error: {error}")))?;
299 let report = &execution.report;
300 tracing::trace!(
301 command = report.command_name,
302 cmd_id = report.cmd_id,
303 channel = ?report.channel,
304 outcome = ?report.outcome,
305 response_body_len = report.response_body_len,
306 "command runtime executed trade read"
307 );
308 execution.into_response()
309}
310
311pub async fn execute_backend_extension(
312 backend: &BackendConn,
313 operation: BackendExtensionOperation,
314 body: Bytes,
315) -> FutuResult<CommandResponse> {
316 let spec = backend_extension_command(operation);
317 let runtime = login_runtime_with_transport(ChannelBoundBackendTransport::new(
318 backend,
319 spec.runtime.channel,
320 ));
321 let execution = runtime
322 .execute(CommandExecutionContext::new(
323 CommandSpecId::BackendExtension(operation),
324 body,
325 ))
326 .await
327 .map_err(|error| {
328 FutuError::Codec(format!("backend-extension command spec error: {error}"))
329 })?;
330 let report = &execution.report;
331 tracing::trace!(
332 command = report.command_name,
333 cmd_id = report.cmd_id,
334 channel = ?report.channel,
335 evidence_kind = ?report.command_evidence.kind,
336 outcome = ?report.outcome,
337 response_body_len = report.response_body_len,
338 "command runtime executed backend extension"
339 );
340 execution.into_response()
341}
342
343pub async fn execute_backend_extension_guarded(
344 backend: &BackendConn,
345 operation: BackendExtensionOperation,
346 body: Bytes,
347 guard: std::sync::Arc<dyn Fn(u64) -> bool + Send + Sync>,
348) -> FutuResult<CommandResponse> {
349 let spec = backend_extension_command(operation);
350 let transport = ChannelBoundBackendTransport::new(backend, spec.runtime.channel)
351 .with_writer_admission_guard(guard);
352 let runtime = login_runtime_with_transport(transport);
353 let execution = runtime
354 .execute(CommandExecutionContext::new(
355 CommandSpecId::BackendExtension(operation),
356 body,
357 ))
358 .await
359 .map_err(|error| {
360 FutuError::Codec(format!(
361 "guarded backend-extension command spec error: {error}"
362 ))
363 })?;
364 execution.into_response()
365}
366
367pub async fn execute_trade_auth(
368 backend: &BackendConn,
369 operation: TradeAuthOperation,
370 body: Bytes,
371) -> FutuResult<CommandResponse> {
372 let runtime = login_runtime_with_transport(ChannelBoundBackendTransport::new(
373 backend,
374 BackendChannelKind::Broker,
375 ));
376 let execution = runtime
377 .execute(CommandExecutionContext::new(
378 CommandSpecId::TradeAuth(operation),
379 body,
380 ))
381 .await
382 .map_err(|error| FutuError::Codec(format!("trade-auth command spec error: {error}")))?;
383 let report = &execution.report;
384 tracing::trace!(
385 command = report.command_name,
386 cmd_id = report.cmd_id,
387 channel = ?report.channel,
388 outcome = ?report.outcome,
389 response_body_len = report.response_body_len,
390 "command runtime executed trade auth"
391 );
392 execution.into_response()
393}
394
395pub async fn execute_trade_account_discovery(
396 backend: &BackendConn,
397 operation: TradeAccountDiscoveryOperation,
398 body: Bytes,
399) -> FutuResult<CommandResponse> {
400 let channel = match trade_account_discovery_command(operation).channel {
401 TradeBackendChannel::Broker => BackendChannelKind::Broker,
402 TradeBackendChannel::Platform => BackendChannelKind::Platform,
403 };
404 let runtime = login_runtime_with_transport(ChannelBoundBackendTransport::new(backend, channel));
405 let execution = runtime
406 .execute(CommandExecutionContext::new(
407 CommandSpecId::TradeAccountDiscovery(operation),
408 body,
409 ))
410 .await
411 .map_err(|error| {
412 FutuError::Codec(format!(
413 "trade-account-discovery command spec error: {error}"
414 ))
415 })?;
416 let report = &execution.report;
417 tracing::trace!(
418 command = report.command_name,
419 cmd_id = report.cmd_id,
420 channel = ?report.channel,
421 outcome = ?report.outcome,
422 response_body_len = report.response_body_len,
423 "command runtime executed trade account discovery"
424 );
425 execution.into_response()
426}
427
428pub async fn execute_connection_discovery(
429 backend: &BackendConn,
430 operation: ConnectionDiscoveryOperation,
431 body: Bytes,
432) -> FutuResult<CommandResponse> {
433 let spec = connection_discovery_command(operation);
434 let runtime =
435 login_runtime_with_transport(ChannelBoundBackendTransport::new(backend, spec.channel));
436 let execution = runtime
437 .execute(CommandExecutionContext::new(
438 CommandSpecId::ConnectionDiscovery(operation),
439 body,
440 ))
441 .await
442 .map_err(|error| {
443 FutuError::Codec(format!("connection-discovery command spec error: {error}"))
444 })?;
445 let report = &execution.report;
446 tracing::trace!(
447 command = report.command_name,
448 cmd_id = report.cmd_id,
449 channel = ?report.channel,
450 outcome = ?report.outcome,
451 response_body_len = report.response_body_len,
452 "command runtime executed connection discovery"
453 );
454 execution.into_response()
455}
456
457pub async fn execute_broker_discovery(
458 backend: &BackendConn,
459 operation: BrokerDiscoveryOperation,
460 body: Bytes,
461) -> FutuResult<CommandResponse> {
462 if operation == BrokerDiscoveryOperation::ValidBrokerListChangedPush {
463 return Err(FutuError::Codec(
464 "CMD20177 is push-only and cannot be executed as a request".into(),
465 ));
466 }
467 let spec = broker_discovery_command(operation);
468 let mut transport = ChannelBoundBackendTransport::new(backend, spec.runtime.channel);
469 if operation == BrokerDiscoveryOperation::ValidBrokerList {
470 transport = transport.keep_connection_on_timeout();
471 }
472 let runtime = login_runtime_with_transport(transport);
473 let execution = runtime
474 .execute(CommandExecutionContext::new(
475 CommandSpecId::BrokerDiscovery(operation),
476 body,
477 ))
478 .await
479 .map_err(|error| {
480 FutuError::Codec(format!("broker-discovery command spec error: {error}"))
481 })?;
482 let report = &execution.report;
483 tracing::trace!(
484 command = report.command_name,
485 cmd_id = report.cmd_id,
486 channel = ?report.channel,
487 outcome = ?report.outcome,
488 response_body_len = report.response_body_len,
489 "command runtime executed broker discovery"
490 );
491 execution.into_response()
492}
493
494pub async fn execute_heartbeat(
495 backend: &BackendConn,
496 operation: HeartbeatOperation,
497 body: Bytes,
498) -> FutuResult<CommandResponse> {
499 let spec = heartbeat_command(operation);
500 let runtime = login_runtime_with_transport(ChannelBoundBackendTransport::new(
501 backend,
502 spec.runtime.channel,
503 ));
504 let execution = runtime
505 .execute(CommandExecutionContext::new(
506 CommandSpecId::Heartbeat(operation),
507 body,
508 ))
509 .await
510 .map_err(|error| FutuError::Codec(format!("heartbeat command spec error: {error}")))?;
511 let report = &execution.report;
512 tracing::trace!(
513 command = report.command_name,
514 cmd_id = report.cmd_id,
515 channel = ?report.channel,
516 outcome = ?report.outcome,
517 response_body_len = report.response_body_len,
518 "command runtime executed heartbeat"
519 );
520 execution.into_response()
521}
522
523pub async fn execute_trade_write(
524 backend: &BackendConn,
525 operation: TradeWriteOperation,
526 environment: TradeQueryEnvironment,
527 trade_cipher: Option<&[u8]>,
528 body: Bytes,
529) -> FutuResult<CommandResponse> {
530 let channel = match environment {
531 TradeQueryEnvironment::Real => BackendChannelKind::Broker,
532 TradeQueryEnvironment::Sim => BackendChannelKind::Platform,
533 };
534 let spec_id = CommandSpecId::TradeWrite {
535 operation,
536 environment,
537 };
538 let runtime = runtime_with_transport(
539 ChannelBoundBackendTransport::new(backend, channel),
540 authorization_for_command(spec_id, trade_cipher)?,
541 );
542 let execution = runtime
543 .execute(CommandExecutionContext::new(spec_id, body))
544 .await
545 .map_err(|error| FutuError::Codec(format!("trade-write command spec error: {error}")))?;
546 let report = &execution.report;
547 tracing::trace!(
548 command = report.command_name,
549 cmd_id = report.cmd_id,
550 channel = ?report.channel,
551 outcome = ?report.outcome,
552 response_body_len = report.response_body_len,
553 "command runtime executed trade write"
554 );
555 execution.into_response()
556}
557
558pub async fn execute_crypto_trade_command(
559 backend: &BackendConn,
560 operation: CryptoTradeOperation,
561 trade_cipher: Option<&[u8]>,
562 body: Bytes,
563) -> FutuResult<CommandResponse> {
564 let spec_id = CommandSpecId::CryptoTrade(operation);
565 let runtime = runtime_with_transport(
566 ChannelBoundBackendTransport::new(backend, BackendChannelKind::Broker),
567 authorization_for_command(spec_id, trade_cipher)?,
568 );
569 let execution = runtime
570 .execute(CommandExecutionContext::new(spec_id, body))
571 .await
572 .map_err(|error| FutuError::Codec(format!("crypto-trade command spec error: {error}")))?;
573 let report = &execution.report;
574 tracing::trace!(
575 command = report.command_name,
576 cmd_id = report.cmd_id,
577 channel = ?report.channel,
578 outcome = ?report.outcome,
579 response_body_len = report.response_body_len,
580 "command runtime executed crypto trade command"
581 );
582 execution.into_response()
583}
584
585pub async fn execute_system_read(
586 backend: &BackendConn,
587 operation: SystemReadOperation,
588 body: Bytes,
589) -> FutuResult<CommandResponse> {
590 let spec = system_read_command(operation);
591 let runtime = login_runtime_with_transport(ChannelBoundBackendTransport::new(
592 backend,
593 spec.runtime.channel,
594 ));
595 let execution = runtime
596 .execute(CommandExecutionContext::new(
597 CommandSpecId::SystemRead(operation),
598 body,
599 ))
600 .await
601 .map_err(|error| FutuError::Codec(format!("system-read command spec error: {error}")))?;
602 let report = &execution.report;
603 tracing::trace!(
604 command = report.command_name,
605 cmd_id = report.cmd_id,
606 channel = ?report.channel,
607 evidence_kind = ?report.command_evidence.kind,
608 outcome = ?report.outcome,
609 response_body_len = report.response_body_len,
610 "command runtime executed system read"
611 );
612 execution.into_response()
613}
614
615pub async fn execute_indicator_read(
616 backend: &BackendConn,
617 operation: IndicatorCommandOperation,
618 body: Bytes,
619) -> FutuResult<CommandResponse> {
620 let spec = indicator_command(operation);
621 let runtime = login_runtime_with_transport(ChannelBoundBackendTransport::new(
622 backend,
623 spec.runtime.channel,
624 ));
625 let execution = runtime
626 .execute(CommandExecutionContext::new(
627 CommandSpecId::Indicator(operation),
628 body,
629 ))
630 .await
631 .map_err(|error| FutuError::Codec(format!("indicator command spec error: {error}")))?;
632 execution.into_response()
633}
634
635pub async fn execute_static_data_read(
636 backend: &BackendConn,
637 operation: StaticDataReadOperation,
638 body: Bytes,
639) -> FutuResult<CommandResponse> {
640 let spec = static_data_read_command(operation);
641 let runtime = login_runtime_with_transport(ChannelBoundBackendTransport::new(
642 backend,
643 spec.runtime.channel,
644 ));
645 let execution = runtime
646 .execute(CommandExecutionContext::new(
647 CommandSpecId::StaticDataRead(operation),
648 body,
649 ))
650 .await
651 .map_err(|error| {
652 FutuError::Codec(format!("static-data-read command spec error: {error}"))
653 })?;
654 let report = &execution.report;
655 tracing::trace!(
656 command = report.command_name,
657 cmd_id = report.cmd_id,
658 channel = ?report.channel,
659 outcome = ?report.outcome,
660 response_body_len = report.response_body_len,
661 "command runtime executed static-data read"
662 );
663 execution.into_response()
664}
665
666pub async fn execute_user_cloud(
667 backend: &BackendConn,
668 operation: UserCloudOperation,
669 body: Bytes,
670) -> FutuResult<CommandResponse> {
671 if operation == UserCloudOperation::UpdatePush {
672 return Err(FutuError::Codec(
673 "user-cloud CMD20175 is push-only and cannot be executed as a request".to_string(),
674 ));
675 }
676 let spec = user_cloud_command(operation);
677 if spec.runtime.channel != BackendChannelKind::Platform {
678 return Err(FutuError::Codec(format!(
679 "user-cloud command {} is bound to {:?}",
680 spec.runtime.name, spec.runtime.channel
681 )));
682 }
683 let runtime = login_runtime_with_transport(ChannelBoundBackendTransport::new(
684 backend,
685 spec.runtime.channel,
686 ));
687 let execution = runtime
688 .execute(CommandExecutionContext::new(
689 CommandSpecId::UserCloud(operation),
690 body,
691 ))
692 .await
693 .map_err(|error| FutuError::Codec(format!("user-cloud command spec error: {error}")))?;
694 let report = &execution.report;
695 tracing::trace!(
696 command = report.command_name,
697 cmd_id = report.cmd_id,
698 channel = ?report.channel,
699 outcome = ?report.outcome,
700 response_body_len = report.response_body_len,
701 "command runtime executed user cloud command"
702 );
703 execution.into_response()
704}
705
706pub async fn execute_qot_plaintext(
707 backend: &BackendConn,
708 cmd_id: u16,
709 body: Bytes,
710 reserved: [u8; 10],
711) -> FutuResult<CommandResponse> {
712 execute_qot_command(backend, CommandSpecId::QotPlaintext(cmd_id), body, reserved).await
713}
714
715pub async fn execute_qot_write(
716 backend: &BackendConn,
717 operation: QotWriteOperation,
718 body: Bytes,
719 reserved: [u8; 10],
720) -> FutuResult<CommandResponse> {
721 let id = CommandSpecId::QotWrite(operation);
722 let spec = futu_command_spec::command_spec_by_id(id).ok_or_else(|| {
723 FutuError::Codec(format!("qot-write command spec missing for {operation:?}"))
724 })?;
725 let runtime =
726 login_runtime_with_transport(ChannelBoundBackendTransport::new(backend, spec.channel));
727 let execution = runtime
728 .execute(CommandExecutionContext::new(id, body).with_reserved(reserved))
729 .await
730 .map_err(|error| FutuError::Codec(format!("qot-write command spec error: {error}")))?;
731 execution.into_response()
732}
733
734pub async fn execute_qot_write_guarded(
735 backend: &BackendConn,
736 operation: QotWriteOperation,
737 body: Bytes,
738 reserved: [u8; 10],
739 publish_terminal: std::sync::Arc<QotWritePublishTerminal>,
740 accepted: std::sync::Arc<dyn Fn(u64) + Send + Sync>,
741) -> FutuResult<CommandResponse> {
742 let id = CommandSpecId::QotWrite(operation);
743 let spec = futu_command_spec::command_spec_by_id(id).ok_or_else(|| {
744 FutuError::Codec(format!("qot-write command spec missing for {operation:?}"))
745 })?;
746 let transport = ChannelBoundBackendTransport::new(backend, spec.channel)
747 .with_writer_admission_publish_terminal(publish_terminal)
748 .with_writer_admission_accepted(accepted);
749 let runtime = login_runtime_with_transport(transport);
750 let execution = runtime
751 .execute(CommandExecutionContext::new(id, body).with_reserved(reserved))
752 .await
753 .map_err(|error| {
754 FutuError::Codec(format!("guarded qot-write command spec error: {error}"))
755 })?;
756 execution.into_response()
757}
758
759async fn execute_qot_command(
760 backend: &BackendConn,
761 spec_id: CommandSpecId,
762 body: Bytes,
763 reserved: [u8; 10],
764) -> FutuResult<CommandResponse> {
765 let runtime = login_runtime_with_transport(ChannelBoundBackendTransport::new(
766 backend,
767 BackendChannelKind::Qot,
768 ));
769 let execution = runtime
770 .execute(CommandExecutionContext::new(spec_id, body).with_reserved(reserved))
771 .await
772 .map_err(|error| FutuError::Codec(format!("qot command spec error: {error}")))?;
773 let report = &execution.report;
774 tracing::trace!(
775 command = report.command_name,
776 cmd_id = report.cmd_id,
777 channel = ?report.channel,
778 outcome = ?report.outcome,
779 response_body_len = report.response_body_len,
780 "command runtime executed QOT command"
781 );
782 execution.into_response()
783}
784
785async fn execute_on_backend(
786 backend: &BackendConn,
787 request: CommandRequest,
788 request_timeout: std::time::Duration,
789 timeout_policy: RequestTimeoutPolicy,
790 observer: Option<WriterAdmissionObserver>,
791) -> FutuResult<CommandResponse> {
792 let frame = backend
793 .request_with_reserved_timeout_policy_observed(
794 request.cmd_id,
795 request.body.to_vec(),
796 request.reserved,
797 request_timeout,
798 timeout_policy,
799 observer,
800 )
801 .await?;
802 Ok(CommandResponse {
803 request_serial_no: frame.header.serial_no,
804 cmd_id: frame.header.cmd_id,
805 body: frame.body,
806 ex_head: frame.ex_head,
807 })
808}