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