Skip to main content

opc_da_client/
com_worker.rs

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
20/// Represents a asynchronous request dispatched to the COM worker thread.
21pub enum ComRequest {
22    /// Request to enumerate available OPC DA servers on a host.
23    ListServers {
24        /// Hostname or IP address to target.
25        host: String,
26        /// One-shot channel to send back the server enumeration result.
27        reply: oneshot::Sender<OpcResult<Vec<String>>>,
28    },
29    /// Request to read current values, quality, and timestamps for tag IDs.
30    ReadTagValues {
31        /// OPC server ProgID.
32        server: String,
33        /// List of fully qualified tag identifiers to read.
34        tag_ids: Vec<String>,
35        /// One-shot channel to send back the tag values result.
36        reply: oneshot::Sender<OpcResult<Vec<TagValue>>>,
37    },
38    /// Request to write a typed value to a single tag.
39    WriteTagValue {
40        /// OPC server ProgID.
41        server: String,
42        /// Tag identifier to write.
43        tag_id: String,
44        /// Typed value to write.
45        value: OpcValue,
46        /// One-shot channel to send back the write operation result.
47        reply: oneshot::Sender<OpcResult<WriteResult>>,
48    },
49    /// Request to recursively browse available tags on a server.
50    BrowseTags {
51        /// OPC server ProgID.
52        server: String,
53        /// Maximum number of tags to discover before stopping.
54        max_tags: usize,
55        /// Atomic counter tracking total tags discovered.
56        progress: Arc<AtomicUsize>,
57        /// Shared mutex-protected vector storing discovered tag names incrementally.
58        tags_sink: Arc<std::sync::Mutex<Vec<String>>>,
59        /// One-shot channel to send back the complete tag discovery list.
60        reply: oneshot::Sender<OpcResult<Vec<String>>>,
61    },
62    /// Request the native browse capabilities of a server.
63    BrowseCapabilities {
64        /// OPC server ProgID.
65        server: String,
66        /// One-shot channel to send back the capabilities.
67        reply: oneshot::Sender<OpcResult<BrowseCapabilities>>,
68    },
69    /// Open an isolated native browse session.
70    OpenBrowseSession {
71        /// OPC server ProgID.
72        server: String,
73        /// One-shot channel to send back the opaque session token.
74        reply: oneshot::Sender<OpcResult<BrowseSessionToken>>,
75    },
76    /// Request one bounded native browse page.
77    BrowsePage {
78        /// Opaque browse session token.
79        session: BrowseSessionToken,
80        /// One-level browse request.
81        request: BrowsePageRequest,
82        /// One-shot channel to send back the page.
83        reply: oneshot::Sender<OpcResult<BrowsePage>>,
84    },
85    /// Close an isolated native browse session.
86    CloseBrowseSession {
87        /// Opaque browse session token.
88        session: BrowseSessionToken,
89        /// One-shot channel to report completion.
90        reply: oneshot::Sender<OpcResult<()>>,
91    },
92}
93
94/// Dedicated background worker thread manager handling COM MTA apartment thread affinity.
95///
96/// Dispatches requests received over an `mpsc` channel to Windows COM interfaces while maintaining
97/// a persistent connection pool and transparently evicting stale connection handles on RPC errors.
98pub struct ComWorker<C: ServerConnector + 'static> {
99    /// Channel sender for dispatching requests to the worker loop.
100    pub sender: mpsc::Sender<ComRequest>,
101    /// Thread join handle for clean worker thread teardown.
102    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    /// Creates a dummy/closed `ComWorker` handle used when background worker initialization fails.
121    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        // RemoteArray::len() returns u32; tag_ids.len() returns usize.
415        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                // RPC server unavailable (0x800706BA) triggers connection eviction
853                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        // Wait for implementation
1069    }
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        // Initial connect
1257        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        // Enable connection error flag to trigger eviction on next operation
1270        state
1271            .should_fail_with_connection_error
1272            .store(true, Ordering::Relaxed);
1273
1274        // Next request triggers eviction and reconnect attempt
1275        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        // Dropping worker handle closes channel gracefully
1336        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}