1use crate::backend::connector::{ConnectedGroup, ConnectedServer, ServerConnector};
2use crate::bindings::da::{
3 OPC_BRANCH, OPC_BROWSE_DOWN, OPC_BROWSE_UP, OPC_DS_DEVICE, OPC_LEAF, OPC_NS_FLAT, tagOPCITEMDEF,
4};
5use crate::helpers::{
6 filetime_to_string, format_hresult, opc_value_to_variant, quality_to_string,
7 variant_to_display_string, variant_to_string,
8};
9use crate::native_browse::{BrowseSessions, capabilities_for_server};
10use crate::opc_da::errors::{OpcError, OpcResult};
11use crate::opc_da::typedefs::{GroupHandle, ItemHandle};
12use crate::provider::{
13 BrowseCapabilities, BrowsePage, BrowsePageRequest, BrowseSessionToken, OpcValue, TagValue,
14 WriteResult,
15};
16use std::collections::HashMap;
17use std::sync::Arc;
18use std::sync::atomic::{AtomicUsize, Ordering};
19use tokio::sync::{mpsc, oneshot};
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum ReadPresentation {
24 Semantic,
26 Display,
28}
29
30pub enum ComRequest {
32 ListServers {
34 host: String,
36 reply: oneshot::Sender<OpcResult<Vec<String>>>,
38 },
39 ReadTagValues {
41 server: String,
43 tag_ids: Vec<String>,
45 presentation: ReadPresentation,
47 reply: oneshot::Sender<OpcResult<Vec<TagValue>>>,
49 },
50 WriteTagValue {
52 server: String,
54 tag_id: String,
56 value: OpcValue,
58 reply: oneshot::Sender<OpcResult<WriteResult>>,
60 },
61 BrowseTags {
63 server: String,
65 max_tags: usize,
67 progress: Arc<AtomicUsize>,
69 tags_sink: Arc<std::sync::Mutex<Vec<String>>>,
71 reply: oneshot::Sender<OpcResult<Vec<String>>>,
73 },
74 BrowseCapabilities {
76 server: String,
78 reply: oneshot::Sender<OpcResult<BrowseCapabilities>>,
80 },
81 OpenBrowseSession {
83 server: String,
85 reply: oneshot::Sender<OpcResult<BrowseSessionToken>>,
87 },
88 BrowsePage {
90 session: BrowseSessionToken,
92 request: BrowsePageRequest,
94 reply: oneshot::Sender<OpcResult<BrowsePage>>,
96 },
97 CloseBrowseSession {
99 session: BrowseSessionToken,
101 reply: oneshot::Sender<OpcResult<()>>,
103 },
104}
105
106pub struct ComWorker<C: ServerConnector + 'static> {
111 pub sender: mpsc::Sender<ComRequest>,
113 pub handle: Option<std::thread::JoinHandle<()>>,
115 _phantom: std::marker::PhantomData<C>,
116}
117
118#[allow(clippy::cast_possible_wrap)]
119fn is_connection_error(err: &OpcError) -> bool {
120 if let OpcError::Com { source } = err {
121 let code = source.code().0;
122 code == windows::core::HRESULT(0x8007_06BA_u32 as i32).0
123 || code == windows::core::HRESULT(0x8007_06BF_u32 as i32).0
124 || code == windows::core::HRESULT(0x8007_06BE_u32 as i32).0
125 || code == windows::core::HRESULT(0x8008_0005_u32 as i32).0
126 } else {
127 false
128 }
129}
130
131impl<C: ServerConnector + 'static> ComWorker<C> {
132 pub fn closed() -> Self {
134 let (tx, _rx) = mpsc::channel(1);
135 Self {
136 sender: tx,
137 handle: None,
138 _phantom: std::marker::PhantomData,
139 }
140 }
141
142 #[allow(clippy::too_many_lines)]
143 #[tracing::instrument(skip(connector))]
144 pub fn start(connector: Arc<C>) -> Result<Self, OpcError> {
145 let (tx, mut rx) = mpsc::channel(32);
146 let (init_tx, init_rx) = std::sync::mpsc::channel();
147
148 let handle = std::thread::spawn(move || {
149 tracing::debug!("COM worker thread spawned, initializing COM (MTA)");
150 let _guard = match crate::ComGuard::new() {
151 Ok(g) => {
152 tracing::info!("COM MTA initialized successfully on worker thread");
153 let _ = init_tx.send(Ok(()));
154 g
155 }
156 Err(e) => {
157 tracing::error!(error = ?e, "COM worker failed to initialize MTA");
158 let _ =
159 init_tx.send(Err(OpcError::Internal("COM init failed on worker".into())));
160 return;
161 }
162 };
163
164 let mut cache: HashMap<String, C::Server> = HashMap::new();
165 let mut browse_sessions = BrowseSessions::default();
166
167 while let Some(req) = rx.blocking_recv() {
168 browse_sessions.cleanup_expired();
169 match req {
170 ComRequest::ListServers { host, reply } => {
171 let span = tracing::info_span!("opc.list_servers", host = %host);
172 let _enter = span.enter();
173 #[cfg(feature = "dev-diagnostics")]
174 tracing::trace!(host = %host, "list_servers: starting operation");
175 let start = std::time::Instant::now();
176 let servers = connector.enumerate_servers();
177 if let Ok(s) = &servers {
178 tracing::info!(
179 count = s.len(),
180 elapsed_ms =
181 u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX),
182 "list_servers completed"
183 );
184 } else if let Err(e) = &servers {
185 crate::opc_da::errors::log_opc_error(e, "list_servers");
186 tracing::error!(
187 error = ?e,
188 elapsed_ms = u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX),
189 "list_servers failed"
190 );
191 }
192 let _ = reply.send(servers);
193 }
194
195 ComRequest::ReadTagValues {
196 server,
197 tag_ids,
198 presentation,
199 reply,
200 } => {
201 let result = Self::dispatch_with_retry(
202 &mut cache,
203 &connector,
204 &server,
205 |opc_server| {
206 Self::handle_read(&server, &tag_ids, presentation, opc_server)
207 },
208 );
209 let _ = reply.send(result);
210 }
211 ComRequest::WriteTagValue {
212 server,
213 tag_id,
214 value,
215 reply,
216 } => {
217 let result = Self::dispatch_with_retry(
218 &mut cache,
219 &connector,
220 &server,
221 |opc_server| Self::handle_write(&server, &tag_id, &value, opc_server),
222 );
223 let _ = reply.send(result);
224 }
225 ComRequest::BrowseTags {
226 server,
227 max_tags,
228 progress,
229 tags_sink,
230 reply,
231 } => {
232 let result = Self::dispatch_with_retry(
233 &mut cache,
234 &connector,
235 &server,
236 |opc_server| {
237 Self::handle_browse(
238 &server, max_tags, &progress, &tags_sink, opc_server,
239 )
240 },
241 );
242 let _ = reply.send(result);
243 }
244 ComRequest::BrowseCapabilities { server, reply } => {
245 if reply.is_closed() {
246 continue;
247 }
248 let result = Self::dispatch_with_retry(
249 &mut cache,
250 &connector,
251 &server,
252 capabilities_for_server,
253 );
254 let _ = reply.send(result);
255 }
256 ComRequest::OpenBrowseSession { server, reply } => {
257 if reply.is_closed() {
258 continue;
259 }
260 let result = connector
261 .connect(&server)
262 .and_then(|opc_server| browse_sessions.open(opc_server));
263 if let Err(Ok(session)) = reply.send(result) {
264 let _ = browse_sessions.close(&session);
265 }
266 }
267 ComRequest::BrowsePage {
268 session,
269 request,
270 reply,
271 } => {
272 if reply.is_closed() {
273 let _ = browse_sessions.close(&session);
274 continue;
275 }
276 let result = browse_sessions.page(&session, request);
277 if reply.send(result).is_err() {
278 let _ = browse_sessions.close(&session);
279 }
280 }
281 ComRequest::CloseBrowseSession { session, reply } => {
282 let result = browse_sessions.close(&session);
283 let _ = reply.send(result);
284 }
285 }
286 }
287
288 tracing::debug!("COM worker thread exiting cleanly");
289 });
290
291 init_rx
292 .recv()
293 .map_err(|_| OpcError::Internal("COM worker thread panicked during init".into()))??;
294
295 tracing::debug!("COM worker thread started");
296
297 Ok(Self {
298 sender: tx,
299 handle: Some(handle),
300 _phantom: std::marker::PhantomData,
301 })
302 }
303
304 #[tracing::instrument(skip(self, req_builder))]
305 pub async fn send_request<F, R>(&self, req_builder: F) -> OpcResult<R>
306 where
307 F: FnOnce(oneshot::Sender<OpcResult<R>>) -> ComRequest,
308 {
309 if self
310 .handle
311 .as_ref()
312 .is_some_and(std::thread::JoinHandle::is_finished)
313 {
314 tracing::error!("COM worker thread panicked or exited unexpectedly");
315 return Err(OpcError::Internal("COM worker thread panicked".into()));
316 }
317
318 let (tx, rx) = oneshot::channel();
319 let req = req_builder(tx);
320
321 self.sender
322 .send(req)
323 .await
324 .map_err(|_| OpcError::Internal("COM worker channel closed (worker stopped)".into()))?;
325
326 rx.await
327 .map_err(|_| OpcError::Internal("COM worker shut down during request".into()))?
328 }
329
330 fn dispatch_with_retry<F, R>(
331 cache: &mut HashMap<String, C::Server>,
332 connector: &Arc<C>,
333 server_name: &str,
334 operation: F,
335 ) -> OpcResult<R>
336 where
337 F: Fn(&C::Server) -> OpcResult<R>,
338 {
339 let server_ref = match cache.entry(server_name.to_string()) {
340 std::collections::hash_map::Entry::Occupied(e) => {
341 tracing::trace!(server = %server_name, "Cache hit");
342 e.into_mut()
343 }
344 std::collections::hash_map::Entry::Vacant(e) => {
345 tracing::debug!(server = %server_name, "Cache miss, connecting");
346 let srv = connector.connect(server_name)?;
347 tracing::info!(server = %server_name, "Connection established, added to pool");
348 e.insert(srv)
349 }
350 };
351
352 match operation(server_ref) {
353 Err(e) if is_connection_error(&e) => {
354 tracing::warn!(server = %server_name, error = ?e, "Evicting stale connection");
355 cache.remove(server_name);
356 tracing::debug!(server = %server_name, "Reconnecting");
357 let fresh_srv = connector.connect(server_name).map_err(|connect_e| {
358 tracing::error!(error = ?connect_e, "Reconnect failed");
359 connect_e
360 })?;
361 let fresh_ref = &fresh_srv;
362 let result = operation(fresh_ref);
363 tracing::info!(server = %server_name, "Reconnection successful, pool updated");
364 cache.insert(server_name.to_string(), fresh_srv);
365 result
366 }
367 other => other,
368 }
369 }
370
371 #[allow(clippy::too_many_lines)]
372 fn handle_read(
373 server_name: &str,
374 tag_ids: &[String],
375 presentation: ReadPresentation,
376 opc_server: &C::Server,
377 ) -> OpcResult<Vec<TagValue>> {
378 let span = tracing::info_span!(
379 "opc.read_tag_values",
380 server = %server_name,
381 tag_count = tag_ids.len()
382 );
383 let _enter = span.enter();
384 #[cfg(feature = "dev-diagnostics")]
385 tracing::trace!(
386 server = %server_name,
387 tag_count = tag_ids.len(),
388 sample_tags = ?tag_ids.iter().take(5).collect::<Vec<_>>(),
389 "read_tag_values: starting operation"
390 );
391 let start = std::time::Instant::now();
392
393 let mut revised_update_rate = 0u32;
394 let mut server_handle = GroupHandle::default();
395 let group = opc_server.add_group(
396 "opc-da-client-read",
397 true,
398 1000,
399 server_handle,
400 0,
401 0.0,
402 0,
403 &mut revised_update_rate,
404 &mut server_handle,
405 )?;
406
407 let item_id_wides: Vec<Vec<u16>> = tag_ids
408 .iter()
409 .map(|tag_id| tag_id.encode_utf16().chain(std::iter::once(0)).collect())
410 .collect();
411
412 let item_defs: Vec<tagOPCITEMDEF> = item_id_wides
413 .iter()
414 .enumerate()
415 .map(|(idx, wide)| tagOPCITEMDEF {
416 szAccessPath: windows::core::PWSTR::null(),
417 szItemID: windows::core::PWSTR(wide.as_ptr().cast_mut()),
418 bActive: windows::Win32::Foundation::TRUE,
419 #[allow(clippy::cast_possible_truncation)]
420 hClient: idx as u32,
421 dwBlobSize: 0,
422 pBlob: std::ptr::null_mut(),
423 vtRequestedDataType: 0,
424 wReserved: 0,
425 })
426 .collect();
427
428 let (results, errors) = group.add_items(&item_defs)?;
429
430 if results.len() as usize != tag_ids.len() || errors.len() as usize != tag_ids.len() {
432 if let Err(e) = opc_server.remove_group(server_handle, true) {
433 tracing::warn!(error = ?e, operation = "read_tag_values", "Failed to remove OPC group during cleanup");
434 }
435 return Err(OpcError::Internal(
436 "OPC server returned mismatched result array sizes".into(),
437 ));
438 }
439
440 let mut tag_values: Vec<TagValue> = tag_ids
441 .iter()
442 .map(|tag_id| TagValue {
443 tag_id: tag_id.clone(),
444 value: "Error".to_string(),
445 quality: "Bad — not added to group".to_string(),
446 timestamp: String::new(),
447 })
448 .collect();
449
450 let mut server_handles: Vec<ItemHandle> = Vec::new();
451 let mut valid_indices = Vec::new();
452
453 for (idx, (item_result, error)) in results
454 .as_slice()
455 .iter()
456 .zip(errors.as_slice().iter())
457 .enumerate()
458 {
459 if error.is_ok() {
460 server_handles.push(ItemHandle(item_result.hServer));
461 valid_indices.push(idx);
462 } else {
463 let hint = format_hresult(*error);
464 tracing::warn!(
465 tag = %tag_ids[idx],
466 error = %hint,
467 "read_tag_values: add_items rejected tag"
468 );
469 tag_values[idx].quality = format!("Bad — {hint}");
470 }
471 }
472
473 if server_handles.is_empty() {
474 if let Err(e) = opc_server.remove_group(server_handle, true) {
475 tracing::warn!(error = ?e, operation = "read_tag_values", "Failed to remove OPC group during cleanup");
476 }
477 return Ok(tag_values);
478 }
479
480 let (item_states, read_errors) = group.read(OPC_DS_DEVICE, &server_handles)?;
481 let item_states_slice = item_states.as_slice();
482 let read_errors_slice = read_errors.as_slice();
483
484 for (i, idx) in valid_indices.iter().enumerate() {
485 let state = &item_states_slice[i];
486 let read_error = &read_errors_slice[i];
487
488 let (value_str, quality_str) = if read_error.is_ok() {
489 (
490 match presentation {
491 ReadPresentation::Semantic => variant_to_string(&state.vDataValue),
492 ReadPresentation::Display => variant_to_display_string(&state.vDataValue),
493 },
494 quality_to_string(state.wQuality),
495 )
496 } else {
497 let full_msg = format_hresult(*read_error);
498 tracing::warn!(
499 tag = %tag_ids[*idx],
500 error = ?read_error,
501 hint = %full_msg,
502 "read_tag_values: per-item read error"
503 );
504 ("Error".to_string(), format!("Bad — {full_msg}"))
505 };
506
507 tag_values[*idx] = TagValue {
508 tag_id: tag_ids[*idx].clone(),
509 value: value_str,
510 quality: quality_str,
511 timestamp: filetime_to_string(state.ftTimeStamp),
512 };
513 }
514
515 tracing::info!(
516 count = tag_values.len(),
517 elapsed_ms = u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX),
518 "read_tag_values completed"
519 );
520 if let Err(e) = opc_server.remove_group(server_handle, true) {
521 tracing::warn!(error = ?e, operation = "read_tag_values", "Failed to remove OPC group during cleanup");
522 }
523 Ok(tag_values)
524 }
525
526 #[allow(clippy::too_many_lines)]
527 fn handle_write(
528 server_name: &str,
529 tag_id: &str,
530 value: &OpcValue,
531 opc_server: &C::Server,
532 ) -> OpcResult<WriteResult> {
533 let span = tracing::info_span!(
534 "opc.write_tag_value",
535 server = %server_name,
536 tag = %tag_id
537 );
538 let _enter = span.enter();
539 #[cfg(feature = "dev-diagnostics")]
540 tracing::trace!(
541 server = %server_name,
542 tag = %tag_id,
543 value = ?value,
544 "write_tag_value: starting operation"
545 );
546 let start = std::time::Instant::now();
547
548 let mut revised_update_rate = 0u32;
549 let mut server_handle = GroupHandle::default();
550 let group = opc_server.add_group(
551 "opc-da-client-write",
552 true,
553 1000,
554 GroupHandle(0),
555 0,
556 0.0,
557 0,
558 &mut revised_update_rate,
559 &mut server_handle,
560 )?;
561
562 let mut item_id_wide: Vec<u16> = tag_id.encode_utf16().chain(std::iter::once(0)).collect();
563 let item_def = tagOPCITEMDEF {
564 szAccessPath: windows::core::PWSTR::null(),
565 szItemID: windows::core::PWSTR(item_id_wide.as_mut_ptr()),
566 bActive: windows::Win32::Foundation::TRUE,
567 hClient: 0,
568 dwBlobSize: 0,
569 pBlob: std::ptr::null_mut(),
570 vtRequestedDataType: 0,
571 wReserved: 0,
572 };
573
574 let (results, errors) = group.add_items(&[item_def])?;
575 let item_res = results
576 .as_slice()
577 .first()
578 .ok_or_else(|| OpcError::Internal("Server returned empty item results".to_string()))?;
579 let item_err = errors
580 .as_slice()
581 .first()
582 .ok_or_else(|| OpcError::Internal("Server returned empty item errors".to_string()))?;
583
584 if let Err(e) = item_err.ok() {
585 tracing::warn!(error = ?e, "write_tag_value: failed to add tag to group");
586 if let Err(e) = opc_server.remove_group(server_handle, true) {
587 tracing::warn!(error = ?e, operation = "write_tag_value", "Failed to remove OPC group during cleanup");
588 }
589 return Ok(WriteResult {
590 tag_id: tag_id.to_string(),
591 success: false,
592 error: Some(format!("Failed to add tag: {}", format_hresult(*item_err))),
593 });
594 }
595
596 let item_handle = ItemHandle(item_res.hServer);
597 let variant = opc_value_to_variant(value);
598
599 let write_errors = group.write(&[item_handle], &[variant])?;
600 let write_err = write_errors
601 .as_slice()
602 .first()
603 .ok_or_else(|| OpcError::Internal("Server returned empty write errors".to_string()))?;
604
605 let write_result = if write_err.is_ok() {
606 tracing::info!(
607 elapsed_ms = u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX),
608 "write_tag_value completed"
609 );
610 WriteResult {
611 tag_id: tag_id.to_string(),
612 success: true,
613 error: None,
614 }
615 } else {
616 let msg = format_hresult(*write_err);
617 tracing::warn!(
618 error = %msg,
619 elapsed_ms = u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX),
620 "write_tag_value: server rejected write"
621 );
622 WriteResult {
623 tag_id: tag_id.to_string(),
624 success: false,
625 error: Some(msg),
626 }
627 };
628
629 if let Err(e) = opc_server.remove_group(server_handle, true) {
630 tracing::warn!(error = ?e, operation = "write_tag_value", "Failed to remove OPC group during cleanup");
631 }
632 Ok(write_result)
633 }
634
635 fn handle_browse(
636 server_name: &str,
637 max_tags: usize,
638 progress: &Arc<AtomicUsize>,
639 tags_sink: &Arc<std::sync::Mutex<Vec<String>>>,
640 opc_server: &C::Server,
641 ) -> OpcResult<Vec<String>> {
642 let span = tracing::info_span!("opc.browse_tags", server = %server_name, max_tags);
643 let _enter = span.enter();
644 #[cfg(feature = "dev-diagnostics")]
645 tracing::trace!(
646 server = %server_name,
647 max_tags,
648 "browse_tags: starting operation"
649 );
650 let start = std::time::Instant::now();
651
652 let org = opc_server.query_organization()?;
653 let mut tags = Vec::new();
654
655 if org == OPC_NS_FLAT.0 as u32 {
656 let mut string_iter = opc_server.begin_da2_browse(OPC_LEAF.0 as u32, Some(""), 0, 0)?;
657 while let Some(tag_res) = string_iter.next_string() {
658 if tags.len() >= max_tags {
659 break;
660 }
661 let tag = tag_res?;
662 tags.push(tag.clone());
663 if let Ok(mut sink) = tags_sink.lock() {
664 sink.push(tag);
665 }
666 progress.fetch_add(1, Ordering::Relaxed);
667 }
668 } else {
669 Self::browse_recursive(opc_server, &mut tags, max_tags, progress, tags_sink, 0)?;
670 }
671 tracing::info!(
672 count = tags.len(),
673 elapsed_ms = u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX),
674 "browse_tags completed"
675 );
676 Ok(tags)
677 }
678
679 fn browse_recursive(
680 server: &C::Server,
681 tags: &mut Vec<String>,
682 max_tags: usize,
683 progress: &Arc<AtomicUsize>,
684 tags_sink: &Arc<std::sync::Mutex<Vec<String>>>,
685 depth: usize,
686 ) -> OpcResult<()> {
687 const MAX_DEPTH: usize = 50;
688 if depth > MAX_DEPTH || tags.len() >= max_tags {
689 if depth > MAX_DEPTH {
690 tracing::warn!(depth, "Max browse depth reached, truncating");
691 }
692 return Ok(());
693 }
694
695 let mut branch_enum = server.begin_da2_browse(OPC_BRANCH.0 as u32, Some(""), 0, 0)?;
696 let mut branches = Vec::new();
697 while let Some(result) = branch_enum.next_string() {
698 match result {
699 Ok(name) => branches.push(name),
700 Err(e) => {
701 tracing::warn!(error = ?e, "Branch iteration error, skipping");
702 }
703 }
704 }
705
706 let mut leaf_enum = server.begin_da2_browse(OPC_LEAF.0 as u32, Some(""), 0, 0)?;
707 while let Some(tag_res) = leaf_enum.next_string() {
708 if tags.len() >= max_tags {
709 return Ok(());
710 }
711 let browse_name = tag_res?;
712 let tag = match server.get_item_id(&browse_name) {
713 Ok(id) => id,
714 Err(e) => {
715 tracing::warn!(
716 browse_name = %browse_name,
717 error = ?e,
718 "get_item_id failed, using browse name as fallback"
719 );
720 browse_name
721 }
722 };
723 tags.push(tag.clone());
724 if let Ok(mut sink) = tags_sink.lock() {
725 sink.push(tag);
726 }
727 progress.fetch_add(1, Ordering::Relaxed);
728 }
729
730 for branch in branches {
731 if tags.len() >= max_tags {
732 return Ok(());
733 }
734 if let Err(e) = server.change_browse_position(OPC_BROWSE_DOWN.0 as u32, &branch) {
735 tracing::warn!(
736 branch = %branch,
737 error = ?e,
738 "Failed to browse down, skipping branch"
739 );
740 continue;
741 }
742
743 if let Err(e) =
744 Self::browse_recursive(server, tags, max_tags, progress, tags_sink, depth + 1)
745 {
746 tracing::warn!(error = ?e, "browse_recursive error");
747 }
748
749 if let Err(e) = server.change_browse_position(OPC_BROWSE_UP.0 as u32, "") {
750 tracing::warn!(error = ?e, "Failed to browse up, stopping recursion");
751 break;
752 }
753 }
754
755 Ok(())
756 }
757}
758
759impl<C: ServerConnector + 'static> Drop for ComWorker<C> {
760 fn drop(&mut self) {
761 tracing::debug!("ComWorker dropping — channel closing, signaling thread shutdown");
762 }
763}
764
765#[cfg(test)]
766mod tests {
767 #![allow(
768 clippy::single_char_pattern,
769 clippy::cast_possible_wrap,
770 clippy::ptr_as_ptr,
771 clippy::borrow_as_ptr,
772 clippy::mixed_attributes_style,
773 clippy::unreadable_literal,
774 clippy::undocumented_unsafe_blocks,
775 clippy::manual_assert
776 )]
777 use super::*;
778 use crate::backend::connector::{
779 BrowseStringIterator, ConnectedGroup, ConnectedServer, RemoteArray, ServerConnector,
780 StringIterator,
781 };
782 use crate::bindings::da::OPC_FLAT;
783 use crate::bindings::da::{tagOPCDATASOURCE, tagOPCITEMDEF, tagOPCITEMRESULT, tagOPCITEMSTATE};
784 use crate::provider::BrowseNodeFilter;
785
786 use std::sync::Mutex;
787 use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
788
789 #[derive(Default)]
790 struct MockState {
791 connect_count: AtomicUsize,
792 should_fail_connect: AtomicBool,
793 should_fail_write: AtomicBool,
794 should_fail_with_connection_error: AtomicBool,
795 should_panic_on_request: AtomicBool,
796 read_value: Mutex<String>,
797 }
798
799 struct ConfigurableMockConnector {
800 state: Arc<MockState>,
801 }
802
803 struct ConfigurableMockServer {
804 state: Arc<MockState>,
805 }
806
807 struct ConfigurableMockGroup {
808 state: Arc<MockState>,
809 }
810
811 impl ConnectedGroup for ConfigurableMockGroup {
812 fn add_items(
813 &self,
814 _items: &[tagOPCITEMDEF],
815 ) -> OpcResult<(
816 RemoteArray<tagOPCITEMRESULT>,
817 RemoteArray<windows::core::HRESULT>,
818 )> {
819 use windows::Win32::Foundation::S_OK;
820
821 let res = tagOPCITEMRESULT {
822 hServer: 1,
823 vtCanonicalDataType: 0,
824 wReserved: 0,
825 dwAccessRights: 1,
826 dwBlobSize: 0,
827 pBlob: std::ptr::null_mut(),
828 };
829
830 let res_ptr = unsafe {
831 windows::Win32::System::Com::CoTaskMemAlloc(std::mem::size_of::<tagOPCITEMRESULT>())
832 } as *mut tagOPCITEMRESULT;
833 unsafe {
834 std::ptr::write(res_ptr, res);
835 }
836 let res_array = RemoteArray::from_mut_ptr(res_ptr, 1);
837
838 let err_ptr = unsafe {
839 windows::Win32::System::Com::CoTaskMemAlloc(std::mem::size_of::<
840 windows::core::HRESULT,
841 >())
842 } as *mut windows::core::HRESULT;
843 unsafe {
844 std::ptr::write(err_ptr, S_OK);
845 }
846 let err_array = RemoteArray::from_mut_ptr(err_ptr, 1);
847
848 Ok((res_array, err_array))
849 }
850
851 fn read(
852 &self,
853 _source: tagOPCDATASOURCE,
854 _server_handles: &[crate::opc_da::typedefs::ItemHandle],
855 ) -> OpcResult<(
856 RemoteArray<tagOPCITEMSTATE>,
857 RemoteArray<windows::core::HRESULT>,
858 )> {
859 use windows::Win32::Foundation::S_OK;
860
861 let value = self.state.read_value.lock().unwrap().clone();
862 let item_state = tagOPCITEMSTATE {
863 hClient: 0,
864 ftTimeStamp: windows::Win32::Foundation::FILETIME::default(),
865 wQuality: 0xC0,
866 wReserved: 0,
867 vDataValue: opc_value_to_variant(&OpcValue::String(value)),
868 };
869 let state_ptr = unsafe {
870 windows::Win32::System::Com::CoTaskMemAlloc(std::mem::size_of::<tagOPCITEMSTATE>())
871 } as *mut tagOPCITEMSTATE;
872 unsafe {
873 std::ptr::write(state_ptr, item_state);
874 }
875
876 let error_ptr = unsafe {
877 windows::Win32::System::Com::CoTaskMemAlloc(std::mem::size_of::<
878 windows::core::HRESULT,
879 >())
880 } as *mut windows::core::HRESULT;
881 unsafe {
882 std::ptr::write(error_ptr, S_OK);
883 }
884
885 Ok((
886 RemoteArray::from_mut_ptr(state_ptr, 1),
887 RemoteArray::from_mut_ptr(error_ptr, 1),
888 ))
889 }
890
891 fn write(
892 &self,
893 _server_handles: &[crate::opc_da::typedefs::ItemHandle],
894 _values: &[windows::Win32::System::Variant::VARIANT],
895 ) -> OpcResult<RemoteArray<windows::core::HRESULT>> {
896 if self
897 .state
898 .should_fail_with_connection_error
899 .load(Ordering::Relaxed)
900 {
901 return Err(OpcError::Com {
903 source: windows::core::Error::from_hresult(windows::core::HRESULT(
904 0x800706BA_u32 as i32,
905 )),
906 });
907 }
908
909 let hr = if self.state.should_fail_write.load(Ordering::Relaxed) {
910 windows::Win32::Foundation::E_FAIL
911 } else {
912 windows::Win32::Foundation::S_OK
913 };
914
915 let hr_ptr = unsafe {
916 windows::Win32::System::Com::CoTaskMemAlloc(std::mem::size_of::<
917 windows::core::HRESULT,
918 >())
919 } as *mut windows::core::HRESULT;
920 unsafe {
921 std::ptr::write(hr_ptr, hr);
922 }
923
924 Ok(RemoteArray::from_mut_ptr(hr_ptr, 1))
925 }
926 }
927
928 impl ConnectedServer for ConfigurableMockServer {
929 type Group = ConfigurableMockGroup;
930
931 fn query_organization(&self) -> OpcResult<u32> {
932 Ok(0)
933 }
934
935 fn browse_opc_item_ids(
936 &self,
937 _browse_type: u32,
938 _filter: Option<&str>,
939 _data_type: u16,
940 _access_rights: u32,
941 ) -> OpcResult<StringIterator> {
942 Err(OpcError::NotImplemented("mock".into()))
943 }
944
945 fn change_browse_position(&self, _direction: u32, _name: &str) -> OpcResult<()> {
946 Ok(())
947 }
948
949 fn get_item_id(&self, _item_name: &str) -> OpcResult<String> {
950 Ok(String::new())
951 }
952
953 fn add_group(
954 &self,
955 _name: &str,
956 _active: bool,
957 _update_rate: u32,
958 _client_handle: crate::opc_da::typedefs::GroupHandle,
959 _time_bias: i32,
960 _percent_deadband: f32,
961 _locale_id: u32,
962 _revised_update_rate: &mut u32,
963 _server_handle: &mut crate::opc_da::typedefs::GroupHandle,
964 ) -> OpcResult<Self::Group> {
965 if self.state.should_panic_on_request.load(Ordering::Relaxed) {
966 panic!("Simulated worker panic");
967 }
968 Ok(ConfigurableMockGroup {
969 state: self.state.clone(),
970 })
971 }
972
973 fn remove_group(
974 &self,
975 _server_group: crate::opc_da::typedefs::GroupHandle,
976 _force: bool,
977 ) -> OpcResult<()> {
978 Ok(())
979 }
980 }
981
982 impl ServerConnector for ConfigurableMockConnector {
983 type Server = ConfigurableMockServer;
984
985 fn enumerate_servers(&self) -> OpcResult<Vec<String>> {
986 if self.state.should_fail_connect.load(Ordering::Relaxed) {
987 Err(OpcError::Internal("Server enumeration failed".into()))
988 } else {
989 Ok(vec!["Mock.Server.1".into()])
990 }
991 }
992
993 fn connect(&self, _server_name: &str) -> OpcResult<Self::Server> {
994 if self.state.should_fail_connect.load(Ordering::Relaxed) {
995 Err(OpcError::Internal("Connection failed".into()))
996 } else {
997 self.state.connect_count.fetch_add(1, Ordering::Relaxed);
998 Ok(ConfigurableMockServer {
999 state: self.state.clone(),
1000 })
1001 }
1002 }
1003 }
1004
1005 struct WorkerMockConnector;
1006 struct WorkerMockServer;
1007 struct WorkerMockGroup;
1008
1009 impl ConnectedGroup for WorkerMockGroup {
1010 fn add_items(
1011 &self,
1012 _items: &[tagOPCITEMDEF],
1013 ) -> OpcResult<(
1014 RemoteArray<tagOPCITEMRESULT>,
1015 RemoteArray<windows::core::HRESULT>,
1016 )> {
1017 Err(OpcError::NotImplemented("mock".into()))
1018 }
1019 fn read(
1020 &self,
1021 _source: tagOPCDATASOURCE,
1022 _server_handles: &[crate::opc_da::typedefs::ItemHandle],
1023 ) -> OpcResult<(
1024 RemoteArray<tagOPCITEMSTATE>,
1025 RemoteArray<windows::core::HRESULT>,
1026 )> {
1027 Err(OpcError::NotImplemented("mock".into()))
1028 }
1029 fn write(
1030 &self,
1031 _server_handles: &[crate::opc_da::typedefs::ItemHandle],
1032 _values: &[windows::Win32::System::Variant::VARIANT],
1033 ) -> OpcResult<RemoteArray<windows::core::HRESULT>> {
1034 Err(OpcError::NotImplemented("mock".into()))
1035 }
1036 }
1037
1038 impl ConnectedServer for WorkerMockServer {
1039 type Group = WorkerMockGroup;
1040 fn query_organization(&self) -> OpcResult<u32> {
1041 Err(OpcError::NotImplemented("mock".into()))
1042 }
1043 fn browse_opc_item_ids(
1044 &self,
1045 _browse_type: u32,
1046 _filter: Option<&str>,
1047 _data_type: u16,
1048 _access_rights: u32,
1049 ) -> OpcResult<StringIterator> {
1050 Err(OpcError::NotImplemented("mock".into()))
1051 }
1052 fn change_browse_position(&self, _direction: u32, _name: &str) -> OpcResult<()> {
1053 Err(OpcError::NotImplemented("mock".into()))
1054 }
1055 fn get_item_id(&self, _item_name: &str) -> OpcResult<String> {
1056 Err(OpcError::NotImplemented("mock".into()))
1057 }
1058 fn add_group(
1059 &self,
1060 _name: &str,
1061 _active: bool,
1062 _update_rate: u32,
1063 _client_handle: crate::opc_da::typedefs::GroupHandle,
1064 _time_bias: i32,
1065 _percent_deadband: f32,
1066 _locale_id: u32,
1067 _revised_update_rate: &mut u32,
1068 _server_handle: &mut crate::opc_da::typedefs::GroupHandle,
1069 ) -> OpcResult<Self::Group> {
1070 Err(OpcError::NotImplemented("mock".into()))
1071 }
1072 fn remove_group(
1073 &self,
1074 _server_group: crate::opc_da::typedefs::GroupHandle,
1075 _force: bool,
1076 ) -> OpcResult<()> {
1077 Err(OpcError::NotImplemented("mock".into()))
1078 }
1079 }
1080
1081 impl ServerConnector for WorkerMockConnector {
1082 type Server = WorkerMockServer;
1083 fn enumerate_servers(&self) -> OpcResult<Vec<String>> {
1084 Ok(vec!["Mock.Server.1".into()])
1085 }
1086 fn connect(&self, _server_name: &str) -> OpcResult<Self::Server> {
1087 Ok(WorkerMockServer)
1088 }
1089 }
1090
1091 #[tokio::test]
1092 async fn test_worker_starts_and_stops() {
1093 let worker = tokio::task::spawn_blocking(|| {
1094 ComWorker::start(Arc::new(WorkerMockConnector)).unwrap()
1095 })
1096 .await
1097 .unwrap();
1098 drop(worker);
1099 }
1100
1101 #[tokio::test]
1102 async fn test_worker_list_servers() {
1103 let worker = tokio::task::spawn_blocking(|| {
1104 ComWorker::start(Arc::new(WorkerMockConnector)).unwrap()
1105 })
1106 .await
1107 .unwrap();
1108 let (reply, _rx) = oneshot::channel();
1109 worker
1110 .sender
1111 .send(ComRequest::ListServers {
1112 host: "localhost".into(),
1113 reply,
1114 })
1115 .await
1116 .unwrap();
1117 }
1119
1120 struct MismatchedConnector;
1121 struct MismatchedServer;
1122 struct MismatchedGroup;
1123
1124 impl ConnectedGroup for MismatchedGroup {
1125 fn add_items(
1126 &self,
1127 _items: &[tagOPCITEMDEF],
1128 ) -> OpcResult<(
1129 RemoteArray<tagOPCITEMRESULT>,
1130 RemoteArray<windows::core::HRESULT>,
1131 )> {
1132 Ok((RemoteArray::empty(), RemoteArray::empty()))
1133 }
1134 fn read(
1135 &self,
1136 _source: tagOPCDATASOURCE,
1137 _server_handles: &[crate::opc_da::typedefs::ItemHandle],
1138 ) -> OpcResult<(
1139 RemoteArray<tagOPCITEMSTATE>,
1140 RemoteArray<windows::core::HRESULT>,
1141 )> {
1142 Ok((RemoteArray::empty(), RemoteArray::empty()))
1143 }
1144 fn write(
1145 &self,
1146 _server_handles: &[crate::opc_da::typedefs::ItemHandle],
1147 _values: &[windows::Win32::System::Variant::VARIANT],
1148 ) -> OpcResult<RemoteArray<windows::core::HRESULT>> {
1149 Ok(RemoteArray::empty())
1150 }
1151 }
1152
1153 impl ConnectedServer for MismatchedServer {
1154 type Group = MismatchedGroup;
1155 fn query_organization(&self) -> OpcResult<u32> {
1156 Ok(0)
1157 }
1158 fn browse_opc_item_ids(
1159 &self,
1160 _b: u32,
1161 _f: Option<&str>,
1162 _d: u16,
1163 _a: u32,
1164 ) -> OpcResult<StringIterator> {
1165 Err(OpcError::NotImplemented("mock".into()))
1166 }
1167 fn change_browse_position(&self, _direction: u32, _name: &str) -> OpcResult<()> {
1168 Ok(())
1169 }
1170 fn get_item_id(&self, _item_name: &str) -> OpcResult<String> {
1171 Ok(String::new())
1172 }
1173 fn add_group(
1174 &self,
1175 _name: &str,
1176 _active: bool,
1177 _update_rate: u32,
1178 _client_handle: crate::opc_da::typedefs::GroupHandle,
1179 _time_bias: i32,
1180 _percent_deadband: f32,
1181 _locale_id: u32,
1182 _revised_update_rate: &mut u32,
1183 _server_handle: &mut crate::opc_da::typedefs::GroupHandle,
1184 ) -> OpcResult<Self::Group> {
1185 Ok(MismatchedGroup)
1186 }
1187 fn remove_group(
1188 &self,
1189 _server_group: crate::opc_da::typedefs::GroupHandle,
1190 _force: bool,
1191 ) -> OpcResult<()> {
1192 Ok(())
1193 }
1194 }
1195
1196 impl ServerConnector for MismatchedConnector {
1197 type Server = MismatchedServer;
1198 fn enumerate_servers(&self) -> OpcResult<Vec<String>> {
1199 Ok(vec![])
1200 }
1201 fn connect(&self, _server_name: &str) -> OpcResult<Self::Server> {
1202 Ok(MismatchedServer)
1203 }
1204 }
1205
1206 #[tokio::test]
1207 async fn test_worker_read_tag_values_mismatched_lengths() {
1208 let worker = tokio::task::spawn_blocking(|| {
1209 ComWorker::start(Arc::new(MismatchedConnector)).unwrap()
1210 })
1211 .await
1212 .unwrap();
1213
1214 let result = worker
1215 .send_request(|reply| ComRequest::ReadTagValues {
1216 server: "MockServer".to_string(),
1217 tag_ids: vec!["Tag1".to_string(), "Tag2".to_string()],
1218 presentation: ReadPresentation::Semantic,
1219 reply,
1220 })
1221 .await;
1222
1223 assert!(
1224 result.is_err(),
1225 "Expected read to fail due to mismatched lengths"
1226 );
1227 if let Err(OpcError::Internal(msg)) = result {
1228 assert!(msg.contains("mismatched result array sizes"));
1229 } else {
1230 panic!("Expected OpcError::Internal, got {:?}", result);
1231 }
1232 }
1233
1234 #[tokio::test]
1235 async fn test_worker_routes_read_presentation() {
1236 let state = Arc::new(MockState {
1237 read_value: Mutex::new("AUT".to_string()),
1238 ..MockState::default()
1239 });
1240 let connector = Arc::new(ConfigurableMockConnector { state });
1241 let worker = tokio::task::spawn_blocking(move || ComWorker::start(connector).unwrap())
1242 .await
1243 .unwrap();
1244
1245 let semantic = worker
1246 .send_request(|reply| ComRequest::ReadTagValues {
1247 server: "Mock.Server.1".to_string(),
1248 tag_ids: vec!["StringTag".to_string()],
1249 presentation: ReadPresentation::Semantic,
1250 reply,
1251 })
1252 .await
1253 .unwrap();
1254 assert_eq!(semantic[0].value, "AUT");
1255
1256 let display = worker
1257 .send_request(|reply| ComRequest::ReadTagValues {
1258 server: "Mock.Server.1".to_string(),
1259 tag_ids: vec!["StringTag".to_string()],
1260 presentation: ReadPresentation::Display,
1261 reply,
1262 })
1263 .await
1264 .unwrap();
1265 assert_eq!(display[0].value, "\"AUT\"");
1266 }
1267
1268 #[tokio::test]
1269 async fn test_worker_write_tag_value() {
1270 let state = Arc::new(MockState::default());
1271 let connector = Arc::new(ConfigurableMockConnector {
1272 state: state.clone(),
1273 });
1274 let worker = tokio::task::spawn_blocking(move || ComWorker::start(connector).unwrap())
1275 .await
1276 .unwrap();
1277
1278 let result = worker
1279 .send_request(|reply| ComRequest::WriteTagValue {
1280 server: "Mock.Server.1".to_string(),
1281 tag_id: "Random.Int4".to_string(),
1282 value: OpcValue::Int(42),
1283 reply,
1284 })
1285 .await
1286 .expect("Request should succeed");
1287
1288 assert_eq!(result.tag_id, "Random.Int4");
1289 assert!(result.success, "Write should be successful");
1290 assert!(result.error.is_none());
1291 }
1292
1293 #[tokio::test]
1294 async fn test_connection_cache_reuse() {
1295 let state = Arc::new(MockState::default());
1296 let connector = Arc::new(ConfigurableMockConnector {
1297 state: state.clone(),
1298 });
1299 let worker = tokio::task::spawn_blocking(move || ComWorker::start(connector).unwrap())
1300 .await
1301 .unwrap();
1302
1303 let _ = worker
1304 .send_request(|reply| ComRequest::WriteTagValue {
1305 server: "Mock.Server.1".to_string(),
1306 tag_id: "Tag1".to_string(),
1307 value: OpcValue::Int(1),
1308 reply,
1309 })
1310 .await
1311 .unwrap();
1312
1313 let _ = worker
1314 .send_request(|reply| ComRequest::WriteTagValue {
1315 server: "Mock.Server.1".to_string(),
1316 tag_id: "Tag2".to_string(),
1317 value: OpcValue::Int(2),
1318 reply,
1319 })
1320 .await
1321 .unwrap();
1322
1323 assert_eq!(
1324 state.connect_count.load(Ordering::Relaxed),
1325 1,
1326 "Server connection should be cached and reused"
1327 );
1328 }
1329
1330 #[tokio::test]
1331 async fn test_stale_connection_eviction() {
1332 let state = Arc::new(MockState::default());
1333 let connector = Arc::new(ConfigurableMockConnector {
1334 state: state.clone(),
1335 });
1336 let worker = tokio::task::spawn_blocking(move || ComWorker::start(connector).unwrap())
1337 .await
1338 .unwrap();
1339
1340 let _ = worker
1342 .send_request(|reply| ComRequest::WriteTagValue {
1343 server: "Mock.Server.1".to_string(),
1344 tag_id: "Tag1".to_string(),
1345 value: OpcValue::Int(1),
1346 reply,
1347 })
1348 .await
1349 .unwrap();
1350
1351 assert_eq!(state.connect_count.load(Ordering::Relaxed), 1);
1352
1353 state
1355 .should_fail_with_connection_error
1356 .store(true, Ordering::Relaxed);
1357
1358 let _ = worker
1360 .send_request(|reply| ComRequest::WriteTagValue {
1361 server: "Mock.Server.1".to_string(),
1362 tag_id: "Tag2".to_string(),
1363 value: OpcValue::Int(2),
1364 reply,
1365 })
1366 .await;
1367
1368 assert_eq!(
1369 state.connect_count.load(Ordering::Relaxed),
1370 2,
1371 "Stale connection should be evicted and reconnected"
1372 );
1373 }
1374
1375 #[tokio::test]
1376 async fn test_worker_panic_propagation() {
1377 let state = Arc::new(MockState::default());
1378 state.should_panic_on_request.store(true, Ordering::Relaxed);
1379 let connector = Arc::new(ConfigurableMockConnector {
1380 state: state.clone(),
1381 });
1382 let worker = tokio::task::spawn_blocking(move || ComWorker::start(connector).unwrap())
1383 .await
1384 .unwrap();
1385
1386 let result = worker
1387 .send_request(|reply| ComRequest::WriteTagValue {
1388 server: "Mock.Server.1".to_string(),
1389 tag_id: "Tag1".to_string(),
1390 value: OpcValue::Int(1),
1391 reply,
1392 })
1393 .await;
1394
1395 assert!(result.is_err());
1396 if let Err(OpcError::Internal(msg)) = result {
1397 assert!(
1398 msg.contains("shut down")
1399 || msg.contains("channel closed")
1400 || msg.contains("panicked"),
1401 "Expected worker termination message, got: {}",
1402 msg
1403 );
1404 } else {
1405 panic!("Expected OpcError::Internal, got {:?}", result);
1406 }
1407 }
1408
1409 #[tokio::test]
1410 async fn test_drop_during_active_request() {
1411 let state = Arc::new(MockState::default());
1412 let connector = Arc::new(ConfigurableMockConnector {
1413 state: state.clone(),
1414 });
1415 let worker = tokio::task::spawn_blocking(move || ComWorker::start(connector).unwrap())
1416 .await
1417 .unwrap();
1418
1419 drop(worker);
1421 }
1422
1423 #[tokio::test]
1424 async fn test_worker_init_failure() {
1425 struct FailingInitConnector;
1426 impl ServerConnector for FailingInitConnector {
1427 type Server = ConfigurableMockServer;
1428 fn enumerate_servers(&self) -> OpcResult<Vec<String>> {
1429 Err(OpcError::Internal("COM subsystem failed".into()))
1430 }
1431 fn connect(&self, _name: &str) -> OpcResult<Self::Server> {
1432 Err(OpcError::Internal("COM subsystem failed".into()))
1433 }
1434 }
1435
1436 let worker = tokio::task::spawn_blocking(|| {
1437 ComWorker::start(Arc::new(FailingInitConnector)).unwrap()
1438 })
1439 .await
1440 .unwrap();
1441
1442 let result = worker
1443 .send_request(|reply| ComRequest::ListServers {
1444 host: "localhost".into(),
1445 reply,
1446 })
1447 .await;
1448
1449 assert!(
1450 result.is_err(),
1451 "ListServers request should fail when connector enumeration fails"
1452 );
1453 }
1454
1455 #[derive(Default)]
1456 struct BranchOnlyFlatState {
1457 flat_calls: AtomicUsize,
1458 position: Mutex<Vec<String>>,
1459 }
1460
1461 struct BranchOnlyFlatConnector {
1462 state: Arc<BranchOnlyFlatState>,
1463 }
1464
1465 struct BranchOnlyFlatServer {
1466 state: Arc<BranchOnlyFlatState>,
1467 }
1468
1469 impl ConnectedServer for BranchOnlyFlatServer {
1470 type Group = WorkerMockGroup;
1471
1472 fn query_organization(&self) -> OpcResult<u32> {
1473 Ok(crate::bindings::da::OPC_NS_HIERARCHIAL.0.cast_unsigned())
1474 }
1475
1476 fn browse_opc_item_ids(
1477 &self,
1478 _browse_type: u32,
1479 _filter: Option<&str>,
1480 _data_type: u16,
1481 _access_rights: u32,
1482 ) -> OpcResult<StringIterator> {
1483 Err(OpcError::NotImplemented("mock".to_string()))
1484 }
1485
1486 fn begin_da2_browse(
1487 &self,
1488 browse_type: u32,
1489 _filter: Option<&str>,
1490 _data_type: u16,
1491 _access_rights: u32,
1492 ) -> OpcResult<Box<dyn BrowseStringIterator>> {
1493 let position = self.state.position.lock().unwrap();
1494 let values = if browse_type == OPC_FLAT.0.cast_unsigned() {
1495 self.state.flat_calls.fetch_add(1, Ordering::Relaxed);
1496 vec!["Area".to_string()]
1497 } else if browse_type == OPC_BRANCH.0.cast_unsigned() && position.is_empty() {
1498 vec!["Area".to_string()]
1499 } else if browse_type == OPC_LEAF.0.cast_unsigned() && position.as_slice() == ["Area"] {
1500 vec!["Tag".to_string()]
1501 } else {
1502 vec![]
1503 };
1504 Ok(Box::new(values.into_iter().map(Ok)))
1505 }
1506
1507 fn change_browse_position(&self, direction: u32, name: &str) -> OpcResult<()> {
1508 let mut position = self.state.position.lock().unwrap();
1509 if direction == OPC_BROWSE_DOWN.0.cast_unsigned() {
1510 position.push(name.to_string());
1511 } else if direction == OPC_BROWSE_UP.0.cast_unsigned() {
1512 position.pop();
1513 }
1514 drop(position);
1515 Ok(())
1516 }
1517
1518 fn get_item_id(&self, item_name: &str) -> OpcResult<String> {
1519 let position = self.state.position.lock().unwrap();
1520 let item_id = format!("{}.{}", position.join("."), item_name);
1521 drop(position);
1522 Ok(item_id)
1523 }
1524
1525 fn add_group(
1526 &self,
1527 _name: &str,
1528 _active: bool,
1529 _update_rate: u32,
1530 _client_handle: GroupHandle,
1531 _time_bias: i32,
1532 _percent_deadband: f32,
1533 _locale_id: u32,
1534 _revised_update_rate: &mut u32,
1535 _server_handle: &mut GroupHandle,
1536 ) -> OpcResult<Self::Group> {
1537 Err(OpcError::NotImplemented("mock".to_string()))
1538 }
1539
1540 fn remove_group(&self, _server_group: GroupHandle, _force: bool) -> OpcResult<()> {
1541 Err(OpcError::NotImplemented("mock".to_string()))
1542 }
1543 }
1544
1545 impl ServerConnector for BranchOnlyFlatConnector {
1546 type Server = BranchOnlyFlatServer;
1547
1548 fn enumerate_servers(&self) -> OpcResult<Vec<String>> {
1549 Ok(vec![])
1550 }
1551
1552 fn connect(&self, _server_name: &str) -> OpcResult<Self::Server> {
1553 Ok(BranchOnlyFlatServer {
1554 state: self.state.clone(),
1555 })
1556 }
1557 }
1558
1559 #[tokio::test]
1560 async fn hierarchical_browse_does_not_treat_branch_only_opc_flat_as_items() {
1561 let state = Arc::new(BranchOnlyFlatState::default());
1562 let connector = Arc::new(BranchOnlyFlatConnector {
1563 state: state.clone(),
1564 });
1565 let worker = tokio::task::spawn_blocking(move || ComWorker::start(connector).unwrap())
1566 .await
1567 .unwrap();
1568
1569 let result = worker
1570 .send_request(|reply| ComRequest::BrowseTags {
1571 server: "Mock.Server".to_string(),
1572 max_tags: 10,
1573 progress: Arc::new(AtomicUsize::new(0)),
1574 tags_sink: Arc::new(Mutex::new(Vec::new())),
1575 reply,
1576 })
1577 .await
1578 .unwrap();
1579
1580 assert_eq!(result, vec!["Area.Tag"]);
1581 assert_eq!(state.flat_calls.load(Ordering::Relaxed), 0);
1582 }
1583
1584 #[derive(Default)]
1585 struct CancelledBrowseState {
1586 connect_count: AtomicUsize,
1587 drop_count: AtomicUsize,
1588 }
1589
1590 struct CancelledBrowseConnector {
1591 state: Arc<CancelledBrowseState>,
1592 }
1593
1594 struct CancelledBrowseServer {
1595 state: Arc<CancelledBrowseState>,
1596 }
1597
1598 impl Drop for CancelledBrowseServer {
1599 fn drop(&mut self) {
1600 self.state.drop_count.fetch_add(1, Ordering::Relaxed);
1601 }
1602 }
1603
1604 impl ConnectedServer for CancelledBrowseServer {
1605 type Group = WorkerMockGroup;
1606
1607 fn query_organization(&self) -> OpcResult<u32> {
1608 Ok(OPC_NS_FLAT.0.cast_unsigned())
1609 }
1610
1611 fn browse_opc_item_ids(
1612 &self,
1613 _browse_type: u32,
1614 _filter: Option<&str>,
1615 _data_type: u16,
1616 _access_rights: u32,
1617 ) -> OpcResult<StringIterator> {
1618 Err(OpcError::NotImplemented("mock".to_string()))
1619 }
1620
1621 fn begin_da2_browse(
1622 &self,
1623 _browse_type: u32,
1624 _filter: Option<&str>,
1625 _data_type: u16,
1626 _access_rights: u32,
1627 ) -> OpcResult<Box<dyn BrowseStringIterator>> {
1628 Ok(Box::new(std::iter::empty()))
1629 }
1630
1631 fn change_browse_position(&self, _direction: u32, _name: &str) -> OpcResult<()> {
1632 Ok(())
1633 }
1634
1635 fn get_item_id(&self, _item_name: &str) -> OpcResult<String> {
1636 Err(OpcError::NotImplemented("mock".to_string()))
1637 }
1638
1639 fn add_group(
1640 &self,
1641 _name: &str,
1642 _active: bool,
1643 _update_rate: u32,
1644 _client_handle: GroupHandle,
1645 _time_bias: i32,
1646 _percent_deadband: f32,
1647 _locale_id: u32,
1648 _revised_update_rate: &mut u32,
1649 _server_handle: &mut GroupHandle,
1650 ) -> OpcResult<Self::Group> {
1651 Err(OpcError::NotImplemented("mock".to_string()))
1652 }
1653
1654 fn remove_group(&self, _server_group: GroupHandle, _force: bool) -> OpcResult<()> {
1655 Ok(())
1656 }
1657 }
1658
1659 impl ServerConnector for CancelledBrowseConnector {
1660 type Server = CancelledBrowseServer;
1661
1662 fn enumerate_servers(&self) -> OpcResult<Vec<String>> {
1663 Ok(vec![])
1664 }
1665
1666 fn connect(&self, _server_name: &str) -> OpcResult<Self::Server> {
1667 self.state.connect_count.fetch_add(1, Ordering::Relaxed);
1668 Ok(CancelledBrowseServer {
1669 state: self.state.clone(),
1670 })
1671 }
1672 }
1673
1674 #[tokio::test]
1675 async fn cancelled_native_browse_requests_release_or_avoid_sessions() {
1676 let state = Arc::new(CancelledBrowseState::default());
1677 let connector = Arc::new(CancelledBrowseConnector {
1678 state: state.clone(),
1679 });
1680 let worker = tokio::task::spawn_blocking(move || ComWorker::start(connector).unwrap())
1681 .await
1682 .unwrap();
1683
1684 let session = worker
1685 .send_request(|reply| ComRequest::OpenBrowseSession {
1686 server: "Mock.Server".to_string(),
1687 reply,
1688 })
1689 .await
1690 .unwrap();
1691 assert_eq!(state.connect_count.load(Ordering::Relaxed), 1);
1692
1693 let (page_reply, page_receiver) = oneshot::channel();
1694 drop(page_receiver);
1695 worker
1696 .sender
1697 .send(ComRequest::BrowsePage {
1698 session,
1699 request: BrowsePageRequest {
1700 parent: None,
1701 filter: BrowseNodeFilter::All,
1702 max_elements: 10,
1703 continuation: None,
1704 },
1705 reply: page_reply,
1706 })
1707 .await
1708 .unwrap();
1709 worker
1710 .send_request(|reply| ComRequest::ListServers {
1711 host: "localhost".to_string(),
1712 reply,
1713 })
1714 .await
1715 .unwrap();
1716 assert_eq!(state.drop_count.load(Ordering::Relaxed), 1);
1717
1718 let (open_reply, open_receiver) = oneshot::channel();
1719 drop(open_receiver);
1720 worker
1721 .sender
1722 .send(ComRequest::OpenBrowseSession {
1723 server: "Mock.Server".to_string(),
1724 reply: open_reply,
1725 })
1726 .await
1727 .unwrap();
1728 worker
1729 .send_request(|reply| ComRequest::ListServers {
1730 host: "localhost".to_string(),
1731 reply,
1732 })
1733 .await
1734 .unwrap();
1735 assert_eq!(state.connect_count.load(Ordering::Relaxed), 1);
1736 }
1737}