• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

node-opcua / node-opcua / 14295375049

06 Apr 2025 06:24PM UTC coverage: 90.857% (-0.04%) from 90.894%
14295375049

push

github

erossignon
v2.151.0

10976 of 13930 branches covered (78.79%)

29999 of 33018 relevant lines covered (90.86%)

323220.28 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.55
/packages/node-opcua-server/source/server_engine.ts
1
/**
2
 * @module node-opcua-server
3
 */
4
import { EventEmitter } from "events";
6✔
5
import { types } from "util";
6✔
6
import async from "async";
6✔
7
import chalk from "chalk";
6✔
8
import { assert } from "node-opcua-assert";
6✔
9
import { BinaryStream } from "node-opcua-binary-stream";
6✔
10
import {
6✔
11
    addElement,
12
    AddressSpace,
13
    bindExtObjArrayNode,
14
    ensureObjectIsSecure,
15
    MethodFunctor,
16
    removeElement,
17
    SessionContext,
18
    UADynamicVariableArray,
19
    UAMethod,
20
    UAObject,
21
    UAServerDiagnosticsSummary,
22
    UAServerStatus,
23
    UAVariable,
24
    UAServerDiagnostics,
25
    BindVariableOptions,
26
    ISessionContext,
27
    DTServerStatus,
28
    IServerBase
29
} from "node-opcua-address-space";
30
import { generateAddressSpace } from "node-opcua-address-space/nodeJS";
6✔
31
import { DataValue } from "node-opcua-data-value";
32
import {
6✔
33
    ServerDiagnosticsSummaryDataType,
34
    ServerState,
35
    ServerStatusDataType,
36
    SubscriptionDiagnosticsDataType
37
} from "node-opcua-common";
38
import { AttributeIds, coerceLocalizedText, LocalizedTextLike, makeAccessLevelFlag, NodeClass } from "node-opcua-data-model";
6✔
39
import { coerceNodeId, makeNodeId, NodeId, NodeIdLike, NodeIdType, resolveNodeId } from "node-opcua-nodeid";
6✔
40
import { BrowseResult } from "node-opcua-service-browse";
41
import { UInt32 } from "node-opcua-basic-types";
42
import { CreateSubscriptionRequestLike } from "node-opcua-client";
43
import { DataTypeIds, MethodIds, ObjectIds, VariableIds } from "node-opcua-constants";
6✔
44
import { getCurrentClock, getMinOPCUADate } from "node-opcua-date-time";
6✔
45
import { checkDebugFlag, make_debugLog, make_errorLog, make_warningLog, traceFromThisProjectOnly } from "node-opcua-debug";
6✔
46
import { nodesets } from "node-opcua-nodesets";
6✔
47
import { ObjectRegistry } from "node-opcua-object-registry";
6✔
48
import { CallMethodResult } from "node-opcua-service-call";
6✔
49
import { TransferResult } from "node-opcua-service-subscription";
6✔
50
import { ApplicationDescription } from "node-opcua-service-endpoints";
6✔
51
import { HistoryReadRequest, HistoryReadResult, HistoryReadValueId } from "node-opcua-service-history";
52
import { StatusCode, StatusCodes, CallbackT } from "node-opcua-status-code";
6✔
53
import {
6✔
54
    BrowseDescription,
55
    BrowsePath,
56
    BrowsePathResult,
57
    BuildInfo,
58
    BuildInfoOptions,
59
    SessionDiagnosticsDataType,
60
    SessionSecurityDiagnosticsDataType,
61
    WriteValue,
62
    ReadValueId,
63
    TimeZoneDataType,
64
    ProgramDiagnosticDataType,
65
    CallMethodResultOptions,
66
    ReadRequestOptions,
67
    BrowseDescriptionOptions,
68
    CallMethodRequest,
69
    ApplicationType
70
} from "node-opcua-types";
71
import { DataType, isValidVariant, Variant, VariantArrayType } from "node-opcua-variant";
6✔
72

73
import { HistoryServerCapabilities, HistoryServerCapabilitiesOptions } from "./history_server_capabilities";
6✔
74
import { MonitoredItem } from "./monitored_item";
6✔
75
import { ServerCapabilities, ServerCapabilitiesOptions, ServerOperationLimits, defaultServerCapabilities } from "./server_capabilities";
6✔
76
import { ServerSidePublishEngine } from "./server_publish_engine";
6✔
77
import { ServerSidePublishEngineForOrphanSubscription } from "./server_publish_engine_for_orphan_subscriptions";
6✔
78
import { ServerSession } from "./server_session";
6✔
79
import { Subscription } from "./server_subscription";
6✔
80
import { sessionsCompatibleForTransfer } from "./sessions_compatible_for_transfer";
6✔
81
import { OPCUAServerOptions } from "./opcua_server";
82
import { IAddressSpaceAccessor } from "./i_address_space_accessor";
83
import { AddressSpaceAccessor } from "./addressSpace_accessor";
6✔
84

85
const debugLog = make_debugLog(__filename);
6✔
86
const errorLog = make_errorLog(__filename);
6✔
87
const warningLog = make_warningLog(__filename);
6✔
88
const doDebug = checkDebugFlag(__filename);
6✔
89

90
function upperCaseFirst(str: string) {
91
    return str.slice(0, 1).toUpperCase() + str.slice(1);
2,652✔
92
}
93

94
async function shutdownAndDisposeAddressSpace(this: ServerEngine) {
95
    if (this.addressSpace) {
224✔
96
        await this.addressSpace.shutdown();
221✔
97
        this.addressSpace.dispose();
221✔
98
        delete (this as any).addressSpace;
221✔
99
    }
100
}
101

102
function setSubscriptionDurable(
103
    this: ServerEngine,
104
    inputArguments: Variant[],
105
    context: ISessionContext,
106
    callback: CallbackT<CallMethodResultOptions>
107
) {
108
    // see https://reference.opcfoundation.org/v104/Core/docs/Part5/9.3/
109
    // https://reference.opcfoundation.org/v104/Core/docs/Part4/6.8/
110
    assert(typeof callback === "function");
×
111

112
    const data = _getSubscription.call(this, inputArguments, context);
×
113
    if (data.statusCode) return callback(null, { statusCode: data.statusCode });
×
114
    const { subscription } = data;
×
115

116
    const lifetimeInHours = inputArguments[1].value as UInt32;
×
117
    if (subscription.monitoredItemCount > 0) {
×
118
        // This is returned when a Subscription already contains MonitoredItems.
119
        return callback(null, { statusCode: StatusCodes.BadInvalidState });
×
120
    }
121

122
    /**
123
     * MonitoredItems are used to monitor Variable Values for data changes and event notifier
124
     * Objects for new Events. Subscriptions are used to combine data changes and events of
125
     * the assigned MonitoredItems to an optimized stream of network messages. A reliable
126
     * delivery is ensured as long as the lifetime of the Subscription and the queues in the
127
     * MonitoredItems are long enough for a network interruption between OPC UA Client and
128
     * Server. All queues that ensure reliable delivery are normally kept in memory and a
129
     * Server restart would delete them.
130
     * There are use cases where OPC UA Clients have no permanent network connection to the
131
     * OPC UA Server or where reliable delivery of data changes and events is necessary
132
     * even if the OPC UA Server is restarted or the network connection is interrupted
133
     * for a longer time.
134
     * To ensure this reliable delivery, the OPC UA Server must store collected data and
135
     * events in non-volatile memory until the OPC UA Client has confirmed reception.
136
     * It is possible that there will be data lost if the Server is not shut down gracefully
137
     * or in case of power failure. But the OPC UA Server should store the queues frequently
138
     * even if the Server is not shut down.
139
     * The Method SetSubscriptionDurable defined in OPC 10000-5 is used to set a Subscription
140
     * into this durable mode and to allow much longer lifetimes and queue sizes than for normal
141
     * Subscriptions. The Method shall be called before the MonitoredItems are created in the
142
     * durable Subscription. The Server shall verify that the Method is called within the
143
     * Session context of the Session that owns the Subscription.
144
     *
145
     * A value of 0 for the parameter lifetimeInHours requests the highest lifetime supported by the Server.
146
     */
147

148
    const highestLifetimeInHours = 24 * 100;
×
149

150
    const revisedLifetimeInHours =
151
        lifetimeInHours === 0 ? highestLifetimeInHours : Math.max(1, Math.min(lifetimeInHours, highestLifetimeInHours));
×
152

153
    // also adjust subscription life time
154
    const currentLifeTimeInHours = (subscription.lifeTimeCount * subscription.publishingInterval) / (1000 * 60 * 60);
×
155
    if (currentLifeTimeInHours < revisedLifetimeInHours) {
×
156
        const requestedLifetimeCount = Math.ceil((revisedLifetimeInHours * (1000 * 60 * 60)) / subscription.publishingInterval);
×
157

158
        subscription.modify({
×
159
            requestedMaxKeepAliveCount: subscription.maxKeepAliveCount,
160
            requestedPublishingInterval: subscription.publishingInterval,
161
            maxNotificationsPerPublish: subscription.maxNotificationsPerPublish,
162
            priority: subscription.priority,
163
            requestedLifetimeCount
164
        });
165
    }
166

167
    const callMethodResult = new CallMethodResult({
×
168
        statusCode: StatusCodes.Good,
169
        outputArguments: [{ dataType: DataType.UInt32, arrayType: VariantArrayType.Scalar, value: revisedLifetimeInHours }]
170
    });
171
    callback(null, callMethodResult);
×
172
}
173

174
function requestServerStateChange(
175
    this: ServerEngine,
176
    inputArguments: Variant[],
177
    context: ISessionContext,
178
    callback: CallbackT<CallMethodResultOptions>
179
) {
180
    assert(Array.isArray(inputArguments));
1✔
181
    assert(typeof callback === "function");
1✔
182
    assert(Object.prototype.hasOwnProperty.call(context, "session"), " expecting a session id in the context object");
1✔
183
    const session = context.session as ServerSession;
1✔
184
    if (!session) {
1!
185
        return callback(null, { statusCode: StatusCodes.BadInternalError });
×
186
    }
187

188
    return callback(null, { statusCode: StatusCodes.BadNotImplemented });
1✔
189
}
190

191
function _getSubscription(
192
    this: ServerEngine,
193
    inputArguments: Variant[],
194
    context: ISessionContext
195
): { subscription: Subscription; statusCode?: never } | { statusCode: StatusCode; subscription?: never } {
196
    assert(Array.isArray(inputArguments));
63✔
197
    assert(Object.prototype.hasOwnProperty.call(context, "session"), " expecting a session id in the context object");
63✔
198
    const session = context.session as ServerSession;
63✔
199
    if (!session) {
63!
200
        return { statusCode: StatusCodes.BadInternalError };
×
201
    }
202
    const subscriptionId = inputArguments[0].value;
63✔
203
    const subscription = session.getSubscription(subscriptionId);
63✔
204
    if (!subscription) {
63✔
205
        // subscription may belongs to a different session  that ours
206
        if (this.findSubscription(subscriptionId)) {
53!
207
            // if yes, then access to  Subscription data should be denied
208
            return { statusCode: StatusCodes.BadUserAccessDenied };
×
209
        }
210
        return { statusCode: StatusCodes.BadSubscriptionIdInvalid };
53✔
211
    }
212
    return { subscription };
10✔
213
}
214
function resendData(
215
    this: ServerEngine,
216
    inputArguments: Variant[],
217
    context: ISessionContext,
218
    callback: CallbackT<CallMethodResultOptions>
219
): void {
220
    assert(typeof callback === "function");
3✔
221

222
    const data = _getSubscription.call(this, inputArguments, context);
3✔
223
    if (data.statusCode) return callback(null, { statusCode: data.statusCode });
3!
224
    const { subscription } = data;
3✔
225

226
    subscription
3✔
227
        .resendInitialValues()
228
        .then(() => {
229
            callback(null, { statusCode: StatusCodes.Good });
3✔
230
        })
231
        .catch((err) => callback(err));
×
232
}
233

