UNPKG

node-opcua-server

Version:

pure nodejs OPCUA SDK - module server

1,132 lines (967 loc) 88.2 kB
/** * @module node-opcua-server */ import { EventEmitter } from "events"; import { types } from "util"; import async from "async"; import chalk from "chalk"; import { assert } from "node-opcua-assert"; import { BinaryStream } from "node-opcua-binary-stream"; import { addElement, AddressSpace, bindExtObjArrayNode, ensureObjectIsSecure, MethodFunctor, removeElement, SessionContext, UADynamicVariableArray, UAMethod, UAObject, UAServerDiagnosticsSummary, UAServerStatus, UAVariable, UAServerDiagnostics, BindVariableOptions, ISessionContext, DTServerStatus, IServerBase } from "node-opcua-address-space"; import { generateAddressSpace } from "node-opcua-address-space/nodeJS"; import { DataValue } from "node-opcua-data-value"; import { ServerDiagnosticsSummaryDataType, ServerState, ServerStatusDataType, SubscriptionDiagnosticsDataType } from "node-opcua-common"; import { AttributeIds, coerceLocalizedText, LocalizedTextLike, makeAccessLevelFlag, NodeClass } from "node-opcua-data-model"; import { coerceNodeId, makeNodeId, NodeId, NodeIdLike, NodeIdType, resolveNodeId } from "node-opcua-nodeid"; import { BrowseResult } from "node-opcua-service-browse"; import { UInt32 } from "node-opcua-basic-types"; import { CreateSubscriptionRequestLike } from "node-opcua-client"; import { DataTypeIds, MethodIds, ObjectIds, VariableIds } from "node-opcua-constants"; import { getCurrentClock, getMinOPCUADate } from "node-opcua-date-time"; import { checkDebugFlag, make_debugLog, make_errorLog, make_warningLog, traceFromThisProjectOnly } from "node-opcua-debug"; import { nodesets } from "node-opcua-nodesets"; import { ObjectRegistry } from "node-opcua-object-registry"; import { CallMethodResult } from "node-opcua-service-call"; import { TransferResult } from "node-opcua-service-subscription"; import { ApplicationDescription } from "node-opcua-service-endpoints"; import { HistoryReadRequest, HistoryReadResult, HistoryReadValueId } from "node-opcua-service-history"; import { StatusCode, StatusCodes, CallbackT } from "node-opcua-status-code"; import { BrowseDescription, BrowsePath, BrowsePathResult, BuildInfo, BuildInfoOptions, SessionDiagnosticsDataType, SessionSecurityDiagnosticsDataType, WriteValue, ReadValueId, TimeZoneDataType, ProgramDiagnosticDataType, CallMethodResultOptions, ReadRequestOptions, BrowseDescriptionOptions, CallMethodRequest, ApplicationType } from "node-opcua-types"; import { DataType, isValidVariant, Variant, VariantArrayType } from "node-opcua-variant"; import { HistoryServerCapabilities, HistoryServerCapabilitiesOptions } from "./history_server_capabilities"; import { MonitoredItem } from "./monitored_item"; import { ServerCapabilities, ServerCapabilitiesOptions, ServerOperationLimits, defaultServerCapabilities } from "./server_capabilities"; import { ServerSidePublishEngine } from "./server_publish_engine"; import { ServerSidePublishEngineForOrphanSubscription } from "./server_publish_engine_for_orphan_subscriptions"; import { ServerSession } from "./server_session"; import { Subscription } from "./server_subscription"; import { sessionsCompatibleForTransfer } from "./sessions_compatible_for_transfer"; import { OPCUAServerOptions } from "./opcua_server"; import { IAddressSpaceAccessor } from "./i_address_space_accessor"; import { AddressSpaceAccessor } from "./addressSpace_accessor"; const debugLog = make_debugLog(__filename); const errorLog = make_errorLog(__filename); const warningLog = make_warningLog(__filename); const doDebug = checkDebugFlag(__filename); function upperCaseFirst(str: string) { return str.slice(0, 1).toUpperCase() + str.slice(1); } async function shutdownAndDisposeAddressSpace(this: ServerEngine) { if (this.addressSpace) { await this.addressSpace.shutdown(); this.addressSpace.dispose(); delete (this as any).addressSpace; } } function setSubscriptionDurable( this: ServerEngine, inputArguments: Variant[], context: ISessionContext, callback: CallbackT<CallMethodResultOptions> ) { // see https://reference.opcfoundation.org/v104/Core/docs/Part5/9.3/ // https://reference.opcfoundation.org/v104/Core/docs/Part4/6.8/ assert(typeof callback === "function"); const data = _getSubscription.call(this, inputArguments, context); if (data.statusCode) return callback(null, { statusCode: data.statusCode }); const { subscription } = data; const lifetimeInHours = inputArguments[1].value as UInt32; if (subscription.monitoredItemCount > 0) { // This is returned when a Subscription already contains MonitoredItems. return callback(null, { statusCode: StatusCodes.BadInvalidState }); } /** * MonitoredItems are used to monitor Variable Values for data changes and event notifier * Objects for new Events. Subscriptions are used to combine data changes and events of * the assigned MonitoredItems to an optimized stream of network messages. A reliable * delivery is ensured as long as the lifetime of the Subscription and the queues in the * MonitoredItems are long enough for a network interruption between OPC UA Client and * Server. All queues that ensure reliable delivery are normally kept in memory and a * Server restart would delete them. * There are use cases where OPC UA Clients have no permanent network connection to the * OPC UA Server or where reliable delivery of data changes and events is necessary * even if the OPC UA Server is restarted or the network connection is interrupted * for a longer time. * To ensure this reliable delivery, the OPC UA Server must store collected data and * events in non-volatile memory until the OPC UA Client has confirmed reception. * It is possible that there will be data lost if the Server is not shut down gracefully * or in case of power failure. But the OPC UA Server should store the queues frequently * even if the Server is not shut down. * The Method SetSubscriptionDurable defined in OPC 10000-5 is used to set a Subscription * into this durable mode and to allow much longer lifetimes and queue sizes than for normal * Subscriptions. The Method shall be called before the MonitoredItems are created in the * durable Subscription. The Server shall verify that the Method is called within the * Session context of the Session that owns the Subscription. * * A value of 0 for the parameter lifetimeInHours requests the highest lifetime supported by the Server. */ const highestLifetimeInHours = 24 * 100; const revisedLifetimeInHours = lifetimeInHours === 0 ? highestLifetimeInHours : Math.max(1, Math.min(lifetimeInHours, highestLifetimeInHours)); // also adjust subscription life time const currentLifeTimeInHours = (subscription.lifeTimeCount * subscription.publishingInterval) / (1000 * 60 * 60); if (currentLifeTimeInHours < revisedLifetimeInHours) { const requestedLifetimeCount = Math.ceil((revisedLifetimeInHours * (1000 * 60 * 60)) / subscription.publishingInterval); subscription.modify({ requestedMaxKeepAliveCount: subscription.maxKeepAliveCount, requestedPublishingInterval: subscription.publishingInterval, maxNotificationsPerPublish: subscription.maxNotificationsPerPublish, priority: subscription.priority, requestedLifetimeCount }); } const callMethodResult = new CallMethodResult({ statusCode: StatusCodes.Good, outputArguments: [{ dataType: DataType.UInt32, arrayType: VariantArrayType.Scalar, value: revisedLifetimeInHours }] }); callback(null, callMethodResult); } function requestServerStateChange( this: ServerEngine, inputArguments: Variant[], context: ISessionContext, callback: CallbackT<CallMethodResultOptions> ) { assert(Array.isArray(inputArguments)); assert(typeof callback === "function"); assert(Object.prototype.hasOwnProperty.call(context, "session"), " expecting a session id in the context object"); const session = context.session as ServerSession; if (!session) { return callback(null, { statusCode: StatusCodes.BadInternalError }); } return callback(null, { statusCode: StatusCodes.BadNotImplemented }); } function _getSubscription( this: ServerEngine, inputArguments: Variant[], context: ISessionContext ): { subscription: Subscription; statusCode?: never } | { statusCode: StatusCode; subscription?: never } { assert(Array.isArray(inputArguments)); assert(Object.prototype.hasOwnProperty.call(context, "session"), " expecting a session id in the context object"); const session = context.session as ServerSession; if (!session) { return { statusCode: StatusCodes.BadInternalError }; } const subscriptionId = inputArguments[0].value; const subscription = session.getSubscription(subscriptionId); if (!subscription) { // subscription may belongs to a different session that ours if (this.findSubscription(subscriptionId)) { // if yes, then access to Subscription data should be denied return { statusCode: StatusCodes.BadUserAccessDenied }; } return { statusCode: StatusCodes.BadSubscriptionIdInvalid }; } return { subscription }; } function resendData( this: ServerEngine, inputArguments: Variant[], context: ISessionContext, callback: CallbackT<CallMethodResultOptions> ): void { assert(typeof callback === "function"); const data = _getSubscription.call(this, inputArguments, context); if (data.statusCode) return callback(null, { statusCode: data.statusCode }); const { subscription } = data; subscription .resendInitialValues() .then(() => { callback(null, { statusCode: StatusCodes.Good }); }) .catch((err) => callback(err)); } // binding methods function getMonitoredItemsId( this: ServerEngine, inputArguments: Variant[], context: ISessionContext, callback: CallbackT<CallMethodResultOptions> ) { assert(typeof callback === "function"); const data = _getSubscription.call(this, inputArguments, context); if (data.statusCode) return callback(null, { statusCode: data.statusCode }); const { subscription } = data; const result = subscription.getMonitoredItems(); assert(result.statusCode); assert(result.serverHandles.length === result.clientHandles.length); const callMethodResult = new CallMethodResult({ statusCode: result.statusCode, outputArguments: [ { dataType: DataType.UInt32, arrayType: VariantArrayType.Array, value: result.serverHandles }, { dataType: DataType.UInt32, arrayType: VariantArrayType.Array, value: result.clientHandles } ] }); callback(null, callMethodResult); } function __bindVariable(self: ServerEngine, nodeId: NodeIdLike, options?: BindVariableOptions) { options = options || {}; const variable = self.addressSpace!.findNode(nodeId) as UAVariable; if (variable && variable.bindVariable) { variable.bindVariable(options, true); assert(typeof variable.asyncRefresh === "function"); assert(typeof (variable as any).refreshFunc === "function"); } else { warningLog( "Warning: cannot bind object with id ", nodeId.toString(), " please check your nodeset.xml file or add this node programmatically" ); } } // note OPCUA 1.03 part 4 page 76 // The Server-assigned identifier for the Subscription (see 7.14 for IntegerId definition). This identifier shall // be unique for the entire Server, not just for the Session, in order to allow the Subscription to be transferred // to another Session using the TransferSubscriptions service. // After Server start-up the generation of subscriptionIds should start from a random IntegerId or continue from // the point before the restart. let next_subscriptionId = Math.ceil(Math.random() * 1000000); export function setNextSubscriptionId(n: number) { next_subscriptionId = Math.max(n, 1); } function _get_next_subscriptionId() { debugLog(" next_subscriptionId = ", next_subscriptionId); return next_subscriptionId++; } export type StringGetter = () => string; export type StringArrayGetter = () => string[]; export type ApplicationTypeGetter = () => ApplicationType; export type BooleanGetter = () => boolean; export interface ServerConfigurationOptions { applicationUri?: string | StringGetter; applicationType?: ApplicationType | ApplicationTypeGetter; // default "Server" hasSecureElement?: boolean | BooleanGetter; // default true multicastDnsEnabled?: boolean | BooleanGetter; // default true productUri?: string | StringGetter; // /** @restricted only in professional version */ // resetToServerDefaults: () => Promise<void>; // /** @restricted only in professional version */ // setAdminPassword?: (password: string) => Promise<void>; /** * The SupportedPrivateKeyFormats specifies the PrivateKey formats supported by the Server. * Possible values include “PEM” (see RFC 5958) or “PFX” (see PKCS #12). * @default ["PEM"] */ supportedPrivateKeyFormat: string[] | StringArrayGetter; /** * The ServerCapabilities Property specifies the capabilities from Annex D * ( see https://reference.opcfoundation.org/GDS/v104/docs/D) which the Server supports. The value is * the same as the value reported to the LocalDiscoveryServer when the Server calls the RegisterServer2 Service. */ serverCapabilities?: string[] | StringArrayGetter; // default|"N/A"] } export interface ServerEngineOptions { applicationUri: string | StringGetter; buildInfo?: BuildInfoOptions; isAuditing?: boolean; /** * set to true to enable serverDiagnostics */ serverDiagnosticsEnabled?: boolean; serverCapabilities?: ServerCapabilitiesOptions; historyServerCapabilities?: HistoryServerCapabilitiesOptions; serverConfiguration?: ServerConfigurationOptions; } export interface CreateSessionOption { clientDescription?: ApplicationDescription; sessionTimeout?: number; server?: IServerBase; } export type ClosingReason = "Timeout" | "Terminated" | "CloseSession" | "Forcing"; export type ServerEngineShutdownTask = (this: ServerEngine) => void | Promise<void>; /** * */ export class ServerEngine extends EventEmitter implements IAddressSpaceAccessor { public static readonly registry = new ObjectRegistry(); public isAuditing: boolean; public serverDiagnosticsSummary: ServerDiagnosticsSummaryDataType; public serverDiagnosticsEnabled: boolean; public serverCapabilities: ServerCapabilities; public historyServerCapabilities: HistoryServerCapabilities; public serverConfiguration: ServerConfigurationOptions; public clientDescription?: ApplicationDescription; public addressSpace: AddressSpace | null; public addressSpaceAccessor: IAddressSpaceAccessor | null = null; // pseudo private public _internalState: "creating" | "initializing" | "initialized" | "shutdown" | "disposed"; private _sessions: { [key: string]: ServerSession }; private _closedSessions: { [key: string]: ServerSession }; private _orphanPublishEngine?: ServerSidePublishEngineForOrphanSubscription; private _shutdownTasks: ServerEngineShutdownTask[]; private _applicationUri: string; private _expectedShutdownTime!: Date; private _serverStatus: ServerStatusDataType; private _globalCounter: { totalMonitoredItemCount: number } = { totalMonitoredItemCount: 0 }; constructor(options?: ServerEngineOptions) { super(); options = options || ({ applicationUri: "" } as ServerEngineOptions); options.buildInfo = options.buildInfo || {}; ServerEngine.registry.register(this); this._sessions = {}; this._closedSessions = {}; this._orphanPublishEngine = undefined; // will be constructed on demand this.isAuditing = typeof options.isAuditing === "boolean" ? options.isAuditing : false; options.buildInfo.buildDate = options.buildInfo.buildDate || new Date(); // ---------------------------------------------------- ServerStatusDataType this._serverStatus = new ServerStatusDataType({ buildInfo: options.buildInfo, currentTime: new Date(), secondsTillShutdown: 0, shutdownReason: { text: "" }, startTime: new Date(), state: ServerState.NoConfiguration }); // --------------------------------------------------- ServerCapabilities options.serverCapabilities = options.serverCapabilities || {}; options.serverConfiguration = options.serverConfiguration || { supportedPrivateKeyFormat: ["PEM"] }; // https://profiles.opcfoundation.org/profile options.serverCapabilities.serverProfileArray = options.serverCapabilities.serverProfileArray || [ "http://opcfoundation.org/UA-Profile/Server/Standard", // Standard UA Server Profile", "http://opcfoundation.org/UA-Profile/Server/DataAccess", "http://opcfoundation.org/UA-Profile/Server/Events", "http://opcfoundation.org/UA-Profile/Client/HistoricalAccess", "http://opcfoundation.org/UA-Profile/Server/Methods", "http://opcfoundation.org/UA-Profile/Server/StandardEventSubscription", "http://opcfoundation.org/UA-Profile/Transport/uatcp-uasc-uabinary", "http://opcfoundation.org/UA-Profile/Server/FileAccess", "http://opcfoundation.org/UA-Profile/Server/StateMachine" // "http://opcfoundation.org/UA-Profile/Transport/wss-uajson", // "http://opcfoundation.org/UA-Profile/Transport/wss-uasc-uabinary" // "http://opcfoundation.org/UA-Profile/Server/DurableSubscription" // "http://opcfoundation.org/UA-Profile/Server/ReverseConnect", // "http://opcfoundation.org/UAProfile/Server/NodeManagement", // "Embedded UA Server Profile", // "Micro Embedded Device Server Profile", // "Nano Embedded Device Server Profile" ]; options.serverCapabilities.localeIdArray = options.serverCapabilities.localeIdArray || ["en-EN", "fr-FR"]; this.serverCapabilities = new ServerCapabilities(options.serverCapabilities); // to do when spec is clear about what goes here! // spec 1.04 says (in Part 4 7.33 SignedSoftwareCertificate // Note: Details on SoftwareCertificates need to be defined in a future version. this.serverCapabilities.softwareCertificates = [ // new SignedSoftwareCertificate({}) ]; // make sure minSupportedSampleRate matches MonitoredItem.minimumSamplingInterval (this.serverCapabilities as any).__defineGetter__("minSupportedSampleRate", () => { return options!.serverCapabilities?.minSupportedSampleRate || MonitoredItem.minimumSamplingInterval; }); this.serverConfiguration = options.serverConfiguration; this.historyServerCapabilities = new HistoryServerCapabilities(options.historyServerCapabilities); // --------------------------------------------------- serverDiagnosticsSummary extension Object this.serverDiagnosticsSummary = new ServerDiagnosticsSummaryDataType(); assert(Object.prototype.hasOwnProperty.call(this.serverDiagnosticsSummary, "currentSessionCount")); // note spelling is different for serverDiagnosticsSummary.currentSubscriptionCount // and sessionDiagnostics.currentSubscriptionsCount ( with an s) assert(Object.prototype.hasOwnProperty.call(this.serverDiagnosticsSummary, "currentSubscriptionCount")); (this.serverDiagnosticsSummary as any).__defineGetter__("currentSubscriptionCount", () => { // currentSubscriptionCount returns the total number of subscriptions // that are currently active on all sessions let counter = 0; Object.values(this._sessions).forEach((session: ServerSession) => { counter += session.currentSubscriptionCount; }); // we also need to add the orphan subscriptions counter += this._orphanPublishEngine ? this._orphanPublishEngine.subscriptions.length : 0; return counter; }); this._internalState = "creating"; this.setServerState(ServerState.NoConfiguration); this.addressSpace = null; this._shutdownTasks = []; this._applicationUri = ""; if (typeof options.applicationUri === "function") { (this as any).__defineGetter__("_applicationUri", options.applicationUri); } else { this._applicationUri = options.applicationUri || "<unset _applicationUri>"; } options.serverDiagnosticsEnabled = Object.prototype.hasOwnProperty.call(options, "serverDiagnosticsEnable") ? options.serverDiagnosticsEnabled : true; this.serverDiagnosticsEnabled = options.serverDiagnosticsEnabled!; } public isStarted(): boolean { return !!this._serverStatus!; } public dispose(): void { this.addressSpace = null; assert(Object.keys(this._sessions).length === 0, "ServerEngine#_sessions not empty"); this._sessions = {}; // todo fix me this._closedSessions = {}; assert(Object.keys(this._closedSessions).length === 0, "ServerEngine#_closedSessions not empty"); this._closedSessions = {}; if (this._orphanPublishEngine) { this._orphanPublishEngine.dispose(); this._orphanPublishEngine = undefined; } this._shutdownTasks = []; this._serverStatus = null as any as ServerStatusDataType; this._internalState = "disposed"; this.removeAllListeners(); ServerEngine.registry.unregister(this); } public get startTime(): Date { return this._serverStatus.startTime!; } public get currentTime(): Date { return this._serverStatus.currentTime!; } public get buildInfo(): BuildInfo { return this._serverStatus.buildInfo; } /** * register a function that will be called when the server will perform its shut down. */ public registerShutdownTask(task: ServerEngineShutdownTask): void { assert(typeof task === "function"); this._shutdownTasks.push(task); } /** */ public async shutdown(): Promise<void> { debugLog("ServerEngine#shutdown"); this._internalState = "shutdown"; this.setServerState(ServerState.Shutdown); // delete any existing sessions const tokens = Object.keys(this._sessions).map((key: string) => { const session = this._sessions[key]; return session.authenticationToken; }); // delete and close any orphan subscriptions if (this._orphanPublishEngine) { this._orphanPublishEngine.shutdown(); } for (const token of tokens) { this.closeSession(token, true, "Terminated"); } // all sessions must have been terminated assert(this.currentSessionCount === 0); // all subscriptions must have been terminated assert(this.currentSubscriptionCount === 0, "all subscriptions must have been terminated"); this._shutdownTasks.push(shutdownAndDisposeAddressSpace); // perform registerShutdownTask for (const task of this._shutdownTasks) { await task.call(this); } this.setServerState(ServerState.Invalid); this.dispose(); } /** * the number of active sessions */ public get currentSessionCount(): number { return this.serverDiagnosticsSummary.currentSessionCount; } /** * the cumulated number of sessions that have been opened since this object exists */ public get cumulatedSessionCount(): number { return this.serverDiagnosticsSummary.cumulatedSessionCount; } /** * the number of active subscriptions. */ public get currentSubscriptionCount(): number { return this.serverDiagnosticsSummary.currentSubscriptionCount; } /** * the cumulated number of subscriptions that have been created since this object exists */ public get cumulatedSubscriptionCount(): number { return this.serverDiagnosticsSummary.cumulatedSubscriptionCount; } public get rejectedSessionCount(): number { return this.serverDiagnosticsSummary.rejectedSessionCount; } public get rejectedRequestsCount(): number { return this.serverDiagnosticsSummary.rejectedRequestsCount; } public get sessionAbortCount(): number { return this.serverDiagnosticsSummary.sessionAbortCount; } public get sessionTimeoutCount(): number { return this.serverDiagnosticsSummary.sessionTimeoutCount; } public get publishingIntervalCount(): number { return this.serverDiagnosticsSummary.publishingIntervalCount; } public incrementSessionTimeoutCount(): void { if (this.serverDiagnosticsSummary && this.serverDiagnosticsEnabled) { // The requests include all Services defined in Part 4 of the OPC UA Specification, also requests to create sessions. This number includes the securityRejectedRequestsCount. this.serverDiagnosticsSummary.sessionTimeoutCount += 1; } } public incrementSessionAbortCount(): void { if (this.serverDiagnosticsSummary && this.serverDiagnosticsEnabled) { // The requests include all Services defined in Part 4 of the OPC UA Specification, also requests to create sessions. This number includes the securityRejectedRequestsCount. this.serverDiagnosticsSummary.sessionAbortCount += 1; } } public incrementRejectedRequestsCount(): void { if (this.serverDiagnosticsSummary && this.serverDiagnosticsEnabled) { // The requests include all Services defined in Part 4 of the OPC UA Specification, also requests to create sessions. This number includes the securityRejectedRequestsCount. this.serverDiagnosticsSummary.rejectedRequestsCount += 1; } } /** * increment rejected session count (also increment rejected requests count) */ public incrementRejectedSessionCount(): void { if (this.serverDiagnosticsSummary && this.serverDiagnosticsEnabled) { // The requests include all Services defined in Part 4 of the OPC UA Specification, also requests to create sessions. This number includes the securityRejectedRequestsCount. this.serverDiagnosticsSummary.rejectedSessionCount += 1; } this.incrementRejectedRequestsCount(); } public incrementSecurityRejectedRequestsCount(): void { if (this.serverDiagnosticsSummary && this.serverDiagnosticsEnabled) { // The requests include all Services defined in Part 4 of the OPC UA Specification, also requests to create sessions. This number includes the securityRejectedRequestsCount. this.serverDiagnosticsSummary.securityRejectedRequestsCount += 1; } this.incrementRejectedRequestsCount(); } /** * increment rejected session count (also increment rejected requests count) */ public incrementSecurityRejectedSessionCount(): void { if (this.serverDiagnosticsSummary && this.serverDiagnosticsEnabled) { // The requests include all Services defined in Part 4 of the OPC UA Specification, also requests to create sessions. This number includes the securityRejectedRequestsCount. this.serverDiagnosticsSummary.securityRejectedSessionCount += 1; } this.incrementSecurityRejectedRequestsCount(); } public setShutdownTime(date: Date): void { this._expectedShutdownTime = date; } public setShutdownReason(reason: LocalizedTextLike): void { this.addressSpace?.rootFolder.objects.server.serverStatus.shutdownReason.setValueFromSource({ dataType: DataType.LocalizedText, value: coerceLocalizedText(reason)! }); } /** * @return the approximate number of seconds until the server will be shut down. The * value is only relevant once the state changes into SHUTDOWN. */ public secondsTillShutdown(): number { if (!this._expectedShutdownTime) { return 0; } // ToDo: implement a correct solution here const now = Date.now(); return Math.max(0, Math.ceil((this._expectedShutdownTime.getTime() - now) / 1000)); } /** * the name of the server */ public get serverName(): string { return this._serverStatus.buildInfo!.productName!; } /** * the server urn */ public get serverNameUrn(): string { return this._applicationUri; } /** * the urn of the server namespace */ public get serverNamespaceUrn(): string { return this._applicationUri; // "urn:" + engine.serverName; } public get serverStatus(): ServerStatusDataType { return this._serverStatus; } public setServerState(serverState: ServerState): void { assert(serverState !== null && serverState !== undefined); this.addressSpace?.rootFolder?.objects?.server?.serverStatus?.state?.setValueFromSource({ dataType: DataType.Int32, value: serverState }); } public getServerDiagnosticsEnabledFlag(): boolean { const server = this.addressSpace!.rootFolder.objects.server; const serverDiagnostics = server.getComponentByName("ServerDiagnostics") as UAVariable; if (!serverDiagnostics) { return false; } return serverDiagnostics.readValue().value.value; } /** * */ public initialize(options: OPCUAServerOptions, callback: (err?: Error | null) => void): void { assert(!this.addressSpace); // check that 'initialize' has not been already called this._internalState = "initializing"; options = options || {}; assert(typeof callback === "function"); options.nodeset_filename = options.nodeset_filename || nodesets.standard; const startTime = new Date(); debugLog("Loading ", options.nodeset_filename, "..."); this.addressSpace = AddressSpace.create(); this.addressSpaceAccessor = new AddressSpaceAccessor(this.addressSpace); if (!options.skipOwnNamespace) { // register namespace 1 (our namespace); const serverNamespace = this.addressSpace.registerNamespace(this.serverNamespaceUrn); assert(serverNamespace.index === 1); } // eslint-disable-next-line max-statements generateAddressSpace(this.addressSpace, options.nodeset_filename) .catch((err) => { console.log(err.message); callback(err); }) .then(() => { /* istanbul ignore next */ if (!this.addressSpace) { throw new Error("Internal error"); } const addressSpace = this.addressSpace; const endTime = new Date(); debugLog("Loading ", options.nodeset_filename, " done : ", endTime.getTime() - startTime.getTime(), " ms"); const bindVariableIfPresent = (nodeId: NodeId, opts: any) => { assert(!nodeId.isEmpty()); const obj = addressSpace.findNode(nodeId); if (obj) { __bindVariable(this, nodeId, opts); } return obj; }; // -------------------------------------------- install default get/put handler const server_NamespaceArray_Id = makeNodeId(VariableIds.Server_NamespaceArray); // ns=0;i=2255 bindVariableIfPresent(server_NamespaceArray_Id, { get() { return new Variant({ arrayType: VariantArrayType.Array, dataType: DataType.String, value: addressSpace.getNamespaceArray().map((x) => x.namespaceUri) }); }, set: null // read only }); const server_NameUrn_var = new Variant({ arrayType: VariantArrayType.Array, dataType: DataType.String, value: [ this.serverNameUrn // this is us ! ] }); const server_ServerArray_Id = makeNodeId(VariableIds.Server_ServerArray); // ns=0;i=2254 bindVariableIfPresent(server_ServerArray_Id, { get() { return server_NameUrn_var; }, set: null // read only }); // fix DefaultUserRolePermissions and DefaultUserRolePermissions // of namespaces const namespaces = makeNodeId(ObjectIds.Server_Namespaces); const namespacesNode = addressSpace.findNode(namespaces) as UAObject; if (namespacesNode) { for (const ns of namespacesNode.getComponents()) { const defaultUserRolePermissions = ns.getChildByName("DefaultUserRolePermissions") as UAVariable | null; if (defaultUserRolePermissions) { defaultUserRolePermissions.setValueFromSource({ dataType: DataType.Null }); } const defaultRolePermissions = ns.getChildByName("DefaultRolePermissions") as UAVariable | null; if (defaultRolePermissions) { defaultRolePermissions.setValueFromSource({ dataType: DataType.Null }); } } } const bindStandardScalar = ( id: number, dataType: DataType, func: () => any, setter_func?: (value: any) => void ) => { assert(typeof id === "number", "expecting id to be a number"); assert(typeof func === "function"); assert(typeof setter_func === "function" || !setter_func); assert(dataType !== null); // check invalid dataType let setter_func2 = null; if (setter_func) { setter_func2 = (variant: Variant) => { const variable2 = !!variant.value; setter_func(variable2); return StatusCodes.Good; }; } const nodeId = makeNodeId(id); // make sur the provided function returns a valid value for the variant type // This test may not be exhaustive but it will detect obvious mistakes. /* istanbul ignore next */ if (!isValidVariant(VariantArrayType.Scalar, dataType, func())) { errorLog("func", func()); throw new Error("bindStandardScalar : func doesn't provide an value of type " + DataType[dataType]); } return bindVariableIfPresent(nodeId, { get() { return new Variant({ arrayType: VariantArrayType.Scalar, dataType, value: func() }); }, set: setter_func2 }); }; const bindStandardArray = (id: number, variantDataType: DataType, dataType: any, func: () => any[]) => { assert(typeof func === "function"); assert(variantDataType !== null); // check invalid dataType const nodeId = makeNodeId(id); // make sur the provided function returns a valid value for the variant type // This test may not be exhaustive but it will detect obvious mistakes. assert(isValidVariant(VariantArrayType.Array, variantDataType, func())); bindVariableIfPresent(nodeId, { get() { const value = func(); assert(Array.isArray(value)); return new Variant({ arrayType: VariantArrayType.Array, dataType: variantDataType, value }); }, set: null // read only }); }; bindStandardScalar(VariableIds.Server_EstimatedReturnTime, DataType.DateTime, () => getMinOPCUADate()); // TimeZoneDataType const timeZoneDataType = addressSpace.findDataType(resolveNodeId(DataTypeIds.TimeZoneDataType))!; const timeZone = new TimeZoneDataType({ daylightSavingInOffset: /* boolean*/ false, offset: /* int16 */ 0 }); bindStandardScalar(VariableIds.Server_LocalTime, DataType.ExtensionObject, () => { return timeZone; }); bindStandardScalar(VariableIds.Server_ServiceLevel, DataType.Byte, () => { return 255; }); bindStandardScalar(VariableIds.Server_Auditing, DataType.Boolean, () => { return this.isAuditing; }); // eslint-disable-next-line @typescript-eslint/no-this-alias const engine = this; const makeNotReadableIfEnabledFlagIsFalse = (variable: UAVariable) => { const originalIsReadable = variable.isReadable; variable.isUserReadable = checkReadableFlag; function checkReadableFlag(this: UAVariable, context: SessionContext): boolean { const isEnabled = engine.serverDiagnosticsEnabled; return originalIsReadable.call(this, context) && isEnabled; } for (const c of variable.getAggregates()) { if (c.nodeClass === NodeClass.Variable) { makeNotReadableIfEnabledFlagIsFalse(c as UAVariable); } } }; const bindServerDiagnostics = () => { bindStandardScalar( VariableIds.Server_ServerDiagnostics_EnabledFlag, DataType.Boolean, () => { return this.serverDiagnosticsEnabled; }, (newFlag: boolean) => { this.serverDiagnosticsEnabled = newFlag; } ); const nodeId = makeNodeId(VariableIds.Server_ServerDiagnostics_ServerDiagnosticsSummary); const serverDiagnosticsSummaryNode = addressSpace.findNode( nodeId ) as UAServerDiagnosticsSummary<ServerDiagnosticsSummaryDataType>; if (serverDiagnosticsSummaryNode) { serverDiagnosticsSummaryNode.bindExtensionObject(this.serverDiagnosticsSummary); this.serverDiagnosticsSummary = serverDiagnosticsSummaryNode.$extensionObject; makeNotReadableIfEnabledFlagIsFalse(serverDiagnosticsSummaryNode); } }; const bindServerStatus = () => { const serverStatusNode = addressSpace.findNode( makeNodeId(VariableIds.Server_ServerStatus) ) as UAServerStatus<DTServerStatus>; if (!serverStatusNode) { return; } if (serverStatusNode) { serverStatusNode.bindExtensionObject(this._serverStatus); serverStatusNode.minimumSamplingInterval = 1000; } const currentTimeNode = addressSpace.findNode( makeNodeId(VariableIds.Server_ServerStatus_CurrentTime) ) as UAVariable; if (currentTimeNode) { currentTimeNode.minimumSamplingInterval = 1000; } const secondsTillShutdown = addressSpace.findNode( makeNodeId(VariableIds.Server_ServerStatus_SecondsTillShutdown) ) as UAVariable; if (secondsTillShutdown) { secondsTillShutdown.minimumSamplingInterval = 1000; } assert(serverStatusNode.$extensionObject); serverStatusNode.$extensionObject = new Proxy(serverStatusNode.$extensionObject, { get(target, prop) { if (prop === "currentTime") { serverStatusNode.currentTime.touchValue(); return new Date(); } else if (prop === "secondsTillShutdown") { serverStatusNode.secondsTillShutdown.touchValue(); return engine.secondsTillShutdown(); } return (target as any)[prop]; } }); this._serverStatus = serverStatusNode.$extensionObject; }; const bindServerCapabilities = () => { bindStandardArray( VariableIds.Server_ServerCapabilities_ServerProfileArray, DataType.String, DataType.String, () => { return this.serverCapabilities.serverProfileArray; } ); bindStandardArray(VariableIds.Server_ServerCapabilities_LocaleIdArray, DataType.String, "LocaleId", () => { return this.serverCapabilities.localeIdArray; }); bindStandardScalar(VariableIds.Server_ServerCapabilities_MinSupportedSampleRate, DataType.Double, () => { return Math.max( this.serverCapabilities.minSupportedSampleRate, defaultServerCapabilities.minSupportedSampleRate ); }); bindStandardScalar(VariableIds.Server_ServerCapabilities_MaxBrowseContinuationPoints, DataType.UInt16, () => { return this.serverCapabilities.maxBrowseContinuationPoints; }); bindStandardScalar(VariableIds.Server_ServerCapabilities_MaxQueryContinuationPoints, DataType.UInt16, () => { return this.serverCapabilities.maxQueryContinuationPoints; }); bindStandardScalar(VariableIds.Server_ServerCapabilities_MaxHistoryContinuationPoints, DataType.UInt16, () => { return this.serverCapabilities.maxHistoryContinuationPoints; }); // new in 1.05 bindStandardScalar(VariableIds.Server_ServerCapabilities_MaxSessions, DataType.UInt32, () => { return this.serverCapabilities.maxSessions; }); bindStandardScalar(VariableIds.Server_ServerCapabilities_MaxSubscriptions, DataType.UInt32, () => { return this.serverCapabilities.maxSubscriptions; }); bindStandardScalar(VariableIds.Server_ServerCapabilities_MaxMonitoredItems, DataType.UInt32, () => { return this.serverCapabilities.maxMonitoredItems; }); bindStandardScalar(VariableIds.Server_ServerCapabilities_MaxSubscriptionsPerSession, DataType.UInt32, () => { return this.serverCapabilities.maxSubscriptionsPerSession; }); bindStandardScalar(VariableIds.Server_ServerCapabilities_MaxSelectClauseParameters, DataType.UInt32, () => { return this.serverCapabilities.maxSelectClauseParameters; }); bindStandardScalar(VariableIds.Server_ServerCapabilities_MaxWhereClauseParameters, DataType.UInt32, () => { return this.serverCapabilities.maxWhereClauseParameters; }); //bindStandardArray(VariableIds.Server_ServerCapabilities_ConformanceUnits, DataType.QualifiedName, () => { // return this.serverCapabilities.conformanceUnits; //}); bindStandardScalar( VariableIds.Server_ServerCapabilities_MaxMonitoredItemsPerSubscription, DataType.UInt32, () => { return this.serverCapabilities.maxMonitoredItemsPerSubscription; } ); // added by DI : Server-specific period of time in milliseconds until the Server will revoke a lock. // TODO bindStandardScalar(VariableIds.Server_ServerCapabilities_MaxInactiveLockTime, // TODO DataType.UInt16, function () { // TODO return self.serverCapabilities.maxInactiveLockTime; // TODO }); bindStandardArray( VariableIds.Server_ServerCapabilities_SoftwareCertificates, DataType.ExtensionObject, "SoftwareCertificates", () => { return this.serverCapabilities.softwareCertificates; } ); bindStandardScalar(VariableIds.Server_ServerCapabilities_MaxArrayLength, DataType.UInt32, () => { return Math.min(this.serverCapabilities.maxArrayLength, Variant.maxArrayLength); }); bindStandardScalar(VariableIds.Server_ServerCapabilities_MaxStringLength, DataType.UInt32, () => { return Math.min(this.serverCapabilities.maxStringLength, BinaryStream.maxStringLength); }); bindStandardScalar(VariableIds.Server_ServerCapabilities_MaxByteStringLength, DataType.UInt32, () => { return Math.min(this.serverCapabilities.maxByteStringLength, BinaryStream.maxByteStringLength); }); bindStandardScalar(VariableIds.Server_ServerCapabilities_MaxMonitoredItemsQueueSize, DataType.UInt32, () => { return Math.max(1, this.serverCapabilities.maxMonitoredItemsQueueSize); }); const bindOperationLimits = (operationLimits: ServerOperationLimits) => { assert(operationLimits !== null && typeof operationLimits === "object"); const keys = Object.keys(operationLimits); keys.forEach((key: string) => { const uid = "Server_ServerCapabilities_OperationLimits_" + upperCaseFirst(key); const nodeId = makeNodeId((VariableIds as any)[uid]); assert(!nodeId.isEmpty()); bindStandardScalar((VariableIds as any)[uid], DataType.UInt32, () => { return (operationLimits as any)[key]; }); }); }; bindOperationLimits(this.serverCapabilities.operationLimits); // i=2399 [ProgramStateMachineType_ProgramDiagnostics]; function fix_ProgramStateMachineType_ProgramDiagnostics() { const nodeId = coerceNodeId("i=2399"); // ProgramStateMachineType_ProgramDiagnostics const variable = addressSpace.findNode(nodeId) as UAVariable; if (variable) { (variable as any).$extensionObject = new ProgramDiagnosticDataType({}); // variable.setValueFromSource({ // dataType: DataType.ExtensionObject, // // value: new ProgramDiagnostic2DataType() // value: new ProgramDiagnosticDataType({}