node-opcua-server 2.51.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/.mocharc.yml +10 -0
  2. package/LICENSE +20 -0
  3. package/dist/base_server.d.ts +110 -0
  4. package/dist/base_server.js +476 -0
  5. package/dist/base_server.js.map +1 -0
  6. package/dist/factory.d.ts +10 -0
  7. package/dist/factory.js +24 -0
  8. package/dist/factory.js.map +1 -0
  9. package/dist/history_server_capabilities.d.ts +35 -0
  10. package/dist/history_server_capabilities.js +44 -0
  11. package/dist/history_server_capabilities.js.map +1 -0
  12. package/dist/i_channel_data.d.ts +13 -0
  13. package/dist/i_channel_data.js +3 -0
  14. package/dist/i_channel_data.js.map +1 -0
  15. package/dist/i_register_server_manager.d.ts +16 -0
  16. package/dist/i_register_server_manager.js +3 -0
  17. package/dist/i_register_server_manager.js.map +1 -0
  18. package/dist/i_server_side_publish_engine.d.ts +36 -0
  19. package/dist/i_server_side_publish_engine.js +50 -0
  20. package/dist/i_server_side_publish_engine.js.map +1 -0
  21. package/dist/i_socket_data.d.ts +11 -0
  22. package/dist/i_socket_data.js +3 -0
  23. package/dist/i_socket_data.js.map +1 -0
  24. package/dist/index.d.ts +14 -0
  25. package/dist/index.js +27 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/monitored_item.d.ts +173 -0
  28. package/dist/monitored_item.js +1006 -0
  29. package/dist/monitored_item.js.map +1 -0
  30. package/dist/node_sampler.d.ts +3 -0
  31. package/dist/node_sampler.js +76 -0
  32. package/dist/node_sampler.js.map +1 -0
  33. package/dist/opcua_server.d.ts +668 -0
  34. package/dist/opcua_server.js +2407 -0
  35. package/dist/opcua_server.js.map +1 -0
  36. package/dist/queue.d.ts +11 -0
  37. package/dist/queue.js +71 -0
  38. package/dist/queue.js.map +1 -0
  39. package/dist/register_server_manager.d.ts +92 -0
  40. package/dist/register_server_manager.js +574 -0
  41. package/dist/register_server_manager.js.map +1 -0
  42. package/dist/register_server_manager_hidden.d.ts +17 -0
  43. package/dist/register_server_manager_hidden.js +28 -0
  44. package/dist/register_server_manager_hidden.js.map +1 -0
  45. package/dist/register_server_manager_mdns_only.d.ts +19 -0
  46. package/dist/register_server_manager_mdns_only.js +58 -0
  47. package/dist/register_server_manager_mdns_only.js.map +1 -0
  48. package/dist/server_capabilities.d.ts +61 -0
  49. package/dist/server_capabilities.js +109 -0
  50. package/dist/server_capabilities.js.map +1 -0
  51. package/dist/server_end_point.d.ts +180 -0
  52. package/dist/server_end_point.js +825 -0
  53. package/dist/server_end_point.js.map +1 -0
  54. package/dist/server_engine.d.ts +311 -0
  55. package/dist/server_engine.js +1659 -0
  56. package/dist/server_engine.js.map +1 -0
  57. package/dist/server_publish_engine.d.ts +109 -0
  58. package/dist/server_publish_engine.js +531 -0
  59. package/dist/server_publish_engine.js.map +1 -0
  60. package/dist/server_publish_engine_for_orphan_subscriptions.d.ts +16 -0
  61. package/dist/server_publish_engine_for_orphan_subscriptions.js +50 -0
  62. package/dist/server_publish_engine_for_orphan_subscriptions.js.map +1 -0
  63. package/dist/server_session.d.ts +176 -0
  64. package/dist/server_session.js +734 -0
  65. package/dist/server_session.js.map +1 -0
  66. package/dist/server_subscription.d.ts +393 -0
  67. package/dist/server_subscription.js +1313 -0
  68. package/dist/server_subscription.js.map +1 -0
  69. package/dist/sessions_compatible_for_transfer.d.ts +2 -0
  70. package/dist/sessions_compatible_for_transfer.js +36 -0
  71. package/dist/sessions_compatible_for_transfer.js.map +1 -0
  72. package/dist/validate_filter.d.ts +5 -0
  73. package/dist/validate_filter.js +64 -0
  74. package/dist/validate_filter.js.map +1 -0
  75. package/package.json +88 -0
  76. package/source/base_server.ts +617 -0
  77. package/source/factory.ts +25 -0
  78. package/source/history_server_capabilities.ts +75 -0
  79. package/source/i_channel_data.ts +17 -0
  80. package/source/i_register_server_manager.ts +24 -0
  81. package/source/i_server_side_publish_engine.ts +77 -0
  82. package/source/i_socket_data.ts +11 -0
  83. package/source/index.ts +14 -0
  84. package/source/monitored_item.ts +1303 -0
  85. package/source/node_sampler.ts +82 -0
  86. package/source/opcua_server.ts +3742 -0
  87. package/source/queue.ts +73 -0
  88. package/source/register_server_manager.ts +744 -0
  89. package/source/register_server_manager_hidden.ts +33 -0
  90. package/source/register_server_manager_mdns_only.ts +69 -0
  91. package/source/server_capabilities.ts +177 -0
  92. package/source/server_end_point.ts +1182 -0
  93. package/source/server_engine.ts +2167 -0
  94. package/source/server_publish_engine.ts +657 -0
  95. package/source/server_publish_engine_for_orphan_subscriptions.ts +52 -0
  96. package/source/server_session.ts +931 -0
  97. package/source/server_subscription.ts +1792 -0
  98. package/source/sessions_compatible_for_transfer.ts +33 -0
  99. package/source/validate_filter.ts +86 -0
  100. package/test_helpers/create_certificates.js +1 -0
@@ -0,0 +1,2167 @@
1
+ /**
2
+ * @module node-opcua-server
3
+ */
4
+ // tslint:disable:no-console
5
+ import * as async from "async";
6
+ import * as chalk from "chalk";
7
+ import { EventEmitter } from "events";
8
+ import { assert } from "node-opcua-assert";
9
+
10
+ import {
11
+ addElement,
12
+ AddressSpace,
13
+ BaseNode,
14
+ bindExtObjArrayNode,
15
+ DataValueCallback,
16
+ ensureDatatypeExtractedWithCallback,
17
+ ensureObjectIsSecure,
18
+ MethodFunctor,
19
+ removeElement,
20
+ SessionContext,
21
+ UADataType,
22
+ UADynamicVariableArray,
23
+ UAMethod,
24
+ UAObject,
25
+ UAServerDiagnosticsSummary,
26
+ UAServerStatus,
27
+ UAVariable,
28
+ UAServerDiagnostics,
29
+ BindVariableOptions,
30
+ MethodFunctorCallback,
31
+ ISessionContext,
32
+ DTServerStatus,
33
+ } from "node-opcua-address-space";
34
+
35
+ import { generateAddressSpace } from "node-opcua-address-space/nodeJS";
36
+
37
+ import { DataValue, coerceTimestampsToReturn, apply_timestamps_no_copy } from "node-opcua-data-value";
38
+
39
+ import {
40
+ ServerDiagnosticsSummaryDataType,
41
+ ServerState,
42
+ ServerStatusDataType,
43
+ SubscriptionDiagnosticsDataType
44
+ } from "node-opcua-common";
45
+ import {
46
+ AttributeIds,
47
+ BrowseDirection,
48
+ coerceLocalizedText,
49
+ LocalizedTextLike,
50
+ makeAccessLevelFlag,
51
+ NodeClass,
52
+ QualifiedName
53
+ } from "node-opcua-data-model";
54
+ import { coerceNodeId, makeNodeId, NodeId, NodeIdLike, NodeIdType, resolveNodeId } from "node-opcua-nodeid";
55
+ import { BrowseResult } from "node-opcua-service-browse";
56
+ import { ReadRequest, TimestampsToReturn } from "node-opcua-service-read";
57
+
58
+ import { TransferResult } from "node-opcua-service-subscription";
59
+
60
+ import { CreateSubscriptionRequestLike } from "node-opcua-client";
61
+ import { ExtraDataTypeManager } from "node-opcua-client-dynamic-extension-object";
62
+ import { DataTypeIds, MethodIds, ObjectIds, VariableIds } from "node-opcua-constants";
63
+ import { getCurrentClock, minOPCUADate } from "node-opcua-date-time";
64
+ import { checkDebugFlag, make_debugLog, make_errorLog, make_warningLog, traceFromThisProjectOnly } from "node-opcua-debug";
65
+ import { nodesets } from "node-opcua-nodesets";
66
+ import { ObjectRegistry } from "node-opcua-object-registry";
67
+ import { CallMethodResult } from "node-opcua-service-call";
68
+ import { ApplicationDescription } from "node-opcua-service-endpoints";
69
+ import { HistoryReadDetails, HistoryReadRequest, HistoryReadResult, HistoryReadValueId } from "node-opcua-service-history";
70
+ import { StatusCode, StatusCodes, CallbackT, StatusCodeCallback } from "node-opcua-status-code";
71
+ import {
72
+ BrowseDescription,
73
+ BrowsePath,
74
+ BrowsePathResult,
75
+ BuildInfo,
76
+ BuildInfoOptions,
77
+ ProgramDiagnostic2DataType,
78
+ ReadAtTimeDetails,
79
+ ReadEventDetails,
80
+ ReadProcessedDetails,
81
+ ReadRawModifiedDetails,
82
+ SessionDiagnosticsDataType,
83
+ SessionSecurityDiagnosticsDataType,
84
+ WriteValue,
85
+ ReadValueId,
86
+ TimeZoneDataType,
87
+ ProgramDiagnosticDataType,
88
+ CallMethodResultOptions
89
+ } from "node-opcua-types";
90
+ import { DataType, isValidVariant, Variant, VariantArrayType, VariantLike } from "node-opcua-variant";
91
+
92
+ import { HistoryServerCapabilities, HistoryServerCapabilitiesOptions } from "./history_server_capabilities";
93
+ import { MonitoredItem } from "./monitored_item";
94
+ import { OperationLimits, ServerCapabilities, ServerCapabilitiesOptions } from "./server_capabilities";
95
+ import { ServerSidePublishEngine } from "./server_publish_engine";
96
+ import { ServerSidePublishEngineForOrphanSubscription } from "./server_publish_engine_for_orphan_subscriptions";
97
+ import { ServerSession } from "./server_session";
98
+ import { Subscription } from "./server_subscription";
99
+ import { sessionsCompatibleForTransfer } from "./sessions_compatible_for_transfer";
100
+ import { NumericRange } from "node-opcua-numeric-range";
101
+ import { UInt32 } from "node-opcua-basic-types";
102
+ import { resolveOpaqueOnAddressSpace } from "node-opcua-address-space";
103
+
104
+ const debugLog = make_debugLog(__filename);
105
+ const errorLog = make_errorLog(__filename);
106
+ const warningLog = make_warningLog(__filename);
107
+ const doDebug = checkDebugFlag(__filename);
108
+
109
+ function upperCaseFirst(str: string) {
110
+ return str.slice(0, 1).toUpperCase() + str.slice(1);
111
+ }
112
+
113
+ function shutdownAndDisposeAddressSpace(this: ServerEngine) {
114
+ if (this.addressSpace) {
115
+ this.addressSpace.shutdown();
116
+ this.addressSpace.dispose();
117
+ delete (this as any).addressSpace;
118
+ }
119
+ }
120
+
121
+ function setSubscriptionDurable(
122
+ this: ServerEngine,
123
+ inputArguments: Variant[],
124
+ context: ISessionContext,
125
+ callback: MethodFunctorCallback
126
+ ) {
127
+ // see https://reference.opcfoundation.org/v104/Core/docs/Part5/9.3/
128
+ // https://reference.opcfoundation.org/v104/Core/docs/Part4/6.8/
129
+ assert(Array.isArray(inputArguments));
130
+ assert(typeof callback === "function");
131
+ assert(context.hasOwnProperty("session"), " expecting a session id in the context object");
132
+ const session = context.session as ServerSession;
133
+ if (!session) {
134
+ return callback(null, { statusCode: StatusCodes.BadInternalError });
135
+ }
136
+ const subscriptionId = inputArguments[0].value as UInt32;
137
+ const lifetimeInHours = inputArguments[1].value as UInt32;
138
+
139
+ const subscription = session.getSubscription(subscriptionId);
140
+ if (!subscription) {
141
+ // subscription may belongs to a different session that ours
142
+ if (this.findSubscription(subscriptionId)) {
143
+ // if yes, then access to Subscription data should be denied
144
+ return callback(null, { statusCode: StatusCodes.BadUserAccessDenied });
145
+ }
146
+ return callback(null, { statusCode: StatusCodes.BadSubscriptionIdInvalid });
147
+ }
148
+ if (subscription.monitoredItemCount > 0) {
149
+ // This is returned when a Subscription already contains MonitoredItems.
150
+ return callback(null, { statusCode: StatusCodes.BadInvalidState });
151
+ }
152
+
153
+ /**
154
+ * MonitoredItems are used to monitor Variable Values for data changes and event notifier
155
+ * Objects for new Events. Subscriptions are used to combine data changes and events of
156
+ * the assigned MonitoredItems to an optimized stream of network messages. A reliable
157
+ * delivery is ensured as long as the lifetime of the Subscription and the queues in the
158
+ * MonitoredItems are long enough for a network interruption between OPC UA Client and
159
+ * Server. All queues that ensure reliable delivery are normally kept in memory and a
160
+ * Server restart would delete them.
161
+ * There are use cases where OPC UA Clients have no permanent network connection to the
162
+ * OPC UA Server or where reliable delivery of data changes and events is necessary
163
+ * even if the OPC UA Server is restarted or the network connection is interrupted
164
+ * for a longer time.
165
+ * To ensure this reliable delivery, the OPC UA Server must store collected data and
166
+ * events in non-volatile memory until the OPC UA Client has confirmed reception.
167
+ * It is possible that there will be data lost if the Server is not shut down gracefully
168
+ * or in case of power failure. But the OPC UA Server should store the queues frequently
169
+ * even if the Server is not shut down.
170
+ * The Method SetSubscriptionDurable defined in OPC 10000-5 is used to set a Subscription
171
+ * into this durable mode and to allow much longer lifetimes and queue sizes than for normal
172
+ * Subscriptions. The Method shall be called before the MonitoredItems are created in the
173
+ * durable Subscription. The Server shall verify that the Method is called within the
174
+ * Session context of the Session that owns the Subscription.
175
+ *
176
+ * A value of 0 for the parameter lifetimeInHours requests the highest lifetime supported by the Server.
177
+ */
178
+
179
+ const highestLifetimeInHours = 24 * 100;
180
+
181
+ const revisedLifetimeInHours =
182
+ lifetimeInHours === 0 ? highestLifetimeInHours : Math.max(1, Math.min(lifetimeInHours, highestLifetimeInHours));
183
+
184
+ // also adjust subscription life time
185
+ const currentLifeTimeInHours = (subscription.lifeTimeCount * subscription.publishingInterval) / (1000 * 60 * 60);
186
+ if (currentLifeTimeInHours < revisedLifetimeInHours) {
187
+ const requestedLifetimeCount = Math.ceil((revisedLifetimeInHours * (1000 * 60 * 60)) / subscription.publishingInterval);
188
+
189
+ subscription.modify({
190
+ requestedMaxKeepAliveCount: subscription.maxKeepAliveCount,
191
+ requestedPublishingInterval: subscription.publishingInterval,
192
+ maxNotificationsPerPublish: subscription.maxNotificationsPerPublish,
193
+ priority: subscription.priority,
194
+ requestedLifetimeCount
195
+ });
196
+ }
197
+
198
+ const callMethodResult = new CallMethodResult({
199
+ statusCode: StatusCodes.Good,
200
+ outputArguments: [{ dataType: DataType.UInt32, arrayType: VariantArrayType.Scalar, value: revisedLifetimeInHours }]
201
+ });
202
+ callback(null, callMethodResult);
203
+ }
204
+
205
+ // binding methods
206
+ function getMonitoredItemsId(
207
+ this: ServerEngine,
208
+ inputArguments: Variant[],
209
+ context: ISessionContext,
210
+ callback: MethodFunctorCallback
211
+ ) {
212
+ assert(Array.isArray(inputArguments));
213
+ assert(typeof callback === "function");
214
+ assert(context.hasOwnProperty("session"), " expecting a session id in the context object");
215
+
216
+ const session = context.session as ServerSession;
217
+ if (!session) {
218
+ return callback(null, { statusCode: StatusCodes.BadInternalError });
219
+ }
220
+
221
+ const subscriptionId = inputArguments[0].value;
222
+ const subscription = session.getSubscription(subscriptionId);
223
+ if (!subscription) {
224
+ // subscription may belongs to a different session that ours
225
+ if (this.findSubscription(subscriptionId)) {
226
+ // if yes, then access to Subscription data should be denied
227
+ return callback(null, { statusCode: StatusCodes.BadUserAccessDenied });
228
+ }
229
+
230
+ return callback(null, { statusCode: StatusCodes.BadSubscriptionIdInvalid });
231
+ }
232
+ const result = subscription.getMonitoredItems();
233
+ assert(result.statusCode);
234
+ assert(result.serverHandles.length === result.clientHandles.length);
235
+ const callMethodResult = new CallMethodResult({
236
+ statusCode: result.statusCode,
237
+ outputArguments: [
238
+ { dataType: DataType.UInt32, arrayType: VariantArrayType.Array, value: new Uint32Array(result.serverHandles) },
239
+ { dataType: DataType.UInt32, arrayType: VariantArrayType.Array, value: new Uint32Array(result.clientHandles) }
240
+ ]
241
+ });
242
+ callback(null, callMethodResult);
243
+ }
244
+
245
+ function __bindVariable(self: ServerEngine, nodeId: NodeIdLike, options?: BindVariableOptions) {
246
+ options = options || {};
247
+
248
+ const variable = self.addressSpace!.findNode(nodeId) as UAVariable;
249
+ if (variable && variable.bindVariable) {
250
+ variable.bindVariable(options, true);
251
+ assert(typeof variable.asyncRefresh === "function");
252
+ assert(typeof (variable as any).refreshFunc === "function");
253
+ } else {
254
+ warningLog(
255
+ "Warning: cannot bind object with id ",
256
+ nodeId.toString(),
257
+ " please check your nodeset.xml file or add this node programmatically"
258
+ );
259
+ }
260
+ }
261
+
262
+ // note OPCUA 1.03 part 4 page 76
263
+ // The Server-assigned identifier for the Subscription (see 7.14 for IntegerId definition). This identifier shall
264
+ // be unique for the entire Server, not just for the Session, in order to allow the Subscription to be transferred
265
+ // to another Session using the TransferSubscriptions service.
266
+ // After Server start-up the generation of subscriptionIds should start from a random IntegerId or continue from
267
+ // the point before the restart.
268
+ let next_subscriptionId = Math.ceil(Math.random() * 1000000);
269
+
270
+ function _get_next_subscriptionId() {
271
+ debugLog(" next_subscriptionId = ", next_subscriptionId);
272
+ return next_subscriptionId++;
273
+ }
274
+
275
+ export type StringGetter = () => string;
276
+
277
+ export interface ServerEngineOptions {
278
+ applicationUri: string | StringGetter;
279
+
280
+ buildInfo?: BuildInfoOptions;
281
+ isAuditing?: boolean;
282
+ /**
283
+ * set to true to enable serverDiagnostics
284
+ */
285
+ serverDiagnosticsEnabled?: boolean;
286
+ serverCapabilities?: ServerCapabilitiesOptions;
287
+ historyServerCapabilities?: HistoryServerCapabilitiesOptions;
288
+ }
289
+
290
+ export interface CreateSessionOption {
291
+ clientDescription?: ApplicationDescription;
292
+ sessionTimeout?: number;
293
+ }
294
+
295
+ export type ClosingReason = "Timeout" | "Terminated" | "CloseSession" | "Forcing";
296
+
297
+ /**
298
+ *
299
+ */
300
+ export class ServerEngine extends EventEmitter {
301
+ public static readonly registry = new ObjectRegistry();
302
+
303
+ public isAuditing: boolean;
304
+ public serverDiagnosticsSummary: ServerDiagnosticsSummaryDataType;
305
+ public serverDiagnosticsEnabled: boolean;
306
+ public serverCapabilities: ServerCapabilities;
307
+ public historyServerCapabilities: HistoryServerCapabilities;
308
+ public clientDescription?: ApplicationDescription;
309
+
310
+ public addressSpace: AddressSpace | null;
311
+
312
+ // pseudo private
313
+ public _internalState: "creating" | "initializing" | "initialized" | "shutdown" | "disposed";
314
+
315
+ private _sessions: { [key: string]: ServerSession };
316
+ private _closedSessions: { [key: string]: ServerSession };
317
+ private _orphanPublishEngine?: ServerSidePublishEngineForOrphanSubscription;
318
+ private _shutdownTask: any[];
319
+ private _applicationUri: string;
320
+ private _expectedShutdownTime!: Date;
321
+ private _serverStatus: ServerStatusDataType;
322
+
323
+ constructor(options: ServerEngineOptions) {
324
+ super();
325
+
326
+ options = options || ({ applicationUri: "" } as ServerEngineOptions);
327
+ options.buildInfo = options.buildInfo || {};
328
+
329
+ ServerEngine.registry.register(this);
330
+
331
+ this._sessions = {};
332
+ this._closedSessions = {};
333
+ this._orphanPublishEngine = undefined; // will be constructed on demand
334
+
335
+ this.isAuditing = typeof options.isAuditing === "boolean" ? options.isAuditing : false;
336
+
337
+ options.buildInfo.buildDate = options.buildInfo.buildDate || new Date();
338
+ // ---------------------------------------------------- ServerStatusDataType
339
+ this._serverStatus = new ServerStatusDataType({
340
+ buildInfo: options.buildInfo,
341
+ currentTime: new Date(),
342
+ secondsTillShutdown: 0,
343
+ shutdownReason: { text: "" },
344
+ startTime: new Date(),
345
+ state: ServerState.NoConfiguration
346
+ });
347
+
348
+ // --------------------------------------------------- ServerCapabilities
349
+ options.serverCapabilities = options.serverCapabilities || {};
350
+ options.serverCapabilities.serverProfileArray = options.serverCapabilities.serverProfileArray || [
351
+ "Standard UA Server Profile",
352
+ "Embedded UA Server Profile",
353
+ "Micro Embedded Device Server Profile",
354
+ "Nano Embedded Device Server Profile"
355
+ ];
356
+ options.serverCapabilities.localeIdArray = options.serverCapabilities.localeIdArray || ["en-EN", "fr-FR"];
357
+
358
+ this.serverCapabilities = new ServerCapabilities(options.serverCapabilities);
359
+
360
+ // to do when spec is clear about what goes here!
361
+ // spec 1.04 says (in Part 4 7.33 SignedSoftwareCertificate
362
+ // Note: Details on SoftwareCertificates need to be defined in a future version.
363
+ this.serverCapabilities.softwareCertificates = [
364
+ // new SignedSoftwareCertificate({})
365
+ ];
366
+
367
+ // make sure minSupportedSampleRate matches MonitoredItem.minimumSamplingInterval
368
+ (this.serverCapabilities as any).__defineGetter__("minSupportedSampleRate", () => {
369
+ return MonitoredItem.minimumSamplingInterval;
370
+ });
371
+
372
+ this.historyServerCapabilities = new HistoryServerCapabilities(options.historyServerCapabilities);
373
+
374
+ // --------------------------------------------------- serverDiagnosticsSummary extension Object
375
+ this.serverDiagnosticsSummary = new ServerDiagnosticsSummaryDataType();
376
+ assert(this.serverDiagnosticsSummary.hasOwnProperty("currentSessionCount"));
377
+
378
+ // note spelling is different for serverDiagnosticsSummary.currentSubscriptionCount
379
+ // and sessionDiagnostics.currentSubscriptionsCount ( with an s)
380
+ assert(this.serverDiagnosticsSummary.hasOwnProperty("currentSubscriptionCount"));
381
+
382
+ (this.serverDiagnosticsSummary as any).__defineGetter__("currentSubscriptionCount", () => {
383
+ // currentSubscriptionCount returns the total number of subscriptions
384
+ // that are currently active on all sessions
385
+ let counter = 0;
386
+ Object.values(this._sessions).forEach((session: ServerSession) => {
387
+ counter += session.currentSubscriptionCount;
388
+ });
389
+ return counter;
390
+ });
391
+
392
+ this._internalState = "creating";
393
+
394
+ this.setServerState(ServerState.NoConfiguration);
395
+
396
+ this.addressSpace = null;
397
+
398
+ this._shutdownTask = [];
399
+
400
+ this._applicationUri = "";
401
+ if (typeof options.applicationUri === "function") {
402
+ (this as any).__defineGetter__("_applicationUri", options.applicationUri);
403
+ } else {
404
+ this._applicationUri = options.applicationUri || "<unset _applicationUri>";
405
+ }
406
+
407
+ options.serverDiagnosticsEnabled = Object.prototype.hasOwnProperty.call(options,"serverDiagnosticsEnable")
408
+ ? options.serverDiagnosticsEnabled
409
+ : true;
410
+
411
+ this.serverDiagnosticsEnabled = options.serverDiagnosticsEnabled!;
412
+ }
413
+ public isStarted(): boolean {
414
+ return !!this._serverStatus!;
415
+ }
416
+
417
+ public dispose() {
418
+ this.addressSpace = null;
419
+
420
+ assert(Object.keys(this._sessions).length === 0, "ServerEngine#_sessions not empty");
421
+ this._sessions = {};
422
+
423
+ // todo fix me
424
+ this._closedSessions = {};
425
+ assert(Object.keys(this._closedSessions).length === 0, "ServerEngine#_closedSessions not empty");
426
+ this._closedSessions = {};
427
+
428
+ if (this._orphanPublishEngine) {
429
+ this._orphanPublishEngine.dispose();
430
+ this._orphanPublishEngine = undefined;
431
+ }
432
+
433
+ this._shutdownTask = [];
434
+ this._serverStatus = null as any as ServerStatusDataType;
435
+ this._internalState = "disposed";
436
+ this.removeAllListeners();
437
+
438
+ ServerEngine.registry.unregister(this);
439
+ }
440
+
441
+ public get startTime(): Date {
442
+ return this._serverStatus.startTime!;
443
+ }
444
+
445
+ public get currentTime(): Date {
446
+ return this._serverStatus.currentTime!;
447
+ }
448
+
449
+ public get buildInfo(): BuildInfo {
450
+ return this._serverStatus.buildInfo;
451
+ }
452
+
453
+ /**
454
+ * register a function that will be called when the server will perform its shut down.
455
+ * @method registerShutdownTask
456
+ */
457
+ public registerShutdownTask(task: any) {
458
+ assert(typeof task === "function");
459
+ this._shutdownTask.push(task);
460
+ }
461
+
462
+ /**
463
+ * @method shutdown
464
+ */
465
+ public shutdown(): void {
466
+ debugLog("ServerEngine#shutdown");
467
+
468
+ this._internalState = "shutdown";
469
+ this.setServerState(ServerState.Shutdown);
470
+
471
+ // delete any existing sessions
472
+ const tokens = Object.keys(this._sessions).map((key: string) => {
473
+ const session = this._sessions[key];
474
+ return session.authenticationToken;
475
+ });
476
+
477
+ // delete and close any orphan subscriptions
478
+ if (this._orphanPublishEngine) {
479
+ this._orphanPublishEngine.shutdown();
480
+ }
481
+
482
+ // xx console.log("xxxxxxxxx ServerEngine.shutdown must terminate "+ tokens.length," sessions");
483
+
484
+ for (const token of tokens) {
485
+ this.closeSession(token, true, "Terminated");
486
+ }
487
+
488
+ // all sessions must have been terminated
489
+ assert(this.currentSessionCount === 0);
490
+
491
+ // all subscriptions must have been terminated
492
+ assert(this.currentSubscriptionCount === 0, "all subscriptions must have been terminated");
493
+
494
+ this._shutdownTask.push(shutdownAndDisposeAddressSpace);
495
+
496
+ // perform registerShutdownTask
497
+ for (const task of this._shutdownTask) {
498
+ task.call(this);
499
+ }
500
+
501
+ this.dispose();
502
+ }
503
+
504
+ /**
505
+ * the number of active sessions
506
+ */
507
+ public get currentSessionCount() {
508
+ return this.serverDiagnosticsSummary.currentSessionCount;
509
+ }
510
+
511
+ /**
512
+ * the cumulated number of sessions that have been opened since this object exists
513
+ */
514
+ public get cumulatedSessionCount() {
515
+ return this.serverDiagnosticsSummary.cumulatedSessionCount;
516
+ }
517
+
518
+ /**
519
+ * the number of active subscriptions.
520
+ */
521
+ public get currentSubscriptionCount() {
522
+ return this.serverDiagnosticsSummary.currentSubscriptionCount;
523
+ }
524
+
525
+ /**
526
+ * the cumulated number of subscriptions that have been created since this object exists
527
+ */
528
+ public get cumulatedSubscriptionCount(): number {
529
+ return this.serverDiagnosticsSummary.cumulatedSubscriptionCount;
530
+ }
531
+
532
+ public get rejectedSessionCount(): number {
533
+ return this.serverDiagnosticsSummary.rejectedSessionCount;
534
+ }
535
+
536
+ public get rejectedRequestsCount(): number {
537
+ return this.serverDiagnosticsSummary.rejectedRequestsCount;
538
+ }
539
+
540
+ public get sessionAbortCount(): number {
541
+ return this.serverDiagnosticsSummary.sessionAbortCount;
542
+ }
543
+
544
+ public get sessionTimeoutCount(): number {
545
+ return this.serverDiagnosticsSummary.sessionTimeoutCount;
546
+ }
547
+
548
+ public get publishingIntervalCount(): number {
549
+ return this.serverDiagnosticsSummary.publishingIntervalCount;
550
+ }
551
+
552
+ public incrementSessionTimeoutCount(): void {
553
+ if (this.serverDiagnosticsSummary && this.serverDiagnosticsEnabled) {
554
+ // The requests include all Services defined in Part 4 of the OPC UA Specification, also requests to create sessions. This number includes the securityRejectedRequestsCount.
555
+ this.serverDiagnosticsSummary.sessionTimeoutCount += 1;
556
+ }
557
+ }
558
+ public incrementSessionAbortCount(): void {
559
+ if (this.serverDiagnosticsSummary && this.serverDiagnosticsEnabled) {
560
+ // The requests include all Services defined in Part 4 of the OPC UA Specification, also requests to create sessions. This number includes the securityRejectedRequestsCount.
561
+ this.serverDiagnosticsSummary.sessionAbortCount += 1;
562
+ }
563
+ }
564
+ public incrementRejectedRequestsCount(): void {
565
+ if (this.serverDiagnosticsSummary && this.serverDiagnosticsEnabled) {
566
+ // The requests include all Services defined in Part 4 of the OPC UA Specification, also requests to create sessions. This number includes the securityRejectedRequestsCount.
567
+ this.serverDiagnosticsSummary.rejectedRequestsCount += 1;
568
+ }
569
+ }
570
+
571
+ /**
572
+ * increment rejected session count (also increment rejected requests count)
573
+ */
574
+ public incrementRejectedSessionCount(): void {
575
+ if (this.serverDiagnosticsSummary && this.serverDiagnosticsEnabled) {
576
+ // The requests include all Services defined in Part 4 of the OPC UA Specification, also requests to create sessions. This number includes the securityRejectedRequestsCount.
577
+ this.serverDiagnosticsSummary.rejectedSessionCount += 1;
578
+ }
579
+ this.incrementRejectedRequestsCount();
580
+ }
581
+
582
+ public incrementSecurityRejectedRequestsCount(): void {
583
+ if (this.serverDiagnosticsSummary && this.serverDiagnosticsEnabled) {
584
+ // The requests include all Services defined in Part 4 of the OPC UA Specification, also requests to create sessions. This number includes the securityRejectedRequestsCount.
585
+ this.serverDiagnosticsSummary.securityRejectedRequestsCount += 1;
586
+ }
587
+ this.incrementRejectedRequestsCount();
588
+ }
589
+
590
+ /**
591
+ * increment rejected session count (also increment rejected requests count)
592
+ */
593
+ public incrementSecurityRejectedSessionCount(): void {
594
+ if (this.serverDiagnosticsSummary && this.serverDiagnosticsEnabled) {
595
+ // The requests include all Services defined in Part 4 of the OPC UA Specification, also requests to create sessions. This number includes the securityRejectedRequestsCount.
596
+ this.serverDiagnosticsSummary.securityRejectedSessionCount += 1;
597
+ }
598
+ this.incrementSecurityRejectedRequestsCount();
599
+ }
600
+
601
+ public setShutdownTime(date: Date) {
602
+ this._expectedShutdownTime = date;
603
+ }
604
+ public setShutdownReason(reason: LocalizedTextLike): void {
605
+ this.addressSpace?.rootFolder.objects.server.serverStatus.shutdownReason.setValueFromSource({
606
+ dataType: DataType.LocalizedText,
607
+ value: coerceLocalizedText(reason)!
608
+ });
609
+ }
610
+ /**
611
+ * @method secondsTillShutdown
612
+ * @return the approximate number of seconds until the server will be shut down. The
613
+ * value is only relevant once the state changes into SHUTDOWN.
614
+ */
615
+ public secondsTillShutdown(): number {
616
+ if (!this._expectedShutdownTime) {
617
+ return 0;
618
+ }
619
+ // ToDo: implement a correct solution here
620
+ const now = Date.now();
621
+ return Math.max(0, Math.ceil((this._expectedShutdownTime.getTime() - now) / 1000));
622
+ }
623
+
624
+ /**
625
+ * the name of the server
626
+ */
627
+ public get serverName(): string {
628
+ return this._serverStatus.buildInfo!.productName!;
629
+ }
630
+
631
+ /**
632
+ * the server urn
633
+ */
634
+ public get serverNameUrn() {
635
+ return this._applicationUri;
636
+ }
637
+
638
+ /**
639
+ * the urn of the server namespace
640
+ */
641
+ public get serverNamespaceUrn() {
642
+ return this._applicationUri; // "urn:" + engine.serverName;
643
+ }
644
+ public get serverStatus() {
645
+ return this._serverStatus;
646
+ }
647
+
648
+ public setServerState(serverState: ServerState) {
649
+ assert(serverState !== null && serverState !== undefined);
650
+ this.addressSpace?.rootFolder?.objects?.server?.serverStatus?.state?.setValueFromSource({
651
+ dataType: DataType.UInt32,
652
+ value: serverState
653
+ });
654
+ }
655
+
656
+ public getServerDiagnosticsEnabledFlag(): boolean {
657
+ const server = this.addressSpace!.rootFolder.objects.server;
658
+ const serverDiagnostics = server.getComponentByName("ServerDiagnostics") as UAVariable;
659
+ if (!serverDiagnostics) {
660
+ return false;
661
+ }
662
+ return serverDiagnostics.readValue().value.value;
663
+ }
664
+
665
+ /**
666
+ * @method initialize
667
+ * @async
668
+ *
669
+ * @param options {Object}
670
+ * @param options.nodeset_filename {String} - [option](default : 'mini.Node.Set2.xml' )
671
+ * @param callback
672
+ */
673
+ public initialize(options: any, callback: any) {
674
+ assert(!this.addressSpace); // check that 'initialize' has not been already called
675
+
676
+ this._internalState = "initializing";
677
+
678
+ options = options || {};
679
+ assert(typeof callback === "function");
680
+
681
+ options.nodeset_filename = options.nodeset_filename || nodesets.standard;
682
+
683
+ const startTime = new Date();
684
+
685
+ debugLog("Loading ", options.nodeset_filename, "...");
686
+
687
+ this.addressSpace = AddressSpace.create();
688
+
689
+ // register namespace 1 (our namespace);
690
+ const serverNamespace = this.addressSpace.registerNamespace(this.serverNamespaceUrn);
691
+ assert(serverNamespace.index === 1);
692
+
693
+ generateAddressSpace(this.addressSpace, options.nodeset_filename, () => {
694
+ /* istanbul ignore next */
695
+ if (!this.addressSpace) {
696
+ throw new Error("Internal error");
697
+ }
698
+ const addressSpace = this.addressSpace;
699
+
700
+ const endTime = new Date();
701
+ debugLog("Loading ", options.nodeset_filename, " done : ", endTime.getTime() - startTime.getTime(), " ms");
702
+
703
+ const bindVariableIfPresent = (nodeId: NodeId, opts: any) => {
704
+ assert(nodeId instanceof NodeId);
705
+ assert(!nodeId.isEmpty());
706
+ const obj = addressSpace.findNode(nodeId);
707
+ if (obj) {
708
+ __bindVariable(this, nodeId, opts);
709
+ }
710
+ return obj;
711
+ };
712
+
713
+ // -------------------------------------------- install default get/put handler
714
+ const server_NamespaceArray_Id = makeNodeId(VariableIds.Server_NamespaceArray); // ns=0;i=2255
715
+ bindVariableIfPresent(server_NamespaceArray_Id, {
716
+ get() {
717
+ return new Variant({
718
+ arrayType: VariantArrayType.Array,
719
+ dataType: DataType.String,
720
+ value: addressSpace.getNamespaceArray().map((x) => x.namespaceUri)
721
+ });
722
+ },
723
+ set: null // read only
724
+ });
725
+
726
+ const server_NameUrn_var = new Variant({
727
+ arrayType: VariantArrayType.Array,
728
+ dataType: DataType.String,
729
+ value: [
730
+ this.serverNameUrn // this is us !
731
+ ]
732
+ });
733
+ const server_ServerArray_Id = makeNodeId(VariableIds.Server_ServerArray); // ns=0;i=2254
734
+
735
+ bindVariableIfPresent(server_ServerArray_Id, {
736
+ get() {
737
+ return server_NameUrn_var;
738
+ },
739
+ set: null // read only
740
+ });
741
+
742
+ // fix DefaultUserRolePermissions and DefaultUserRolePermissions
743
+ // of namespaces
744
+ const namespaces = makeNodeId(ObjectIds.Server_Namespaces);
745
+ const namespacesNode = addressSpace.findNode(namespaces) as UAObject;
746
+ if (namespacesNode) {
747
+ for (const ns of namespacesNode.getComponents()) {
748
+ const defaultUserRolePermissions = ns.getChildByName("DefaultUserRolePermissions") as UAVariable | null;
749
+ if (defaultUserRolePermissions) {
750
+ defaultUserRolePermissions.setValueFromSource({ dataType: DataType.Null });
751
+ }
752
+ const defaultRolePermissions = ns.getChildByName("DefaultRolePermissions") as UAVariable | null;
753
+ if (defaultRolePermissions) {
754
+ defaultRolePermissions.setValueFromSource({ dataType: DataType.Null });
755
+ }
756
+ }
757
+ }
758
+
759
+ const bindStandardScalar = (id: number, dataType: DataType, func: () => any, setter_func?: (value: any) => void) => {
760
+ assert(typeof id === "number", "expecting id to be a number");
761
+ assert(typeof func === "function");
762
+ assert(typeof setter_func === "function" || !setter_func);
763
+ assert(dataType !== null); // check invalid dataType
764
+
765
+ let setter_func2 = null;
766
+ if (setter_func) {
767
+ setter_func2 = (variant: Variant) => {
768
+ const variable2 = !!variant.value;
769
+ setter_func(variable2);
770
+ return StatusCodes.Good;
771
+ };
772
+ }
773
+
774
+ const nodeId = makeNodeId(id);
775
+
776
+ // make sur the provided function returns a valid value for the variant type
777
+ // This test may not be exhaustive but it will detect obvious mistakes.
778
+
779
+ /* istanbul ignore next */
780
+ if (!isValidVariant(VariantArrayType.Scalar, dataType, func())) {
781
+ errorLog("func", func());
782
+ throw new Error("bindStandardScalar : func doesn't provide an value of type " + DataType[dataType]);
783
+ }
784
+
785
+ return bindVariableIfPresent(nodeId, {
786
+ get() {
787
+ return new Variant({
788
+ arrayType: VariantArrayType.Scalar,
789
+ dataType,
790
+ value: func()
791
+ });
792
+ },
793
+ set: setter_func2
794
+ });
795
+ };
796
+
797
+ const bindStandardArray = (id: number, variantDataType: DataType, dataType: any, func: () => any[]) => {
798
+ assert(typeof func === "function");
799
+ assert(variantDataType !== null); // check invalid dataType
800
+
801
+ const nodeId = makeNodeId(id);
802
+
803
+ // make sur the provided function returns a valid value for the variant type
804
+ // This test may not be exhaustive but it will detect obvious mistakes.
805
+ assert(isValidVariant(VariantArrayType.Array, variantDataType, func()));
806
+
807
+ bindVariableIfPresent(nodeId, {
808
+ get() {
809
+ const value = func();
810
+ assert(Array.isArray(value));
811
+ return new Variant({
812
+ arrayType: VariantArrayType.Array,
813
+ dataType: variantDataType,
814
+ value
815
+ });
816
+ },
817
+ set: null // read only
818
+ });
819
+ };
820
+
821
+ bindStandardScalar(VariableIds.Server_EstimatedReturnTime, DataType.DateTime, () => minOPCUADate);
822
+
823
+ // TimeZoneDataType
824
+ const timeZoneDataType = addressSpace.findDataType(resolveNodeId(DataTypeIds.TimeZoneDataType))!;
825
+ // xx console.log(timeZoneDataType.toString());
826
+
827
+ const timeZone = new TimeZoneDataType({
828
+ daylightSavingInOffset: /* boolean*/ false,
829
+ offset: /* int16 */ 0
830
+ });
831
+ bindStandardScalar(VariableIds.Server_LocalTime, DataType.ExtensionObject, () => {
832
+ return timeZone;
833
+ });
834
+
835
+ bindStandardScalar(VariableIds.Server_ServiceLevel, DataType.Byte, () => {
836
+ return 255;
837
+ });
838
+
839
+ bindStandardScalar(VariableIds.Server_Auditing, DataType.Boolean, () => {
840
+ return this.isAuditing;
841
+ });
842
+
843
+ const engine = this;
844
+ const makeNotReadableIfEnabledFlagIsFalse = (variable: UAVariable) => {
845
+ const originalIsReadable = variable.isReadable;
846
+ variable.isUserReadable = checkReadableFlag;
847
+ function checkReadableFlag(this: UAVariable, context: SessionContext): boolean {
848
+ const isEnabled = engine.serverDiagnosticsEnabled;
849
+ return originalIsReadable.call(this, context) && isEnabled;
850
+ }
851
+ for (const c of variable.getAggregates()) {
852
+ if (c.nodeClass === NodeClass.Variable) {
853
+ makeNotReadableIfEnabledFlagIsFalse(c as UAVariable);
854
+ }
855
+ }
856
+ };
857
+
858
+ const bindServerDiagnostics = () => {
859
+ bindStandardScalar(
860
+ VariableIds.Server_ServerDiagnostics_EnabledFlag,
861
+ DataType.Boolean,
862
+ () => {
863
+ return this.serverDiagnosticsEnabled;
864
+ },
865
+ (newFlag: boolean) => {
866
+ this.serverDiagnosticsEnabled = newFlag;
867
+ }
868
+ );
869
+ const nodeId = makeNodeId(VariableIds.Server_ServerDiagnostics_ServerDiagnosticsSummary);
870
+ const serverDiagnosticsSummaryNode = addressSpace.findNode(nodeId) as UAServerDiagnosticsSummary<ServerDiagnosticsSummaryDataType>;
871
+
872
+ if (serverDiagnosticsSummaryNode) {
873
+ serverDiagnosticsSummaryNode.bindExtensionObject(this.serverDiagnosticsSummary);
874
+ this.serverDiagnosticsSummary = serverDiagnosticsSummaryNode.$extensionObject;
875
+ makeNotReadableIfEnabledFlagIsFalse(serverDiagnosticsSummaryNode);
876
+ }
877
+ };
878
+
879
+ const bindServerStatus = () => {
880
+ const serverStatusNode = addressSpace.findNode(makeNodeId(VariableIds.Server_ServerStatus)) as UAServerStatus<DTServerStatus>;
881
+
882
+ if (!serverStatusNode) {
883
+ return;
884
+ }
885
+ if (serverStatusNode) {
886
+ serverStatusNode.bindExtensionObject(this._serverStatus);
887
+ serverStatusNode.minimumSamplingInterval = 1000;
888
+ }
889
+
890
+ const currentTimeNode = addressSpace.findNode(
891
+ makeNodeId(VariableIds.Server_ServerStatus_CurrentTime)
892
+ ) as UAVariable;
893
+
894
+ if (currentTimeNode) {
895
+ currentTimeNode.minimumSamplingInterval = 1000;
896
+ }
897
+ const secondsTillShutdown = addressSpace.findNode(
898
+ makeNodeId(VariableIds.Server_ServerStatus_SecondsTillShutdown)
899
+ ) as UAVariable;
900
+
901
+ if (secondsTillShutdown) {
902
+ secondsTillShutdown.minimumSamplingInterval = 1000;
903
+ }
904
+
905
+ assert(serverStatusNode.$extensionObject);
906
+
907
+ serverStatusNode.$extensionObject = new Proxy(serverStatusNode.$extensionObject, {
908
+ get(target, prop) {
909
+ if (prop === "currentTime") {
910
+ serverStatusNode.currentTime.touchValue();
911
+ return new Date();
912
+ } else if (prop === "secondsTillShutdown") {
913
+ serverStatusNode.secondsTillShutdown.touchValue();
914
+ return engine.secondsTillShutdown();
915
+ }
916
+ return (target as any)[prop];
917
+ }
918
+ });
919
+ this._serverStatus = serverStatusNode.$extensionObject;
920
+ };
921
+
922
+ const bindServerCapabilities = () => {
923
+ bindStandardArray(
924
+ VariableIds.Server_ServerCapabilities_ServerProfileArray,
925
+ DataType.String,
926
+ DataType.String,
927
+ () => {
928
+ return this.serverCapabilities.serverProfileArray;
929
+ }
930
+ );
931
+
932
+ bindStandardArray(VariableIds.Server_ServerCapabilities_LocaleIdArray, DataType.String, "LocaleId", () => {
933
+ return this.serverCapabilities.localeIdArray;
934
+ });
935
+
936
+ bindStandardScalar(VariableIds.Server_ServerCapabilities_MinSupportedSampleRate, DataType.Double, () => {
937
+ return this.serverCapabilities.minSupportedSampleRate;
938
+ });
939
+
940
+ bindStandardScalar(VariableIds.Server_ServerCapabilities_MaxBrowseContinuationPoints, DataType.UInt16, () => {
941
+ return this.serverCapabilities.maxBrowseContinuationPoints;
942
+ });
943
+
944
+ bindStandardScalar(VariableIds.Server_ServerCapabilities_MaxQueryContinuationPoints, DataType.UInt16, () => {
945
+ return this.serverCapabilities.maxQueryContinuationPoints;
946
+ });
947
+
948
+ bindStandardScalar(VariableIds.Server_ServerCapabilities_MaxHistoryContinuationPoints, DataType.UInt16, () => {
949
+ return this.serverCapabilities.maxHistoryContinuationPoints;
950
+ });
951
+
952
+ // added by DI : Server-specific period of time in milliseconds until the Server will revoke a lock.
953
+ // TODO bindStandardScalar(VariableIds.Server_ServerCapabilities_MaxInactiveLockTime,
954
+ // TODO DataType.UInt16, function () {
955
+ // TODO return self.serverCapabilities.maxInactiveLockTime;
956
+ // TODO });
957
+
958
+ bindStandardArray(
959
+ VariableIds.Server_ServerCapabilities_SoftwareCertificates,
960
+ DataType.ExtensionObject,
961
+ "SoftwareCertificates",
962
+ () => {
963
+ return this.serverCapabilities.softwareCertificates;
964
+ }
965
+ );
966
+
967
+ bindStandardScalar(VariableIds.Server_ServerCapabilities_MaxArrayLength, DataType.UInt32, () => {
968
+ return this.serverCapabilities.maxArrayLength;
969
+ });
970
+
971
+ bindStandardScalar(VariableIds.Server_ServerCapabilities_MaxStringLength, DataType.UInt32, () => {
972
+ return this.serverCapabilities.maxStringLength;
973
+ });
974
+
975
+ bindStandardScalar(VariableIds.Server_ServerCapabilities_MaxByteStringLength, DataType.UInt32, () => {
976
+ return this.serverCapabilities.maxByteStringLength;
977
+ });
978
+
979
+ const bindOperationLimits = (operationLimits: OperationLimits) => {
980
+ assert(operationLimits !== null && typeof operationLimits === "object");
981
+
982
+ const keys = Object.keys(operationLimits);
983
+
984
+ keys.forEach((key: string) => {
985
+ const uid = "Server_ServerCapabilities_OperationLimits_" + upperCaseFirst(key);
986
+ const nodeId = makeNodeId((VariableIds as any)[uid]);
987
+ assert(!nodeId.isEmpty());
988
+
989
+ bindStandardScalar((VariableIds as any)[uid], DataType.UInt32, () => {
990
+ return (operationLimits as any)[key];
991
+ });
992
+ });
993
+ };
994
+
995
+ bindOperationLimits(this.serverCapabilities.operationLimits);
996
+
997
+ // i=2399 [ProgramStateMachineType_ProgramDiagnostics];
998
+ function fix_ProgramStateMachineType_ProgramDiagnostics() {
999
+ const nodeId = coerceNodeId("i=2399"); // ProgramStateMachineType_ProgramDiagnostics
1000
+ const variable = addressSpace.findNode(nodeId) as UAVariable;
1001
+ if (variable) {
1002
+ (variable as any).$extensionObject = new ProgramDiagnosticDataType({});
1003
+ // variable.setValueFromSource({
1004
+ // dataType: DataType.ExtensionObject,
1005
+ // // value: new ProgramDiagnostic2DataType()
1006
+ // value: new ProgramDiagnosticDataType({})
1007
+ // });
1008
+ }
1009
+ }
1010
+ fix_ProgramStateMachineType_ProgramDiagnostics();
1011
+ };
1012
+
1013
+ const bindHistoryServerCapabilities = () => {
1014
+ bindStandardScalar(VariableIds.HistoryServerCapabilities_MaxReturnDataValues, DataType.UInt32, () => {
1015
+ return this.historyServerCapabilities.maxReturnDataValues;
1016
+ });
1017
+
1018
+ bindStandardScalar(VariableIds.HistoryServerCapabilities_MaxReturnEventValues, DataType.UInt32, () => {
1019
+ return this.historyServerCapabilities.maxReturnEventValues;
1020
+ });
1021
+
1022
+ bindStandardScalar(VariableIds.HistoryServerCapabilities_AccessHistoryDataCapability, DataType.Boolean, () => {
1023
+ return this.historyServerCapabilities.accessHistoryDataCapability;
1024
+ });
1025
+ bindStandardScalar(VariableIds.HistoryServerCapabilities_AccessHistoryEventsCapability, DataType.Boolean, () => {
1026
+ return this.historyServerCapabilities.accessHistoryEventsCapability;
1027
+ });
1028
+ bindStandardScalar(VariableIds.HistoryServerCapabilities_InsertDataCapability, DataType.Boolean, () => {
1029
+ return this.historyServerCapabilities.insertDataCapability;
1030
+ });
1031
+ bindStandardScalar(VariableIds.HistoryServerCapabilities_ReplaceDataCapability, DataType.Boolean, () => {
1032
+ return this.historyServerCapabilities.replaceDataCapability;
1033
+ });
1034
+ bindStandardScalar(VariableIds.HistoryServerCapabilities_UpdateDataCapability, DataType.Boolean, () => {
1035
+ return this.historyServerCapabilities.updateDataCapability;
1036
+ });
1037
+
1038
+ bindStandardScalar(VariableIds.HistoryServerCapabilities_InsertEventCapability, DataType.Boolean, () => {
1039
+ return this.historyServerCapabilities.insertEventCapability;
1040
+ });
1041
+
1042
+ bindStandardScalar(VariableIds.HistoryServerCapabilities_ReplaceEventCapability, DataType.Boolean, () => {
1043
+ return this.historyServerCapabilities.replaceEventCapability;
1044
+ });
1045
+
1046
+ bindStandardScalar(VariableIds.HistoryServerCapabilities_UpdateEventCapability, DataType.Boolean, () => {
1047
+ return this.historyServerCapabilities.updateEventCapability;
1048
+ });
1049
+
1050
+ bindStandardScalar(VariableIds.HistoryServerCapabilities_DeleteEventCapability, DataType.Boolean, () => {
1051
+ return this.historyServerCapabilities.deleteEventCapability;
1052
+ });
1053
+
1054
+ bindStandardScalar(VariableIds.HistoryServerCapabilities_DeleteRawCapability, DataType.Boolean, () => {
1055
+ return this.historyServerCapabilities.deleteRawCapability;
1056
+ });
1057
+
1058
+ bindStandardScalar(VariableIds.HistoryServerCapabilities_DeleteAtTimeCapability, DataType.Boolean, () => {
1059
+ return this.historyServerCapabilities.deleteAtTimeCapability;
1060
+ });
1061
+
1062
+ bindStandardScalar(VariableIds.HistoryServerCapabilities_InsertAnnotationCapability, DataType.Boolean, () => {
1063
+ return this.historyServerCapabilities.insertAnnotationCapability;
1064
+ });
1065
+ };
1066
+
1067
+ bindServerDiagnostics();
1068
+
1069
+ bindServerStatus();
1070
+
1071
+ bindServerCapabilities();
1072
+
1073
+ bindHistoryServerCapabilities();
1074
+
1075
+ const bindExtraStuff = () => {
1076
+ // mainly for compliance
1077
+
1078
+ // The version number for the data type description. i=104
1079
+ bindStandardScalar(VariableIds.DataTypeDescriptionType_DataTypeVersion, DataType.UInt16, () => {
1080
+ return 0.0;
1081
+ });
1082
+
1083
+ const namingRuleDataTypeNode = addressSpace.findDataType(resolveNodeId(DataTypeIds.NamingRuleType))! as UADataType;
1084
+ // xx console.log(nrt.toString());
1085
+ if (namingRuleDataTypeNode) {
1086
+ const namingRuleType = (namingRuleDataTypeNode as any)._getEnumerationInfo().nameIndex; // getEnumeration("NamingRuleType");
1087
+ if (!namingRuleType) {
1088
+ throw new Error("Cannot find Enumeration definition for NamingRuleType");
1089
+ }
1090
+ // i=111
1091
+ bindStandardScalar(VariableIds.ModellingRuleType_NamingRule, DataType.UInt16, () => {
1092
+ return 0;
1093
+ });
1094
+
1095
+ // i=112
1096
+ bindStandardScalar(VariableIds.ModellingRule_Mandatory_NamingRule, DataType.UInt16, () => {
1097
+ return namingRuleType.Mandatory ? namingRuleType.Mandatory.value : 0;
1098
+ });
1099
+
1100
+ // i=113
1101
+ bindStandardScalar(VariableIds.ModellingRule_Optional_NamingRule, DataType.UInt16, () => {
1102
+ return namingRuleType.Optional ? namingRuleType.Optional.value : 0;
1103
+ });
1104
+ // i=114
1105
+ bindStandardScalar(VariableIds.ModellingRule_ExposesItsArray_NamingRule, DataType.UInt16, () => {
1106
+ return namingRuleType.ExposesItsArray ? namingRuleType.ExposesItsArray.value : 0;
1107
+ });
1108
+ bindStandardScalar(VariableIds.ModellingRule_MandatoryPlaceholder_NamingRule, DataType.UInt16, () => {
1109
+ return namingRuleType.MandatoryPlaceholder ? namingRuleType.MandatoryPlaceholder.value : 0;
1110
+ });
1111
+ }
1112
+ };
1113
+
1114
+ bindExtraStuff();
1115
+
1116
+ this.__internal_bindMethod(makeNodeId(MethodIds.Server_GetMonitoredItems), getMonitoredItemsId.bind(this));
1117
+ this.__internal_bindMethod(makeNodeId(MethodIds.Server_SetSubscriptionDurable), setSubscriptionDurable.bind(this));
1118
+
1119
+ // fix getMonitoredItems.outputArguments arrayDimensions
1120
+ const fixGetMonitoredItemArgs = () => {
1121
+ const objects = this.addressSpace!.rootFolder?.objects;
1122
+ if (!objects || !objects.server || !objects.server.getMonitoredItems) {
1123
+ return;
1124
+ }
1125
+ const outputArguments = objects.server.getMonitoredItems.outputArguments!;
1126
+ const dataValue = outputArguments.readValue();
1127
+ assert(dataValue.value.value[0].arrayDimensions.length === 1 && dataValue.value.value[0].arrayDimensions[0] === 0);
1128
+ assert(dataValue.value.value[1].arrayDimensions.length === 1 && dataValue.value.value[1].arrayDimensions[0] === 0);
1129
+ };
1130
+ fixGetMonitoredItemArgs();
1131
+
1132
+ const prepareServerDiagnostics = () => {
1133
+ const addressSpace1 = this.addressSpace!;
1134
+
1135
+ if (!addressSpace1.rootFolder.objects) {
1136
+ return;
1137
+ }
1138
+ const server = addressSpace1.rootFolder.objects.server;
1139
+
1140
+ if (!server) {
1141
+ return;
1142
+ }
1143
+
1144
+ // create SessionsDiagnosticsSummary
1145
+ const serverDiagnosticsNode = server.getComponentByName("ServerDiagnostics") as UAServerDiagnostics;
1146
+ if (!serverDiagnosticsNode) {
1147
+ return;
1148
+ }
1149
+ if (true) {
1150
+ // set serverDiagnosticsNode enabledFlag writeable for admin user only
1151
+ // TO DO ...
1152
+ serverDiagnosticsNode.enabledFlag.userAccessLevel = makeAccessLevelFlag("CurrentRead");
1153
+ serverDiagnosticsNode.enabledFlag.accessLevel = makeAccessLevelFlag("CurrentRead");
1154
+ }
1155
+
1156
+ // A Server may not expose the SamplingIntervalDiagnosticsArray if it does not use fixed sampling rates.
1157
+ // because we are not using fixed sampling rate, we need to remove the optional SamplingIntervalDiagnosticsArray
1158
+ // component
1159
+ const samplingIntervalDiagnosticsArray = serverDiagnosticsNode.getComponentByName(
1160
+ "SamplingIntervalDiagnosticsArray"
1161
+ );
1162
+ if (samplingIntervalDiagnosticsArray) {
1163
+ addressSpace.deleteNode(samplingIntervalDiagnosticsArray);
1164
+ const s = serverDiagnosticsNode.getComponents();
1165
+ // xx console.log(s.map((x) => x.browseName.toString()).join(" "));
1166
+ }
1167
+
1168
+ const subscriptionDiagnosticsArrayNode = serverDiagnosticsNode.getComponentByName(
1169
+ "SubscriptionDiagnosticsArray"
1170
+ )! as UADynamicVariableArray<SessionDiagnosticsDataType>;
1171
+ assert(subscriptionDiagnosticsArrayNode.nodeClass === NodeClass.Variable);
1172
+ bindExtObjArrayNode(subscriptionDiagnosticsArrayNode, "SubscriptionDiagnosticsType", "subscriptionId");
1173
+
1174
+ makeNotReadableIfEnabledFlagIsFalse(subscriptionDiagnosticsArrayNode);
1175
+
1176
+ const sessionsDiagnosticsSummary = serverDiagnosticsNode.getComponentByName("SessionsDiagnosticsSummary")!;
1177
+
1178
+ const sessionDiagnosticsArray = sessionsDiagnosticsSummary.getComponentByName(
1179
+ "SessionDiagnosticsArray"
1180
+ )! as UADynamicVariableArray<SessionDiagnosticsDataType>;
1181
+ assert(sessionDiagnosticsArray.nodeClass === NodeClass.Variable);
1182
+
1183
+ bindExtObjArrayNode(sessionDiagnosticsArray, "SessionDiagnosticsVariableType", "sessionId");
1184
+
1185
+ const varType = addressSpace.findVariableType("SessionSecurityDiagnosticsType");
1186
+ if (!varType) {
1187
+ debugLog("Warning cannot find SessionSecurityDiagnosticsType variable Type");
1188
+ } else {
1189
+ const sessionSecurityDiagnosticsArray = sessionsDiagnosticsSummary.getComponentByName(
1190
+ "SessionSecurityDiagnosticsArray"
1191
+ )! as UADynamicVariableArray<SessionSecurityDiagnosticsDataType>;
1192
+ assert(sessionSecurityDiagnosticsArray.nodeClass === NodeClass.Variable);
1193
+ bindExtObjArrayNode(sessionSecurityDiagnosticsArray, "SessionSecurityDiagnosticsType", "sessionId");
1194
+ ensureObjectIsSecure(sessionSecurityDiagnosticsArray);
1195
+ }
1196
+ };
1197
+
1198
+ prepareServerDiagnostics();
1199
+
1200
+ this._internalState = "initialized";
1201
+ this.setServerState(ServerState.Running);
1202
+ setImmediate(callback);
1203
+ });
1204
+ }
1205
+
1206
+ /**
1207
+ *
1208
+ * @method browseSingleNode
1209
+ * @param nodeId {NodeId|String} : the nodeid of the element to browse
1210
+ * @param browseDescription
1211
+ * @param browseDescription.browseDirection {BrowseDirection} :
1212
+ * @param browseDescription.referenceTypeId {String|NodeId}
1213
+ * @param [context]
1214
+ * @return the browse result
1215
+ */
1216
+ public browseSingleNode(nodeId: NodeIdLike, browseDescription: BrowseDescription, context?: ISessionContext): BrowseResult {
1217
+ const addressSpace = this.addressSpace!;
1218
+ return addressSpace.browseSingleNode(nodeId, browseDescription, context);
1219
+ }
1220
+
1221
+ public async browseWithAutomaticExpansion(nodesToBrowse: BrowseDescription[], context?: ISessionContext) {
1222
+ // do expansion first
1223
+ for (const browseDescription of nodesToBrowse) {
1224
+ const nodeId = resolveNodeId(browseDescription.nodeId);
1225
+ const node = this.addressSpace!.findNode(nodeId);
1226
+ if (node) {
1227
+ if (node.onFirstBrowseAction) {
1228
+ try {
1229
+ await node.onFirstBrowseAction();
1230
+ node.onFirstBrowseAction = undefined;
1231
+ } catch (err) {
1232
+ if (err instanceof Error) {
1233
+ console.log("onFirstBrowseAction method has failed", err.message);
1234
+ }
1235
+ console.log(err);
1236
+ }
1237
+ assert(node.onFirstBrowseAction === undefined, "expansion can only be made once");
1238
+ }
1239
+ }
1240
+ }
1241
+ return this.browse(nodesToBrowse, context);
1242
+ }
1243
+ /**
1244
+ *
1245
+ */
1246
+ public browse(nodesToBrowse: BrowseDescription[], context?: ISessionContext): BrowseResult[] {
1247
+ const results: BrowseResult[] = [];
1248
+ for (const browseDescription of nodesToBrowse) {
1249
+ const nodeId = resolveNodeId(browseDescription.nodeId);
1250
+ const r = this.browseSingleNode(nodeId, browseDescription, context);
1251
+ results.push(r);
1252
+ }
1253
+ return results;
1254
+ }
1255
+
1256
+ /**
1257
+ *
1258
+ * @method readSingleNode
1259
+ * @param context
1260
+ * @param nodeId
1261
+ * @param attributeId
1262
+ * @param [timestampsToReturn=TimestampsToReturn.Neither]
1263
+ * @return DataValue
1264
+ */
1265
+ public readSingleNode(
1266
+ context: ISessionContext,
1267
+ nodeId: NodeId | string,
1268
+ attributeId: AttributeIds,
1269
+ timestampsToReturn?: TimestampsToReturn
1270
+ ): DataValue {
1271
+ context.currentTime = getCurrentClock();
1272
+ return this._readSingleNode(
1273
+ context,
1274
+ new ReadValueId({
1275
+ attributeId,
1276
+ nodeId: resolveNodeId(nodeId)
1277
+ }),
1278
+ timestampsToReturn
1279
+ );
1280
+ }
1281
+
1282
+ /**
1283
+ *
1284
+ *
1285
+ * Maximum age of the value to be read in milliseconds. The age of the value is based on the difference between
1286
+ * the ServerTimestamp and the time when the Server starts processing the request. For example if the Client
1287
+ * specifies a maxAge of 500 milliseconds and it takes 100 milliseconds until the Server starts processing
1288
+ * the request, the age of the returned value could be 600 milliseconds prior to the time it was requested.
1289
+ * If the Server has one or more values of an Attribute that are within the maximum age, it can return any one
1290
+ * of the values or it can read a new value from the data source. The number of values of an Attribute that
1291
+ * a Server has depends on the number of MonitoredItems that are defined for the Attribute. In any case,
1292
+ * the Client can make no assumption about which copy of the data will be returned.
1293
+ * If the Server does not have a value that is within the maximum age, it shall attempt to read a new value
1294
+ * from the data source.
1295
+ * If the Server cannot meet the requested maxAge, it returns its 'best effort' value rather than rejecting the
1296
+ * request.
1297
+ * This may occur when the time it takes the Server to process and return the new data value after it has been
1298
+ * accessed is greater than the specified maximum age.
1299
+ * If maxAge is set to 0, the Server shall attempt to read a new value from the data source.
1300
+ * If maxAge is set to the max Int32 value or greater, the Server shall attempt to get a cached value.
1301
+ * Negative values are invalid for maxAge.
1302
+ *
1303
+ * @return an array of DataValue
1304
+ */
1305
+ public read(context: ISessionContext, readRequest: ReadRequest): DataValue[] {
1306
+ assert(context instanceof SessionContext);
1307
+ assert(readRequest instanceof ReadRequest);
1308
+ assert(readRequest.maxAge >= 0);
1309
+
1310
+ const timestampsToReturn = readRequest.timestampsToReturn;
1311
+
1312
+ const nodesToRead = readRequest.nodesToRead || [];
1313
+ assert(Array.isArray(nodesToRead));
1314
+
1315
+ context.currentTime = getCurrentClock();
1316
+
1317
+ const dataValues: DataValue[] = [];
1318
+ for (const readValueId of nodesToRead) {
1319
+ const dataValue = this._readSingleNode(context, readValueId, timestampsToReturn);
1320
+ if (timestampsToReturn === TimestampsToReturn.Server) {
1321
+ dataValue.sourceTimestamp = null;
1322
+ dataValue.sourcePicoseconds = 0;
1323
+ }
1324
+ if (
1325
+ (timestampsToReturn === TimestampsToReturn.Both || timestampsToReturn === TimestampsToReturn.Server) &&
1326
+ (!dataValue.serverTimestamp || dataValue.serverTimestamp.getTime() === minOPCUADate.getTime())
1327
+ ) {
1328
+ dataValue.serverTimestamp = context.currentTime.timestamp;
1329
+ dataValue.sourcePicoseconds = 0; // context.currentTime.picosecond // do we really need picosecond here ? this would inflate binary data
1330
+ }
1331
+ dataValues.push(dataValue);
1332
+ }
1333
+ return dataValues;
1334
+ }
1335
+
1336
+ /**
1337
+ *
1338
+ * @method writeSingleNode
1339
+ * @param context
1340
+ * @param writeValue
1341
+ * @param callback
1342
+ * @param callback.err
1343
+ * @param callback.statusCode
1344
+ * @async
1345
+ */
1346
+ public writeSingleNode(
1347
+ context: ISessionContext,
1348
+ writeValue: WriteValue,
1349
+ callback: (err: Error | null, statusCode?: StatusCode) => void
1350
+ ) {
1351
+ assert(context instanceof SessionContext);
1352
+ assert(typeof callback === "function");
1353
+ assert(writeValue.schema.name === "WriteValue");
1354
+ assert(writeValue.value instanceof DataValue);
1355
+
1356
+ if (writeValue.value.value === null) {
1357
+ return callback(null, StatusCodes.BadTypeMismatch);
1358
+ }
1359
+
1360
+ assert(writeValue.value.value instanceof Variant);
1361
+
1362
+ const nodeId = writeValue.nodeId;
1363
+
1364
+ const obj = this.__findNode(nodeId) as UAVariable;
1365
+ if (!obj) {
1366
+ return callback(null, StatusCodes.BadNodeIdUnknown);
1367
+ } else {
1368
+ obj.writeAttribute(context, writeValue, callback);
1369
+ }
1370
+ }
1371
+
1372
+ /**
1373
+ * write a collection of nodes
1374
+ * @method write
1375
+ * @param context
1376
+ * @param nodesToWrite
1377
+ * @param callback
1378
+ * @param callback.err
1379
+ * @param callback.results
1380
+ * @async
1381
+ */
1382
+ public write(
1383
+ context: ISessionContext,
1384
+ nodesToWrite: WriteValue[],
1385
+ callback: (err: Error | null, statusCodes?: StatusCode[]) => void
1386
+ ) {
1387
+ assert(context instanceof SessionContext);
1388
+ assert(typeof callback === "function");
1389
+
1390
+ context.currentTime = getCurrentClock();
1391
+
1392
+ ensureDatatypeExtractedWithCallback(
1393
+ this.addressSpace!,
1394
+ (err2: Error | null, extraDataTypeManager?: ExtraDataTypeManager) => {
1395
+ if (err2) {
1396
+ return callback(err2);
1397
+ }
1398
+ const performWrite = (writeValue: WriteValue, inner_callback: StatusCodeCallback) => {
1399
+ resolveOpaqueOnAddressSpace(this.addressSpace!, writeValue.value.value!)
1400
+ .then(() => {
1401
+ this.writeSingleNode(context, writeValue, inner_callback);
1402
+ })
1403
+ .catch(inner_callback);
1404
+ };
1405
+ // tslint:disable:array-type
1406
+ async.map(nodesToWrite, performWrite, (err?: Error | null, statusCodes?: (StatusCode | undefined)[]) => {
1407
+ assert(Array.isArray(statusCodes));
1408
+ callback(err!, statusCodes as StatusCode[]);
1409
+ });
1410
+ }
1411
+ );
1412
+ }
1413
+
1414
+ /**
1415
+ *
1416
+ */
1417
+ public historyReadSingleNode(
1418
+ context: ISessionContext,
1419
+ nodeId: NodeId,
1420
+ attributeId: AttributeIds,
1421
+ historyReadDetails: ReadRawModifiedDetails | ReadEventDetails | ReadProcessedDetails | ReadAtTimeDetails,
1422
+ timestampsToReturn: TimestampsToReturn,
1423
+ callback: (err: Error | null, results?: HistoryReadResult) => void
1424
+ ): void {
1425
+ if (timestampsToReturn === TimestampsToReturn.Invalid) {
1426
+ callback(
1427
+ null,
1428
+ new HistoryReadResult({
1429
+ statusCode: StatusCodes.BadTimestampsToReturnInvalid
1430
+ })
1431
+ );
1432
+ return;
1433
+ }
1434
+ assert(context instanceof SessionContext);
1435
+ this._historyReadSingleNode(
1436
+ context,
1437
+ new HistoryReadValueId({
1438
+ nodeId
1439
+ }),
1440
+ historyReadDetails,
1441
+ timestampsToReturn,
1442
+ callback
1443
+ );
1444
+ }
1445
+
1446
+ /**
1447
+ *
1448
+ * @method historyRead
1449
+ * @param context {SessionContext}
1450
+ * @param historyReadRequest {HistoryReadRequest}
1451
+ * @param historyReadRequest.requestHeader {RequestHeader}
1452
+ * @param historyReadRequest.historyReadDetails {HistoryReadDetails}
1453
+ * @param historyReadRequest.timestampsToReturn {TimestampsToReturn}
1454
+ * @param historyReadRequest.releaseContinuationPoints {Boolean}
1455
+ * @param historyReadRequest.nodesToRead {HistoryReadValueId[]}
1456
+ * @param callback
1457
+ * @param callback.err
1458
+ * @param callback.results {HistoryReadResult[]}
1459
+ */
1460
+ public historyRead(
1461
+ context: ISessionContext,
1462
+ historyReadRequest: HistoryReadRequest,
1463
+ callback: (err: Error | null, results: HistoryReadResult[]) => void
1464
+ ) {
1465
+ assert(context instanceof SessionContext);
1466
+ assert(historyReadRequest instanceof HistoryReadRequest);
1467
+ assert(typeof callback === "function");
1468
+
1469
+ const timestampsToReturn = historyReadRequest.timestampsToReturn;
1470
+ const historyReadDetails = historyReadRequest.historyReadDetails! as HistoryReadDetails;
1471
+
1472
+ const nodesToRead = historyReadRequest.nodesToRead || ([] as HistoryReadValueId[]);
1473
+
1474
+ assert(historyReadDetails instanceof HistoryReadDetails);
1475
+ assert(Array.isArray(nodesToRead));
1476
+
1477
+ // special cases with ReadProcessedDetails
1478
+ const historyData: HistoryReadResult[] = [];
1479
+ if (historyReadDetails instanceof ReadProcessedDetails) {
1480
+ //
1481
+ if (!historyReadDetails.aggregateType || historyReadDetails.aggregateType.length !== nodesToRead.length) {
1482
+ return callback(null, [new HistoryReadResult({ statusCode: StatusCodes.BadInvalidArgument })]);
1483
+ }
1484
+ interface M {
1485
+ nodeToRead: HistoryReadValueId;
1486
+ processDetail: ReadProcessedDetails;
1487
+ indexes: number[];
1488
+ }
1489
+ // const map: Map<string, M> = new Map();
1490
+ // for (let i = 0; i < nodesToRead.length; i++) {
1491
+ // const nodeToRead = nodesToRead[i];
1492
+ // const aggregateType = historyReadDetails.aggregateType[i];
1493
+ // const key = nodesToRead.toString();
1494
+ // if (!map.has(key)) {
1495
+ // map.set(key, {
1496
+ // nodeToRead,
1497
+ // indexes: [],
1498
+ // processDetail: new ReadProcessedDetails({ ...historyReadDetails, aggregateType: [] })
1499
+ // });
1500
+ // }
1501
+ // map.get(key)!.processDetail.aggregateType?.push(aggregateType);
1502
+ // map.get(key)!.indexes.push(i);
1503
+ // }
1504
+ // const m = [...map.values()];
1505
+ const elements: M[] = [];
1506
+ for (let i = 0; i < nodesToRead.length; i++) {
1507
+ const nodeToRead = nodesToRead[i];
1508
+ const aggregateType = historyReadDetails.aggregateType[i];
1509
+ elements.push({
1510
+ indexes: [i],
1511
+ nodeToRead,
1512
+ processDetail: new ReadProcessedDetails({ ...historyReadDetails, aggregateType: [aggregateType] })
1513
+ });
1514
+ }
1515
+
1516
+ async.forEach(
1517
+ elements,
1518
+ (m: M, _local_callback: (err: Error | null) => void) => {
1519
+ this._historyReadSingleNode(
1520
+ context,
1521
+ m.nodeToRead,
1522
+ m.processDetail,
1523
+ timestampsToReturn,
1524
+ (err: Error | null, result?: any) => {
1525
+ if (err && !result) {
1526
+ result = new HistoryReadResult({ statusCode: StatusCodes.BadInternalError });
1527
+ }
1528
+ historyData.push(result);
1529
+ _local_callback(null);
1530
+ }
1531
+ );
1532
+ },
1533
+ (err?: Error | null) => {
1534
+ callback(err!, historyData);
1535
+ }
1536
+ );
1537
+ return;
1538
+ }
1539
+ async.eachSeries(
1540
+ nodesToRead,
1541
+ (nodeToRead: HistoryReadValueId, cbNode: () => void) => {
1542
+ this._historyReadSingleNode(
1543
+ context,
1544
+ nodeToRead,
1545
+ historyReadDetails,
1546
+ timestampsToReturn,
1547
+ (err: Error | null, result?: any) => {
1548
+ if (err && !result) {
1549
+ result = new HistoryReadResult({ statusCode: StatusCodes.BadInternalError });
1550
+ }
1551
+ historyData.push(result);
1552
+ cbNode();
1553
+ // it's not guaranteed that the historical read process is really asynchronous
1554
+ }
1555
+ );
1556
+ },
1557
+ (err?: Error | null) => {
1558
+ assert(historyData.length === nodesToRead.length);
1559
+ callback(err || null, historyData);
1560
+ }
1561
+ );
1562
+ }
1563
+
1564
+ public getOldestUnactivatedSession(): ServerSession | null {
1565
+ const tmp = Object.values(this._sessions).filter((session1: ServerSession) => {
1566
+ return session1.status === "new";
1567
+ });
1568
+ if (tmp.length === 0) {
1569
+ return null;
1570
+ }
1571
+ let session = tmp[0];
1572
+ for (let i = 1; i < tmp.length; i++) {
1573
+ const c = tmp[i];
1574
+ if (session.creationDate.getTime() < c.creationDate.getTime()) {
1575
+ session = c;
1576
+ }
1577
+ }
1578
+ return session;
1579
+ }
1580
+
1581
+ /**
1582
+ * create a new server session object.
1583
+ * @class ServerEngine
1584
+ * @method createSession
1585
+ * @param [options] {Object}
1586
+ * @param [options.sessionTimeout = 1000] {Number} sessionTimeout
1587
+ * @param [options.clientDescription] {ApplicationDescription}
1588
+ * @return {ServerSession}
1589
+ */
1590
+ public createSession(options: CreateSessionOption): ServerSession {
1591
+ options = options || {};
1592
+
1593
+ debugLog("createSession : increasing serverDiagnosticsSummary cumulatedSessionCount/currentSessionCount ");
1594
+ this.serverDiagnosticsSummary.cumulatedSessionCount += 1;
1595
+ this.serverDiagnosticsSummary.currentSessionCount += 1;
1596
+
1597
+ this.clientDescription = options.clientDescription || new ApplicationDescription({});
1598
+
1599
+ const sessionTimeout = options.sessionTimeout || 1000;
1600
+ assert(typeof sessionTimeout === "number");
1601
+
1602
+ const session = new ServerSession(this, sessionTimeout);
1603
+
1604
+ debugLog("createSession :sessionTimeout = ", session.sessionTimeout);
1605
+
1606
+ const key = session.authenticationToken.toString();
1607
+
1608
+ this._sessions[key] = session;
1609
+
1610
+ // see spec OPC Unified Architecture, Part 2 page 26 Release 1.02
1611
+ // TODO : When a Session is created, the Server adds an entry for the Client
1612
+ // in its SessionDiagnosticsArray Variable
1613
+
1614
+ session.on("new_subscription", (subscription: Subscription) => {
1615
+ this.serverDiagnosticsSummary.cumulatedSubscriptionCount += 1;
1616
+ // add the subscription diagnostics in our subscriptions diagnostics array
1617
+ // note currentSubscriptionCount is handled directly with a special getter
1618
+ });
1619
+
1620
+ session.on("subscription_terminated", (subscription: Subscription) => {
1621
+ // remove the subscription diagnostics in our subscriptions diagnostics array
1622
+ // note currentSubscriptionCount is handled directly with a special getter
1623
+ });
1624
+
1625
+ // OPC Unified Architecture, Part 4 23 Release 1.03
1626
+ // Sessions are terminated by the Server automatically if the Client fails to issue a Service request on the
1627
+ // Session within the timeout period negotiated by the Server in the CreateSession Service response.
1628
+ // This protects the Server against Client failures and against situations where a failed underlying
1629
+ // connection cannot be re-established. Clients shall be prepared to submit requests in a timely manner
1630
+ // prevent the Session from closing automatically. Clients may explicitly terminate sessions using the
1631
+ // CloseSession Service.
1632
+ session.on("timeout", () => {
1633
+ // the session hasn't been active for a while , probably because the client has disconnected abruptly
1634
+ // it is now time to close the session completely
1635
+ this.serverDiagnosticsSummary.sessionTimeoutCount += 1;
1636
+ session.sessionName = session.sessionName || "";
1637
+
1638
+ const channel = session.channel;
1639
+ errorLog(
1640
+ chalk.cyan("Server: closing SESSION "),
1641
+ session.status,
1642
+ chalk.yellow(session.sessionName),
1643
+ chalk.yellow(session.nodeId.toString()),
1644
+ chalk.cyan(" because of timeout = "),
1645
+ session.sessionTimeout,
1646
+ chalk.cyan(" has expired without a keep alive"),
1647
+ chalk.bgCyan("channel = "),
1648
+ channel?.remoteAddress,
1649
+ " port = ",
1650
+ channel?.remotePort
1651
+ );
1652
+
1653
+ // If a Server terminates a Session for any other reason, Subscriptions associated with the Session,
1654
+ // are not deleted. => deleteSubscription= false
1655
+ this.closeSession(session.authenticationToken, /*deleteSubscription=*/ false, /* reason =*/ "Timeout");
1656
+
1657
+ this.incrementSessionTimeoutCount();
1658
+ });
1659
+
1660
+ return session;
1661
+ }
1662
+
1663
+ /**
1664
+ * @method closeSession
1665
+ * @param authenticationToken
1666
+ * @param deleteSubscriptions {Boolean} : true if session's subscription shall be deleted
1667
+ * @param {String} [reason = "CloseSession"] the reason for closing the session (
1668
+ * shall be "Timeout", "Terminated" or "CloseSession")
1669
+ *
1670
+ *
1671
+ * what the specs say:
1672
+ * -------------------
1673
+ *
1674
+ * If a Client invokes the CloseSession Service then all Subscriptions associated with the Session are also deleted
1675
+ * if the deleteSubscriptions flag is set to TRUE. If a Server terminates a Session for any other reason,
1676
+ * Subscriptions associated with the Session, are not deleted. Each Subscription has its own lifetime to protect
1677
+ * against data loss in the case of a Session termination. In these cases, the Subscription can be reassigned to
1678
+ * another Client before its lifetime expires.
1679
+ */
1680
+ public closeSession(authenticationToken: NodeId, deleteSubscriptions: boolean, reason: ClosingReason) {
1681
+ reason = reason || "CloseSession";
1682
+ assert(typeof reason === "string");
1683
+ assert(reason === "Timeout" || reason === "Terminated" || reason === "CloseSession" || reason === "Forcing");
1684
+
1685
+ debugLog("ServerEngine.closeSession ", authenticationToken.toString(), deleteSubscriptions);
1686
+
1687
+ const session = this.getSession(authenticationToken);
1688
+
1689
+ // istanbul ignore next
1690
+ if (!session) {
1691
+ throw new Error("cannot find session with this authenticationToken " + authenticationToken.toString());
1692
+ }
1693
+
1694
+ if (!deleteSubscriptions) {
1695
+ // Live Subscriptions will not be deleted, but transferred to the orphanPublishEngine
1696
+ // until they time out or until a other session transfer them back to it.
1697
+ if (!this._orphanPublishEngine) {
1698
+ this._orphanPublishEngine = new ServerSidePublishEngineForOrphanSubscription({ maxPublishRequestInQueue: 0 });
1699
+ }
1700
+
1701
+ debugLog("transferring remaining live subscription to orphanPublishEngine !");
1702
+ ServerSidePublishEngine.transferSubscriptionsToOrphan(session.publishEngine, this._orphanPublishEngine);
1703
+ }
1704
+
1705
+ session.close(deleteSubscriptions, reason);
1706
+
1707
+ assert(session.status === "closed");
1708
+
1709
+ debugLog(" engine.serverDiagnosticsSummary.currentSessionCount -= 1;");
1710
+ this.serverDiagnosticsSummary.currentSessionCount -= 1;
1711
+
1712
+ // xx //TODO make sure _closedSessions gets cleaned at some point
1713
+ // xx self._closedSessions[key] = session;
1714
+
1715
+ // remove sessionDiagnostics from server.ServerDiagnostics.SessionsDiagnosticsSummary.SessionDiagnosticsSummary
1716
+ delete this._sessions[authenticationToken.toString()];
1717
+ session.dispose();
1718
+ }
1719
+
1720
+ public findSubscription(subscriptionId: number): Subscription | null {
1721
+ const subscriptions: Subscription[] = [];
1722
+ Object.values(this._sessions).map((session) => {
1723
+ if (subscriptions.length) {
1724
+ return;
1725
+ }
1726
+ const subscription = session.publishEngine.getSubscriptionById(subscriptionId);
1727
+ if (subscription) {
1728
+ // xx console.log("foundSubscription ", subscriptionId, " in session", session.sessionName);
1729
+ subscriptions.push(subscription);
1730
+ }
1731
+ });
1732
+ if (subscriptions.length) {
1733
+ assert(subscriptions.length === 1);
1734
+ return subscriptions[0];
1735
+ }
1736
+ return this.findOrphanSubscription(subscriptionId);
1737
+ }
1738
+
1739
+ public findOrphanSubscription(subscriptionId: number): Subscription | null {
1740
+ if (!this._orphanPublishEngine) {
1741
+ return null;
1742
+ }
1743
+ return this._orphanPublishEngine.getSubscriptionById(subscriptionId);
1744
+ }
1745
+
1746
+ public deleteOrphanSubscription(subscription: Subscription): StatusCode {
1747
+ if (!this._orphanPublishEngine) {
1748
+ return StatusCodes.BadInternalError;
1749
+ }
1750
+ assert(this.findSubscription(subscription.id));
1751
+
1752
+ const c = this._orphanPublishEngine.subscriptionCount;
1753
+ subscription.terminate();
1754
+ subscription.dispose();
1755
+ assert(this._orphanPublishEngine.subscriptionCount === c - 1);
1756
+ return StatusCodes.Good;
1757
+ }
1758
+
1759
+ /**
1760
+ * @method transferSubscription
1761
+ * @param session {ServerSession} - the new session that will own the subscription
1762
+ * @param subscriptionId {IntegerId} - the subscription Id to transfer
1763
+ * @param sendInitialValues {Boolean} - true if initial values will be resent.
1764
+ * @return {TransferResult}
1765
+ */
1766
+ public async transferSubscription(
1767
+ session: ServerSession,
1768
+ subscriptionId: number,
1769
+ sendInitialValues: boolean
1770
+ ): Promise<TransferResult> {
1771
+ if (subscriptionId <= 0) {
1772
+ return new TransferResult({ statusCode: StatusCodes.BadSubscriptionIdInvalid });
1773
+ }
1774
+
1775
+ const subscription = this.findSubscription(subscriptionId);
1776
+ if (!subscription) {
1777
+ return new TransferResult({ statusCode: StatusCodes.BadSubscriptionIdInvalid });
1778
+ }
1779
+ // istanbul ignore next
1780
+ if (!subscription.$session) {
1781
+ return new TransferResult({ statusCode: StatusCodes.BadInternalError });
1782
+ }
1783
+
1784
+ // check that session have same userIdentity
1785
+ if (!sessionsCompatibleForTransfer(subscription.$session, session)) {
1786
+ return new TransferResult({ statusCode: StatusCodes.BadUserAccessDenied });
1787
+ }
1788
+
1789
+ // update diagnostics
1790
+ subscription.subscriptionDiagnostics.transferRequestCount++;
1791
+
1792
+ // now check that new session has sufficient right
1793
+ // if (session.authenticationToken.toString() !== subscription.authenticationToken.toString()) {
1794
+ // console.log("ServerEngine#transferSubscription => BadUserAccessDenied");
1795
+ // return new TransferResult({ statusCode: StatusCodes.BadUserAccessDenied });
1796
+ // }
1797
+ if ((session.publishEngine as any) === subscription.publishEngine) {
1798
+ // subscription is already in this session !!
1799
+ return new TransferResult({ statusCode: StatusCodes.BadNothingToDo });
1800
+ }
1801
+ if (session === subscription.$session) {
1802
+ // subscription is already in this session !!
1803
+ return new TransferResult({ statusCode: StatusCodes.BadNothingToDo });
1804
+ }
1805
+
1806
+ // The number of times the subscription has been transferred to an alternate client.
1807
+ subscription.subscriptionDiagnostics.transferredToAltClientCount++;
1808
+ // The number of times the subscription has been transferred to an alternate session for the same client.
1809
+ subscription.subscriptionDiagnostics.transferredToSameClientCount++;
1810
+
1811
+ const nbSubscriptionBefore = session.publishEngine.subscriptionCount;
1812
+
1813
+ subscription.$session._unexposeSubscriptionDiagnostics(subscription);
1814
+ await ServerSidePublishEngine.transferSubscription(subscription, session.publishEngine, sendInitialValues);
1815
+
1816
+ subscription.$session = session;
1817
+
1818
+ session._exposeSubscriptionDiagnostics(subscription);
1819
+
1820
+ assert((subscription.publishEngine as any) === session.publishEngine);
1821
+ // assert(session.publishEngine.subscriptionCount === nbSubscriptionBefore + 1);
1822
+
1823
+ const result = new TransferResult({
1824
+ availableSequenceNumbers: subscription.getAvailableSequenceNumbers(),
1825
+ statusCode: StatusCodes.Good
1826
+ });
1827
+
1828
+ // istanbul ignore next
1829
+ if (doDebug) {
1830
+ debugLog("TransferResult", result.toString());
1831
+ }
1832
+
1833
+ return result;
1834
+ }
1835
+
1836
+ /**
1837
+ * retrieve a session by its authenticationToken.
1838
+ *
1839
+ * @method getSession
1840
+ * @param authenticationToken
1841
+ * @param activeOnly
1842
+ * @return {ServerSession}
1843
+ */
1844
+ public getSession(authenticationToken: NodeId, activeOnly?: boolean): ServerSession | null {
1845
+ if (
1846
+ !authenticationToken ||
1847
+ (authenticationToken.identifierType && authenticationToken.identifierType !== NodeIdType.BYTESTRING)
1848
+ ) {
1849
+ return null; // wrong type !
1850
+ }
1851
+ const key = authenticationToken.toString();
1852
+ let session = this._sessions[key];
1853
+ if (!activeOnly && !session) {
1854
+ session = this._closedSessions[key];
1855
+ }
1856
+ return session;
1857
+ }
1858
+
1859
+ /**
1860
+ */
1861
+ public browsePath(browsePath: BrowsePath): BrowsePathResult {
1862
+ return this.addressSpace!.browsePath(browsePath);
1863
+ }
1864
+
1865
+ /**
1866
+ *
1867
+ * performs a call to ```asyncRefresh``` on all variable nodes that provide an async refresh func.
1868
+ *
1869
+ * @method refreshValues
1870
+ * @param nodesToRefresh {Array<Object>} an array containing the node to consider
1871
+ * Each element of the array shall be of the form { nodeId: <xxx>, attributeIds: <value> }.
1872
+ * @param callback
1873
+ * @param callback.err
1874
+ * @param callback.data an array containing value read
1875
+ * The array length matches the number of nodeIds that are candidate for an async refresh (i.e: nodes that
1876
+ * are of type Variable with asyncRefresh func }
1877
+ *
1878
+ * @async
1879
+ */
1880
+ public refreshValues(
1881
+ nodesToRefresh: ReadValueId[] | HistoryReadValueId[],
1882
+ maxAge: number,
1883
+ callback: (err: Error | null, dataValues?: DataValue[]) => void
1884
+ ): void {
1885
+ const referenceTime = new Date(Date.now() - maxAge);
1886
+
1887
+ assert(callback instanceof Function);
1888
+ const objectMap: any = {};
1889
+ for (const nodeToRefresh of nodesToRefresh) {
1890
+ // only consider node for which the caller wants to read the Value attribute
1891
+ // assuming that Value is requested if attributeId is missing,
1892
+ if (nodeToRefresh instanceof ReadValueId && nodeToRefresh.attributeId !== AttributeIds.Value) {
1893
+ continue;
1894
+ }
1895
+ // ... and that are valid object and instances of Variables ...
1896
+ const obj = this.addressSpace!.findNode(nodeToRefresh.nodeId);
1897
+ if (!obj || !(obj.nodeClass === NodeClass.Variable)) {
1898
+ continue;
1899
+ }
1900
+ // ... and that have been declared as asynchronously updating
1901
+ if (typeof (obj as any).refreshFunc !== "function") {
1902
+ continue;
1903
+ }
1904
+ const key = obj.nodeId.toString();
1905
+ if (objectMap[key]) {
1906
+ continue;
1907
+ }
1908
+
1909
+ objectMap[key] = obj;
1910
+ }
1911
+ if (Object.keys(objectMap).length === 0) {
1912
+ // nothing to do
1913
+ return callback(null, []);
1914
+ }
1915
+ // perform all asyncRefresh in parallel
1916
+ async.map(
1917
+ objectMap,
1918
+ (obj: BaseNode, inner_callback: DataValueCallback) => {
1919
+ if (obj.nodeClass !== NodeClass.Variable) {
1920
+ inner_callback(
1921
+ null,
1922
+ new DataValue({
1923
+ statusCode: StatusCodes.BadNodeClassInvalid
1924
+ })
1925
+ );
1926
+ return;
1927
+ }
1928
+ (obj as UAVariable).asyncRefresh(referenceTime, inner_callback);
1929
+ },
1930
+ (err?: Error | null, arrResult?: (DataValue | undefined)[]) => {
1931
+ callback(err || null, arrResult as DataValue[]);
1932
+ }
1933
+ );
1934
+ }
1935
+
1936
+ private _exposeSubscriptionDiagnostics(subscription: Subscription): void {
1937
+ debugLog("ServerEngine#_exposeSubscriptionDiagnostics");
1938
+ const subscriptionDiagnosticsArray = this._getServerSubscriptionDiagnosticsArrayNode();
1939
+ const subscriptionDiagnostics = subscription.subscriptionDiagnostics;
1940
+ assert((subscriptionDiagnostics as any).$subscription === subscription);
1941
+ assert(subscriptionDiagnostics instanceof SubscriptionDiagnosticsDataType);
1942
+
1943
+ if (subscriptionDiagnostics && subscriptionDiagnosticsArray) {
1944
+ addElement(subscriptionDiagnostics, subscriptionDiagnosticsArray);
1945
+ }
1946
+ }
1947
+
1948
+ private _unexposeSubscriptionDiagnostics(subscription: Subscription) {
1949
+ const subscriptionDiagnosticsArray = this._getServerSubscriptionDiagnosticsArrayNode();
1950
+ const subscriptionDiagnostics = subscription.subscriptionDiagnostics;
1951
+ assert(subscriptionDiagnostics instanceof SubscriptionDiagnosticsDataType);
1952
+ if (subscriptionDiagnostics && subscriptionDiagnosticsArray) {
1953
+ const node = (subscriptionDiagnosticsArray as any)[subscription.id];
1954
+ removeElement(subscriptionDiagnosticsArray, subscriptionDiagnostics);
1955
+ /*assert(
1956
+ !(subscriptionDiagnosticsArray as any)[subscription.id],
1957
+ " subscription node must have been removed from subscriptionDiagnosticsArray"
1958
+ );
1959
+ */
1960
+ }
1961
+ debugLog("ServerEngine#_unexposeSubscriptionDiagnostics");
1962
+ }
1963
+
1964
+ /**
1965
+ * create a new subscription
1966
+ * @return {Subscription}
1967
+ */
1968
+ public _createSubscriptionOnSession(session: ServerSession, request: CreateSubscriptionRequestLike) {
1969
+ assert(request.hasOwnProperty("requestedPublishingInterval")); // Duration
1970
+ assert(request.hasOwnProperty("requestedLifetimeCount")); // Counter
1971
+ assert(request.hasOwnProperty("requestedMaxKeepAliveCount")); // Counter
1972
+ assert(request.hasOwnProperty("maxNotificationsPerPublish")); // Counter
1973
+ assert(request.hasOwnProperty("publishingEnabled")); // Boolean
1974
+ assert(request.hasOwnProperty("priority")); // Byte
1975
+
1976
+ // adjust publishing parameters
1977
+ const publishingInterval = request.requestedPublishingInterval || 0;
1978
+ const maxKeepAliveCount = request.requestedMaxKeepAliveCount || 0;
1979
+ const lifeTimeCount = request.requestedLifetimeCount || 0;
1980
+
1981
+ const subscription = new Subscription({
1982
+ id: _get_next_subscriptionId(),
1983
+ lifeTimeCount,
1984
+ maxKeepAliveCount,
1985
+ maxNotificationsPerPublish: request.maxNotificationsPerPublish,
1986
+ priority: request.priority || 0,
1987
+ publishEngine: session.publishEngine as any, //
1988
+ publishingEnabled: request.publishingEnabled,
1989
+ publishingInterval,
1990
+ // -------------------
1991
+ sessionId: NodeId.nullNodeId
1992
+ });
1993
+
1994
+ // add subscriptionDiagnostics
1995
+ this._exposeSubscriptionDiagnostics(subscription);
1996
+
1997
+ assert((subscription.publishEngine as any) === session.publishEngine);
1998
+ session.publishEngine.add_subscription(subscription);
1999
+
2000
+ const engine = this;
2001
+ subscription.once("terminated", function (this: Subscription) {
2002
+ engine._unexposeSubscriptionDiagnostics(this);
2003
+ });
2004
+
2005
+ return subscription;
2006
+ }
2007
+
2008
+ private __findNode(nodeId: NodeId): BaseNode | null {
2009
+ if (nodeId.namespace >= (this.addressSpace?.getNamespaceArray().length || 0)) {
2010
+ return null;
2011
+ }
2012
+ const namespace = this.addressSpace?.getNamespace(nodeId.namespace)!;
2013
+ return namespace.findNode2(nodeId)!;
2014
+ }
2015
+
2016
+ private _readSingleNode(context: ISessionContext, nodeToRead: ReadValueId, timestampsToReturn?: TimestampsToReturn): DataValue {
2017
+ assert(context instanceof SessionContext);
2018
+ const nodeId: NodeId = nodeToRead.nodeId!;
2019
+ const attributeId: AttributeIds = nodeToRead.attributeId!;
2020
+ const indexRange: NumericRange = nodeToRead.indexRange!;
2021
+ const dataEncoding: QualifiedName = nodeToRead.dataEncoding;
2022
+
2023
+ if (timestampsToReturn === TimestampsToReturn.Invalid) {
2024
+ return new DataValue({ statusCode: StatusCodes.BadTimestampsToReturnInvalid });
2025
+ }
2026
+
2027
+ timestampsToReturn = coerceTimestampsToReturn(timestampsToReturn);
2028
+
2029
+ const obj = this.__findNode(nodeId!);
2030
+
2031
+ let dataValue;
2032
+ if (!obj) {
2033
+ // may be return BadNodeIdUnknown in dataValue instead ?
2034
+ // Object Not Found
2035
+ return new DataValue({ statusCode: StatusCodes.BadNodeIdUnknown });
2036
+ } else {
2037
+ // check access
2038
+ // BadUserAccessDenied
2039
+ // BadNotReadable
2040
+ // invalid attributes : BadNodeAttributesInvalid
2041
+ // invalid range : BadIndexRangeInvalid
2042
+ dataValue = obj.readAttribute(context, attributeId, indexRange, dataEncoding);
2043
+ dataValue = apply_timestamps_no_copy(dataValue, timestampsToReturn, attributeId);
2044
+
2045
+ return dataValue;
2046
+ }
2047
+ }
2048
+
2049
+ private _historyReadSingleNode(
2050
+ context: ISessionContext,
2051
+ nodeToRead: HistoryReadValueId,
2052
+ historyReadDetails: HistoryReadDetails,
2053
+ timestampsToReturn: TimestampsToReturn,
2054
+ callback: CallbackT<HistoryReadResult>
2055
+ ): void {
2056
+ assert(context instanceof SessionContext);
2057
+ assert(callback instanceof Function);
2058
+
2059
+ const nodeId = nodeToRead.nodeId;
2060
+ const indexRange = nodeToRead.indexRange;
2061
+ const dataEncoding = nodeToRead.dataEncoding;
2062
+ const continuationPoint = nodeToRead.continuationPoint;
2063
+
2064
+ timestampsToReturn = coerceTimestampsToReturn(timestampsToReturn);
2065
+
2066
+ const obj = this.__findNode(nodeId) as UAVariable;
2067
+
2068
+ if (!obj) {
2069
+ // may be return BadNodeIdUnknown in dataValue instead ?
2070
+ // Object Not Found
2071
+ callback(null, new HistoryReadResult({ statusCode: StatusCodes.BadNodeIdUnknown }));
2072
+ return;
2073
+ } else {
2074
+ // istanbul ignore next
2075
+ if (!obj.historyRead) {
2076
+ // note : Object and View may also support historyRead to provide Event historical data
2077
+ // todo implement historyRead for Object and View
2078
+ const msg =
2079
+ " this node doesn't provide historyRead! probably not a UAVariable\n " +
2080
+ obj.nodeId.toString() +
2081
+ " " +
2082
+ obj.browseName.toString() +
2083
+ "\n" +
2084
+ "with " +
2085
+ nodeToRead.toString() +
2086
+ "\n" +
2087
+ "HistoryReadDetails " +
2088
+ historyReadDetails.toString();
2089
+ if (doDebug) {
2090
+ console.log(chalk.cyan("ServerEngine#_historyReadSingleNode "), chalk.white.bold(msg));
2091
+ }
2092
+ const err = new Error(msg);
2093
+ // object has no historyRead method
2094
+ setImmediate(callback.bind(null, err));
2095
+ return;
2096
+ }
2097
+ // check access
2098
+ // BadUserAccessDenied
2099
+ // BadNotReadable
2100
+ // invalid attributes : BadNodeAttributesInvalid
2101
+ // invalid range : BadIndexRangeInvalid
2102
+ obj.historyRead(
2103
+ context,
2104
+ historyReadDetails,
2105
+ indexRange,
2106
+ dataEncoding,
2107
+ continuationPoint,
2108
+ (err: Error | null, result?: HistoryReadResult) => {
2109
+ if (err || !result) {
2110
+ return callback(err);
2111
+ }
2112
+ assert(result!.statusCode instanceof StatusCode);
2113
+ assert(result!.isValid());
2114
+ // result = apply_timestamps(result, timestampsToReturn, attributeId);
2115
+ callback(err, result);
2116
+ }
2117
+ );
2118
+ }
2119
+ }
2120
+
2121
+ /**
2122
+ */
2123
+ private __internal_bindMethod(nodeId: NodeId, func: MethodFunctor) {
2124
+ assert(typeof func === "function");
2125
+ assert(nodeId instanceof NodeId);
2126
+
2127
+ const methodNode = this.addressSpace!.findNode(nodeId)! as UAMethod;
2128
+ if (!methodNode) {
2129
+ return;
2130
+ }
2131
+ // istanbul ignore else
2132
+ if (methodNode && methodNode.bindMethod) {
2133
+ methodNode.bindMethod(func);
2134
+ } else {
2135
+ console.log(
2136
+ chalk.yellow("WARNING: cannot bind a method with id ") +
2137
+ chalk.cyan(nodeId.toString()) +
2138
+ chalk.yellow(". please check your nodeset.xml file or add this node programmatically")
2139
+ );
2140
+
2141
+ console.log(traceFromThisProjectOnly());
2142
+ }
2143
+ }
2144
+
2145
+ private _getServerSubscriptionDiagnosticsArrayNode(): UADynamicVariableArray<SubscriptionDiagnosticsDataType> | null {
2146
+ // istanbul ignore next
2147
+ if (!this.addressSpace) {
2148
+ if (doDebug) {
2149
+ console.warn("ServerEngine#_getServerSubscriptionDiagnosticsArray : no addressSpace");
2150
+ }
2151
+ return null; // no addressSpace
2152
+ }
2153
+ const subscriptionDiagnosticsType = this.addressSpace.findVariableType("SubscriptionDiagnosticsType");
2154
+ if (!subscriptionDiagnosticsType) {
2155
+ if (doDebug) {
2156
+ console.warn("ServerEngine#_getServerSubscriptionDiagnosticsArray " + ": cannot find SubscriptionDiagnosticsType");
2157
+ }
2158
+ }
2159
+
2160
+ // SubscriptionDiagnosticsArray = i=2290
2161
+ const subscriptionDiagnosticsArrayNode = this.addressSpace.findNode(
2162
+ makeNodeId(VariableIds.Server_ServerDiagnostics_SubscriptionDiagnosticsArray)
2163
+ )!;
2164
+
2165
+ return subscriptionDiagnosticsArrayNode as UADynamicVariableArray<SubscriptionDiagnosticsDataType>;
2166
+ }
2167
+ }