234
// binding methods
235
function getMonitoredItemsId(
236
    this: ServerEngine,
237
    inputArguments: Variant[],
238
    context: ISessionContext,
239
    callback: CallbackT<CallMethodResultOptions>
240
) {
241
    assert(typeof callback === "function");
60✔
242

243
    const data = _getSubscription.call(this, inputArguments, context);
60✔
244
    if (data.statusCode) return callback(null, { statusCode: data.statusCode });
60✔
245
    const { subscription } = data;
7✔
246

247
    const result = subscription.getMonitoredItems();
7✔
248
    assert(result.statusCode);
7✔
249
    assert(result.serverHandles.length === result.clientHandles.length);
7✔
250
    const callMethodResult = new CallMethodResult({
7✔
251
        statusCode: result.statusCode,
252
        outputArguments: [
253
            { dataType: DataType.UInt32, arrayType: VariantArrayType.Array, value: result.serverHandles },
254
            { dataType: DataType.UInt32, arrayType: VariantArrayType.Array, value: result.clientHandles }
255
        ]
256
    });
257
    callback(null, callMethodResult);
7✔
258
}
259

260
function __bindVariable(self: ServerEngine, nodeId: NodeIdLike, options?: BindVariableOptions) {
261
    options = options || {};
10,184!
262

263
    const variable = self.addressSpace!.findNode(nodeId) as UAVariable;
10,184✔
264
    if (variable && variable.bindVariable) {
10,184!
265
        variable.bindVariable(options, true);
10,184✔
266
        assert(typeof variable.asyncRefresh === "function");
10,184✔
267
        assert(typeof (variable as any).refreshFunc === "function");
10,184✔
268
    } else {
269
        warningLog(
×
270
            "Warning: cannot bind object with id ",
271
            nodeId.toString(),
272
            " please check your nodeset.xml file or add this node programmatically"
273
        );
274
    }
275
}
276

277
// note OPCUA 1.03 part 4 page 76
278
// The Server-assigned identifier for the Subscription (see 7.14 for IntegerId definition). This identifier shall
279
// be unique for the entire Server, not just for the Session, in order to allow the Subscription to be transferred
280
// to another Session using the TransferSubscriptions service.
281
// After Server start-up the generation of subscriptionIds should start from a random IntegerId or continue from
282
// the point before the restart.
283
let next_subscriptionId = Math.ceil(Math.random() * 1000000);
6✔
284
export function setNextSubscriptionId(n: number) {
6✔
285
    next_subscriptionId = Math.max(n, 1);
×
286
}
287
function _get_next_subscriptionId() {
288
    debugLog(" next_subscriptionId = ", next_subscriptionId);
486✔
289
    return next_subscriptionId++;
486✔
290
}
291

292
export type StringGetter = () => string;
293
export type StringArrayGetter = () => string[];
294
export type ApplicationTypeGetter = () => ApplicationType;
295
export type BooleanGetter = () => boolean;
296

297
export interface ServerConfigurationOptions {
298
    applicationUri?: string | StringGetter;
299
    applicationType?: ApplicationType | ApplicationTypeGetter; // default "Server"
300

301
    hasSecureElement?: boolean | BooleanGetter; // default true
302

303
    multicastDnsEnabled?: boolean | BooleanGetter; // default true
304

305
    productUri?: string | StringGetter;
306

307
    // /** @restricted only in professional version */
308
    // resetToServerDefaults: () => Promise<void>;
309
    // /** @restricted only in professional version */
310
    // setAdminPassword?: (password: string) => Promise<void>;
311

312
    /**
313
     * The SupportedPrivateKeyFormats specifies the PrivateKey formats supported by the Server.
314
     * Possible values include “PEM” (see RFC 5958) or “PFX” (see PKCS #12).
315
     * @default ["PEM"]
316
     */
317
    supportedPrivateKeyFormat: string[] | StringArrayGetter;
318

319
    /**
320
     * The ServerCapabilities Property specifies the capabilities from Annex D
321
     * ( see https://reference.opcfoundation.org/GDS/v104/docs/D)  which the Server supports. The value is
322
     * the same as the value reported to the LocalDiscoveryServer when the Server calls the RegisterServer2 Service.
323
     */
324
    serverCapabilities?: string[] | StringArrayGetter; // default|"N/A"]
325
}
326
export interface ServerEngineOptions {
327
    applicationUri: string | StringGetter;
328

329
    buildInfo?: BuildInfoOptions;
330
    isAuditing?: boolean;
331
    /**
332
     * set to true to enable serverDiagnostics
333
     */
334
    serverDiagnosticsEnabled?: boolean;
335
    serverCapabilities?: ServerCapabilitiesOptions;
336
    historyServerCapabilities?: HistoryServerCapabilitiesOptions;
337
    serverConfiguration?: ServerConfigurationOptions;
338
}
339

340
export interface CreateSessionOption {
341
    clientDescription?: ApplicationDescription;
342
    sessionTimeout?: number;
343
    server?: IServerBase;
344
}
345

346
export type ClosingReason = "Timeout" | "Terminated" | "CloseSession" | "Forcing";
347

348
export type ServerEngineShutdownTask = (this: ServerEngine) => void | Promise<void>;
349

350
/**
351
 *
352
 */
