Completed
Pull Request — master (#353)
by
unknown
03:33
created

  C

Complexity

Total Complexity 56

Size/Duplication

Total Lines 240
Duplicated Lines 0 %

Test Coverage

Coverage 75.21%

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 240
ccs 88
cts 117
cp 0.7521
rs 5.5555
wmc 56

25 Methods

Rating   Name   Duplication   Size   Complexity  
A ddressSpace.__init__() 0 8 1
A ddressSpace.keys() 0 3 2
A ddressSpace.__setitem__() 0 3 2
A ddressSpace.__getitem__() 0 3 2
A ddressSpace.empty() 0 6 2
B ddressSpace.generate_nodeid() 0 14 6
A ddressSpace.__contains__() 0 3 2
A ddressSpace.__delitem__() 0 3 2
A ddressSpace.add_datachange_callback() 0 14 4
F ddressSpace.set_attribute_value() 0 27 9
A azyLoadingDict.__init__() 0 3 1
A azyLoadingDict.__contains__() 0 2 1
F ddressSpace.dump() 0 33 9
A ddressSpace.load_cache() 0 51 1
A azyLoadingDict.__setitem__() 0 3 1
A azyLoadingDict.shelve_item() 0 9 2
A azyLoadingDict.__delitem__() 0 3 1
A azyLoadingDict.__getitem__() 0 7 2
A ddressSpace.add_method_callback() 0 4 2
A azyLoadingDict.__iter__() 0 3 1
A ddressSpace.delete_datachange_callback() 0 4 2
B ddressSpace.get_attribute_value() 0 16 5
A azyLoadingDict.__len__() 0 3 1
A ddressSpace.make_cache() 0 11 2
A ddressSpace.load() 0 13 3

How to fix   Complexity   

Complex Class

Complex classes like often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1 1
from threading import RLock
2 1
import logging
3 1
from datetime import datetime
4 1
import collections
5 1
import shelve
6 1
from itertools import chain
7 1
try:
8 1
    import cPickle as pickle
9 1
except:
10
    import pickle
11 1
12 1
from opcua import ua
13
from opcua.server.users import User
14
15 1
16
class AttributeValue(object):
17 1
18 1
    def __init__(self, value):
19 1
        self.value = value
20 1
        self.value_callback = None
21
        self.datachange_callbacks = {}
22 1
23
    def __str__(self):
24 1
        return "AttributeValue({})".format(self.value)
25
    __repr__ = __str__
26
27 1
28
class NodeData(object):
29 1
30 1
    def __init__(self, nodeid):
31 1
        self.nodeid = nodeid
32 1
        self.attributes = {}
33 1
        self.references = []
34
        self.call = None
35 1
36
    def __str__(self):
37 1
        return "NodeData(id:{}, attrs:{}, refs:{})".format(self.nodeid, self.attributes, self.references)
38
    __repr__ = __str__
39
40 1
41
class AttributeService(object):
42 1
43 1
    def __init__(self, aspace):
44 1
        self.logger = logging.getLogger(__name__)
45
        self._aspace = aspace
46 1
47 1
    def read(self, params):
48 1
        self.logger.debug("read %s", params)
49 1
        res = []
50 1
        for readvalue in params.NodesToRead:
51 1
            res.append(self._aspace.get_attribute_value(readvalue.NodeId, readvalue.AttributeId))
52
        return res
53 1
54 1
    def write(self, params, user=User.Admin):
55 1
        self.logger.debug("write %s as user %s", params, user)
56 1
        res = []
57 1
        for writevalue in params.NodesToWrite:
58 1
            if user != User.Admin:
59 1
                if writevalue.AttributeId != ua.AttributeIds.Value:
60 1
                    res.append(ua.StatusCode(ua.StatusCodes.BadUserAccessDenied))
61 1
                    continue
62 1
                al = self._aspace.get_attribute_value(writevalue.NodeId, ua.AttributeIds.AccessLevel)
63 1
                ual = self._aspace.get_attribute_value(writevalue.NodeId, ua.AttributeIds.UserAccessLevel)
64 1
                if not ua.ua_binary.test_bit(al.Value.Value, ua.AccessLevel.CurrentWrite) or not ua.ua_binary.test_bit(ual.Value.Value, ua.AccessLevel.CurrentWrite):
65 1
                    res.append(ua.StatusCode(ua.StatusCodes.BadUserAccessDenied))
66 1
                    continue
67 1
            res.append(self._aspace.set_attribute_value(writevalue.NodeId, writevalue.AttributeId, writevalue.Value))
68
        return res
69
70 1
71
class ViewService(object):
72 1
73 1
    def __init__(self, aspace):
74 1
        self.logger = logging.getLogger(__name__)
75
        self._aspace = aspace
76 1
77 1
    def browse(self, params):
78 1
        self.logger.debug("browse %s", params)
79 1
        res = []
80 1
        for desc in params.NodesToBrowse:
81 1
            res.append(self._browse(desc))
82
        return res
83 1
84 1
    def _browse(self, desc):
85 1
        res = ua.BrowseResult()
86 1
        if desc.NodeId not in self._aspace:
87 1
            res.StatusCode = ua.StatusCode(ua.StatusCodes.BadNodeIdInvalid)
88 1
            return res
89 1
        node = self._aspace[desc.NodeId]
90 1
        for ref in node.references:
91 1
            if not self._is_suitable_ref(desc, ref):
92 1
                continue
93 1
            res.References.append(ref)
94
        return res
95 1
96 1
    def _is_suitable_ref(self, desc, ref):
97 1
        if not self._suitable_direction(desc.BrowseDirection, ref.IsForward):
98 1
            self.logger.debug("%s is not suitable due to direction", ref)
99 1
            return False
100 1
        if not self._suitable_reftype(desc.ReferenceTypeId, ref.ReferenceTypeId, desc.IncludeSubtypes):
101 1
            self.logger.debug("%s is not suitable due to type", ref)
102 1
            return False
103 1
        if desc.NodeClassMask and ((desc.NodeClassMask & ref.NodeClass) == 0):
104 1
            self.logger.debug("%s is not suitable due to class", ref)
105 1
            return False
106 1
        self.logger.debug("%s is a suitable ref for desc %s", ref, desc)
107
        return True
108 1
109
    def _suitable_reftype(self, ref1, ref2, subtypes):
110
        """
111 1
        """
112 1
        if not subtypes and ref2.Identifier == ua.ObjectIds.HasSubtype:
113
            return False
114 1
        if ref1.Identifier == ref2.Identifier:
115 1
            return True
116 1
        oktypes = self._get_sub_ref(ref1)
117 1
        if not subtypes and ua.NodeId(ua.ObjectIds.HasSubtype) in oktypes:
118
            oktypes.remove(ua.NodeId(ua.ObjectIds.HasSubtype))
119 1
        return ref2 in oktypes
120 1
121 1
    def _get_sub_ref(self, ref):
122 1
        res = []
123 1
        nodedata = self._aspace[ref]
124 1
        for ref in nodedata.references:
125 1
            if ref.ReferenceTypeId.Identifier == ua.ObjectIds.HasSubtype and ref.IsForward:
126 1
                res.append(ref.NodeId)
127
                res += self._get_sub_ref(ref.NodeId)
128 1
        return res
129 1
130 1
    def _suitable_direction(self, desc, isforward):
131 1
        if desc == ua.BrowseDirection.Both:
132 1
            return True
133 1
        if desc == ua.BrowseDirection.Forward and isforward:
134 1
            return True
135 1
        if desc == ua.BrowseDirection.Inverse and not isforward:
136
            return True
137 1
        return False
138 1
139 1
    def translate_browsepaths_to_nodeids(self, browsepaths):
140 1
        self.logger.debug("translate browsepath: %s", browsepaths)
141 1
        results = []
142 1
        for path in browsepaths:
143
            results.append(self._translate_browsepath_to_nodeid(path))
144 1
        return results
145 1
146 1
    def _translate_browsepath_to_nodeid(self, path):
147 1
        self.logger.debug("looking at path: %s", path)
148 1
        res = ua.BrowsePathResult()
149 1
        if path.StartingNode not in self._aspace:
150 1
            res.StatusCode = ua.StatusCode(ua.StatusCodes.BadNodeIdInvalid)
151 1
            return res
152 1
        current = path.StartingNode
153 1
        for el in path.RelativePath.Elements:
154 1
            nodeid = self._find_element_in_node(el, current)
155 1
            if not nodeid:
156 1
                res.StatusCode = ua.StatusCode(ua.StatusCodes.BadNoMatch)
157 1
                return res
158 1
            current = nodeid
159 1
        target = ua.BrowsePathTarget()
160 1
        target.TargetId = current
161 1
        target.RemainingPathIndex = 4294967295
162
        res.Targets = [target]
163 1
        return res
164 1
165 1
    def _find_element_in_node(self, el, nodeid):
166
        nodedata = self._aspace[nodeid]
167 1
        for ref in nodedata.references:
168 1
            # FIXME: here we should check other arguments!!
169 1
            if ref.BrowseName == el.TargetName:
170 1
                return ref.NodeId
171
        self.logger.info("element %s was not found in node %s", el, nodeid)
172
        return None
173 1
174
175 1
class NodeManagementService(object):
176 1
177 1
    def __init__(self, aspace):
178
        self.logger = logging.getLogger(__name__)
179 1
        self._aspace = aspace
180 1
181 1
    def add_nodes(self, addnodeitems, user=User.Admin):
182 1
        results = []
183 1
        for item in addnodeitems:
184
            results.append(self._add_node(item, user))
185 1
        return results
186 1
187
    def _add_node(self, item, user):
188
        result = ua.AddNodesResult()
189
190
        # If Identifier of requested NodeId is null we generate a new NodeId using
191 1
        # the namespace of the nodeid, this is an extention of the spec to allow
192 1
        # to requests the server to generate a new nodeid in a specified namespace
193 1
        if item.RequestedNewNodeId.has_null_identifier():
194
            self.logger.debug("RequestedNewNodeId has null identifier, generating Identifier")
195 1
            nodedata = NodeData(self._aspace.generate_nodeid(item.RequestedNewNodeId.NamespaceIndex))
196
        else:
197 1
            nodedata = NodeData(item.RequestedNewNodeId)
198 1
199 1
        if nodedata.nodeid in self._aspace:
200 1
            self.logger.warning("AddNodesItem: node already exists")
201
            result.StatusCode = ua.StatusCode(ua.StatusCodes.BadNodeIdExists)
202 1
            return result
203
204
        if item.ParentNodeId.is_null():
205 1
            # self.logger.warning("add_node: creating node %s without parent", nodedata.nodeid)
206 1
            # should return Error here, but the standard namespace define many nodes without parents...
207 1
            pass
208 1
        elif item.ParentNodeId not in self._aspace:
209 1
            self.logger.warning("add_node: while adding node %s, requested parent node %s does not exists", nodedata.nodeid, item.ParentNodeId)
210
            result.StatusCode = ua.StatusCode(ua.StatusCodes.BadParentNodeIdInvalid)
211 1
            return result
212 1
213 1
        if not user == User.Admin:
214
            result.StatusCode = ua.StatusCode(ua.StatusCodes.BadUserAccessDenied)
215 1
            return result
216
217
        self._add_node_attributes(nodedata, item)
218 1
219
        # now add our node to db
220 1
        self._aspace[nodedata.nodeid] = nodedata
221 1
222 1
        if not item.ParentNodeId.is_null():
223
            self._add_ref_from_parent(nodedata, item)
224
            self._add_ref_to_parent(nodedata, item, user)
225 1
226 1
        # add type definition
227
        if item.TypeDefinition != ua.NodeId():
228 1
            self._add_type_definition(nodedata, item, user)
229 1
230
        result.StatusCode = ua.StatusCode()
231 1
        result.AddedNodeId = nodedata.nodeid
232
233 1
        return result
234
235 1
    def _add_node_attributes(self, nodedata, item):
236
        # add common attrs
237
        nodedata.attributes[ua.AttributeIds.NodeId] = AttributeValue(
238 1
            ua.DataValue(ua.Variant(nodedata.nodeid, ua.VariantType.NodeId))
239
        )
240
        nodedata.attributes[ua.AttributeIds.BrowseName] = AttributeValue(
241 1
            ua.DataValue(ua.Variant(item.BrowseName, ua.VariantType.QualifiedName))
242
        )
243
        nodedata.attributes[ua.AttributeIds.NodeClass] = AttributeValue(
244
            ua.DataValue(ua.Variant(item.NodeClass, ua.VariantType.Int32))
245 1
        )
246
        # add requested attrs
247 1
        self._add_nodeattributes(item.NodeAttributes, nodedata)
248 1
249 1
    def _add_ref_from_parent(self, nodedata, item):
250 1
        desc = ua.ReferenceDescription()
251 1
        desc.ReferenceTypeId = item.ReferenceTypeId
252 1
        desc.NodeId = nodedata.nodeid
253 1
        desc.NodeClass = item.NodeClass
254 1
        desc.BrowseName = item.BrowseName
255 1
        desc.DisplayName = ua.LocalizedText(item.BrowseName.Name)
256 1
        desc.TypeDefinition = item.TypeDefinition
257
        desc.IsForward = True
258 1
        self._aspace[item.ParentNodeId].references.append(desc)
259 1
260 1
    def _add_ref_to_parent(self, nodedata, item, user):
261 1
        addref = ua.AddReferencesItem()
262 1
        addref.ReferenceTypeId = item.ReferenceTypeId
263 1
        addref.SourceNodeId = nodedata.nodeid
264 1
        addref.TargetNodeId = item.ParentNodeId
265 1
        addref.TargetNodeClass = self._aspace[item.ParentNodeId].attributes[ua.AttributeIds.NodeClass].value.Value.Value
266
        addref.IsForward = False
267 1
        self._add_reference(addref, user)
268 1
269 1
    def _add_type_definition(self, nodedata, item, user):
270 1
        addref = ua.AddReferencesItem()
271 1
        addref.SourceNodeId = nodedata.nodeid
272 1
        addref.IsForward = True
273 1
        addref.ReferenceTypeId = ua.NodeId(ua.ObjectIds.HasTypeDefinition)
274 1
        addref.TargetNodeId = item.TypeDefinition
275
        addref.TargetNodeClass = ua.NodeClass.DataType
276 1
        self._add_reference(addref, user)
277 1
278 1
    def delete_nodes(self, deletenodeitems, user=User.Admin):
279 1
        results = []
280 1
        for item in deletenodeitems.NodesToDelete:
281
            results.append(self._delete_node(item, user))
282 1
        return results
283 1
284
    def _delete_node(self, item, user):
285
        if user != User.Admin:
286 1
            return ua.StatusCode(ua.StatusCodes.BadUserAccessDenied)
287
288
        if item.NodeId not in self._aspace:
289
            self.logger.warning("DeleteNodesItem: node does not exists")
290 1
            return ua.StatusCode(ua.StatusCodes.BadNodeIdUnknown)
291 1
292 1
        if item.DeleteTargetReferences:
293 1
            for elem in self._aspace.keys():
294 1
                for rdesc in self._aspace[elem].references:
295
                    if rdesc.NodeId == item.NodeId:
296 1
                        self._aspace[elem].references.remove(rdesc)
297
298 1
        self._delete_node_callbacks(self._aspace[item.NodeId])
299
300 1
        del(self._aspace[item.NodeId])
301
302 1
        return ua.StatusCode()
303 1
304 1
    def _delete_node_callbacks(self, nodedata):
305
        if ua.AttributeIds.Value in nodedata.attributes:
306
            for handle, callback in nodedata.attributes[ua.AttributeIds.Value].datachange_callbacks.items():
307
                try:
308
                    callback(handle, None, ua.StatusCode(ua.StatusCodes.BadNodeIdUnknown))
309
                    self._aspace.delete_datachange_callback(handle)
310
                except Exception as ex:
311 1
                    self.logger.exception("Error calling delete node callback callback %s, %s, %s", nodedata, ua.AttributeIds.Value, ex)
312 1
313 1
    def add_references(self, refs, user=User.Admin):
314 1
        result = []
315 1
        for ref in refs:
316
            result.append(self._add_reference(ref, user))
317 1
        return result
318 1
319
    def _add_reference(self, addref, user):
320 1
        if addref.SourceNodeId not in self._aspace:
321 1
            return ua.StatusCode(ua.StatusCodes.BadSourceNodeIdInvalid)
322 1
        if addref.TargetNodeId not in self._aspace:
323
            return ua.StatusCode(ua.StatusCodes.BadTargetNodeIdInvalid)
324 1
        if user != User.Admin:
325 1
            return ua.StatusCode(ua.StatusCodes.BadUserAccessDenied)
326 1
        rdesc = ua.ReferenceDescription()
327 1
        rdesc.ReferenceTypeId = addref.ReferenceTypeId
328 1
        rdesc.IsForward = addref.IsForward
329 1
        rdesc.NodeId = addref.TargetNodeId
330 1
        rdesc.NodeClass = addref.TargetNodeClass
331 1
        bname = self._aspace.get_attribute_value(addref.TargetNodeId, ua.AttributeIds.BrowseName).Value.Value
332 1
        if bname:
333 1
            rdesc.BrowseName = bname
334 1
        dname = self._aspace.get_attribute_value(addref.TargetNodeId, ua.AttributeIds.DisplayName).Value.Value
335 1
        if dname:
336 1
            rdesc.DisplayName = dname
337
        self._aspace[addref.SourceNodeId].references.append(rdesc)
338 1
        return ua.StatusCode()
339
340
    def delete_references(self, refs, user=User.Admin):
341
        result = []
342
        for ref in refs:
343
            result.append(self._delete_reference(ref, user))
344 1
        return result
345
346
    def _delete_reference(self, item, user):
347
        if item.SourceNodeId not in self._aspace:
348
            return ua.StatusCode(ua.StatusCodes.BadSourceNodeIdInvalid)
349
        if item.TargetNodeId not in self._aspace:
350
            return ua.StatusCode(ua.StatusCodes.BadTargetNodeIdInvalid)
351
        if user != User.Admin:
352
            return ua.StatusCode(ua.StatusCodes.BadUserAccessDenied)
353
354
        for rdesc in self._aspace[item.SourceNodeId].references:
355
            if rdesc.NodeId is item.TargetNodeId:
356
                if rdesc.RefrenceTypeId != item.RefrenceTypeId:
357
                    return ua.StatusCode(ua.StatusCodes.BadReferenceTypeIdInvalid)
358
                if rdesc.IsForward == item.IsForward or item.DeleteBidirectional:
359
                    self._aspace[item.SourceNodeId].references.remove(rdesc)
360
361
        for rdesc in self._aspace[item.TargetNodeId].references:
362
            if rdesc.NodeId is item.SourceNodeId:
363
                if rdesc.RefrenceTypeId != item.RefrenceTypeId:
364
                    return ua.StatusCode(ua.StatusCodes.BadReferenceTypeIdInvalid)
365
                if rdesc.IsForward == item.IsForward or item.DeleteBidirectional:
366
                    self._aspace[item.SourceNodeId].references.remove(rdesc)
367
368 1
        return ua.StatusCode()
369 1
370 1
    def _add_node_attr(self, item, nodedata, name, vtype=None):
371 1
        if item.SpecifiedAttributes & getattr(ua.NodeAttributesMask, name):
372 1
            dv = ua.DataValue(ua.Variant(getattr(item, name), vtype))
373 1
            dv.ServerTimestamp = datetime.utcnow()
374
            dv.SourceTimestamp = datetime.utcnow()
375 1
            nodedata.attributes[getattr(ua.AttributeIds, name)] = AttributeValue(dv)
376 1
377 1
    def _add_nodeattributes(self, item, nodedata):
378 1
        self._add_node_attr(item, nodedata, "AccessLevel", ua.VariantType.Byte)
379 1
        self._add_node_attr(item, nodedata, "ArrayDimensions", ua.VariantType.UInt32)
380 1
        self._add_node_attr(item, nodedata, "BrowseName", ua.VariantType.QualifiedName)
381 1
        self._add_node_attr(item, nodedata, "ContainsNoLoops", ua.VariantType.Boolean)
382 1
        self._add_node_attr(item, nodedata, "DataType", ua.VariantType.NodeId)
383 1
        self._add_node_attr(item, nodedata, "Description", ua.VariantType.LocalizedText)
384 1
        self._add_node_attr(item, nodedata, "DisplayName", ua.VariantType.LocalizedText)
385 1
        self._add_node_attr(item, nodedata, "EventNotifier", ua.VariantType.Byte)
386 1
        self._add_node_attr(item, nodedata, "Executable", ua.VariantType.Boolean)
387 1
        self._add_node_attr(item, nodedata, "Historizing", ua.VariantType.Boolean)
388 1
        self._add_node_attr(item, nodedata, "InverseName", ua.VariantType.LocalizedText)
389 1
        self._add_node_attr(item, nodedata, "IsAbstract", ua.VariantType.Boolean)
390 1
        self._add_node_attr(item, nodedata, "MinimumSamplingInterval", ua.VariantType.Double)
391 1
        self._add_node_attr(item, nodedata, "NodeClass", ua.VariantType.UInt32)
392 1
        self._add_node_attr(item, nodedata, "NodeId", ua.VariantType.NodeId)
393 1
        self._add_node_attr(item, nodedata, "Symmetric", ua.VariantType.Boolean)
394 1
        self._add_node_attr(item, nodedata, "UserAccessLevel", ua.VariantType.Byte)
395 1
        self._add_node_attr(item, nodedata, "UserExecutable", ua.VariantType.Boolean)
396 1
        self._add_node_attr(item, nodedata, "UserWriteMask", ua.VariantType.Byte)
397 1
        self._add_node_attr(item, nodedata, "ValueRank", ua.VariantType.Int32)
398 1
        self._add_node_attr(item, nodedata, "WriteMask", ua.VariantType.UInt32)
399
        self._add_node_attr(item, nodedata, "UserWriteMask", ua.VariantType.UInt32)
400
        self._add_node_attr(item, nodedata, "Value")
401 1
402
403 1
class MethodService(object):
404 1
405 1
    def __init__(self, aspace):
406
        self.logger = logging.getLogger(__name__)
407 1
        self._aspace = aspace
408 1
409 1
    def call(self, methods):
410 1
        results = []
411 1
        for method in methods:
412
            results.append(self._call(method))
413 1
        return results
414 1
415 1
    def _call(self, method):
416 1
        res = ua.CallMethodResult()
417
        if method.ObjectId not in self._aspace or method.MethodId not in self._aspace:
418 1
            res.StatusCode = ua.StatusCode(ua.StatusCodes.BadNodeIdInvalid)
419 1
        else:
420
            node = self._aspace[method.MethodId]
421
            if node.call is None:
422 1
                res.StatusCode = ua.StatusCode(ua.StatusCodes.BadNothingToDo)
423 1
            else:
424 1
                try:
425 1
                    res.OutputArguments = node.call(method.ObjectId, *method.InputArguments)
426 1
                    for _ in method.InputArguments:
427 1
                        res.InputArgumentResults.append(ua.StatusCode())
428 1
                except Exception:
429 1
                    self.logger.exception("Error executing method call %s, an exception was raised: ", method)
430
                    res.StatusCode = ua.StatusCode(ua.StatusCodes.BadUnexpectedError)
431
        return res
432 1
433
434
class AddressSpace(object):
435
436
    """
437
    The address space object stores all the nodes of the OPC-UA server
438
    and helper methods.
439
    The methods are thread safe
440 1
    """
441 1
442 1
    def __init__(self):
443 1
        self.logger = logging.getLogger(__name__)
444 1
        self._nodes = {}
445 1
        self._lock = RLock()  # FIXME: should use multiple reader, one writter pattern
446 1
        self._datachange_callback_counter = 200
447 1
        self._handle_to_attribute_map = {}
448
        self._default_idx = 2
449 1
        self._nodeid_counter = {0: 20000, 1: 2000}
450 1
451 1
    def __getitem__(self, nodeid):
452
        with self._lock:
453 1
            return self._nodes.__getitem__(nodeid)
454 1
455 1
    def __setitem__(self, nodeid, value):
456
        with self._lock:
457 1
            return self._nodes.__setitem__(nodeid, value)
458 1
459 1
    def __contains__(self, nodeid):
460
        with self._lock:
461 1
            return self._nodes.__contains__(nodeid)
462 1
463 1
    def __delitem__(self, nodeid):
464
        with self._lock:
465 1
            self._nodes.__delitem__(nodeid)
466 1
467 1
    def generate_nodeid(self, idx=None):
468 1
        if idx is None:
469 1
            idx = self._default_idx
470
        if idx in self._nodeid_counter:
471 1
            self._nodeid_counter[idx] += 1
472 1
        else:
473 1
            self._nodeid_counter[idx] = 1
474
        nodeid = ua.NodeId(self._nodeid_counter[idx], idx)
475 1
        with self._lock:  # OK since reentrant lock
476
            while True:
477
                if nodeid in self._nodes:
478
                    nodeid = self.generate_nodeid(idx)
479
                else:
480
                    return nodeid
481
482
    def keys(self):
483
        with self._lock:
484 1
            return self._nodes.keys()
485
486
    def empty(self):
487
        """
488
        Delete all nodes in address space
489
        """
490
        with self._lock:
491
            self._nodes = {}
492
493
    def dump(self, path):
494
        """
495
        dump address space as binary to file; note that server must be stopped for this method to work
496
        if caching is enabled when dumping the cache files and binary file are required to restore address space state!
497
        """
498
        dump_cache = False
499
        if hasattr(self._nodes, "cache"):
500
            dump_cache = True
501
502
        # prepare nodes in address space for being serialized
503
        for nodeid in list(self._nodes.keys()):
504
            node = self._nodes[nodeid]
505
506
            # if the node has a reference to a method call, remove it so the object can be serialized
507
            if hasattr(node, "call"):
508
                self._nodes[nodeid].call = None
509
510
            # if the address space is using a cache, update the shelve if the node exists in the shelve
511
            if dump_cache:
512
                if self._nodes.shelve_item(nodeid, self._nodes[nodeid]):
513
                    del self._nodes.cache[nodeid]
514
515
        # save the shelve changes to disk
516
        if dump_cache:
517 1
            self._nodes.source.sync()
518 1
519 1
        with open(path, 'wb') as f:
520 1
            # if address space is using a cache, only dump whatever is remaining in cache (user nodes)
521 1
            # the rest is saved in the shelve files
522 1
            if dump_cache:
523 1
                pickle.dump(self._nodes.cache, f, pickle.HIGHEST_PROTOCOL)
524 1
            else:
525 1
                pickle.dump(self._nodes, f, pickle.HIGHEST_PROTOCOL)
526 1
527 1
    def load(self, path):
528 1
        """
529 1
        load address space from file, overwriting everything in current address space
530 1
        if the binary was created by a server using caching you must use caching! (the binary won't have
531
        standard address space because it's in the cache files)
532 1
        """
533
        with open(path, 'rb') as f:
534 1
            # if using a cache, restore the data to cache
535 1
            if hasattr(self._nodes, "cache"):
536 1
                self._nodes.cache = pickle.load(f)
537 1
            # otherwise overwrite the entire address space
538 1
            else:
539 1
                self._nodes = pickle.load(f)
540 1
541 1
    def make_cache(self, path):
542 1
        """
543 1
        make a shelve for storing nodes from the standard address space; nodes are moved to a cache
544 1
        by the LazyLoadingDict class when they are accessed
545 1
        """
546
        s = shelve.open(path, "n", protocol=pickle.HIGHEST_PROTOCOL)
547 1
        for nodeid in self._nodes.keys():
548 1
            s[nodeid.to_string()] = self._nodes[nodeid]
549 1
        s.close()
550 1
551 1
        self.load_cache(path)
552 1
553
    def load_cache(self, path):
554 1
        """
555 1
        load the standard address space from the LazyLoadingDict as needed
556 1
        """
557
        class LazyLoadingDict(collections.MutableMapping):
558
            """
559
            Special dict that only loads nodes as they are accessed. If a node is accessed it gets copied
560 1
            to the cache dict. All user nodes are saved in the cache ONLY
561
            """
562 1
            def __init__(self, source):
563 1
                self.source = source  # python shelve
564 1
                self.cache = {}  # internal dict
565 1
566
            def __getitem__(self, key):
567 1
                # try to get the item (node) from the cache, if it isn't there get it from the shelve
568 1
                try:
569 1
                    return self.cache[key]
570 1
                except KeyError:
571 1
                    node = self.cache[key] = self.source[key.to_string()]
572 1
                    return node
573 1
574 1
            def __setitem__(self, key, value):
575 1
                # add a new item to the cache; if this item is in the shelve it is not updated
576
                self.cache[key] = value
577 1
578 1
            def __contains__(self, key):
579 1
                return key in self.cache or key.to_string() in self.source
580 1
581
            def __delitem__(self, key):
582 1
                # only deleting items from the cache is allowed
583 1
                del(self.cache[key])
584 1
585 1
            def __iter__(self):
586
                # only the cache can be iterated over
587
                return iter(self.cache.keys())
588
589
            def __len__(self):
590
                # only returns the length of items in the cache, not unaccessed items in the shelve
591
                return len(self.cache)
592
593
            def shelve_item(self, key, value):
594
                # update the shelf with a new value
595
                # must call sync on the shelve after using this method to save to disk!
596
                key = key.to_string()
597
                if key in self.source:
598
                    self.source[key] = value
599
                    return True
600
                else:
601
                    return False
602
603
        self._nodes = LazyLoadingDict(shelve.open(path, "r+"))
604
605
    def get_attribute_value(self, nodeid, attr):
606
        with self._lock:
607
            self.logger.debug("get attr val: %s %s", nodeid, attr)
608
            if nodeid not in self._nodes:
609
                dv = ua.DataValue()
610
                dv.StatusCode = ua.StatusCode(ua.StatusCodes.BadNodeIdUnknown)
611
                return dv
612
            node = self._nodes[nodeid]
613
            if attr not in node.attributes:
614
                dv = ua.DataValue()
615
                dv.StatusCode = ua.StatusCode(ua.StatusCodes.BadAttributeIdInvalid)
616
                return dv
617
            attval = node.attributes[attr]
618
            if attval.value_callback:
619
                return attval.value_callback()
620
            return attval.value
621
622
    def set_attribute_value(self, nodeid, attr, value):
623
        with self._lock:
624
            self.logger.debug("set attr val: %s %s %s", nodeid, attr, value)
625
            if nodeid not in self._nodes:
626
                return ua.StatusCode(ua.StatusCodes.BadNodeIdUnknown)
627
            node = self._nodes[nodeid]
628
            if attr not in node.attributes:
629
                return ua.StatusCode(ua.StatusCodes.BadAttributeIdInvalid)
630
            if not value.SourceTimestamp:
631
                value.SourceTimestamp = datetime.utcnow()
632
            if not value.ServerTimestamp:
633
                value.ServerTimestamp = datetime.utcnow()
634
635
            attval = node.attributes[attr]
636
            old = attval.value
637
            attval.value = value
638
            cbs = []
639
            if old.Value != value.Value:  # only send call callback when a value change has happend
640
                cbs = list(attval.datachange_callbacks.items())
641
642
        for k, v in cbs:
643
            try:
644
                v(k, value)
645
            except Exception as ex:
646
                self.logger.exception("Error calling datachange callback %s, %s, %s", k, v, ex)
647
648
        return ua.StatusCode()
649
650
    def add_datachange_callback(self, nodeid, attr, callback):
651
        with self._lock:
652
            self.logger.debug("set attr callback: %s %s %s", nodeid, attr, callback)
653
            if nodeid not in self._nodes:
654
                return ua.StatusCode(ua.StatusCodes.BadNodeIdUnknown), 0
655
            node = self._nodes[nodeid]
656
            if attr not in node.attributes:
657
                return ua.StatusCode(ua.StatusCodes.BadAttributeIdInvalid), 0
658
            attval = node.attributes[attr]
659
            self._datachange_callback_counter += 1
660
            handle = self._datachange_callback_counter
661
            attval.datachange_callbacks[handle] = callback
662
            self._handle_to_attribute_map[handle] = (nodeid, attr)
663
            return ua.StatusCode(), handle
664
665
    def delete_datachange_callback(self, handle):
666
        with self._lock:
667
            nodeid, attr = self._handle_to_attribute_map.pop(handle)
668
            self._nodes[nodeid].attributes[attr].datachange_callbacks.pop(handle)
669
670
    def add_method_callback(self, methodid, callback):
671
        with self._lock:
672
            node = self._nodes[methodid]
673
            node.call = callback
674