Browse Source

feat(ns): Add possibility to blacklist nodes from nodeset generator

Blacklisting nodes is now possible through a special blacklist file.
If a node is blacklisted, the node itself and all references to that node are deleted.

Example blacklist file content for blacklisting specific node ids

(lines starting with `#` are comments/deactivated)

```text
i=79
i=83
i=94
#i=95
#i=96
i=97
i=98
i=99

ns=http://opcfoundation.org/UA/DI/;i=6555
ns=http://opcfoundation.org/UA/DI/;i=6564
#ns=http://opcfoundation.org/UA/DI/;i=15031
#ns=http://opcfoundation.org/UA/DI/;i=15032
```
Stefan Profanter 4 years ago
parent
commit
f68e134460

+ 8 - 0
CMakeLists.txt

@@ -291,6 +291,13 @@ mark_as_advanced(UA_MSVC_FORCE_STATIC_CRT)
 option(UA_FILE_NS0 "Override the NodeSet xml file used to generate namespace zero")
 mark_as_advanced(UA_FILE_NS0)
 
+# Blacklist file passed as --blacklist to the nodeset compiler. All the given nodes will be removed from the generated
+# nodeset, including all the references to and from that node. The format is a node id per line.
+# Supported formats: "i=123" (for NS0), "ns=2;s=asdf" (matches NS2 in that specific file), or recommended
+# "ns=http://opcfoundation.org/UA/DI/;i=123" namespace index independent node id
+option(UA_FILE_NS0_BLACKLIST "File containing blacklisted nodes which should not be included in the generated nodeset code.")
+mark_as_advanced(UA_FILE_NS0_BLACKLIST)
+
 # Semaphores/file system may not be available on embedded devices. It can be
 # disabled with the following option
 option(UA_ENABLE_DISCOVERY_SEMAPHORE "Enable Discovery Semaphore support" ON)
@@ -988,6 +995,7 @@ ua_generate_nodeset(
     NAME "ns0"
     FILE ${UA_FILE_NODESETS} ${UA_NODESET_FILE_DA}
     INTERNAL
+    BLACKLIST ${UA_FILE_NS0_BLACKLIST}
     IGNORE "${PROJECT_SOURCE_DIR}/tools/nodeset_compiler/NodeID_NS0_Base.txt"
     DEPENDS_TARGET "open62541-generator-types"
 )

+ 26 - 3
tools/cmake/macros_public.cmake

@@ -237,6 +237,10 @@ endfunction()
 #   [OUTPUT_DIR]    Optional target directory for the generated files. Default is '${PROJECT_BINARY_DIR}/src_generated'
 #   [IGNORE]        Optional file containing a list of node ids which should be ignored. The file should have one id per line.
 #   [TARGET_PREFIX] Optional prefix for the resulting target. Default `open62541-generator`
+#   [BLACKLIST]     Blacklist file passed as --blacklist to the nodeset compiler. All the given nodes will be removed from the generated
+#                   nodeset, including all the references to and from that node. The format is a node id per line.
+#                   Supported formats: "i=123" (for NS0), "ns=2;s=asdf" (matches NS2 in that specific file), or recommended
+#                   "ns=http://opcfoundation.org/UA/DI/;i=123" namespace index independent node id
 #
 #   Arguments taking multiple values:
 #
@@ -249,7 +253,7 @@ endfunction()
 function(ua_generate_nodeset)
 
     set(options INTERNAL )
-    set(oneValueArgs NAME TYPES_ARRAY OUTPUT_DIR IGNORE TARGET_PREFIX)
+    set(oneValueArgs NAME TYPES_ARRAY OUTPUT_DIR IGNORE TARGET_PREFIX BLACKLIST)
     set(multiValueArgs FILE DEPENDS_TYPES DEPENDS_NS DEPENDS_TARGET)
     cmake_parse_arguments(UA_GEN_NS "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN} )
 
@@ -277,6 +281,14 @@ function(ua_generate_nodeset)
         set(UA_GEN_NS_TARGET_PREFIX "open62541-generator")
     endif()
 
+    # Set blacklist file
+    set(GEN_BLACKLIST "")
+    set(GEN_BLACKLIST_DEPENDS "")
+    if(UA_GEN_NS_BLACKLIST)
+        set(GEN_BLACKLIST "--blacklist=${UA_GEN_NS_BLACKLIST}")
+        set(GEN_BLACKLIST_DEPENDS "${UA_GEN_NS_BLACKLIST}")
+    endif()
+
     # ------ Add custom command and target -----
 
     set(GEN_INTERNAL_HEADERS "")
@@ -334,6 +346,7 @@ function(ua_generate_nodeset)
                        ${GEN_NS0}
                        ${GEN_BIN_SIZE}
                        ${GEN_IGNORE}
+                       ${GEN_BLACKLIST}
                        ${TYPES_ARRAY_LIST}
                        ${DEPENDS_FILE_LIST}
                        ${FILE_LIST}
@@ -348,6 +361,7 @@ function(ua_generate_nodeset)
                        ${open62541_TOOLS_DIR}/nodeset_compiler/backend_open62541_datatypes.py
                        ${UA_GEN_NS_FILE}
                        ${UA_GEN_NS_DEPENDS_NS}
+                       ${GEN_BLACKLIST_DEPENDS}
                        )
 
     add_custom_target(${UA_GEN_NS_TARGET_PREFIX}-${TARGET_SUFFIX}
@@ -410,6 +424,10 @@ endfunction()
 #                   passed which will all combined to one resulting code.
 #   [NAMESPACE_IDX] Optional namespace index of the nodeset, when it is loaded into the server. This parameter is mandatory if FILE_CSV
 #                   or FILE_BSD is set. See ua_generate_datatypes function.
+#   [BLACKLIST]     Blacklist file passed as --blacklist to the nodeset compiler. All the given nodes will be removed from the generated
+#                   nodeset, including all the references to and from that node. The format is a node id per line.
+#                   Supported formats: "i=123" (for NS0), "ns=2;s=asdf" (matches NS2 in that specific file), or recommended
+#                   "ns=http://opcfoundation.org/UA/DI/;i=123" namespace index independent node id
 #   [TARGET_PREFIX] Optional prefix for the resulting targets. Default `open62541-generator`
 #
 #   Arguments taking multiple values:
@@ -420,7 +438,7 @@ endfunction()
 function(ua_generate_nodeset_and_datatypes)
 
     set(options INTERNAL)
-    set(oneValueArgs NAME FILE_NS FILE_CSV FILE_BSD NAMESPACE_IDX OUTPUT_DIR TARGET_PREFIX)
+    set(oneValueArgs NAME FILE_NS FILE_CSV FILE_BSD NAMESPACE_IDX OUTPUT_DIR TARGET_PREFIX BLACKLIST)
     set(multiValueArgs DEPENDS)
     cmake_parse_arguments(UA_GEN "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN} )
 
@@ -499,7 +517,11 @@ function(ua_generate_nodeset_and_datatypes)
 
     # Create a list of nodesets on which this nodeset depends on
     if (NOT UA_GEN_DEPENDS OR "${UA_GEN_DEPENDS}" STREQUAL "" )
-        set(NODESET_DEPENDS "${open62541_NODESET_DIR}/Schema/Opc.Ua.NodeSet2.xml")
+        if(NOT UA_FILE_NS0)
+            set(NODESET_DEPENDS "${open62541_NODESET_DIR}/Schema/Opc.Ua.NodeSet2.xml")
+        else()
+            set(NODESET_DEPENDS "${UA_FILE_NS0}")
+        endif()
         set(TYPES_DEPENDS "UA_TYPES")
     else()
         foreach(f ${UA_GEN_DEPENDS})
@@ -530,6 +552,7 @@ function(ua_generate_nodeset_and_datatypes)
         NAME "${UA_GEN_NAME}"
         FILE "${UA_GEN_FILE_NS}"
         TYPES_ARRAY "${NODESET_TYPES_ARRAY}"
+        BLACKLIST "${UA_GEN_BLACKLIST}"
         ${NODESET_INTERNAL}
         DEPENDS_TYPES ${TYPES_DEPENDS}
         DEPENDS_NS ${NODESET_DEPENDS}

+ 3 - 0
tools/nodeset_compiler/backend_open62541_nodes.py

@@ -136,6 +136,9 @@ def setNodeValueRankRecursive(node, nodeset):
             # Default value
             node.valueRank = -1
     else:
+        if node.parent is None:
+            raise RuntimeError("Node {}: does not have a parent. Probably the parent node was blacklisted?".format(str(node.id)))
+
         # Check if parent node limits the value rank
         setNodeValueRankRecursive(node.parent, nodeset)
 

+ 51 - 1
tools/nodeset_compiler/nodeset.py

@@ -15,7 +15,7 @@ import xml.dom.minidom as dom
 import logging
 import codecs
 import re
-from datatypes import *
+from datatypes import NodeId, valueIsInternalType
 from nodes import *
 from opaque_type_mapping import opaque_type_mapping
 
@@ -195,6 +195,47 @@ class NodeSet(object):
             result.update(dictionary)
         return result
 
+    def getNodeByIDString(self, idStr):
+        # Split id to namespace part and id part
+        m = re.match("ns=([^;]+);(.*)", idStr)
+        if m:
+            ns = m.group(1)
+            # Convert namespace uri to index
+            if not ns.isdigit():
+                if ns not in self.namespaces:
+                    return None
+                ns = self.namespaces.index(ns)
+                idStr = "ns={};{}".format(ns, m.group(2))
+        nodeId = NodeId(idStr)
+        if not nodeId in self.nodes:
+            return None
+        return self.nodes[nodeId]
+
+    def remove_node(self, node):
+
+        def filterRef(r, rt):
+            return (r.referenceType != rt.referenceType) or (not (
+                    rt.target == node.id or rt.source == node.id
+                ))
+
+        for r in node.references:
+            if r.target == node.id:
+                if r.source not in self.nodes:
+                    continue
+                self.nodes[r.source].references = set(filter(
+                    lambda rt: filterRef(r, rt),
+                    self.nodes[r.source].references
+                ))
+            elif r.source == node.id:
+                if r.target not in self.nodes:
+                    continue
+                self.nodes[r.target].references = set(filter(
+                    lambda rt: filterRef(r, rt),
+                    self.nodes[r.target].references
+                ))
+        del self.nodes[node.id]
+
+
     def addNodeSet(self, xmlfile, hidden=False, typesArray="UA_TYPES"):
         # Extract NodeSet DOM
 
@@ -343,7 +384,16 @@ class NodeSet(object):
         parentreftypes = list(map(lambda x: x.id, parentreftypes))
 
         for node in self.nodes.values():
+            if node.id.ns == 0 and node.id.i in [78, 80, 84]:
+                # ModellingRule, Root node do not have a parent
+                continue
+
             parentref = node.getParentReference(parentreftypes)
             if parentref is not None:
                 node.parent = self.nodes[parentref.target]
+                if not node.parent:
+                    raise RuntimeError("Node {}: Did not find parent node: ".format(str(node.id)))
                 node.parentReference = self.nodes[parentref.referenceType]
+            # Some nodes in the full nodeset do not have a parent. So accept this and do not show an error.
+            #else:
+            #    raise RuntimeError("Node {}: HierarchicalReference (or subtype of it) to parent node is missing.".format(str(node.id)))

+ 20 - 13
tools/nodeset_compiler/nodeset_compiler.py

@@ -136,19 +136,6 @@ for xmlfile in args.infiles:
 # for key in namespaceArrayNames:
 #   ns.addNamespace(key, namespaceArrayNames[key])
 
-# Remove blacklisted nodes from the nodeset
-# Doing this now ensures that unlinkable pointers will be cleanly removed
-# during sanitation.
-for blacklist in args.blacklistFiles:
-    for line in blacklist.readlines():
-        line = line.replace(" ", "")
-        id = line.replace("\n", "")
-        if ns.getNodeByIDString(id) is None:
-            logger.info("Can't blacklist node, namespace does currently not contain a node with id " + str(id))
-        else:
-            ns.removeNodeById(line)
-    blacklist.close()
-
 # Set the nodes from the ignore list to hidden. This removes them from dependency calculation
 # and from printing their generated code.
 # These nodes should be already pre-created on the server to avoid any errors during
@@ -172,6 +159,26 @@ ns.allocateVariables()
 
 ns.addInverseReferences()
 
+
+# Remove blacklisted nodes from the nodeset.
+# We need to have the inverse references here to ensure the reference is deleted from the referencing node too
+if args.blacklistFiles:
+    for blacklist in args.blacklistFiles:
+        for line in blacklist.readlines():
+            if line.startswith("#"):
+                continue
+            line = line.replace(" ", "")
+            id = line.replace("\n", "")
+            if len(id) == 0:
+                continue
+            n = ns.getNodeByIDString(id)
+            if n is None:
+                logger.debug("Can't blacklist node, namespace does currently not contain a node with id " + str(id))
+            else:
+                ns.remove_node(n)
+        blacklist.close()
+    ns.sanitize()
+
 ns.setNodeParent()
 
 logger.info("Generating Code for Backend: {}".format(args.backend))