Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add method to implement translate browse paths using browse #23

Merged
merged 1 commit into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions lib/tests/integration/browse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use opcua::{
RelativePathElement, StatusCode, VariableTypeId,
},
};
use opcua_types::{AttributeId, ReadValueId, TimestampsToReturn, Variant};

fn hierarchical_desc(node_id: NodeId) -> BrowseDescription {
BrowseDescription {
Expand Down Expand Up @@ -518,3 +519,63 @@ async fn translate_browse_paths_limits() {
.unwrap_err();
assert_eq!(r, StatusCode::BadTooManyOperations);
}

#[tokio::test]
async fn translate_browse_paths_auto_impl() {
let (_tester, _nm, session) = setup().await;

// Do a translate browse paths call into the diagnostics node manager.
let r = session
.translate_browse_paths_to_node_ids(&[BrowsePath {
starting_node: ObjectId::Server.into(),
relative_path: RelativePath {
elements: Some(
[
(0, "Namespaces"),
(2, "urn:rustopcuatestserver"),
(0, "DefaultAccessRestrictions"),
]
.iter()
.map(|v| RelativePathElement {
reference_type_id: ReferenceTypeId::HierarchicalReferences.into(),
is_inverse: false,
include_subtypes: true,
target_name: opcua_types::QualifiedName {
namespace_index: v.0,
name: v.1.into(),
},
})
.collect(),
),
},
}])
.await
.unwrap();
assert_eq!(1, r.len());
let it = &r[0];
assert_eq!(StatusCode::Good, it.status_code);
let targets = it.targets.clone().unwrap_or_default();
assert_eq!(1, targets.len());
let t = &targets[0];
assert_eq!(t.remaining_path_index, u32::MAX);
let node_id = t.target_id.node_id.clone();

let r = session
.read(
&[ReadValueId {
node_id,
attribute_id: AttributeId::DisplayName as u32,
..Default::default()
}],
TimestampsToReturn::Both,
0.0,
)
.await
.unwrap();
assert_eq!(
r[0].value,
Some(Variant::LocalizedText(Box::new(
"DefaultAccessRestrictions".into()
)))
);
}
14 changes: 11 additions & 3 deletions opcua-server/src/node_manager/memory/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ use crate::{
as_opaque_node_id,
build::NodeManagerBuilder,
from_opaque_node_id,
view::{AddReferenceResult, NodeMetadata},
BrowseNode, DefaultTypeTree, DynNodeManager, NodeManager, NodeManagersRef, ReadNode,
RequestContext, ServerContext, SyncSampler,
view::{impl_translate_browse_paths_using_browse, AddReferenceResult, NodeMetadata},
BrowseNode, BrowsePathItem, DefaultTypeTree, DynNodeManager, NodeManager, NodeManagersRef,
ReadNode, RequestContext, ServerContext, SyncSampler,
},
};
use opcua_types::{
Expand Down Expand Up @@ -646,4 +646,12 @@ impl NodeManager for DiagnosticsNodeManager {
}
Ok(())
}

async fn translate_browse_paths_to_node_ids(
&self,
context: &RequestContext,
nodes: &mut [&mut BrowsePathItem],
) -> Result<(), StatusCode> {
impl_translate_browse_paths_using_browse(self, context, nodes).await
}
}
142 changes: 140 additions & 2 deletions opcua-server/src/node_manager/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ use opcua_crypto::random;
use opcua_nodes::TypeTree;
use opcua_types::{
BrowseDescription, BrowseDescriptionResultMask, BrowseDirection, BrowsePath, BrowseResult,
ByteString, ExpandedNodeId, LocalizedText, NodeClass, NodeClassMask, NodeId, QualifiedName,
ReferenceDescription, RelativePathElement, StatusCode,
BrowseResultMask, ByteString, ExpandedNodeId, LocalizedText, NodeClass, NodeClassMask, NodeId,
QualifiedName, ReferenceDescription, RelativePathElement, StatusCode,
};

use super::{NodeManager, RequestContext};

#[derive(Debug, Clone)]
/// Object describing a node with sufficient context to construct
/// a `ReferenceDescription`.
Expand Down Expand Up @@ -709,3 +711,139 @@ impl RegisterNodeItem {
}
}
}

/// Implementation of translate_browse_path implemented by repeatedly calling browse.
/// Note that this is always less efficient than a dedicated implementation, but
/// for simple node managers it may be a simple solution to get translate browse paths support
/// without a complex implementation.
///
/// Arguments are simply the inputs to translate_browse_paths on `NodeManager`.
pub async fn impl_translate_browse_paths_using_browse<'a>(
mgr: &(impl NodeManager + Send + Sync + 'static),
context: &RequestContext,
nodes: &mut [&mut BrowsePathItem<'a>],
) -> Result<(), StatusCode> {
// For unmatched browse names we first need to check if the node exists.
let mut to_get_metadata: Vec<_> = nodes
.iter_mut()
.filter(|n| n.unmatched_browse_name().is_some())
.map(|r| {
let id = r.node_id().clone();
(
r,
ExternalReferenceRequest::new(
&id,
BrowseDescriptionResultMask::RESULT_MASK_BROWSE_NAME,
),
)
})
.collect();
let mut items_ref: Vec<_> = to_get_metadata.iter_mut().map(|r| &mut r.1).collect();
mgr.resolve_external_references(context, &mut items_ref)
.await;
for (node, ext) in to_get_metadata {
let Some(i) = ext.item else {
continue;
};
if &i.browse_name == node.unmatched_browse_name().unwrap() {
node.set_browse_name_matched(context.current_node_manager_index);
}
}

// Start with a map from the node ID to browse, to the index in the original nodes array.
let mut current_targets = HashMap::new();
for (idx, item) in nodes.iter_mut().enumerate() {
// If the node is still unmatched, don't use it.
if item.unmatched_browse_name.is_some() {
continue;
}
current_targets.insert(item.node_id().clone(), idx);
}
let mut next_targets = HashMap::new();
let mut depth = 0;
loop {
// If we are out of targets, we've reached the end.
if current_targets.is_empty() {
break;
}

// For each target, make a browse node.
let mut targets = Vec::with_capacity(current_targets.len());
let mut target_idx_map = HashMap::new();
for (id, target) in current_targets.iter() {
let node = &mut nodes[*target];
let elem = &node.path()[depth];
target_idx_map.insert(targets.len(), *target);
targets.push(BrowseNode::new(
BrowseDescription {
node_id: id.clone(),
browse_direction: if elem.is_inverse {
BrowseDirection::Inverse
} else {
BrowseDirection::Forward
},
reference_type_id: elem.reference_type_id.clone(),
include_subtypes: elem.include_subtypes,
node_class_mask: NodeClassMask::all().bits(),
result_mask: BrowseResultMask::BrowseName as u32,
},
context
.info
.config
.limits
.operational
.max_references_per_browse_node,
*target,
));
}
mgr.browse(context, &mut targets).await?;

// Call browse until the results are exhausted.
let mut next = targets;
loop {
let mut next_t = Vec::with_capacity(next.len());
for (idx, mut target) in next.into_iter().enumerate() {
// For each node we just browsed, drain the references and external references
// and produce path target elements from them.
let orig_idx = target_idx_map[&idx];
let path_target = &mut nodes[orig_idx];
let path_elem = &path_target.path[depth];
for it in target.references.drain(..) {
// If we found a reference, add it as a target.
if it.browse_name == path_elem.target_name {
// If the path is empty, we shouldn't browse any further.
if path_target.path.len() > depth + 1 {
next_targets.insert(it.node_id.node_id.clone(), orig_idx);
}
path_target.add_element(it.node_id.node_id, depth + 1, None);
}
}
// External references are added as unmatched nodes.
for ext in target.external_references.drain(..) {
path_target.add_element(
ext.target_id.node_id,
depth + 1,
Some(path_elem.target_name.clone()),
);
}

if target.next_continuation_point.is_some() {
target.input_continuation_point = target.next_continuation_point;
target.next_continuation_point = None;
next_t.push(target);
}
}
if next_t.is_empty() {
break;
}
mgr.browse(context, &mut next_t).await?;
next = next_t;
}
std::mem::swap(&mut current_targets, &mut next_targets);
next_targets.clear();

depth += 1;
}

Ok(())
}
7 changes: 2 additions & 5 deletions opcua-server/src/session/services/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -362,10 +362,9 @@ pub async fn translate_browse_paths(
// Item has not yet been marked bad, meaning it failed to resolve somewhere it should.
it.status().is_good()
// Either it's from a previous node manager,
&& (it.node_manager_index() < idx
&& (it.node_manager_index() < idx && it.iteration_number() == iteration
// Or it's not from a later node manager in the previous iteration.
|| it.node_manager_index() > idx
&& it.iteration_number() == iteration - 1)
|| it.node_manager_index() > idx && it.iteration_number() == iteration - 1)
// Or it may be an external reference with an unmatched browse name.
&& (!it.path().is_empty() || it.unmatched_browse_name().is_some() && mgr.owns_node(it.node_id()))
})
Expand Down Expand Up @@ -417,8 +416,6 @@ pub async fn translate_browse_paths(
}
any_new_items_in_iteration = false;
}

idx = (idx + 1) % node_managers.len();
}
// Collect all final paths.
let mut results: Vec<_> = items
Expand Down
2 changes: 1 addition & 1 deletion samples/server.conf
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ pki_dir: ./pki
# discovery_server_url: opc.tcp://localhost:4840/UADiscovery
tcp_config:
hello_timeout: 5
host: localhost
host: 127.0.0.1
port: 4855
limits:
clients_can_modify_address_space: false
Expand Down
Loading