353
export class ServerEngine extends EventEmitter implements IAddressSpaceAccessor {
6✔
354
    public static readonly registry = new ObjectRegistry();
6✔
355

356
    public isAuditing: boolean;
357
    public serverDiagnosticsSummary: ServerDiagnosticsSummaryDataType;
358
    public serverDiagnosticsEnabled: boolean;
359
    public serverCapabilities: ServerCapabilities;
360
    public historyServerCapabilities: HistoryServerCapabilities;
361
    public serverConfiguration: ServerConfigurationOptions;
362
    public clientDescription?: ApplicationDescription;
363

364
    public addressSpace: AddressSpace | null;
365
    public addressSpaceAccessor: IAddressSpaceAccessor | null = null;
223✔
366

367
    // pseudo private
368
    public _internalState: "creating" | "initializing" | "initialized" | "shutdown" | "disposed";
369

370
    private _sessions: { [key: string]: ServerSession };
371
    private _closedSessions: { [key: string]: ServerSession };
372
    private _orphanPublishEngine?: ServerSidePublishEngineForOrphanSubscription;
373
    private _shutdownTasks: ServerEngineShutdownTask[];
374
    private _applicationUri: string;
375
    private _expectedShutdownTime!: Date;
376
    private _serverStatus: ServerStatusDataType;
377
    private _globalCounter: { totalMonitoredItemCount: number } = { totalMonitoredItemCount: 0 };
223✔
378

379
    constructor(options?: ServerEngineOptions) {
380
        super();
223✔
381

382
        options = options || ({ applicationUri: "" } as ServerEngineOptions);
223✔
383
        options.buildInfo = options.buildInfo || {};
223✔
384

385
        ServerEngine.registry.register(this);
223✔
386

387
        this._sessions = {};
223✔
388
        this._closedSessions = {};
223✔
389
        this._orphanPublishEngine = undefined; // will be constructed on demand
223✔
390

391
        this.isAuditing = typeof options.isAuditing === "boolean" ? options.isAuditing : false;
223✔
392

393
        options.buildInfo.buildDate = options.buildInfo.buildDate || new Date();
223✔
394
        // ---------------------------------------------------- ServerStatusDataType
395
        this._serverStatus = new ServerStatusDataType({
223✔
396
            buildInfo: options.buildInfo,
397
            currentTime: new Date(),
398
            secondsTillShutdown: 0,
399
            shutdownReason: { text: "" },
400
            startTime: new Date(),
401
            state: ServerState.NoConfiguration
402
        });
403

404
        // --------------------------------------------------- ServerCapabilities
405
        options.serverCapabilities = options.serverCapabilities || {};
223✔
406

407
        options.serverConfiguration = options.serverConfiguration || {
223✔
408
            supportedPrivateKeyFormat: ["PEM"]
409
        };
410

411
        // https://profiles.opcfoundation.org/profile
412
        options.serverCapabilities.serverProfileArray = options.serverCapabilities.serverProfileArray || [
223✔
413
            "http://opcfoundation.org/UA-Profile/Server/Standard", // Standard UA Server Profile",
414
            "http://opcfoundation.org/UA-Profile/Server/DataAccess",
415
            "http://opcfoundation.org/UA-Profile/Server/Events",
416
            "http://opcfoundation.org/UA-Profile/Client/HistoricalAccess",
417
            "http://opcfoundation.org/UA-Profile/Server/Methods",
418
            "http://opcfoundation.org/UA-Profile/Server/StandardEventSubscription",
419
            "http://opcfoundation.org/UA-Profile/Transport/uatcp-uasc-uabinary",
420
            "http://opcfoundation.org/UA-Profile/Server/FileAccess",
421
            "http://opcfoundation.org/UA-Profile/Server/StateMachine"
422
            // "http://opcfoundation.org/UA-Profile/Transport/wss-uajson",
423
            // "http://opcfoundation.org/UA-Profile/Transport/wss-uasc-uabinary"
424
            // "http://opcfoundation.org/UA-Profile/Server/DurableSubscription"
425

426
            // "http://opcfoundation.org/UA-Profile/Server/ReverseConnect",
427
            // "http://opcfoundation.org/UAProfile/Server/NodeManagement",
428

429
            //  "Embedded UA Server Profile",
430
            // "Micro Embedded Device Server Profile",
431
            // "Nano Embedded Device Server Profile"
432
        ];
433
        options.serverCapabilities.localeIdArray = options.serverCapabilities.localeIdArray || ["en-EN", "fr-FR"];
223✔
434

435
        this.serverCapabilities = new ServerCapabilities(options.serverCapabilities);
223✔
436

437
        // to do when spec is clear about what goes here!
438
        // spec 1.04 says (in Part 4 7.33 SignedSoftwareCertificate
439
        // Note: Details on SoftwareCertificates need to be defined in a future version.
440
        this.serverCapabilities.softwareCertificates = [
223✔
441
            // new SignedSoftwareCertificate({})
442
        ];
443

444
        // make sure minSupportedSampleRate matches MonitoredItem.minimumSamplingInterval
445
        (this.serverCapabilities as any).__defineGetter__("minSupportedSampleRate", () => {
223✔
446
            return options!.serverCapabilities?.minSupportedSampleRate || MonitoredItem.minimumSamplingInterval;
397✔
447
        });
448

449
        this.serverConfiguration = options.serverConfiguration;
223✔
450

451
        this.historyServerCapabilities = new HistoryServerCapabilities(options.historyServerCapabilities);
223✔
452

453
        // --------------------------------------------------- serverDiagnosticsSummary extension Object
454
        this.serverDiagnosticsSummary = new ServerDiagnosticsSummaryDataType();
223✔
455
        assert(Object.prototype.hasOwnProperty.call(this.serverDiagnosticsSummary, "currentSessionCount"));
223✔
456

457
        // note spelling is different for serverDiagnosticsSummary.currentSubscriptionCount
458
        //      and sessionDiagnostics.currentSubscriptionsCount ( with an s)
459
        assert(Object.prototype.hasOwnProperty.call(this.serverDiagnosticsSummary, "currentSubscriptionCount"));
223✔
460

461
        (this.serverDiagnosticsSummary as any).__defineGetter__("currentSubscriptionCount", () => {
223✔
462
            // currentSubscriptionCount returns the total number of subscriptions
463
            // that are currently active on all sessions
464
            let counter = 0;
4,022✔
465
            Object.values(this._sessions).forEach((session: ServerSession) => {
4,022✔
466
                counter += session.currentSubscriptionCount;
5,066✔
467
            });
468
            // we also need to add the orphan subscriptions
469
            counter += this._orphanPublishEngine ? this._orphanPublishEngine.subscriptions.length : 0;
4,022✔
470
            return counter;
4,022✔
471
        });
472

473
        this._internalState = "creating";
223✔
474

475
        this.setServerState(ServerState.NoConfiguration);
223✔
476

477
        this.addressSpace = null;
223✔
478

479
        this._shutdownTasks = [];
223✔
480

481
        this._applicationUri = "";
223✔
482
        if (typeof options.applicationUri === "function") {
223✔
483
            (this as any).__defineGetter__("_applicationUri", options.applicationUri);
189✔
484
        } else {
485
            this._applicationUri = options.applicationUri || "<unset _applicationUri>";
34✔
486
        }
487

488
        options.serverDiagnosticsEnabled = Object.prototype.hasOwnProperty.call(options, "serverDiagnosticsEnable")
223!
489
            ? options.serverDiagnosticsEnabled
490
            : true;
491

492
        this.serverDiagnosticsEnabled = options.serverDiagnosticsEnabled!;
223✔
493
    }
494
    public isStarted(): boolean {
495
        return !!this._serverStatus!;
189✔
496
    }
497

498
    public dispose(): void {
499
        this.addressSpace = null;
431✔
500

501
        assert(Object.keys(this._sessions).length === 0, "ServerEngine#_sessions not empty");
431✔
502
        this._sessions = {};
431✔
503

504
        // todo fix me
505
        this._closedSessions = {};
431✔
506
        assert(Object.keys(this._closedSessions).length === 0, "ServerEngine#_closedSessions not empty");
431✔
507
        this._closedSessions = {};
431✔
508

509
        if (this._orphanPublishEngine) {
431✔
510
            this._orphanPublishEngine.dispose();
9✔
511
            this._orphanPublishEngine = undefined;
9✔
512
        }
513

514
        this._shutdownTasks = [];
431✔
515
        this._serverStatus = null as any as ServerStatusDataType;
431✔
516
        this._internalState = "disposed";
431✔
517
        this.removeAllListeners();
431✔
518

519
        ServerEngine.registry.unregister(this);
431✔
520
    }
521

522
    public get startTime(): Date {
523
        return this._serverStatus.startTime!;
×
524
    }
525

526
    public get currentTime(): Date {
527
        return this._serverStatus.currentTime!;
×
528
    }
529

530
    public get buildInfo(): BuildInfo {
531
        return this._serverStatus.buildInfo;
191✔
532
    }
533

534
    /**
535
     * register a function that will be called when the server will perform its shut down.
536
     */
537
    public registerShutdownTask(task: ServerEngineShutdownTask): void {
538
        assert(typeof task === "function");
1✔
539
        this._shutdownTasks.push(task);
1✔
540
    }
541

542
    /**
543
     */
544
    public async shutdown(): Promise<void> {
545
        debugLog("ServerEngine#shutdown");
224✔
546

547
        this._internalState = "shutdown";
224✔
548
        this.setServerState(ServerState.Shutdown);
224✔
549

550
        // delete any existing sessions
551
        const tokens = Object.keys(this._sessions).map((key: string) => {
224✔
552
            const session = this._sessions[key];
94✔
553
            return session.authenticationToken;
94✔
554
        });
555

556
        // delete and close any orphan subscriptions
557
        if (this._orphanPublishEngine) {
224✔
558
            this._orphanPublishEngine.shutdown();
9✔
559
        }
560

561
        for (const token of tokens) {
224✔
562
            this.closeSession(token, true, "Terminated");
94✔
563
        }
564

565
        // all sessions must have been terminated
566
        assert(this.currentSessionCount === 0);
224✔
567

568
        // all subscriptions must have been terminated
569
        assert(this.currentSubscriptionCount === 0, "all subscriptions must have been terminated");
224✔
570

571
        this._shutdownTasks.push(shutdownAndDisposeAddressSpace);
224✔
572

573
        // perform registerShutdownTask
574
        for (const task of this._shutdownTasks) {
224✔
575
            await task.call(this);
225✔
576
        }
577
        this.setServerState(ServerState.Invalid);
224✔
578

579
        this.dispose();
224✔
580
    }
581

582
    /**
583
     * the number of active sessions
584
     */
585
    public get currentSessionCount(): number {
586
        return this.serverDiagnosticsSummary.currentSessionCount;
3,351✔
587
    }
588

589
    /**
590
     * the cumulated number of sessions that have been opened since this object exists
591
     */
592
    public get cumulatedSessionCount(): number {
593
        return this.serverDiagnosticsSummary.cumulatedSessionCount;
5✔
594
    }
595

596
    /**
597
     * the number of active subscriptions.
598
     */
599
    public get currentSubscriptionCount(): number {
600
        return this.serverDiagnosticsSummary.currentSubscriptionCount;
1,988✔
601
    }
602

603
    /**
604
     * the cumulated number of subscriptions that have been created since this object exists
605
     */
606
    public get cumulatedSubscriptionCount(): number {
607
        return this.serverDiagnosticsSummary.cumulatedSubscriptionCount;
8✔
608
    }
609

610
    public get rejectedSessionCount(): number {
611
        return this.serverDiagnosticsSummary.rejectedSessionCount;
2✔
612
    }
613

614
    public get rejectedRequestsCount(): number {
615
        return this.serverDiagnosticsSummary.rejectedRequestsCount;
2✔
616
    }
617

618
    public get sessionAbortCount(): number {
619
        return this.serverDiagnosticsSummary.sessionAbortCount;
2✔
620
    }
621

622
    public get sessionTimeoutCount(): number {
623
        return this.serverDiagnosticsSummary.sessionTimeoutCount;
×
624
    }
625

626
    public get publishingIntervalCount(): number {
627
        return this.serverDiagnosticsSummary.publishingIntervalCount;
2✔
628
    }
629

630
    public incrementSessionTimeoutCount(): void {
631
        if (this.serverDiagnosticsSummary && this.serverDiagnosticsEnabled) {
11!
632
            // The requests include all Services defined in Part 4 of the OPC UA Specification, also requests to create sessions. This number includes the securityRejectedRequestsCount.
633
            this.serverDiagnosticsSummary.sessionTimeoutCount += 1;
11✔
634
        }
635
    }
636
    public incrementSessionAbortCount(): void {
637
        if (this.serverDiagnosticsSummary && this.serverDiagnosticsEnabled) {
×
638
            // The requests include all Services defined in Part 4 of the OPC UA Specification, also requests to create sessions. This number includes the securityRejectedRequestsCount.
639
            this.serverDiagnosticsSummary.sessionAbortCount += 1;
×
640
        }
641
    }
642
    public incrementRejectedRequestsCount(): void {
643
        if (this.serverDiagnosticsSummary && this.serverDiagnosticsEnabled) {
264!
644
            // The requests include all Services defined in Part 4 of the OPC UA Specification, also requests to create sessions. This number includes the securityRejectedRequestsCount.
645
            this.serverDiagnosticsSummary.rejectedRequestsCount += 1;
264✔
646
        }
647
    }
648

649
    /**
650
     * increment rejected session count (also increment rejected requests count)
651
     */
652
    public incrementRejectedSessionCount(): void {
653
        if (this.serverDiagnosticsSummary && this.serverDiagnosticsEnabled) {
115!
654
            // The requests include all Services defined in Part 4 of the OPC UA Specification, also requests to create sessions. This number includes the securityRejectedRequestsCount.
655
            this.serverDiagnosticsSummary.rejectedSessionCount += 1;
115✔
656
        }
657
        this.incrementRejectedRequestsCount();
115✔
658
    }
659

660
    public incrementSecurityRejectedRequestsCount(): void {
661
        if (this.serverDiagnosticsSummary && this.serverDiagnosticsEnabled) {
55!
662
            // The requests include all Services defined in Part 4 of the OPC UA Specification, also requests to create sessions. This number includes the securityRejectedRequestsCount.
663
            this.serverDiagnosticsSummary.securityRejectedRequestsCount += 1;
55✔
664
        }
665
        this.incrementRejectedRequestsCount();
55✔
666
    }
667

668
    /**
669
     * increment rejected session count (also increment rejected requests count)
670
     */
671
    public incrementSecurityRejectedSessionCount(): void {
672
        if (this.serverDiagnosticsSummary && this.serverDiagnosticsEnabled) {
55!
673
            // The requests include all Services defined in Part 4 of the OPC UA Specification, also requests to create sessions. This number includes the securityRejectedRequestsCount.
674
            this.serverDiagnosticsSummary.securityRejectedSessionCount += 1;
55✔
675
        }
676
        this.incrementSecurityRejectedRequestsCount();
55✔
677
    }
678

679
    public setShutdownTime(date: Date): void {
680
        this._expectedShutdownTime = date;
189✔
681
    }
682
    public setShutdownReason(reason: LocalizedTextLike): void {
683
        this.addressSpace?.rootFolder.objects.server.serverStatus.shutdownReason.setValueFromSource({
1✔
684
            dataType: DataType.LocalizedText,
685
            value: coerceLocalizedText(reason)!
686
        });
687
    }
688
    /**
689
     * @return the approximate number of seconds until the server will be shut down. The
690
     * value is only relevant once the state changes into SHUTDOWN.
691
     */
692
    public secondsTillShutdown(): number {
693
        if (!this._expectedShutdownTime) {
25✔
694
            return 0;
24✔
695
        }
696
        // ToDo: implement a correct solution here
697
        const now = Date.now();
1✔
698
        return Math.max(0, Math.ceil((this._expectedShutdownTime.getTime() - now) / 1000));
1✔
699
    }
700

701
    /**
702
     * the name of the server
703
     */
704
    public get serverName(): string {
705
        return this._serverStatus.buildInfo!.productName!;
×
706
    }
707

708
    /**
709
     * the server urn
710
     */
711
    public get serverNameUrn(): string {
712
        return this._applicationUri;
221✔
713
    }
714

715
    /**
716
     * the urn of the server namespace
717
     */
718
    public get serverNamespaceUrn(): string {
719
        return this._applicationUri; // "urn:" + engine.serverName;
221✔
720
    }
721
    public get serverStatus(): ServerStatusDataType {
722
        return this._serverStatus;
3✔
723
    }
724

725
    public setServerState(serverState: ServerState): void {
726
        assert(serverState !== null && serverState !== undefined);
1,081✔
727
        this.addressSpace?.rootFolder?.objects?.server?.serverStatus?.state?.setValueFromSource({
1,081✔
728
            dataType: DataType.Int32,
729
            value: serverState
730
        });
731
    }
732

733
    public getServerDiagnosticsEnabledFlag(): boolean {
734
        const server = this.addressSpace!.rootFolder.objects.server;
×
735
        const serverDiagnostics = server.getComponentByName("ServerDiagnostics") as UAVariable;
×
736
        if (!serverDiagnostics) {
×
737
            return false;
×
738
        }
739
        return serverDiagnostics.readValue().value.value;
×
740
    }
741

742
    /**
743
     *
744
     */
745
    public initialize(options: OPCUAServerOptions, callback: (err?: Error | null) => void): void {
746
        assert(!this.addressSpace); // check that 'initialize' has not been already called
221✔
747

748
        this._internalState = "initializing";
221✔
749

750
        options = options || {};
221!
751
        assert(typeof callback === "function");
221✔
752

753
        options.nodeset_filename = options.nodeset_filename || nodesets.standard;
221✔
754

755
        const startTime = new Date();
221✔
756

757
        debugLog("Loading ", options.nodeset_filename, "...");
221✔
758

759
        this.addressSpace = AddressSpace.create();
221✔
760

761
        this.addressSpaceAccessor = new AddressSpaceAccessor(this.addressSpace);
221✔
762

763
        if (!options.skipOwnNamespace) {
221!
764
            // register namespace 1 (our namespace);
765
            const serverNamespace = this.addressSpace.registerNamespace(this.serverNamespaceUrn);
221✔
766
            assert(serverNamespace.index === 1);
221✔
767
        }
768
        // eslint-disable-next-line max-statements
769
        generateAddressSpace(this.addressSpace, options.nodeset_filename)
221✔
770
            .catch((err) => {
771
                console.log(err.message);
×
772
                callback(err);
×
773
            })
774
            .then(() => {
775
                /* istanbul ignore next */
776
                if (!this.addressSpace) {
777
                    throw new Error("Internal error");
778
                }
779
                const addressSpace = this.addressSpace;
221✔
780

781
                const endTime = new Date();
221✔
782
                debugLog("Loading ", options.nodeset_filename, " done : ", endTime.getTime() - startTime.getTime(), " ms");
221✔
783

784
                const bindVariableIfPresent = (nodeId: NodeId, opts: any) => {
221✔
785
                    assert(!nodeId.isEmpty());
12,818✔
786
                    const obj = addressSpace.findNode(nodeId);
12,818✔
787
                    if (obj) {
12,818✔
788
                        __bindVariable(this, nodeId, opts);
10,184✔
789
                    }
790
                    return obj;
12,818✔
791
                };
792

793
                // -------------------------------------------- install default get/put handler
794
                const server_NamespaceArray_Id = makeNodeId(VariableIds.Server_NamespaceArray); // ns=0;i=2255
221✔
795
                bindVariableIfPresent(server_NamespaceArray_Id, {
221✔
796
                    get() {
797
                        return new Variant({
1,140✔
798
                            arrayType: VariantArrayType.Array,
799
                            dataType: DataType.String,
800
                            value: addressSpace.getNamespaceArray().map((x) => x.namespaceUri)
2,995✔
801
                        });
802
                    },
803
                    set: null // read only
804
                });
805

806
                const server_NameUrn_var = new Variant({
221✔
807
                    arrayType: VariantArrayType.Array,
808
                    dataType: DataType.String,
809
                    value: [
810
                        this.serverNameUrn // this is us !
811
                    ]
812
                });
813
                const server_ServerArray_Id = makeNodeId(VariableIds.Server_ServerArray); // ns=0;i=2254
221✔
814

815
                bindVariableIfPresent(server_ServerArray_Id, {
221✔
816
                    get() {
817
                        return server_NameUrn_var;
228✔
818
                    },
819
                    set: null // read only
820
                });
821

822
                // fix DefaultUserRolePermissions and DefaultUserRolePermissions
823
                // of namespaces
824
                const namespaces = makeNodeId(ObjectIds.Server_Namespaces);
221✔
825
                const namespacesNode = addressSpace.findNode(namespaces) as UAObject;
221✔
826
                if (namespacesNode) {
221✔
827
                    for (const ns of namespacesNode.getComponents()) {
208✔
828
                        const defaultUserRolePermissions = ns.getChildByName("DefaultUserRolePermissions") as UAVariable | null;
192✔
829
                        if (defaultUserRolePermissions) {
192✔
830
                            defaultUserRolePermissions.setValueFromSource({ dataType: DataType.Null });
177✔
831
                        }
832
                        const defaultRolePermissions = ns.getChildByName("DefaultRolePermissions") as UAVariable | null;
192✔
833
                        if (defaultRolePermissions) {
192✔
834
                            defaultRolePermissions.setValueFromSource({ dataType: DataType.Null });
177✔
835
                        }
836
                    }
837
                }
838

839
                const bindStandardScalar = (
221✔
840
                    id: number,
841
                    dataType: DataType,
842
                    func: () => any,
843
                    setter_func?: (value: any) => void
844
                ) => {
845
                    assert(typeof id === "number", "expecting id to be a number");
11,271✔
846
                    assert(typeof func === "function");
11,271✔
847
                    assert(typeof setter_func === "function" || !setter_func);
11,271✔
848
                    assert(dataType !== null); // check invalid dataType
11,271✔
849

850
                    let setter_func2 = null;
11,271✔
851
                    if (setter_func) {
11,271✔
852
                        setter_func2 = (variant: Variant) => {
221✔
853
                            const variable2 = !!variant.value;
×
854
                            setter_func(variable2);
×
855
                            return StatusCodes.Good;
×
856
                        };
857
                    }
858

859
                    const nodeId = makeNodeId(id);
11,271✔
860

861
                    // make sur the provided function returns a valid value for the variant type
862
                    // This test may not be exhaustive but it will detect obvious mistakes.
863

864
                    /* istanbul ignore next */
865
                    if (!isValidVariant(VariantArrayType.Scalar, dataType, func())) {
866
                        errorLog("func", func());
867
                        throw new Error("bindStandardScalar : func doesn't provide an value of type " + DataType[dataType]);
868
                    }
869

870
                    return bindVariableIfPresent(nodeId, {
11,271✔
871
                        get() {
872
                            return new Variant({
37,114✔
873
                                arrayType: VariantArrayType.Scalar,
874
                                dataType,
875
                                value: func()
876
                            });
877
                        },
878
                        set: setter_func2
879
                    });
880
                };
881

882
                const bindStandardArray = (id: number, variantDataType: DataType, dataType: any, func: () => any[]) => {
221✔
883
                    assert(typeof func === "function");
1,105✔
884
                    assert(variantDataType !== null); // check invalid dataType
1,105✔
885

886
                    const nodeId = makeNodeId(id);
1,105✔
887

888
                    // make sur the provided function returns a valid value for the variant type
889
                    // This test may not be exhaustive but it will detect obvious mistakes.
890
                    assert(isValidVariant(VariantArrayType.Array, variantDataType, func()));
1,105✔
891

892
                    bindVariableIfPresent(nodeId, {
1,105✔
893
                        get() {
894
                            const value = func();
919✔
895
                            assert(Array.isArray(value));
919✔
896
                            return new Variant({
919✔
897
                                arrayType: VariantArrayType.Array,
898
                                dataType: variantDataType,
899
                                value
900
                            });
901
                        },
902
                        set: null // read only
903
                    });
904
                };
905

906
                bindStandardScalar(VariableIds.Server_EstimatedReturnTime, DataType.DateTime, () => getMinOPCUADate());
394✔
907

908
                // TimeZoneDataType
909
                const timeZoneDataType = addressSpace.findDataType(resolveNodeId(DataTypeIds.TimeZoneDataType))!;
221✔
910

911
                const timeZone = new TimeZoneDataType({
221✔
912
                    daylightSavingInOffset: /* boolean*/ false,
913
                    offset: /* int16 */ 0
914
                });
915
                bindStandardScalar(VariableIds.Server_LocalTime, DataType.ExtensionObject, () => {
221✔
916
                    return timeZone;
394✔
917
                });
918

919
                bindStandardScalar(VariableIds.Server_ServiceLevel, DataType.Byte, () => {
221✔
920
                    return 255;
441✔
921
                });
922

923
                bindStandardScalar(VariableIds.Server_Auditing, DataType.Boolean, () => {
221✔
924
                    return this.isAuditing;
443✔
925
                });
926

927
                // eslint-disable-next-line @typescript-eslint/no-this-alias
928
                const engine = this;
221✔
929
                const makeNotReadableIfEnabledFlagIsFalse = (variable: UAVariable) => {
221✔
930
                    const originalIsReadable = variable.isReadable;
2,912✔
931
                    variable.isUserReadable = checkReadableFlag;
2,912✔
932
                    function checkReadableFlag(this: UAVariable, context: SessionContext): boolean {
933
                        const isEnabled = engine.serverDiagnosticsEnabled;
178✔
934
                        return originalIsReadable.call(this, context) && isEnabled;
178✔
935
                    }
936
                    for (const c of variable.getAggregates()) {
2,912✔
937
                        if (c.nodeClass === NodeClass.Variable) {
2,496!
938
                            makeNotReadableIfEnabledFlagIsFalse(c as UAVariable);
2,496✔
939
                        }
940
                    }
941
                };
942

943
                const bindServerDiagnostics = () => {
221✔
944
                    bindStandardScalar(
221✔
945
                        VariableIds.Server_ServerDiagnostics_EnabledFlag,
946
                        DataType.Boolean,
947
                        () => {
948
                            return this.serverDiagnosticsEnabled;
441✔
949
                        },
950
                        (newFlag: boolean) => {
951
                            this.serverDiagnosticsEnabled = newFlag;
×
952
                        }
953
                    );
954
                    const nodeId = makeNodeId(VariableIds.Server_ServerDiagnostics_ServerDiagnosticsSummary);
221✔
955
                    const serverDiagnosticsSummaryNode = addressSpace.findNode(
221✔
956
                        nodeId
957
                    ) as UAServerDiagnosticsSummary<ServerDiagnosticsSummaryDataType>;
958

959
                    if (serverDiagnosticsSummaryNode) {
221✔
960
                        serverDiagnosticsSummaryNode.bindExtensionObject(this.serverDiagnosticsSummary);
208✔
961
                        this.serverDiagnosticsSummary = serverDiagnosticsSummaryNode.$extensionObject;
208✔
962
                        makeNotReadableIfEnabledFlagIsFalse(serverDiagnosticsSummaryNode);
208✔
963
                    }
964
                };
965

966
                const bindServerStatus = () => {
221✔
967
                    const serverStatusNode = addressSpace.findNode(
221✔
968
                        makeNodeId(VariableIds.Server_ServerStatus)
969
                    ) as UAServerStatus<DTServerStatus>;
970

971
                    if (!serverStatusNode) {
221✔
972
                        return;
13✔
973
                    }
974
                    if (serverStatusNode) {
208!
975
                        serverStatusNode.bindExtensionObject(this._serverStatus);
208✔
976
                        serverStatusNode.minimumSamplingInterval = 1000;
208✔
977
                    }
978

979
                    const currentTimeNode = addressSpace.findNode(
208✔
980
                        makeNodeId(VariableIds.Server_ServerStatus_CurrentTime)
981
                    ) as UAVariable;
982

983
                    if (currentTimeNode) {
208!
984
                        currentTimeNode.minimumSamplingInterval = 1000;
208✔
985
                    }
986
                    const secondsTillShutdown = addressSpace.findNode(
208✔
987
                        makeNodeId(VariableIds.Server_ServerStatus_SecondsTillShutdown)
988
                    ) as UAVariable;
989

990
                    if (secondsTillShutdown) {
208!
991
                        secondsTillShutdown.minimumSamplingInterval = 1000;
208✔
992
                    }
993

994
                    assert(serverStatusNode.$extensionObject);
208✔
995

996
                    serverStatusNode.$extensionObject = new Proxy(serverStatusNode.$extensionObject, {
208✔
997
                        get(target, prop) {
998
                            if (prop === "currentTime") {
2,379✔
999
                                serverStatusNode.currentTime.touchValue();
507✔
1000
                                return new Date();
507✔
1001
                            } else if (prop === "secondsTillShutdown") {
1,872✔
1002
                                serverStatusNode.secondsTillShutdown.touchValue();
25✔
1003
                                return engine.secondsTillShutdown();
25✔
1004
                            }
1005
                            return (target as any)[prop];
1,847✔
1006
                        }
1007
                    });
1008
                    this._serverStatus = serverStatusNode.$extensionObject;
208✔
1009
                };
1010

1011
                const bindServerCapabilities = () => {
221✔
1012
                    bindStandardArray(
221✔
1013
                        VariableIds.Server_ServerCapabilities_ServerProfileArray,
1014
                        DataType.String,
1015
                        DataType.String,
1016
                        () => {
1017
                            return this.serverCapabilities.serverProfileArray;
396✔
1018
                        }
1019
                    );
1020

1021
                    bindStandardArray(VariableIds.Server_ServerCapabilities_LocaleIdArray, DataType.String, "LocaleId", () => {
221✔
1022
                        return this.serverCapabilities.localeIdArray;
443✔
1023
                    });
1024

1025
                    bindStandardScalar(VariableIds.Server_ServerCapabilities_MinSupportedSampleRate, DataType.Double, () => {
221✔
1026
                        return Math.max(
396✔
1027
                            this.serverCapabilities.minSupportedSampleRate,
1028
                            defaultServerCapabilities.minSupportedSampleRate
1029
                        );
1030
                    });
1031

1032
                    bindStandardScalar(VariableIds.Server_ServerCapabilities_MaxBrowseContinuationPoints, DataType.UInt16, () => {
221✔
1033
                        return this.serverCapabilities.maxBrowseContinuationPoints;
13,618✔
1034
                    });
1035

1036
                    bindStandardScalar(VariableIds.Server_ServerCapabilities_MaxQueryContinuationPoints, DataType.UInt16, () => {
221✔
1037
                        return this.serverCapabilities.maxQueryContinuationPoints;
396✔
1038
                    });
1039

1040
                    bindStandardScalar(VariableIds.Server_ServerCapabilities_MaxHistoryContinuationPoints, DataType.UInt16, () => {
221✔
1041
                        return this.serverCapabilities.maxHistoryContinuationPoints;
398✔
1042
                    });
1043

1044
                    // new in 1.05
1045
                    bindStandardScalar(VariableIds.Server_ServerCapabilities_MaxSessions, DataType.UInt32, () => {
221✔
1046
                        return this.serverCapabilities.maxSessions;
396✔
1047
                    });
1048
                    bindStandardScalar(VariableIds.Server_ServerCapabilities_MaxSubscriptions, DataType.UInt32, () => {
221✔
1049
                        return this.serverCapabilities.maxSubscriptions;
396✔
1050
                    });
1051
                    bindStandardScalar(VariableIds.Server_ServerCapabilities_MaxMonitoredItems, DataType.UInt32, () => {
221✔
1052
                        return this.serverCapabilities.maxMonitoredItems;
396✔
1053
                    });
1054
                    bindStandardScalar(VariableIds.Server_ServerCapabilities_MaxSubscriptionsPerSession, DataType.UInt32, () => {
221✔
1055
                        return this.serverCapabilities.maxSubscriptionsPerSession;
396✔
1056
                    });
1057
                    bindStandardScalar(VariableIds.Server_ServerCapabilities_MaxSelectClauseParameters, DataType.UInt32, () => {
221✔
1058
                        return this.serverCapabilities.maxSelectClauseParameters;
396✔
1059
                    });
1060
                    bindStandardScalar(VariableIds.Server_ServerCapabilities_MaxWhereClauseParameters, DataType.UInt32, () => {
221✔
1061
                        return this.serverCapabilities.maxWhereClauseParameters;
396✔
1062
                    });
1063
                    //bindStandardArray(VariableIds.Server_ServerCapabilities_ConformanceUnits, DataType.QualifiedName, () => {
1064
                    //    return this.serverCapabilities.conformanceUnits;
1065
                    //});
1066
                    bindStandardScalar(
221✔
1067
                        VariableIds.Server_ServerCapabilities_MaxMonitoredItemsPerSubscription,
1068
                        DataType.UInt32,
1069
                        () => {
1070
                            return this.serverCapabilities.maxMonitoredItemsPerSubscription;
396✔
1071
                        }
1072
                    );
1073

1074
                    // added by DI : Server-specific period of time in milliseconds until the Server will revoke a lock.
1075
                    // TODO bindStandardScalar(VariableIds.Server_ServerCapabilities_MaxInactiveLockTime,
1076
                    // TODO     DataType.UInt16, function () {
1077
                    // TODO         return self.serverCapabilities.maxInactiveLockTime;
1078
                    // TODO });
1079

1080
                    bindStandardArray(
221✔
1081
                        VariableIds.Server_ServerCapabilities_SoftwareCertificates,
1082
                        DataType.ExtensionObject,
1083
                        "SoftwareCertificates",
1084
                        () => {
1085
                            return this.serverCapabilities.softwareCertificates;
396✔
1086
                        }
1087
                    );
1088

1089
                    bindStandardScalar(VariableIds.Server_ServerCapabilities_MaxArrayLength, DataType.UInt32, () => {
221✔
1090
                        return Math.min(this.serverCapabilities.maxArrayLength, Variant.maxArrayLength);
396✔
1091
                    });
1092

1093
                    bindStandardScalar(VariableIds.Server_ServerCapabilities_MaxStringLength, DataType.UInt32, () => {
221✔
1094
                        return Math.min(this.serverCapabilities.maxStringLength, BinaryStream.maxStringLength);
396✔
1095
                    });
1096

1097
                    bindStandardScalar(VariableIds.Server_ServerCapabilities_MaxByteStringLength, DataType.UInt32, () => {
221✔
1098
                        return Math.min(this.serverCapabilities.maxByteStringLength, BinaryStream.maxByteStringLength);
398✔
1099
                    });
1100

1101
                    bindStandardScalar(VariableIds.Server_ServerCapabilities_MaxMonitoredItemsQueueSize, DataType.UInt32, () => {
221✔
1102
                        return Math.max(1, this.serverCapabilities.maxMonitoredItemsQueueSize);
396✔
1103
                    });
1104

1105
                    const bindOperationLimits = (operationLimits: ServerOperationLimits) => {
221✔
1106
                        assert(operationLimits !== null && typeof operationLimits === "object");
221✔
1107

1108
                        const keys = Object.keys(operationLimits);
221✔
1109

1110
                        keys.forEach((key: string) => {
221✔
1111
                            const uid = "Server_ServerCapabilities_OperationLimits_" + upperCaseFirst(key);
2,652✔
1112
                            const nodeId = makeNodeId((VariableIds as any)[uid]);
2,652✔
1113
                            assert(!nodeId.isEmpty());
2,652✔
1114

1115
                            bindStandardScalar((VariableIds as any)[uid], DataType.UInt32, () => {
2,652✔
1116
                                return (operationLimits as any)[key];
19,588✔
1117
                            });
1118
                        });
1119
                    };
1120

1121
                    bindOperationLimits(this.serverCapabilities.operationLimits);
221✔
1122

1123
                    // i=2399 [ProgramStateMachineType_ProgramDiagnostics];
1124
                    function fix_ProgramStateMachineType_ProgramDiagnostics() {
1125
                        const nodeId = coerceNodeId("i=2399"); // ProgramStateMachineType_ProgramDiagnostics
221✔
1126
                        const variable = addressSpace.findNode(nodeId) as UAVariable;
221✔
1127
                        if (variable) {
221✔
1128
                            (variable as any).$extensionObject = new ProgramDiagnosticDataType({});
161✔
1129
                            //  variable.setValueFromSource({
1130
                            //     dataType: DataType.ExtensionObject,
1131
                            //     //     value: new ProgramDiagnostic2DataType()
1132
                            //     value: new ProgramDiagnosticDataType({})
1133
                            // });
1134
                        }
1135
                    }
1136
                    fix_ProgramStateMachineType_ProgramDiagnostics();
221✔
1137
                };
1138

1139
                const bindHistoryServerCapabilities = () => {
221✔
1140
                    bindStandardScalar(VariableIds.HistoryServerCapabilities_MaxReturnDataValues, DataType.UInt32, () => {
221✔
1141
                        return this.historyServerCapabilities.maxReturnDataValues;
396✔
1142
                    });
1143

1144
                    bindStandardScalar(VariableIds.HistoryServerCapabilities_MaxReturnEventValues, DataType.UInt32, () => {
221✔
1145
                        return this.historyServerCapabilities.maxReturnEventValues;
396✔
1146
                    });
1147

1148
                    bindStandardScalar(VariableIds.HistoryServerCapabilities_AccessHistoryDataCapability, DataType.Boolean, () => {
221✔
1149
                        return this.historyServerCapabilities.accessHistoryDataCapability;
396✔
1150
                    });
1151
                    bindStandardScalar(
221✔
1152
                        VariableIds.HistoryServerCapabilities_AccessHistoryEventsCapability,
1153
                        DataType.Boolean,
1154
                        () => {
1155
                            return this.historyServerCapabilities.accessHistoryEventsCapability;
396✔
1156
                        }
1157
                    );
1158
                    bindStandardScalar(VariableIds.HistoryServerCapabilities_InsertDataCapability, DataType.Boolean, () => {
221✔
1159
                        return this.historyServerCapabilities.insertDataCapability;
396✔
1160
                    });
1161
                    bindStandardScalar(VariableIds.HistoryServerCapabilities_ReplaceDataCapability, DataType.Boolean, () => {
221✔
1162
                        return this.historyServerCapabilities.replaceDataCapability;
396✔
1163
                    });
1164
                    bindStandardScalar(VariableIds.HistoryServerCapabilities_UpdateDataCapability, DataType.Boolean, () => {
221✔
1165
                        return this.historyServerCapabilities.updateDataCapability;
396✔
1166
                    });
1167

1168
                    bindStandardScalar(VariableIds.HistoryServerCapabilities_InsertEventCapability, DataType.Boolean, () => {
221✔
1169
                        return this.historyServerCapabilities.insertEventCapability;
396✔
1170
                    });
1171

1172
                    bindStandardScalar(VariableIds.HistoryServerCapabilities_ReplaceEventCapability, DataType.Boolean, () => {
221✔
1173
                        return this.historyServerCapabilities.replaceEventCapability;
396✔
1174
                    });
1175

1176
                    bindStandardScalar(VariableIds.HistoryServerCapabilities_UpdateEventCapability, DataType.Boolean, () => {
221✔
1177
                        return this.historyServerCapabilities.updateEventCapability;
396✔
1178
                    });
1179

1180
                    bindStandardScalar(VariableIds.HistoryServerCapabilities_DeleteEventCapability, DataType.Boolean, () => {
221✔
1181
                        return this.historyServerCapabilities.deleteEventCapability;
396✔
1182
                    });
1183

1184
                    bindStandardScalar(VariableIds.HistoryServerCapabilities_DeleteRawCapability, DataType.Boolean, () => {
221✔
1185
                        return this.historyServerCapabilities.deleteRawCapability;
396✔
1186
                    });
1187

1188
                    bindStandardScalar(VariableIds.HistoryServerCapabilities_DeleteAtTimeCapability, DataType.Boolean, () => {
221✔
1189
                        return this.historyServerCapabilities.deleteAtTimeCapability;
396✔
1190
                    });
1191

1192
                    bindStandardScalar(VariableIds.HistoryServerCapabilities_InsertAnnotationCapability, DataType.Boolean, () => {
221✔
1193
                        return this.historyServerCapabilities.insertAnnotationCapability;
396✔
1194
                    });
1195
                };
1196

1197
                type Getter<T> = () => T;
1198
                function r<T>(a: undefined | T | Getter<T>, defaultValue: T): T {
1199
                    if (a === undefined) return defaultValue;
2,763✔
1200
                    if (typeof a === "function") {
2,194✔
1201
                        return (a as any)();
1,800✔
1202
                    }
1203
                    return a;
394✔
1204
                }
1205
                const bindServerConfigurationBasic = () => {
221✔
1206
                    bindStandardArray(VariableIds.ServerConfiguration_ServerCapabilities, DataType.String, DataType.String, () =>
221✔
1207
                        r(this.serverConfiguration.serverCapabilities, ["NA"])
395✔
1208
                    );
1209
                    bindStandardScalar(VariableIds.ServerConfiguration_ApplicationType, DataType.Int32, () =>
221✔
1210
                        r(this.serverConfiguration.applicationType, ApplicationType.Server)
395✔
1211
                    );
1212
                    bindStandardScalar(VariableIds.ServerConfiguration_ApplicationUri, DataType.String, () =>
221✔
1213
                        r(this.serverConfiguration.applicationUri, "")
395✔
1214
                    );
1215
                    bindStandardScalar(VariableIds.ServerConfiguration_ProductUri, DataType.String, () =>
221✔
1216
                        r(this.serverConfiguration.productUri, "")
395✔
1217
                    );
1218
                    bindStandardScalar(VariableIds.ServerConfiguration_HasSecureElement, DataType.Boolean, () =>
221✔
1219
                        r(this.serverConfiguration.hasSecureElement, false)
394✔
1220
                    );
1221
                    bindStandardScalar(VariableIds.ServerConfiguration_MulticastDnsEnabled, DataType.Boolean, () =>
221✔
1222
                        r(this.serverConfiguration.multicastDnsEnabled, false)
395✔
1223
                    );
1224
                    bindStandardArray(
221✔
1225
                        VariableIds.ServerConfiguration_SupportedPrivateKeyFormats,
1226
                        DataType.String,
1227
                        DataType.String,
1228
                        () => r(this.serverConfiguration.supportedPrivateKeyFormat, ["PEM"])
394✔
1229
                    );
1230
                };
1231

1232
                bindServerDiagnostics();
221✔
1233

1234
                bindServerStatus();
221✔
1235

1236
                bindServerCapabilities();
221✔
1237

1238
                bindServerConfigurationBasic();
221✔
1239

1240
                bindHistoryServerCapabilities();
221✔
1241

1242
                const bindExtraStuff = () => {
221✔
1243
                    // mainly for compliance
1244
                    /*
1245
                // The version number for the data type description. i=104
1246
                bindStandardScalar(VariableIds.DataTypeDescriptionType_DataTypeVersion, DataType.String, () => {
1247
                    return "0";
1248
                });
1249

1250
                const namingRuleDataTypeNode = addressSpace.findDataType(resolveNodeId(DataTypeIds.NamingRuleType))! as UADataType;
1251

1252
                if (namingRuleDataTypeNode) {
1253
                    const namingRuleType = (namingRuleDataTypeNode as any)._getEnumerationInfo().nameIndex; // getEnumeration("NamingRuleType");
1254
                    if (!namingRuleType) {
1255
                        throw new Error("Cannot find Enumeration definition for NamingRuleType");
1256
                    }
1257
                    // i=111
1258
                    bindStandardScalar(VariableIds.ModellingRuleType_NamingRule, DataType.Int32, () => {
1259
                        return 0;
1260
                    });
1261

1262
                    // i=112
1263
                    bindStandardScalar(VariableIds.ModellingRule_Mandatory_NamingRule, DataType.Int32, () => {
1264
                        return namingRuleType.Mandatory ? namingRuleType.Mandatory.value : 0;
1265
                    });
1266

1267
                    // i=113
1268
                    bindStandardScalar(VariableIds.ModellingRule_Optional_NamingRule, DataType.Int32, () => {
1269
                        return namingRuleType.Optional ? namingRuleType.Optional.value : 0;
1270
                    });
1271
                    // i=114
1272
                    bindStandardScalar(VariableIds.ModellingRule_ExposesItsArray_NamingRule, DataType.Int32, () => {
1273
                        return namingRuleType.ExposesItsArray ? namingRuleType.ExposesItsArray.value : 0;
1274
                    });
1275
                    bindStandardScalar(VariableIds.ModellingRule_MandatoryPlaceholder_NamingRule, DataType.Int32, () => {
1276
                        return namingRuleType.MandatoryPlaceholder ? namingRuleType.MandatoryPlaceholder.value : 0;
1277
                    });
1278
                }
1279
*/
1280
                };
1281

1282
                bindExtraStuff();
221✔
1283

1284
                this.__internal_bindMethod(makeNodeId(MethodIds.Server_GetMonitoredItems), getMonitoredItemsId.bind(this));
221✔
1285
                this.__internal_bindMethod(makeNodeId(MethodIds.Server_SetSubscriptionDurable), setSubscriptionDurable.bind(this));
221✔
1286
                this.__internal_bindMethod(makeNodeId(MethodIds.Server_ResendData), resendData.bind(this));
221✔
1287
                this.__internal_bindMethod(
221✔
1288
                    makeNodeId(MethodIds.Server_RequestServerStateChange),
1289
                    requestServerStateChange.bind(this)
1290
                );
1291

1292
                // fix getMonitoredItems.outputArguments arrayDimensions
1293
                const fixGetMonitoredItemArgs = () => {
221✔
1294
                    const objects = this.addressSpace!.rootFolder?.objects;
221✔
1295
                    if (!objects || !objects.server) {
221✔
1296
                        return;
12✔
1297
                    }
1298
                    const getMonitoredItemsMethod = objects.server.getMethodByName("GetMonitoredItems")!;
209✔
1299
                    if (!getMonitoredItemsMethod) {
209✔
1300
                        return;
1✔
1301
                    }
1302
                    const outputArguments = getMonitoredItemsMethod.outputArguments!;
208✔
1303
                        const dataValue = outputArguments.readValue();
208✔
1304
                        if (!dataValue.value?.value) {
208!
1305
                        // value is null or undefined , meaning no arguments necessary
1306
                        return;
×
1307
                    }
1308
                    assert(
208✔
1309
                        dataValue.value.value[0].arrayDimensions.length === 1 && dataValue.value.value[0].arrayDimensions[0] === 0
416✔
1310
                    );
1311
                    assert(
208✔
1312
                        dataValue.value.value[1].arrayDimensions.length === 1 && dataValue.value.value[1].arrayDimensions[0] === 0
416✔
1313
                    );
1314
                };
1315
                fixGetMonitoredItemArgs();
221✔
1316

1317
                const prepareServerDiagnostics = () => {
221✔
1318
                    const addressSpace1 = this.addressSpace!;
221✔
1319

1320
                    if (!addressSpace1.rootFolder.objects) {
221✔
1321
                        return;
12✔
1322
                    }
1323
                    const server = addressSpace1.rootFolder.objects.server;
209✔
1324

1325
                    if (!server) {
209!
1326
                        return;
×
1327
                    }
1328

1329
                    // create SessionsDiagnosticsSummary
1330
                    const serverDiagnosticsNode = server.getComponentByName("ServerDiagnostics") as UAServerDiagnostics;
209✔
1331
                    if (!serverDiagnosticsNode) {
209✔
1332
                        return;
1✔
1333
                    }
1334
                    if (true) {
208!
1335
                        // set serverDiagnosticsNode enabledFlag writeable for admin user only
1336
                        // TO DO ...
1337
                        serverDiagnosticsNode.enabledFlag.userAccessLevel = makeAccessLevelFlag("CurrentRead");
208✔
1338
                        serverDiagnosticsNode.enabledFlag.accessLevel = makeAccessLevelFlag("CurrentRead");
208✔
1339
                    }
1340

1341
                    // A Server may not expose the SamplingIntervalDiagnosticsArray if it does not use fixed sampling rates.
1342
                    // because we are not using fixed sampling rate, we need to remove the optional SamplingIntervalDiagnosticsArray
1343
                    // component
1344
                    const samplingIntervalDiagnosticsArray = serverDiagnosticsNode.getComponentByName(
208✔
1345
                        "SamplingIntervalDiagnosticsArray"
1346
                    );
1347
                    if (samplingIntervalDiagnosticsArray) {
208!
1348
                        addressSpace.deleteNode(samplingIntervalDiagnosticsArray);
208✔
1349
                        const s = serverDiagnosticsNode.getComponents();
208✔
1350
                    }
1351

1352
                    const subscriptionDiagnosticsArrayNode = serverDiagnosticsNode.getComponentByName(
208✔
1353
                        "SubscriptionDiagnosticsArray"
1354
                    )! as UADynamicVariableArray<SessionDiagnosticsDataType>;
1355
                    assert(subscriptionDiagnosticsArrayNode.nodeClass === NodeClass.Variable);
208✔
1356
                    bindExtObjArrayNode(subscriptionDiagnosticsArrayNode, "SubscriptionDiagnosticsType", "subscriptionId");
208✔
1357

1358
                    makeNotReadableIfEnabledFlagIsFalse(subscriptionDiagnosticsArrayNode);
208✔
1359

1360
                    const sessionsDiagnosticsSummary = serverDiagnosticsNode.getComponentByName("SessionsDiagnosticsSummary")!;
208✔
1361

1362
                    const sessionDiagnosticsArray = sessionsDiagnosticsSummary.getComponentByName(
208✔
1363
                        "SessionDiagnosticsArray"
1364
                    )! as UADynamicVariableArray<SessionDiagnosticsDataType>;
1365
                    assert(sessionDiagnosticsArray.nodeClass === NodeClass.Variable);
208✔
1366

1367
                    bindExtObjArrayNode(sessionDiagnosticsArray, "SessionDiagnosticsVariableType", "sessionId");
208✔
1368

1369
                    const varType = addressSpace.findVariableType("SessionSecurityDiagnosticsType");
208✔
1370
                    if (!varType) {
208✔
1371
                        debugLog("Warning cannot find SessionSecurityDiagnosticsType variable Type");
47✔
1372
                    } else {
1373
                        const sessionSecurityDiagnosticsArray = sessionsDiagnosticsSummary.getComponentByName(
161✔
1374
                            "SessionSecurityDiagnosticsArray"
1375
                        )! as UADynamicVariableArray<SessionSecurityDiagnosticsDataType>;
1376
                        assert(sessionSecurityDiagnosticsArray.nodeClass === NodeClass.Variable);
161✔
1377
                        bindExtObjArrayNode(sessionSecurityDiagnosticsArray, "SessionSecurityDiagnosticsType", "sessionId");
161✔
1378
                        ensureObjectIsSecure(sessionSecurityDiagnosticsArray);
161✔
1379
                    }
1380
                };
1381

1382
                prepareServerDiagnostics();
221✔
1383

1384
                this._internalState = "initialized";
221✔
1385
                this.setServerState(ServerState.Running);
221✔
1386
                setImmediate(() => callback());
221✔
1387
            });
1388
    }
1389

1390
    public async browseWithAutomaticExpansion(
1391
        nodesToBrowse: BrowseDescription[],
1392
        context: ISessionContext
1393
    ): Promise<BrowseResult[]> {
1394
        // do expansion first
1395
        for (const browseDescription of nodesToBrowse) {
9,827✔
1396
            const nodeId = resolveNodeId(browseDescription.nodeId);
42,747✔
1397
            const node = this.addressSpace!.findNode(nodeId);
42,747✔
1398
            if (node) {
42,747!
1399
                if (node.onFirstBrowseAction) {
42,747✔
1400
                    try {
1✔
1401
                        await node.onFirstBrowseAction();
1✔
1402
                        node.onFirstBrowseAction = undefined;
1✔
1403
                    } catch (err) {
1404
                        if (types.isNativeError(err)) {
×
1405
                            errorLog("onFirstBrowseAction method has failed", err.message);
×
1406
                        }
1407
                        errorLog(err);
×
1408
                    }
1409
                    assert(node.onFirstBrowseAction === undefined, "expansion can only be made once");
1✔
1410
                }
1411
            }
1412
        }
1413
        return await this.browse(context, nodesToBrowse);
9,827✔
1414
    }
1415
    public async browse(context: ISessionContext, nodesToBrowse: BrowseDescriptionOptions[]): Promise<BrowseResult[]> {
1416
        return this.addressSpaceAccessor!.browse(context, nodesToBrowse);
9,842✔
1417
    }
1418
    public async read(context: ISessionContext, readRequest: ReadRequestOptions): Promise<DataValue[]> {
1419
        return this.addressSpaceAccessor!.read(context, readRequest);
19,496✔
1420
    }
1421
    public async write(context: ISessionContext, nodesToWrite: WriteValue[]): Promise<StatusCode[]> {
1422
        return await this.addressSpaceAccessor!.write(context, nodesToWrite);
158✔
1423
    }
1424
    public async call(context: ISessionContext, methodsToCall: CallMethodRequest[]): Promise<CallMethodResultOptions[]> {
1425
        return await this.addressSpaceAccessor!.call(context, methodsToCall);
86✔
1426
    }
1427
    public async historyRead(context: ISessionContext, historyReadRequest: HistoryReadRequest): Promise<HistoryReadResult[]> {
1428
        return this.addressSpaceAccessor!.historyRead(context, historyReadRequest);
25✔
1429
    }
1430

1431
    public getOldestInactiveSession(): ServerSession | null {
1432
        // search screwed or closed session first
1433
        let tmp = Object.values(this._sessions).filter(
21✔
1434
            (session1: ServerSession) =>
1435
                session1.status === "screwed" || session1.status === "disposed" || session1.status === "closed"
284✔
1436
        );
1437
        if (tmp.length === 0) {
21!
1438
            // if none available, tap into the session that are not yet activated
1439
            tmp = Object.values(this._sessions).filter((session1: ServerSession) => session1.status === "new");
284✔
1440
        }
1441
        if (tmp.length === 0) return null;
21✔
1442
        let session = tmp[0];
14✔
1443
        for (let i = 1; i < tmp.length; i++) {
14✔
1444
            const c = tmp[i];
263✔
1445
            if (session.creationDate.getTime() < c.creationDate.getTime()) {
263✔
1446
                session = c;
202✔
1447
            }
1448
        }
1449
        return session;
14✔
1450
    }
1451

1452
    /**
1453
     * create a new server session object.
1454
     */
1455
    public createSession(options?: CreateSessionOption): ServerSession {
1456
        options = options || {};
929✔
1457
        options.server = options.server || {};
929✔
1458
        debugLog("createSession : increasing serverDiagnosticsSummary cumulatedSessionCount/currentSessionCount ");
929✔
1459
        this.serverDiagnosticsSummary.cumulatedSessionCount += 1;
929✔
1460
        this.serverDiagnosticsSummary.currentSessionCount += 1;
929✔
1461

1462
        this.clientDescription = options.clientDescription || new ApplicationDescription({});
929✔
1463

1464
        const sessionTimeout = options.sessionTimeout || 1000;
929✔
1465
        assert(typeof sessionTimeout === "number");
929✔
1466

1467
        const session = new ServerSession(this, options.server.userManager!, sessionTimeout);
929✔
1468

1469
        debugLog("createSession :sessionTimeout = ", session.sessionTimeout);
929✔
1470

1471
        const key = session.authenticationToken.toString();
929✔
1472

1473
        this._sessions[key] = session;
929✔
1474

1475
        // see spec OPC Unified Architecture,  Part 2 page 26 Release 1.02
1476
        // TODO : When a Session is created, the Server adds an entry for the Client
1477
        //        in its SessionDiagnosticsArray Variable
1478

1479
        session.on("new_subscription", (subscription: Subscription) => {
929✔
1480
            this.serverDiagnosticsSummary.cumulatedSubscriptionCount += 1;
486✔
1481
            // add the subscription diagnostics in our subscriptions diagnostics array
1482
            // note currentSubscriptionCount is handled directly with a special getter
1483
        });
1484

1485
        session.on("subscription_terminated", (subscription: Subscription) => {
929✔
1486
            // remove the subscription diagnostics in our subscriptions diagnostics array
1487
            // note currentSubscriptionCount is handled directly with a special getter
1488
        });
1489

1490
        // OPC Unified Architecture, Part 4 23 Release 1.03
1491
        // Sessions are terminated by the Server automatically if the Client fails to issue a Service request on the
1492
        // Session within the timeout period negotiated by the Server in the CreateSession Service response.
1493
        // This protects the Server against Client failures and against situations where a failed underlying
1494
        // connection cannot be re-established. Clients shall be prepared to submit requests in a timely manner
1495
        // prevent the Session from closing automatically. Clients may explicitly terminate sessions using the
1496
        // CloseSession Service.
1497
        session.on("timeout", () => {
929✔
1498
            // the session hasn't been active for a while , probably because the client has disconnected abruptly
1499
            // it is now time to close the session completely
1500
            this.serverDiagnosticsSummary.sessionTimeoutCount += 1;
11✔
1501
            session.sessionName = session.sessionName || "";
11!
1502

1503
            const channel = session.channel;
11✔
1504
            errorLog(
11✔
1505
                chalk.cyan("Server: closing SESSION "),
1506
                session.status,
1507
                chalk.yellow(session.sessionName),
1508
                chalk.yellow(session.nodeId.toString()),
1509
                chalk.cyan(" because of timeout = "),
1510
                session.sessionTimeout,
1511
                chalk.cyan(" has expired without a keep alive"),
1512
                chalk.bgCyan("channel = "),
1513
                channel?.remoteAddress,
1514
                " port = ",
1515
                channel?.remotePort
1516
            );
1517

1518
            // If a Server terminates a Session for any other reason, Subscriptions  associated with the Session,
1519
            // are not deleted. => deleteSubscription= false
1520
            this.closeSession(session.authenticationToken, /*deleteSubscription=*/ false, /* reason =*/ "Timeout");
11✔
1521

1522
            this.incrementSessionTimeoutCount();
11✔
1523
        });
1524

1525
        return session;
929✔
1526
    }
1527

1528
    /**
1529
     * @param authenticationToken
1530
     * @param deleteSubscriptions {Boolean} : true if session's subscription shall be deleted
1531
     * @param {String} [reason = "CloseSession"] the reason for closing the session (
1532
     *                 shall be "Timeout", "Terminated" or "CloseSession")
1533
     *
1534
     *
1535
     * what the specs say:
1536
     * -------------------
1537
     *
1538
     * If a Client invokes the CloseSession Service then all Subscriptions associated with the Session are also deleted
1539
     * if the deleteSubscriptions flag is set to TRUE. If a Server terminates a Session for any other reason,
1540
     * Subscriptions associated with the Session, are not deleted. Each Subscription has its own lifetime to protect
1541
     * against data loss in the case of a Session termination. In these cases, the Subscription can be reassigned to
1542
     * another Client before its lifetime expires.
1543
     */
1544
    public closeSession(authenticationToken: NodeId, deleteSubscriptions: boolean, reason: ClosingReason): void {
1545
        reason = reason || "CloseSession";
929!
1546
        assert(typeof reason === "string");
929✔
1547
        assert(reason === "Timeout" || reason === "Terminated" || reason === "CloseSession" || reason === "Forcing");
929✔
1548

1549
        debugLog("ServerEngine.closeSession ", authenticationToken.toString(), deleteSubscriptions);
929✔
1550

1551
        const session = this.getSession(authenticationToken);
929✔
1552

1553
        // istanbul ignore next
1554
        if (!session) {
1555
            throw new Error("cannot find session with this authenticationToken " + authenticationToken.toString());
1556
        }
1557

1558
        if (!deleteSubscriptions) {
929✔
1559
            // Live Subscriptions will not be deleted, but transferred to the orphanPublishEngine
1560
            // until they time out or until a other session transfer them back to it.
1561
            if (!this._orphanPublishEngine) {
32✔
1562
                this._orphanPublishEngine = new ServerSidePublishEngineForOrphanSubscription({ maxPublishRequestInQueue: 0 });
9✔
1563
            }
1564

1565
            debugLog("transferring remaining live subscription to orphanPublishEngine !");
32✔
1566
            ServerSidePublishEngine.transferSubscriptionsToOrphan(session.publishEngine, this._orphanPublishEngine);
32✔
1567
        }
1568

1569
        session.close(deleteSubscriptions, reason);
929✔
1570

1571
        assert(session.status === "closed");
929✔
1572

1573
        debugLog(" engine.serverDiagnosticsSummary.currentSessionCount -= 1;");
929✔
1574
        this.serverDiagnosticsSummary.currentSessionCount -= 1;
929✔
1575

1576
        // xx //TODO make sure _closedSessions gets cleaned at some point
1577
        // xx self._closedSessions[key] = session;
1578

1579
        // remove sessionDiagnostics from server.ServerDiagnostics.SessionsDiagnosticsSummary.SessionDiagnosticsSummary
1580
        delete this._sessions[authenticationToken.toString()];
929✔
1581
        session.dispose();
929✔
1582
    }
1583

1584
    public findSubscription(subscriptionId: number): Subscription | null {
1585
        const subscriptions: Subscription[] = [];
116✔
1586
        Object.values(this._sessions).map((session) => {
116✔
1587
            if (subscriptions.length) {
297✔
1588
                return;
15✔
1589
            }
1590
            const subscription = session.publishEngine.getSubscriptionById(subscriptionId);
282✔
1591
            if (subscription) {
282✔
1592
                subscriptions.push(subscription);
16✔
1593
            }
1594
        });
1595
        if (subscriptions.length) {
116✔
1596
            assert(subscriptions.length === 1);
16✔
1597
            return subscriptions[0];
16✔
1598
        }
1599
        return this.findOrphanSubscription(subscriptionId);
100✔
1600
    }
1601

1602
    public findOrphanSubscription(subscriptionId: number): Subscription | null {
1603
        if (!this._orphanPublishEngine) {
405✔
1604
            return null;
362✔
1605
        }
1606
        return this._orphanPublishEngine.getSubscriptionById(subscriptionId);
43✔
1607
    }
1608

1609
    public deleteOrphanSubscription(subscription: Subscription): StatusCode {
1610
        if (!this._orphanPublishEngine) {
×
1611
            return StatusCodes.BadInternalError;
×
1612
        }
1613
        assert(this.findSubscription(subscription.id));
×
1614

1615
        const c = this._orphanPublishEngine.subscriptionCount;
×
1616
        subscription.terminate();
×
1617
        subscription.dispose();
×
1618
        assert(this._orphanPublishEngine.subscriptionCount === c - 1);
×
1619
        return StatusCodes.Good;
×
1620
    }
1621

1622
    /**
1623
     * @param session           {ServerSession}  - the new session that will own the subscription
1624
     * @param subscriptionId    {IntegerId}      - the subscription Id to transfer
1625
     * @param sendInitialValues {Boolean}        - true if initial values will be resent.
1626
     * @return                  {TransferResult}
1627
     */
1628
    public async transferSubscription(
1629
        session: ServerSession,
1630
        subscriptionId: number,
1631
        sendInitialValues: boolean
1632
    ): Promise<TransferResult> {
1633
        if (subscriptionId <= 0) {
63!
1634
            return new TransferResult({ statusCode: StatusCodes.BadSubscriptionIdInvalid });
×
1635
        }
1636

1637
        const subscription = this.findSubscription(subscriptionId);
63✔
1638
        if (!subscription) {
63✔
1639
            return new TransferResult({ statusCode: StatusCodes.BadSubscriptionIdInvalid });
43✔
1640
        }
1641

1642
        // check that session have same userIdentity
1643
        if (!sessionsCompatibleForTransfer(subscription.$session, session)) {
20!
1644
            return new TransferResult({ statusCode: StatusCodes.BadUserAccessDenied });
×
1645
        }
1646

1647
        // update diagnostics
1648
        subscription.subscriptionDiagnostics.transferRequestCount++;
20✔
1649

1650
        // now check that new session has sufficient right
1651
        // if (session.authenticationToken.toString() !== subscription.authenticationToken.toString()) {
1652
        //     warningLog("ServerEngine#transferSubscription => BadUserAccessDenied");
1653
        //     return new TransferResult({ statusCode: StatusCodes.BadUserAccessDenied });
1654
        // }
1655
        if ((session.publishEngine as any) === subscription.publishEngine) {
20!
1656
            // subscription is already in this session !!
1657
            return new TransferResult({ statusCode: StatusCodes.BadNothingToDo });
×
1658
        }
1659
        if (session === subscription.$session) {
20!
1660
            // subscription is already in this session !!
1661
            return new TransferResult({ statusCode: StatusCodes.BadNothingToDo });
×
1662
        }
1663

1664
        // The number of times the subscription has been transferred to an alternate client.
1665
        subscription.subscriptionDiagnostics.transferredToAltClientCount++;
20✔
1666
        // The number of times the subscription has been transferred to an alternate session for the same client.
1667
        subscription.subscriptionDiagnostics.transferredToSameClientCount++;
20✔
1668

1669
        const nbSubscriptionBefore = session.publishEngine.subscriptionCount;
20✔
1670

1671
        if (subscription.$session) {
20✔
1672
            subscription.$session._unexposeSubscriptionDiagnostics(subscription);
16✔
1673
        }
1674

1675
        subscription.$session = session;
20✔
1676

1677
        await ServerSidePublishEngine.transferSubscription(subscription, session.publishEngine, sendInitialValues);
20✔
1678

1679
        session._exposeSubscriptionDiagnostics(subscription);
20✔
1680

1681
        assert((subscription.publishEngine as any) === session.publishEngine);
20✔
1682
        // assert(session.publishEngine.subscriptionCount === nbSubscriptionBefore + 1);
1683

1684
        const result = new TransferResult({
20✔
1685
            availableSequenceNumbers: subscription.getAvailableSequenceNumbers(),
1686
            statusCode: StatusCodes.Good
1687
        });
1688

1689
        // istanbul ignore next
1690
        if (doDebug) {
1691
            debugLog("TransferResult", result.toString());
1692
        }
1693

1694
        return result;
20✔
1695
    }
1696

1697
    /**
1698
     * retrieve a session by its authenticationToken.
1699
     *
1700
     * @param authenticationToken
1701
     * @param activeOnly
1702
     * @return {ServerSession}
1703
     */
1704
    public getSession(authenticationToken: NodeId, activeOnly?: boolean): ServerSession | null {
1705
        if (
40,427✔
1706
            !authenticationToken ||
121,281✔
1707
            (authenticationToken.identifierType && authenticationToken.identifierType !== NodeIdType.BYTESTRING)
1708
        ) {
1709
            return null; // wrong type !
1,970✔
1710
        }
1711
        const key = authenticationToken.toString();
38,457✔
1712
        let session = this._sessions[key];
38,457✔
1713
        if (!activeOnly && !session) {
38,457✔
1714
            session = this._closedSessions[key];
73✔
1715
        }
1716
        return session;
38,457✔
1717
    }
1718

1719
    public async translateBrowsePaths(browsePaths: BrowsePath[]): Promise<BrowsePathResult[]> {
1720
        const browsePathResults: BrowsePathResult[] = [];
147✔
1721
        for (const browsePath of browsePaths) {
147✔
1722
            const result = await this.translateBrowsePath(browsePath);
201✔
1723
            browsePathResults.push(result);
201✔
1724
        }
1725
        return browsePathResults;
147✔
1726
    }
1727
    public async translateBrowsePath(browsePath: BrowsePath): Promise<BrowsePathResult> {
1728
        return this.addressSpace!.browsePath(browsePath);
208✔
1729
    }
1730

1731
    /**
1732
     *
1733
     * performs a call to ```asyncRefresh``` on all variable nodes that provide an async refresh func.
1734
     *
1735
     * @param nodesToRefresh {Array<ReadValueId|HistoryReadValueId>}  an array containing the node to consider
1736
     * Each element of the array shall be of the form { nodeId: <xxx>, attributeIds: <value> }.
1737
     * @param maxAge {number}  the maximum age of the value to be read, in milliseconds.
1738
     * @param callback
1739
     *
1740
     */
1741
    public refreshValues(
1742
        nodesToRefresh: ReadValueId[] | HistoryReadValueId[],
1743
        maxAge: number,
1744
        /**
1745
         * @param err
1746
         * @param dataValues an array containing value read
1747
         * The array length matches the number of  nodeIds that are candidate for an
1748
         * async refresh (i.e: nodes that are of type Variable with asyncRefresh func }
1749
         */
1750
        callback: (err: Error | null, dataValues?: DataValue[]) => void
1751
    ): void {
1752
        const referenceTime = getCurrentClock();
19,475✔
1753
        maxAge && referenceTime.timestamp.setTime(referenceTime.timestamp.getTime() - maxAge);
19,475✔
1754

1755
        assert(typeof callback === "function");
19,475✔
1756

1757
        const nodeMap: Record<string, UAVariable> = {};
19,475✔
1758
        for (const nodeToRefresh of nodesToRefresh) {
19,475✔
1759
            // only consider node  for which the caller wants to read the Value attribute
1760
            // assuming that Value is requested if attributeId is missing,
1761
            if (nodeToRefresh instanceof ReadValueId && nodeToRefresh.attributeId !== AttributeIds.Value) {
223,679✔
1762
                continue;
182,142✔
1763
            }
1764
            // ... and that are valid object and instances of Variables ...
1765
            const uaNode = this.addressSpace!.findNode(nodeToRefresh.nodeId);
41,537✔
1766
            if (!uaNode || !(uaNode.nodeClass === NodeClass.Variable)) {
41,537✔
1767
                continue;
529✔
1768
            }
1769
            // ... and that have been declared as asynchronously updating
1770
            if (typeof (uaNode as any).refreshFunc !== "function") {
41,008✔
1771
                continue;
26,266✔
1772
            }
1773
            const key = uaNode.nodeId.toString();
14,742✔
1774
            if (nodeMap[key]) {
14,742✔
1775
                continue;
1✔
1776
            }
1777
            nodeMap[key] = uaNode as UAVariable;
14,741✔
1778
        }
1779

1780
        const uaVariableArray = Object.values(nodeMap);
19,475✔
1781
        if (uaVariableArray.length === 0) {
19,475✔
1782
            // nothing to do
1783
            return callback(null, []);
12,134✔
1784
        }
1785
        // perform all asyncRefresh in parallel
1786
        async.map(
7,341✔
1787
            uaVariableArray,
1788
            (uaVariable: UAVariable, inner_callback: CallbackT<DataValue>) => {
1789
                try {
14,741✔
1790
                    uaVariable.asyncRefresh(referenceTime, (err, dataValue) => {
14,741✔
1791
                        inner_callback(err, dataValue);
14,741✔
1792
                    });
1793
                } catch (err) {
1794
                    const _err = err as Error;
×
1795
                    errorLog("asyncRefresh internal error", _err.message);
×
1796
                    inner_callback(_err);
×
1797
                }
1798
            },
1799
            (err?: Error | null, arrResult?: (DataValue | undefined)[]) => {
1800
                callback(err || null, arrResult as DataValue[]);
7,341✔
1801
            }
1802
        );
1803
    }
1804

1805
    private _exposeSubscriptionDiagnostics(subscription: Subscription): void {
1806
        try {
486✔
1807
            debugLog("ServerEngine#_exposeSubscriptionDiagnostics", subscription.subscriptionId);
486✔
1808
            const subscriptionDiagnosticsArray = this._getServerSubscriptionDiagnosticsArrayNode();
486✔
1809
            const subscriptionDiagnostics = subscription.subscriptionDiagnostics;
486✔
1810
            assert((subscriptionDiagnostics as any).$subscription === subscription);
486✔
1811
            assert(subscriptionDiagnostics instanceof SubscriptionDiagnosticsDataType);
486✔
1812

1813
            if (subscriptionDiagnostics && subscriptionDiagnosticsArray) {
486✔
1814
                addElement(subscriptionDiagnostics, subscriptionDiagnosticsArray);
484✔
1815
            }
1816
        } catch (err) {
1817
            errorLog("_exposeSubscriptionDiagnostics err", err);
×
1818
        }
1819
    }
1820

1821
    protected _unexposeSubscriptionDiagnostics(subscription: Subscription): void {
1822
        const serverSubscriptionDiagnosticsArray = this._getServerSubscriptionDiagnosticsArrayNode();
486✔
1823
        const subscriptionDiagnostics = subscription.subscriptionDiagnostics;
486✔
1824
        assert(subscriptionDiagnostics instanceof SubscriptionDiagnosticsDataType);
486✔
1825
        if (subscriptionDiagnostics && serverSubscriptionDiagnosticsArray) {
486✔
1826
            const node = (serverSubscriptionDiagnosticsArray as any)[subscription.id];
484✔
1827
            removeElement(serverSubscriptionDiagnosticsArray, (a) => a.subscriptionId === subscription.id);
548✔
1828
            /*assert(
1829
                !(subscriptionDiagnosticsArray as any)[subscription.id],
1830
                " subscription node must have been removed from subscriptionDiagnosticsArray"
1831
            );
1832
            */
1833
        }
1834
        debugLog("ServerEngine#_unexposeSubscriptionDiagnostics", subscription.subscriptionId);
486✔
1835
    }
1836

1837
    /**
1838
     * create a new subscription
1839
     * @return {Subscription}
1840
     */
1841
    public _createSubscriptionOnSession(session: ServerSession, request: CreateSubscriptionRequestLike): Subscription {
1842
        assert(Object.prototype.hasOwnProperty.call(request, "requestedPublishingInterval")); // Duration
486✔
1843
        assert(Object.prototype.hasOwnProperty.call(request, "requestedLifetimeCount")); // Counter
486✔
1844
        assert(Object.prototype.hasOwnProperty.call(request, "requestedMaxKeepAliveCount")); // Counter
486✔
1845
        assert(Object.prototype.hasOwnProperty.call(request, "maxNotificationsPerPublish")); // Counter
486✔
1846
        assert(Object.prototype.hasOwnProperty.call(request, "publishingEnabled")); // Boolean
486✔
1847
        assert(Object.prototype.hasOwnProperty.call(request, "priority")); // Byte
486✔
1848

1849
        // adjust publishing parameters
1850
        const publishingInterval = request.requestedPublishingInterval || 0;
486✔
1851
        const maxKeepAliveCount = request.requestedMaxKeepAliveCount || 0;
486✔
1852
        const lifeTimeCount = request.requestedLifetimeCount || 0;
486✔
1853

1854
        const subscription = new Subscription({
486✔
1855
            id: _get_next_subscriptionId(),
1856
            lifeTimeCount,
1857
            maxKeepAliveCount,
1858
            maxNotificationsPerPublish: request.maxNotificationsPerPublish,
1859
            priority: request.priority || 0,
487✔
1860
            publishEngine: session.publishEngine as any, //
1861
            publishingEnabled: request.publishingEnabled,
1862
            publishingInterval,
1863
            // -------------------
1864
            sessionId: NodeId.nullNodeId,
1865
            globalCounter: this._globalCounter,
1866
            serverCapabilities: this.serverCapabilities // shared
1867
        });
1868

1869
        // add subscriptionDiagnostics
1870
        this._exposeSubscriptionDiagnostics(subscription);
486✔
1871

1872
        assert((subscription.publishEngine as any) === session.publishEngine);
486✔
1873
        session.publishEngine.add_subscription(subscription);
486✔
1874

1875
        // eslint-disable-next-line @typescript-eslint/no-this-alias
1876
        const engine = this;
486✔
1877
        subscription.once("terminated", function (this: Subscription) {
486✔
1878
            engine._unexposeSubscriptionDiagnostics(this);
486✔
1879
        });
1880

1881
        return subscription;
486✔
1882
    }
1883

1884
    /**
1885
     */
1886
    private __internal_bindMethod(nodeId: NodeId, func: MethodFunctor) {
1887
        assert(typeof func === "function");
884✔
1888
        assert(nodeId instanceof NodeId);
884✔
1889

1890
        const methodNode = this.addressSpace!.findNode(nodeId)! as UAMethod;
884✔
1891
        if (!methodNode) {
884✔
1892
            return;
193✔
1893
        }
1894
        // istanbul ignore else
1895
        if (methodNode && methodNode.bindMethod) {
691✔
1896
            methodNode.bindMethod(func);
691✔
1897
        } else {
1898
            warningLog(
1899
                chalk.yellow("WARNING:  cannot bind a method with id ") +
1900
                    chalk.cyan(nodeId.toString()) +
1901
                    chalk.yellow(". please check your nodeset.xml file or add this node programmatically")
1902
            );
1903
            warningLog(traceFromThisProjectOnly());
1904
        }
1905
    }
1906

1907
    private _getServerSubscriptionDiagnosticsArrayNode(): UADynamicVariableArray<SubscriptionDiagnosticsDataType> | null {
1908
        // istanbul ignore next
1909
        if (!this.addressSpace) {
1910
            doDebug && debugLog("ServerEngine#_getServerSubscriptionDiagnosticsArray : no addressSpace");
1911

1912
            return null; // no addressSpace
1913
        }
1914
        const subscriptionDiagnosticsType = this.addressSpace.findVariableType("SubscriptionDiagnosticsType");
972✔
1915
        if (!subscriptionDiagnosticsType) {
972✔
1916
            doDebug &&
4!
1917
                debugLog("ServerEngine#_getServerSubscriptionDiagnosticsArray " + ": cannot find SubscriptionDiagnosticsType");
1918
        }
1919

1920
        // SubscriptionDiagnosticsArray = i=2290
1921
        const subscriptionDiagnosticsArrayNode = this.addressSpace.findNode(
972✔
1922
            makeNodeId(VariableIds.Server_ServerDiagnostics_SubscriptionDiagnosticsArray)
1923
        )!;
1924

1925
        return subscriptionDiagnosticsArrayNode as UADynamicVariableArray<SubscriptionDiagnosticsDataType>;
972✔
1926
    }
1927
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc