/** * @module node-opcua-client-private */ import chalk from "chalk"; import { EventEmitter } from "events"; import { assert } from "node-opcua-assert"; import type { DateTime } from "node-opcua-basic-types"; import { type ExtraDataTypeManager, extractDataValueToPromote, getExtensionObjectConstructor, getExtraDataTypeManager, type PseudoDataValue, promoteOpaqueStructure } from "node-opcua-client-dynamic-extension-object"; import type { AggregateFunction } from "node-opcua-constants"; import type { Certificate, Nonce } from "node-opcua-crypto/web"; import { BrowseDirection, type LocalizedTextLike } from "node-opcua-data-model"; import { DataValue } from "node-opcua-data-value"; import { checkDebugFlag, make_debugLog, make_errorLog, make_warningLog } from "node-opcua-debug"; import type { ExtensionObject } from "node-opcua-extension-object"; import { coerceNodeId, NodeId, type NodeIdLike, resolveNodeId } from "node-opcua-nodeid"; import { type BrowseDescriptionLike, getArgumentDefinitionHelper, getBuiltInDataType, type IBasicSessionAsync2, type IBasicTransportSettings, type NodeAttributes, type ResponseCallback, readAllAttributes, readNamespaceArray } from "node-opcua-pseudo-session"; import type { AnyConstructorFunc } from "node-opcua-schemas"; import { ClientSecureChannelLayer, requestHandleNotSetValue, type SignatureData } from "node-opcua-secure-channel"; import { BrowseDescription, BrowseRequest, BrowseResponse, BrowseResult } from "node-opcua-service-browse"; import { CallMethodRequest, type CallMethodResult, CallRequest, CallResponse } from "node-opcua-service-call"; import type { EndpointDescription } from "node-opcua-service-endpoints"; import { HistoryData, HistoryReadRequest, HistoryReadResponse, type HistoryReadResult, ReadProcessedDetails, ReadRawModifiedDetails } from "node-opcua-service-history"; import { QueryFirstRequest, QueryFirstResponse } from "node-opcua-service-query"; import { AttributeIds, ReadRequest, ReadResponse, ReadValueId, type ReadValueIdOptions, TimestampsToReturn } from "node-opcua-service-read"; import { RegisterNodesRequest, RegisterNodesResponse, UnregisterNodesRequest, UnregisterNodesResponse } from "node-opcua-service-register-node"; import { CreateMonitoredItemsRequest, CreateMonitoredItemsResponse, CreateSubscriptionRequest, CreateSubscriptionResponse, DeleteMonitoredItemsRequest, DeleteMonitoredItemsResponse, DeleteSubscriptionsRequest, DeleteSubscriptionsResponse, ModifyMonitoredItemsRequest, ModifyMonitoredItemsResponse, ModifySubscriptionRequest, ModifySubscriptionResponse, PublishRequest, PublishResponse, RepublishRequest, RepublishResponse, SetMonitoringModeRequest, SetMonitoringModeResponse, SetPublishingModeRequest, SetPublishingModeResponse, SetTriggeringRequest, type SetTriggeringRequestOptions, SetTriggeringResponse, TransferSubscriptionsRequest, TransferSubscriptionsResponse } from "node-opcua-service-subscription"; import { type BrowsePath, type BrowsePathResult, TranslateBrowsePathsToNodeIdsRequest, TranslateBrowsePathsToNodeIdsResponse } from "node-opcua-service-translate-browse-path"; import { WriteRequest, WriteResponse, WriteValue } from "node-opcua-service-write"; import { type Callback, type CallbackT, type ErrorCallback, type StatusCode, StatusCodes } from "node-opcua-status-code"; import { ActivateSessionRequest, type AggregateConfigurationOptions, BrowseNextRequest, BrowseNextResponse, CloseSessionRequest, type HistoryReadValueIdOptions, UserTokenType, type WriteValueOptions } from "node-opcua-types"; import { buffer_ellipsis, getFunctionParameterNames, isNullOrUndefined } from "node-opcua-utils"; import { DataType, type Variant, type VariantLike } from "node-opcua-variant"; import type { ArgumentDefinition, CallMethodRequestLike, ClientSession, CreateMonitoredItemsRequestLike, CreateSubscriptionRequestLike, DeleteMonitoredItemsRequestLike, DeleteSubscriptionsRequestLike, ExtraReadHistoryValueParameters, HistoryReadValueIdOptions2, MethodId, ModifyMonitoredItemsRequestLike, ModifySubscriptionRequestLike, MonitoredItemData, QueryFirstRequestLike, SetMonitoringModeRequestLike, SubscriptionId, TransferSubscriptionsRequestLike } from "../client_session"; import { ClientSessionKeepAliveManager } from "../client_session_keepalive_manager"; import type { ClientSubscription } from "../client_subscription"; import type { Request, Response } from "../common"; import type { UserIdentityInfo } from "../user_identity_info"; import { ClientSidePublishEngine } from "./client_publish_engine"; import { ClientSubscriptionImpl } from "./client_subscription_impl"; import type { IClientBase } from "./i_private_client"; import { repair_client_session } from "./reconnection/reconnection"; const helpAPIChange = process.env.DEBUG && process.env.DEBUG.match(/API/); const debugLog = make_debugLog(__filename); const doDebug = checkDebugFlag(__filename); const warningLog = make_warningLog(__filename); const errorLog = make_errorLog(__filename); let pendingTransactionMessageDisplayed = false; function coerceBrowseDescription(data: any): BrowseDescription { if (typeof data === "string" || data instanceof NodeId) { return coerceBrowseDescription({ browseDirection: BrowseDirection.Forward, includeSubtypes: true, nodeClassMask: 0, nodeId: data, referenceTypeId: "HierarchicalReferences", resultMask: 63 }); } else { data.nodeId = resolveNodeId(data.nodeId); data.referenceTypeId = data.referenceTypeId ? resolveNodeId(data.referenceTypeId) : null; return new BrowseDescription(data); } } function coerceReadValueId(node: any): ReadValueId { if (typeof node === "string" || node instanceof NodeId) { return new ReadValueId({ attributeId: AttributeIds.Value, dataEncoding: undefined, // {namespaceIndex: 0, name: undefined} indexRange: undefined, nodeId: resolveNodeId(node) }); } else { assert(node instanceof Object); return new ReadValueId(node); } } const emptyUint32Array = new Uint32Array(0); type EmptyCallback = (err?: Error) => void; export interface Reconnectable { _reconnecting: { reconnecting: boolean; pendingCallbacks: EmptyCallback[]; pendingTransactions: { request: Request; callback: (err: Error | null, response?: Response) => void }[]; }; } /** * @class ClientSession */ export class ClientSessionImpl extends EventEmitter implements ClientSession, Reconnectable { static reconnectingElement: WeakMap = new WeakMap(); public timeout: number; public authenticationToken?: NodeId; public requestedMaxReferencesPerNode: number; public sessionId: NodeId; public lastRequestSentTime: Date; public lastResponseReceivedTime: Date; public serverCertificate: Certificate; public userIdentityInfo?: UserIdentityInfo; public name = ""; public serverNonce?: Nonce; public serverSignature?: SignatureData; // todo : remove ? public serverEndpoints: EndpointDescription[] = []; public _client: IClientBase | null; public _closed: boolean; public _reconnecting: { reconnecting: boolean; pendingCallbacks: EmptyCallback[]; pendingTransactions: any[]; }; /** * @internal */ public _closeEventHasBeenEmitted: boolean; private _publishEngine: ClientSidePublishEngine | null; private _keepAliveManager?: ClientSessionKeepAliveManager; private $$namespaceArray?: string[]; private recursive_repair_detector = 0; constructor(client: IClientBase) { super(); this.serverCertificate = Buffer.alloc(0); this.sessionId = new NodeId(); this._closeEventHasBeenEmitted = false; this._client = client; this._publishEngine = null; this._closed = false; this._reconnecting = { reconnecting: false, pendingCallbacks: [], pendingTransactions: [] }; this.requestedMaxReferencesPerNode = 10000; this.lastRequestSentTime = new Date(1, 1, 1970); this.lastResponseReceivedTime = new Date(1, 1, 1970); this.timeout = 0; } getTransportSettings(): IBasicTransportSettings { return this._client!.getTransportSettings(); } /** * the endpoint on which this session is operating * @property endpoint * @type {EndpointDescription} */ get endpoint(): EndpointDescription { return this._client!.endpoint!; } get subscriptionCount(): number { return this._publishEngine ? this._publishEngine.subscriptionCount : 0; } get isReconnecting(): boolean { return this._client ? this._client.isReconnecting || this._reconnecting?.reconnecting : false; } public toJSON(): Record { return { name: this.name, sessionId: this.sessionId.toString(), timeout: this.timeout, state: this._closed ? "closed" : "active", isReconnecting: this.isReconnecting ? "true" : "false" }; } public [Symbol.for("nodejs.util.inspect.custom")](): string { return this.toString(); } protected resolveNodeId(nodeId: NodeIdLike): NodeIdLike { return resolveNodeId(nodeId); } public getPublishEngine(): ClientSidePublishEngine { if (!this._publishEngine) { this._publishEngine = new ClientSidePublishEngine(this); } return this._publishEngine!; } public changeUser(userIdentityInfo: UserIdentityInfo): Promise; public changeUser(userIdentityInfo: UserIdentityInfo, callback: CallbackT): void; public changeUser(userIdentityInfo: UserIdentityInfo, callback?: CallbackT): any { userIdentityInfo = userIdentityInfo || { type: UserTokenType.Anonymous }; if (!this._client || !this.userIdentityInfo) { warningLog("changeUser: invalid session"); return callback!(null, StatusCodes.BadInternalError); } const old_userIdentity: UserIdentityInfo = this.userIdentityInfo; this._client._activateSession(this, userIdentityInfo, (err1: Error | null, session2?: ClientSessionImpl) => { if (err1) { this.userIdentityInfo = old_userIdentity; warningLog("activate session error = ", err1.message); return callback!(null, StatusCodes.BadUserAccessDenied); } this.userIdentityInfo = userIdentityInfo; callback!(null, StatusCodes.Good); }); } /** * * @example * * ```javascript * session.browse("RootFolder",function(err,browseResult) { * if(err) return callback(err); * console.log(browseResult.toString()); * callback(); * } ); * ``` * * * @example * * ``` javascript * const browseDescription = { * nodeId: "ObjectsFolder", * referenceTypeId: "Organizes", * browseDirection: BrowseDirection.Inverse, * includeSubtypes: true, * nodeClassMask: 0, * resultMask: 63 * } * session.browse(browseDescription,function(err, browseResult) { * if(err) return callback(err); * console.log(browseResult.toString()); * callback(); * }); * ``` * @example * * ``` javascript * session.browse([ "RootFolder", "ObjectsFolder"],function(err, browseResults) { * assert(browseResults.length === 2); * }); * ``` * * @example * ``` javascript * const browseDescriptions = [ * { * nodeId: "ObjectsFolder", * referenceTypeId: "Organizes", * browseDirection: BrowseDirection.Inverse, * includeSubtypes: true, * nodeClassMask: 0, * resultMask: 63 * }, * // {...} * ] * session.browse(browseDescriptions,function(err, browseResults) { * * }); * ``` * * */ public browse(nodeToBrowse: BrowseDescriptionLike, callback: ResponseCallback): void; public browse(nodesToBrowse: BrowseDescriptionLike[], callback: ResponseCallback): void; public async browse(nodeToBrowse: BrowseDescriptionLike): Promise; public async browse(nodesToBrowse: BrowseDescriptionLike[]): Promise; /** * @internal * @param args */ public browse(...args: any[]): any { const arg0 = args[0]; const isArray = Array.isArray(arg0); const callback: ResponseCallback = args[1]; assert(typeof callback === "function"); assert(isFinite(this.requestedMaxReferencesPerNode)); const nodesToBrowse: BrowseDescription[] = (isArray ? arg0 : [arg0 as BrowseDescription]).map(coerceBrowseDescription); const request = new BrowseRequest({ nodesToBrowse, requestedMaxReferencesPerNode: this.requestedMaxReferencesPerNode }); this.performMessageTransaction(request, (err: Error | null, response?: Response) => { if (err) { return callback(err); } /* c8 ignore next */ if (!response || !(response instanceof BrowseResponse)) { return callback(new Error("Internal Error")); } const results: BrowseResult[] = response.results ? response.results : []; if (this.requestedMaxReferencesPerNode > 0) { for (let i = 0; i < results.length; i++) { const r = results[i]; /* c8 ignore next */ if (r.references && r.references.length > this.requestedMaxReferencesPerNode) { warningLog( chalk.yellow("warning") + " BrowseResponse : the server didn't take into" + " account our requestedMaxReferencesPerNode " ); warningLog(" this.requestedMaxReferencesPerNode= " + this.requestedMaxReferencesPerNode); warningLog(" got " + r.references.length + "for " + nodesToBrowse[i].nodeId.toString()); warningLog(" continuationPoint ", r.continuationPoint); } } } for (const r of results) { r.references = r.references || /* c8 ignore next */[]; } assert(results[0] instanceof BrowseResult); return callback(null, isArray ? results : results[0]); }); } public browseNext( continuationPoint: Buffer, releaseContinuationPoints: boolean, callback: ResponseCallback ): void; public browseNext( continuationPoints: Buffer[], releaseContinuationPoints: boolean, callback: ResponseCallback ): void; public async browseNext(continuationPoint: Buffer, releaseContinuationPoints: boolean): Promise; public async browseNext(continuationPoints: Buffer[], releaseContinuationPoints: boolean): Promise; public browseNext(...args: any[]): any { const arg0 = args[0]; const isArray = Array.isArray(arg0); const releaseContinuationPoints = args[1] as boolean; const callback: any = args[2]; assert(typeof callback === "function", "expecting a callback function here"); const continuationPoints: Buffer[] = isArray ? arg0 : [arg0 as Buffer]; const request = new BrowseNextRequest({ continuationPoints, releaseContinuationPoints }); this.performMessageTransaction(request, (err: Error | null, response?: Response) => { /* c8 ignore next */ if (err) { return callback(err); } /* c8 ignore next */ if (!response || !(response instanceof BrowseNextResponse)) { return callback(new Error("Internal Error")); } const results: BrowseResult[] = response.results ? response.results : []; for (const r of results) { r.references = r.references || []; } assert(results[0] instanceof BrowseResult); return callback(null, isArray ? results : results[0]); }); } /** * * @example * * ```javascript * session.readVariableValue("ns=2;s=Furnace_1.Temperature",function(err,dataValue) { * if(err) { return callback(err); } * if (dataValue.isGood()) { * } * console.log(dataValue.toString()); * callback(); * }); * ``` * * @example * * ```javascript * session.readVariableValue(["ns=0;i=2257","ns=0;i=2258"],function(err,dataValues) { * if (!err) { * console.log(dataValues[0].toString()); * console.log(dataValues[1].toString()); * } * }); * ``` * * @example * ```javascript * const dataValues = await session.readVariableValue(["ns=1;s=Temperature","ns=1;s=Pressure"]); * ``` * * @deprecated */ public readVariableValue(nodeId: NodeIdLike, callback: ResponseCallback): void; public readVariableValue(nodeIds: NodeIdLike[], callback: ResponseCallback): void; public async readVariableValue(nodeId: NodeIdLike): Promise; public async readVariableValue(nodeIds: NodeIdLike[]): Promise; /** * @internal * @param args */ public readVariableValue(...args: any[]): any { const callback = args[1]; assert(typeof callback === "function"); const isArray = Array.isArray(args[0]); const nodes = isArray ? args[0] : [args[0]]; const nodesToRead = nodes.map(coerceReadValueId); const request = new ReadRequest({ nodesToRead, timestampsToReturn: TimestampsToReturn.Neither }); this.performMessageTransaction(request, (err: Error | null, response?: Response) => { /* c8 ignore next */ if (err) { return callback(err); } /* c8 ignore next */ if (!(response instanceof ReadResponse)) { return callback(new Error("Internal Error")); } /* c8 ignore next */ if (response.responseHeader.serviceResult.isNot(StatusCodes.Good)) { return callback(new Error(response.responseHeader.serviceResult.toString())); } /* c8 ignore next */ if (!response.results) { response.results = []; } assert(nodes.length === response.results.length); callback(null, isArray ? response.results : response.results[0]); }); } /** * * @example * * ```javascript * // es5 * session.readHistoryValue( * "ns=5;s=Simulation Examples.Functions.Sine1", * "2015-06-10T09:00:00.000Z", * "2015-06-10T09:01:00.000Z", function(err,dataValues) { * * }); * ``` * * ```javascript * // es6 * const dataValues = await session.readHistoryValue( * "ns=5;s=Simulation Examples.Functions.Sine1", * "2015-06-10T09:00:00.000Z", * "2015-06-10T09:01:00.000Z"); * ``` * @param nodeToRead the read value id * @param start the start time in UTC format * @param end the end time in UTC format * @param callback */ public readHistoryValue( nodesToRead: NodeIdLike[] | HistoryReadValueIdOptions2[], start: DateTime, end: DateTime, callback: (err: Error | null, results?: HistoryReadResult[]) => void ): void; public readHistoryValue( nodesToRead: NodeIdLike[] | HistoryReadValueIdOptions2[], start: DateTime, end: DateTime, options: ExtraReadHistoryValueParameters | undefined, callback: (err: Error | null, results?: HistoryReadResult[]) => void ): void; public async readHistoryValue( nodesToRead: NodeIdLike[] | HistoryReadValueIdOptions2[], start: DateTime, end: DateTime, options?: ExtraReadHistoryValueParameters ): Promise; public readHistoryValue( nodeToRead: NodeIdLike | HistoryReadValueIdOptions2, start: DateTime, end: DateTime, callback: (err: Error | null, results?: HistoryReadResult) => void ): void; public readHistoryValue( nodeToRead: NodeIdLike | HistoryReadValueIdOptions2, start: DateTime, end: DateTime, options: ExtraReadHistoryValueParameters | undefined, callback: (err: Error | null, results?: HistoryReadResult) => void ): void; public async readHistoryValue( nodeToRead: NodeIdLike | HistoryReadValueIdOptions2, start: DateTime, end: DateTime, parameters: ExtraReadHistoryValueParameters ): Promise; public readHistoryValue(...args: any[]): any { const startTime = args[1]; const endTime = args[2]; let options: ExtraReadHistoryValueParameters = {}; let callback = args[3]; if (typeof callback !== "function") { options = args[3]; callback = args[4]; } assert(typeof callback === "function"); // adjust parameters options.numValuesPerNode = options.numValuesPerNode || 0; options.returnBounds = options.returnBounds || options.returnBounds === undefined ? true : false; options.isReadModified = options.isReadModified || false; options.timestampsToReturn = options.timestampsToReturn ?? TimestampsToReturn.Both; const arg0 = args[0]; const isArray = Array.isArray(arg0); const nodes = isArray ? arg0 : [arg0]; const nodesToRead: HistoryReadValueIdOptions[] = []; for (const node of nodes) { if (!node.nodeId) { nodesToRead.push({ continuationPoint: undefined, dataEncoding: undefined, // {namespaceIndex: 0, name: undefined}, indexRange: undefined, nodeId: this.resolveNodeId(node as NodeIdLike) }); } else { nodesToRead.push(node as HistoryReadValueIdOptions); } } const readRawModifiedDetails = new ReadRawModifiedDetails({ endTime, isReadModified: false, numValuesPerNode: options.numValuesPerNode, returnBounds: options.returnBounds, startTime }); const request = new HistoryReadRequest({ historyReadDetails: readRawModifiedDetails, nodesToRead, releaseContinuationPoints: false, timestampsToReturn: options.timestampsToReturn }); request.nodesToRead = request.nodesToRead || []; assert(nodes.length === request.nodesToRead.length); this.historyRead(request, (err: Error | null, response?: HistoryReadResponse) => { /* c8 ignore next */ if (err) { return callback(err); } /* c8 ignore next */ if (!response || !(response instanceof HistoryReadResponse)) { return callback(new Error("Internal Error")); } response.results = response.results || []; assert(nodes.length === response.results.length); callback(null, isArray ? response.results : response.results[0]); }); } public historyRead(request: HistoryReadRequest, callback: Callback): void; public historyRead(request: HistoryReadRequest): Promise; public historyRead(request: HistoryReadRequest, callback?: CallbackT): any { /* c8 ignore next */ if (!callback) { throw new Error("expecting a callback"); } this.performMessageTransaction(request, (err: Error | null, response) => { /* c8 ignore next */ if (err) { return callback(err); } /* c8 ignore next */ if (!response || !(response instanceof HistoryReadResponse)) { return callback(new Error("Internal Error")); } if (response.responseHeader.serviceResult.isNot(StatusCodes.Good)) { return callback(new Error(response.responseHeader.serviceResult.toString())); } response.results = response.results || /* c8 ignore next */[]; // perform ExtensionObject resolution const promises = response.results.map(async (result) => { if (result.historyData && result.historyData instanceof HistoryData) { if (result.historyData.dataValues) { await promoteOpaqueStructure(this, result.historyData.dataValues); } } }); Promise.all(promises) .then(() => { callback(null, response); }) .catch((err) => callback(err)); }); } public readAggregateValue( nodesToRead: HistoryReadValueIdOptions[], startTime: DateTime, endTime: DateTime, aggregateFn: AggregateFunction[], processingInterval: number, callback: Callback ): void; public async readAggregateValue( nodesToRead: HistoryReadValueIdOptions[], startTime: DateTime, endTime: DateTime, aggregateFn: AggregateFunction[], processingInterval: number ): Promise; public readAggregateValue( nodeToRead: HistoryReadValueIdOptions, startTime: DateTime, endTime: DateTime, aggregateFn: AggregateFunction, processingInterval: number, callback: Callback ): void; public async readAggregateValue( nodeToRead: HistoryReadValueIdOptions, startTime: DateTime, endTime: DateTime, aggregateFn: AggregateFunction, processingInterval: number ): Promise; public readAggregateValue( nodesToRead: HistoryReadValueIdOptions[], startTime: DateTime, endTime: DateTime, aggregateFn: AggregateFunction[], processingInterval: number, aggregateConfiguration: AggregateConfigurationOptions, callback: Callback ): void; public async readAggregateValue( nodesToRead: HistoryReadValueIdOptions[], startTime: DateTime, endTime: DateTime, aggregateFn: AggregateFunction[], processingInterval: number, aggregateConfiguration: AggregateConfigurationOptions ): Promise; public readAggregateValue( nodeToRead: HistoryReadValueIdOptions, startTime: DateTime, endTime: DateTime, aggregateFn: AggregateFunction, processingInterval: number, aggregateConfiguration: AggregateConfigurationOptions, callback: Callback ): void; public async readAggregateValue( nodeToRead: HistoryReadValueIdOptions, startTime: DateTime, endTime: DateTime, aggregateFn: AggregateFunction, processingInterval: number, aggregateConfiguration: AggregateConfigurationOptions ): Promise; public readAggregateValue( arg0: HistoryReadValueIdOptions[] | HistoryReadValueIdOptions, startTime: DateTime, endTime: DateTime, aggregateFn: AggregateFunction[] | AggregateFunction, processingInterval: number, ...args: any[] ): any { const callback = typeof args[0] === "function" ? args[0] : args[1]; assert(typeof callback === "function"); const defaultAggregateFunction = { percentDataBad: 100, percentDataGood: 100, treatUncertainAsBad: true, useServerCapabilitiesDefaults: true, useSlopedExtrapolation: false }; const aggregateConfiguration = typeof args[0] === "function" ? defaultAggregateFunction : args[0]; const isArray = Array.isArray(arg0); const nodesToRead: HistoryReadValueIdOptions[] = isArray ? (arg0 as HistoryReadValueIdOptions[]) : [arg0 as HistoryReadValueIdOptions]; const aggregateFns: AggregateFunction[] = Array.isArray(aggregateFn) ? (aggregateFn as AggregateFunction[]) : [aggregateFn as AggregateFunction]; assert(aggregateFns.length === nodesToRead.length); const readProcessedDetails = new ReadProcessedDetails({ aggregateType: aggregateFns, endTime, processingInterval, startTime, aggregateConfiguration }); const request = new HistoryReadRequest({ historyReadDetails: readProcessedDetails, nodesToRead, releaseContinuationPoints: false, timestampsToReturn: TimestampsToReturn.Both }); assert(nodesToRead.length === request.nodesToRead!.length); this.performMessageTransaction(request, (err: Error | null, response) => { /* c8 ignore next */ if (err) { return callback(err); } /* c8 ignore next */ if (!response || !(response instanceof HistoryReadResponse)) { return callback(new Error("Internal Error")); } if (response.responseHeader.serviceResult.isNot(StatusCodes.Good)) { return callback(new Error(response.responseHeader.serviceResult.toString())); } response.results = response.results || /* c8 ignore next */[]; assert(nodesToRead.length === response.results.length); callback(null, isArray ? response.results : response.results[0]); }); } /** * * @param nodesToWrite {WriteValue[]} - the array of value to write. One or more elements. * @param {Function} callback - the callback function * @param callback.err {object|null} the error if write has failed or null if OK * @param callback.statusCodes {StatusCode[]} - an array of status code of each write * * @example * * const nodesToWrite = [ * { * nodeId: "ns=1;s=SetPoint1", * attributeId: opcua.AttributeIds.Value, * value: { * statusCode: Good, * value: { * dataType: opcua.DataType.Double, * value: 100.0 * } * } * }, * { * nodeId: "ns=1;s=SetPoint2", * attributeIds opcua.AttributeIds.Value, * value: { * statusCode: Good, * value: { * dataType: opcua.DataType.Double, * value: 45.0 * } * } * } * ]; * session.write(nodesToWrite,function (err,statusCodes) { * if(err) { return callback(err);} * // * }); * * @param nodeToWrite {WriteValue} - the value to write * @param callback - the callback function * @param callback.err {object|null} the error if write has failed or null if OK * @param callback.statusCode {StatusCodes} - the status code of the write * * @example * * const nodeToWrite = { * nodeId: "ns=1;s=SetPoint", * attributeId: opcua.AttributeIds.Value, * value: { * statusCode: Good, * value: { * dataType: opcua.DataType.Double, * value: 100.0 * } * } * }; * session.write(nodeToWrite,function (err,statusCode) { * if(err) { return callback(err);} * // * }); * * * @param nodeToWrite {WriteValue} - the value to write * @return {Promise} * * @example * * ```javascript * session.write(nodeToWrite).then(function(statusCode) { }); * ``` * * @example * * ```javascript * const statusCode = await session.write(nodeToWrite); * ``` * * @param nodesToWrite {Array} - the value to write * @return {Promise>} * * @example * ```javascript * session.write(nodesToWrite).then(function(statusCodes) { }); * ``` * * @example * ```javascript * const statusCodes = await session.write(nodesToWrite); * ``` */ public write(nodeToWrite: WriteValueOptions, callback: ResponseCallback): void; public write(nodesToWrite: WriteValueOptions[], callback: ResponseCallback): void; public async write(nodesToWrite: WriteValueOptions[]): Promise; public async write(nodeToWrite: WriteValueOptions): Promise; /** * @internal * @param args */ public write(...args: any[]): any { const arg0 = args[0]; const isArray = Array.isArray(arg0); const nodesToWrite = isArray ? arg0 : [arg0]; const callback = args[1]; assert(typeof callback === "function"); const request = new WriteRequest({ nodesToWrite }); this.performMessageTransaction(request, (err: Error | null, response?: Response) => { /* c8 ignore next */ if (err) { return callback(err, response); } /* c8 ignore next */ if (!response || !(response instanceof WriteResponse)) { return callback(new Error("Internal Error")); } /* c8 ignore next */ if (response.responseHeader.serviceResult.isNot(StatusCodes.Good)) { return callback(new Error(response.responseHeader.serviceResult.toString())); } response.results = response.results || /* c8 ignore next */[]; assert(nodesToWrite.length === response.results.length); callback(null, isArray ? response.results : response.results[0]); }); } /** * @deprecated use session.write instead * * @param nodeId {NodeId} - the node id of the node to write * @param value {Variant} - the value to write * @param callback {Function} * @param callback.err {object|null} the error if write has failed or null if OK * @param callback.statusCode {StatusCode} - the status code of the write * * @param nodeId {NodeId} - the node id of the node to write * @param value {Variant} - the value to write * @return {Promise} - the status code of the write * * * @example * // please use session.write instead of session.writeSingleNode * // as follow * const statusCode = await session.write({ * nodeId, * attributeId: AttributeIds.Value, * value: { * statusCode: Good, * sourceTimestamp: new Date(), // optional, some server may not accept this * value: { * dataType: opcua.DataType.Double, * value: 100.0 * } * } * }); * * */ public writeSingleNode(nodeId: NodeIdLike, value: VariantLike, callback: ResponseCallback): void; public writeSingleNode(nodeId: NodeIdLike, value: VariantLike): Promise; public writeSingleNode(...args: any[]): any { const nodeId = args[0] as NodeIdLike; const value = args[1] as VariantLike; const callback = args[2]; assert(typeof callback === "function"); const nodeToWrite = new WriteValue({ attributeId: AttributeIds.Value, indexRange: undefined, nodeId: this.resolveNodeId(nodeId), value: new DataValue({ value }) }); this.write(nodeToWrite, (err, statusCode) => { /* c8 ignore next */ if (err) { return callback(err); } assert(statusCode); callback(null, statusCode); }); } /** * * @example * * * ``` javascript * session.readAllAttributes("ns=2;s=Furnace_1.Temperature",function(err,data) { * if(data.statusCode.isGood()) { * console.log(" nodeId = ",data.nodeId.toString()); * console.log(" browseName = ",data.browseName.toString()); * console.log(" description = ",data.description.toString()); * console.log(" value = ",data.value.toString())); * } * }); * ``` * * @param nodes array of nodeId to read * @param node nodeId to read * @param callback */ public readAllAttributes(node: NodeIdLike, callback: (err: Error | null, data?: NodeAttributes) => void): void; public readAllAttributes(nodes: NodeIdLike[], callback: (err: Error | null, data?: NodeAttributes[]) => void): void; public readAllAttributes(...args: any[]): void { const nodes = args[0] as NodeIdLike[]; const callback = args[1] as (err: Error | null, data?: NodeAttributes[]) => void; readAllAttributes(this, nodes) .then((data: any) => callback(null, data)) .catch((err: Error) => callback(err)); } /** * * * @example * * ```javascript * ``` * * form1: reading a single node * * ``` javascript * const nodeToRead = { * nodeId: "ns=2;s=Furnace_1.Temperature", * attributeId: AttributeIds.BrowseName * }; * * session.read(nodeToRead,function(err,dataValue) { * if (!err) { * console.log(dataValue.toString()); * } * }); * ``` * * * @param nodesToRead {Array} - an array of nodeId to read or a ReadValueId * @param [maxAge] {Number} * @param callback {Function} - the callback function * @param callback.err {Error|null} - the error or null if the transaction was OK} * @param callback.dataValues {Array} * * @example * * ``` javascript * const nodesToRead = [ * { * nodeId: "ns=2;s=Furnace_1.Temperature", * attributeId: AttributeIds.BrowseName * } * ]; * session.read(nodesToRead,function(err,dataValues) { * if (!err) { * dataValues.forEach(dataValue=>console.log(dataValue.toString())); * } * }); * ``` * */ public read(nodeToRead: ReadValueIdOptions, maxAge: number, callback: ResponseCallback): void; public read(nodesToRead: ReadValueIdOptions[], maxAge: number, callback: ResponseCallback): void; public read(nodeToRead: ReadValueIdOptions, callback: ResponseCallback): void; public read(nodesToRead: ReadValueIdOptions[], callback: ResponseCallback): void; public read(nodeToRead: ReadValueIdOptions, maxAge?: number): Promise; public read(nodeToRead: ReadValueIdOptions[], maxAge?: number): Promise; /** * @internal * @param args */ public read(...args: any[]): any { if (args.length === 2) { return this.read(args[0], 0, args[1]); } assert(args.length === 3); const isArray = Array.isArray(args[0]); const nodesToRead = isArray ? args[0] : [args[0]]; assert(Array.isArray(nodesToRead)); const maxAge = args[1]; const callback = args[2]; assert(typeof callback === "function"); /* c8 ignore next */ if (helpAPIChange) { // the read method deprecation detection and warning if ( !(getFunctionParameterNames(callback)[1] === "dataValues" || getFunctionParameterNames(callback)[1] === "dataValue") ) { warningLog(chalk.red("[NODE-OPCUA-E04] the ClientSession#read API has changed !!, please fix the client code")); warningLog(chalk.red(" replace ..:")); warningLog(chalk.cyan(" session.read(nodesToRead,function(err,nodesToRead,results) {}")); warningLog(chalk.red(" with .... :")); warningLog(chalk.cyan(" session.read(nodesToRead,function(err,dataValues) {}")); warningLog(""); warningLog( chalk.yellow( "please make sure to refactor your code and check that " + "the second argument of your callback function is named" ), chalk.cyan("dataValue" + (isArray ? "s" : "")) ); warningLog(chalk.cyan("to make this exception disappear")); throw new Error("ERROR ClientSession#read API has changed !!, please fix the client code"); } } // coerce nodeIds for (const node of nodesToRead) { node.nodeId = this.resolveNodeId(node.nodeId); } const request = new ReadRequest({ maxAge, nodesToRead, timestampsToReturn: TimestampsToReturn.Both }); this.performMessageTransaction(request, (err: Error | null, response?: Response) => { /* c8 ignore next */ if (err) { return callback(err, response); } /* c8 ignore next */ if (!response || !(response instanceof ReadResponse)) { return callback(new Error("Internal Error")); } // perform ExtensionObject resolution promoteOpaqueStructure(this, response.results!) .then(() => { response.results = response.results || /* c8 ignore next */[]; callback(null, isArray ? response.results : response.results[0]); }) .catch((err) => { callback(err); }); }); } public emitCloseEvent(statusCode: StatusCode): void { if (!this._closeEventHasBeenEmitted) { debugLog("ClientSession#emitCloseEvent"); this._closeEventHasBeenEmitted = true; this.emit("session_closed", statusCode); } } public createSubscription( options: CreateSubscriptionRequestLike, callback?: ResponseCallback ): any { this._defaultRequest(CreateSubscriptionRequest, CreateSubscriptionResponse, options, callback); } /** * @param createSubscriptionRequest * @param callback * * * subscription.on("error', function(err){ ... }); * subscription.on("terminate',function(err){ ... }); * const monitoredItem = await subscription.monitor(itemToMonitor,monitoringParameters,requestedParameters); * monitoredItem.on("changed",function( dataValue) {...}); * */ public async createSubscription2(createSubscriptionRequest: CreateSubscriptionRequestLike): Promise; public createSubscription2( createSubscriptionRequest: CreateSubscriptionRequestLike, callback: (err: Error | null, subscription?: ClientSubscription) => void ): void; public createSubscription2(...args: any[]): any { const createSubscriptionRequest = args[0] as CreateSubscriptionRequestLike; let callback = args[1]; const subscription = new ClientSubscriptionImpl(this, createSubscriptionRequest); // tslint:disable-next-line:no-empty subscription.on("error", (err) => { if (callback) { callback(err); callback = null; } }); subscription.on("started", () => { assert(subscription.session === this, "expecting a session here"); if (callback) { callback(null, subscription); callback = null; } }); } public deleteSubscriptions( options: DeleteSubscriptionsRequestLike, callback?: ResponseCallback ): any { this._defaultRequest(DeleteSubscriptionsRequest, DeleteSubscriptionsResponse, options, callback); } public setTriggering(request: SetTriggeringRequestOptions, callback?: ResponseCallback): any { this._defaultRequest(SetTriggeringRequest, SetTriggeringResponse, request, callback); } /** */ public transferSubscriptions( options: TransferSubscriptionsRequestLike, callback?: ResponseCallback ): any { this._defaultRequest(TransferSubscriptionsRequest, TransferSubscriptionsResponse, options, callback); } public createMonitoredItems( options: CreateMonitoredItemsRequestLike, callback?: ResponseCallback ): any { this._defaultRequest(CreateMonitoredItemsRequest, CreateMonitoredItemsResponse, options, callback); } public modifyMonitoredItems( options: ModifyMonitoredItemsRequestLike, callback?: ResponseCallback ): any { this._defaultRequest(ModifyMonitoredItemsRequest, ModifyMonitoredItemsResponse, options, callback); } /** * */ public modifySubscription( options: ModifySubscriptionRequestLike, callback?: ResponseCallback ): any { this._defaultRequest(ModifySubscriptionRequest, ModifySubscriptionResponse, options, callback); } public setMonitoringMode(options: SetMonitoringModeRequestLike, callback?: ResponseCallback): any { this._defaultRequest(SetMonitoringModeRequest, SetMonitoringModeResponse, options, callback); } /** */ public publish(options: PublishRequest, callback: (err: Error | null, response?: PublishResponse) => void): void { this._defaultRequest(PublishRequest, PublishResponse, options, callback); } /** * */ public republish(options: RepublishRequest, callback: (err: Error | null, response?: RepublishResponse) => void): void { this._defaultRequest(RepublishRequest, RepublishResponse, options, callback); } /** * */ public deleteMonitoredItems( options: DeleteMonitoredItemsRequestLike, callback: (err: Error | null, response?: DeleteMonitoredItemsResponse) => void ): void { this._defaultRequest(DeleteMonitoredItemsRequest, DeleteMonitoredItemsResponse, options, callback); } /** * */ public setPublishingMode(publishingEnabled: boolean, subscriptionId: SubscriptionId): Promise; public setPublishingMode(publishingEnabled: boolean, subscriptionIds: SubscriptionId[]): Promise; public setPublishingMode( publishingEnabled: boolean, subscriptionId: SubscriptionId, callback: (err: Error | null, statusCode?: StatusCode) => void ): void; public setPublishingMode( publishingEnabled: boolean, subscriptionIds: SubscriptionId[], callback: (err: Error | null, statusCodes?: StatusCode[]) => void ): void; /** * @internal */ public setPublishingMode(...args: any[]): any { const publishingEnabled = args[0]; const isArray = Array.isArray(args[1]); const subscriptionIds = isArray ? args[1] : [args[1]]; const callback = args[2]; assert(typeof callback === "function"); assert(publishingEnabled === true || publishingEnabled === false); const options = new SetPublishingModeRequest({ publishingEnabled, subscriptionIds }); this._defaultRequest( SetPublishingModeRequest, SetPublishingModeResponse, options, (err: Error | null, response?: SetPublishingModeResponse) => { /* c8 ignore next */ if (err) { return callback(err); } /* c8 ignore next */ if (!response) { return callback(new Error("Internal Error")); } response.results = response.results || /* c8 ignore next */[]; callback(err, isArray ? response.results : response.results[0]); } ); } /** * */ public translateBrowsePath(browsePath: BrowsePath, callback: ResponseCallback): void; public translateBrowsePath(browsesPath: BrowsePath[], callback: ResponseCallback): void; public async translateBrowsePath(browsePath: BrowsePath): Promise; public async translateBrowsePath(browsePaths: BrowsePath[]): Promise; /** * @internal * @param args */ public translateBrowsePath(...args: any[]): any { const isArray = Array.isArray(args[0]); const browsePaths = isArray ? args[0] : [args[0]]; const callback = args[1]; assert(typeof callback === "function"); const request = new TranslateBrowsePathsToNodeIdsRequest({ browsePaths }); this.performMessageTransaction(request, (err: Error | null, response?: Response) => { /* c8 ignore next */ if (err) { return callback(err, response); } /* c8 ignore next */ if (!response || !(response instanceof TranslateBrowsePathsToNodeIdsResponse)) { return callback(new Error("Internal Error")); } response.results = response.results || /* c8 ignore next */[]; callback(null, isArray ? response.results : response.results[0]); }); } public channelId(): number { return this._client !== null && this._client._secureChannel !== null && this._client._secureChannel.isOpened() ? this._client._secureChannel!.channelId : -1; } public isChannelValid(): boolean { /* c8 ignore next */ if (!this._client) { debugLog(chalk.red("Warning SessionClient is null ?")); } return this._client !== null && this._client._secureChannel !== null && this._client._secureChannel.isOpened(); } public performMessageTransaction(request: Request, callback: (err: Error | null, response?: Response) => void): void { if (!this._client) { // session may have been closed by user ... but is still in used !! return callback(new Error("Session has been closed and should not be used to perform a transaction anymore")); } if ( request instanceof PublishRequest || request instanceof ActivateSessionRequest || request instanceof CloseSessionRequest ) { return this._performMessageTransaction(request, callback); } if (this._reconnecting.pendingTransactions.length > 0) { /* c8 ignore next */ if (this._reconnecting.pendingTransactions.length > 10) { if (!pendingTransactionMessageDisplayed) { pendingTransactionMessageDisplayed = true; warningLog( "[NODE-OPCUA-W21]", "Pending transactions: ", this._reconnecting.pendingTransactions.map((a: any) => a.request.constructor.name).join(" ") ); warningLog( "[NODE-OPCUA-W22]", chalk.yellow( "Warning : your opcua client is sending multiple requests simultaneously to the server", request.constructor.name ), "\n", chalk.yellow(" please fix your application code") ); } } else if (this._reconnecting.pendingTransactions.length > 3) { debugLog( chalk.yellow( "Warning : your client is sending multiple requests simultaneously to the server", request.constructor.name ) ); } this._reconnecting.pendingTransactions.push({ request, callback }); return; } this.#reprocessRequest(0, request, callback); } #reprocessRequest(attemptCount: number, request: Request, callback: (err: Error | null, response?: Response) => void): void { attemptCount > 0 && warningLog("reprocessRequest => ", request.constructor.name, this._reconnecting.pendingTransactions.length); this._performMessageTransaction(request, (err: null | Error, response?: Response) => { if (err && err.message.match(/BadSessionIdInvalid/) && request.constructor.name !== "ActivateSessionRequest") { warningLog( "Transaction on Invalid Session ", request.constructor.name, err.message, "isReconnecting?=", this.isReconnecting, "q=", this._reconnecting.pendingTransactions.length ); request.requestHeader.requestHandle = requestHandleNotSetValue; if (this.isReconnecting) { this.once("session_restored", () => { warningLog("redoing", request.constructor.name, this.isReconnecting); this.#reprocessRequest(attemptCount + 1, request, callback); }); } else { this.#_recreate_session_and_reperform_transaction(request, callback); } return; } callback(err, response); const length = this._reconnecting.pendingTransactions.length; // record length before callback is called ! if (length > 0) { debugLog("reprocessRequest => ", this._reconnecting.pendingTransactions.length, " transaction(s) left in queue"); // tslint:disable-next-line: no-shadowed-variable const { request, callback } = this._reconnecting.pendingTransactions.shift(); this.#reprocessRequest(0, request, callback); } }); } public _performMessageTransaction(request: Request, callback: (err: Error | null, response?: Response) => void): void { assert(typeof callback === "function"); /* c8 ignore next */ if (!this._client) { // session may have been closed by user ... but is still in used !! return callback(new Error("Session has been closed and should not be used to perform a transaction anymore")); } if (!this.isChannelValid()) { // the secure channel is broken, may be the server has crashed or the network cable has been disconnected // for a long time // we may need to queue this transaction, as a secure token may be being reprocessed debugLog(chalk.bgWhite.red("!!! Performing transaction on invalid channel !!! ", request.constructor.name)); return callback(new Error("Invalid Channel BadConnectionClosed")); } // is this stuff useful? if (request.requestHeader) { request.requestHeader.authenticationToken = this.authenticationToken!; } this.lastRequestSentTime = new Date(); this._client.performMessageTransaction(request, (err: Error | null, response?: Response) => { this.lastResponseReceivedTime = new Date(); /* c8 ignore next */ if (err) { if (response && response.responseHeader.serviceDiagnostics) { (err as any).serviceDiagnostics = response.responseHeader.serviceDiagnostics; } if (response && (response as any).diagnosticInfos) { (err as any).diagnosticsInfo = (response as any).diagnosticInfos; } return callback(err); } /* c8 ignore next */ if (!response) { return callback(new Error("internal Error")); } /* c8 ignore next */ if (response.responseHeader.serviceResult.isNot(StatusCodes.Good)) { err = new Error( " ServiceResult is " + response.responseHeader.serviceResult.toString() + " request was " + request.constructor.name ); if (response && response.responseHeader.serviceDiagnostics) { (err as any).serviceDiagnostics = response.responseHeader.serviceDiagnostics; } if (response && (response as any).diagnosticInfos) { (err as any).diagnosticsInfo = (response as any).diagnosticInfos; } return callback(err, response); } return callback(null, response); }); } /** * evaluate the remaining time for the session * * * evaluate the time in milliseconds that the session will live * on the server end from now. * The remaining live time is calculated based on when the last message was sent to the server * and the session timeout. * * * In normal operation , when server and client communicates on a regular * basis, evaluateRemainingLifetime will return a number slightly below * session.timeout * * * when the client and server cannot communicate due to a network issue * (or a server crash), evaluateRemainingLifetime returns the estimated number * of milliseconds before the server (if not crash) will keep the session alive * on its end to allow a automatic reconnection with session. * * * When evaluateRemainingLifetime returns zero , this mean that * the session has probably ended on the server side and will have to be recreated * from scratch in case of a reconnection. * * @return the number of milliseconds before session expires */ public evaluateRemainingLifetime(): number { const now = Date.now(); const expiryTime = this.lastRequestSentTime.getTime() + this.timeout; return Math.max(0, expiryTime - now); } public _terminatePublishEngine(): void { if (this._publishEngine) { this._publishEngine.terminate(); this._publishEngine = null; } } /** * */ public close(callback: ErrorCallback): void; public close(deleteSubscription: boolean, callback: ErrorCallback): void; public async close(deleteSubscription?: boolean): Promise; /** * @internal * @param args */ public close(...args: any[]): any { if (arguments.length === 1) { return this.close(true, args[0]); } const deleteSubscription = args[0]; const callback = args[1]; assert(typeof callback === "function"); assert(typeof deleteSubscription === "boolean"); /* c8 ignore next */ if (!this._client) { debugLog("ClientSession#close : warning, client is already closed"); return callback(); // already close ? } assert(this._client); this._terminatePublishEngine(); this._client.closeSession(this, deleteSubscription, (err?: Error) => { debugLog("session Close err ", err ? err.message : "null"); callback(); }); } /** * @return {Boolean} */ public hasBeenClosed(): boolean { return isNullOrUndefined(this._client) || this._closed || this._closeEventHasBeenEmitted; } public async call(methodToCall: CallMethodRequestLike): Promise; public async call(methodToCall: CallMethodRequestLike[]): Promise; public call(methodToCall: CallMethodRequestLike, callback: ResponseCallback): void; public call(methodsToCall: CallMethodRequestLike[], callback: ResponseCallback): void; /** * @internal * @param args */ public call(...args: any[]): any { const isArray = Array.isArray(args[0]); const methodsToCall = isArray ? args[0] : [args[0]]; assert(Array.isArray(methodsToCall)); const callback = args[1]; // Note : The client has no explicit address space and therefore will struggle to // access the method arguments signature. // There are two methods that can be considered: // - get the object definition by querying the server // - load a fake address space to have some thing to query on our end // const request = this._client.factory.constructObjectId("CallRequest",{ methodsToCall: methodsToCall}); const request = new CallRequest({ methodsToCall }); this.performMessageTransaction(request, (err: Error | null, response?: Response) => { /* c8 ignore next */ if (err) { return callback(err); } /* c8 ignore next */ if (!response || !(response instanceof CallResponse)) { return callback(new Error("internal error")); } response.results = response.results || []; promoteOpaqueStructureForCall(this, response.results) .then(() => { callback(null, isArray ? response.results : response.results![0]); }) .catch((err) => { callback(err); }); }); } /** * @param subscriptionId {UInt32} the subscription Id to return * @param callback {Function} * @param callback.err {Error} * @param callback.monitoredItems the monitored Items * @param callback.monitoredItems the monitored Items */ public async getMonitoredItems(subscriptionId: SubscriptionId): Promise; public getMonitoredItems(subscriptionId: SubscriptionId, callback: ResponseCallback): void; public getMonitoredItems(...args: any[]): any { const subscriptionId = args[0] as SubscriptionId; const callback = args[1]; // // // const methodsToCall = new CallMethodRequest({ inputArguments: [ // BaseDataType { dataType: DataType.UInt32, value: subscriptionId } ], methodId: coerceNodeId("ns=0;i=11492"), // MethodIds.Server_GetMonitoredItems; objectId: coerceNodeId("ns=0;i=2253") // ObjectId.Server }); this.call(methodsToCall, (err?: Error | null, result?: CallMethodResult) => { /* c8 ignore next */ if (err) { return callback(err); } /* c8 ignore next */ if (!result) { return callback(new Error("internal error")); } /* c8 ignore next */ if (result.statusCode.isNot(StatusCodes.Good)) { callback(new Error(result.statusCode.toString())); } else { result.outputArguments = result.outputArguments || []; assert(result.outputArguments.length === 2); const data = { clientHandles: result.outputArguments[1].value, serverHandles: result.outputArguments[0].value // }; // Note some server might return null array // let make sure we have Uint32Array and not a null pointer data.serverHandles = data.serverHandles || /* c8 ignore next */ emptyUint32Array; data.clientHandles = data.clientHandles || /* c8 ignore next */ emptyUint32Array; assert(data.serverHandles instanceof Uint32Array); assert(data.clientHandles instanceof Uint32Array); callback(null, data); } }); } /** * */ public async getArgumentDefinition(methodId: MethodId): Promise; public getArgumentDefinition(methodId: MethodId, callback: ResponseCallback): void; /** * @internal */ public getArgumentDefinition(...args: any[]): any { const methodId = args[0] as MethodId; const callback = args[1] as ResponseCallback; assert(typeof callback === "function"); return getArgumentDefinitionHelper(this, methodId) .then((result) => { callback!(null, result); }) .catch((err) => { callback(err); }); } public async registerNodes(nodesToRegister: NodeIdLike[]): Promise; public registerNodes(nodesToRegister: NodeIdLike[], callback: (err: Error | null, registeredNodeIds?: NodeId[]) => void): void; public registerNodes(...args: any[]): any { const nodesToRegister = args[0] as NodeIdLike[]; const callback = args[1] as (err: Error | null, registeredNodeIds?: NodeId[]) => void; assert(typeof callback === "function"); assert(Array.isArray(nodesToRegister)); const request = new RegisterNodesRequest({ nodesToRegister: nodesToRegister.map((n) => this.resolveNodeId(n)) }); this.performMessageTransaction(request, (err: Error | null, response?: Response) => { /* c8 ignore next */ if (err) { return callback(err); } /* c8 ignore next */ if (!response || !(response instanceof RegisterNodesResponse)) { return callback(new Error("Internal Error")); } response.registeredNodeIds = response.registeredNodeIds || /* c8 ignore next */[]; callback(null, response.registeredNodeIds); }); } public async unregisterNodes(nodesToUnregister: NodeIdLike[]): Promise; public unregisterNodes(nodesToUnregister: NodeIdLike[], callback: (err?: Error) => void): void; public unregisterNodes(...args: any[]): any { const nodesToUnregister = args[0] as NodeIdLike[]; const callback = args[1] as (err?: Error) => void; assert(typeof callback === "function"); assert(Array.isArray(nodesToUnregister)); const request = new UnregisterNodesRequest({ nodesToUnregister: nodesToUnregister.map((n) => this.resolveNodeId(n)) }); this.performMessageTransaction(request, (err: Error | null, response?: Response) => { /* c8 ignore next */ if (err) { return callback(err); } /* c8 ignore next */ if (!response || !(response instanceof UnregisterNodesResponse)) { return callback(new Error("Internal Error")); } callback(); }); } public async queryFirst(queryFirstRequest: QueryFirstRequestLike): Promise; public queryFirst(queryFirstRequest: QueryFirstRequestLike, callback: ResponseCallback): void; public queryFirst(...args: any[]): any { const queryFirstRequest = args[0] as QueryFirstRequestLike; const callback = args[1] as ResponseCallback; assert(typeof callback === "function"); const request = new QueryFirstRequest(queryFirstRequest); this.performMessageTransaction(request, (err: Error | null, response?: Response) => { /* c8 ignore next */ if (err) { return callback(err); } /* c8 ignore next */ if (!response || !(response instanceof QueryFirstResponse)) { return callback(new Error("internal error")); } callback(null, response); }); } public startKeepAliveManager(keepAliveInterval?: number): void { if (this._keepAliveManager) { // "keepAliveManger already started" return; } this._keepAliveManager = new ClientSessionKeepAliveManager(this); this._keepAliveManager.on("failure", () => { /** * raised when a keep-alive request has failed on the session, may be the session has timeout * unexpectedly on the server side, may be the connection is broken. * @event keepalive_failure */ this.emit("keepalive_failure"); }); this._keepAliveManager.on("keepalive", (state, count) => { /** * @event keepalive */ this.emit("keepalive", state, count); }); this._keepAliveManager.start(keepAliveInterval); } public stopKeepAliveManager(): void { if (this._keepAliveManager) { this._keepAliveManager.stop(); this._keepAliveManager = undefined; } } public dispose(): void { assert(this._closeEventHasBeenEmitted); this._terminatePublishEngine(); this.stopKeepAliveManager(); this.removeAllListeners(); // const privateThis = this as any; if (!(!privateThis.pendingTransactions || privateThis.pendingTransactions.length === 0)) { // tslint:disable-next-line: no-console warningLog("dispose when pendingTransactions is not empty "); } } public toString(): string { const now = Date.now(); const lap1 = now - this.lastRequestSentTime.getTime(); const lap2 = now - this.lastResponseReceivedTime.getTime(); const timeoutDelay = this.timeout - lap1; const timeoutInfo = timeoutDelay < 0 ? chalk.red(" expired since " + -timeoutDelay / 1000 + " seconds") : chalk.green(" timeout in " + timeoutDelay / 1000 + " seconds"); let str = ""; str += " name..................... " + this.name; str += "\n sessionId................ " + this.sessionId.toString(); str += "\n authenticationToken...... " + (this.authenticationToken ? this.authenticationToken!.toString() : ""); str += "\n timeout.................. " + this.timeout + "ms" + timeoutInfo; str += "\n serverNonce.............. " + (this.serverNonce ? this.serverNonce!.toString("hex") : ""); str += "\n serverCertificate........ " + buffer_ellipsis(this.serverCertificate); str += "\n serverSignature.......... " + this.serverSignature; str += "\n lastRequestSentTime...... " + new Date(this.lastRequestSentTime).toISOString() + " (" + lap1 + ")"; str += "\n lastResponseReceivedTime. " + new Date(this.lastResponseReceivedTime).toISOString() + " (" + lap2 + ")"; str += "\n isReconnecting........... " + this.isReconnecting; str += "\n isValidChannel........... " + this.isChannelValid() + " has been closed " + this.hasBeenClosed(); str += "\n channelId................ " + this.channelId(); str += "\n remaining life time...... " + this.evaluateRemainingLifetime(); str += "\n subscription count....... " + this.subscriptionCount; if (this._client && this._client._secureChannel) { if (this._client._secureChannel.activeSecurityToken) { str += "\n reviseTokenLifetime...... " + this._client._secureChannel.activeSecurityToken.revisedLifetime; } } str += "\n keepAlive ................ " + this._keepAliveManager ? true : false; if (this._keepAliveManager) { str += "\n keepAlive checkInterval.. " + this._keepAliveManager.checkInterval + " ms"; str += "\n (defaultTransportTimeout)." + ClientSecureChannelLayer.defaultTransportTimeout + " ms"; str += "\n session timeout " + this.timeout + " ms"; } return str; } public getBuiltInDataType(...args: any[]): any { const nodeId = args[0]; const callback = args[1]; return getBuiltInDataType(this, nodeId) .then((dataType: DataType) => callback(null, dataType)) .catch(callback); } public async readNamespaceArray(): Promise; public readNamespaceArray(callback: (err: Error | null, namespaceArray?: string[]) => void): void; public readNamespaceArray(...args: any[]): any { const callback = args[0]; readNamespaceArray(this) .then((namespaceArray) => callback(null, namespaceArray)) .catch((err) => { callback(err); }); } public getNamespaceIndex(namespaceUri: string): number { assert(this.$$namespaceArray, "please make sure that readNamespaceArray has been called"); return this.$$namespaceArray!.indexOf(namespaceUri); } // tslint:disable:no-empty // ---------------------------------------- Alarm & condition stub public disableCondition(): void { /** empty */ } public enableCondition(): void { /** empty */ } public addCommentCondition( _conditionId: NodeIdLike, _eventId: Buffer, _comment: LocalizedTextLike, _callback?: Callback ): any { /** empty */ } public confirmCondition( _conditionId: NodeIdLike, _eventId: Buffer, _comment: LocalizedTextLike, _callback?: Callback ): any { /** empty */ } public acknowledgeCondition( _conditionId: NodeId, _eventId: Buffer, _comment: LocalizedTextLike, _callback?: Callback ): any { /** empty */ } /** * @deprecated * @private */ public findMethodId(_nodeId: NodeIdLike, _methodName: string, _callback?: ResponseCallback): any { /** empty */ } public _callMethodCondition( _methodName: string, _conditionId: NodeIdLike, _eventId: Buffer, _comment: LocalizedTextLike, _callback: Callback ): void { /** empty */ } public async extractNamespaceDataType(): Promise { return getExtraDataTypeManager(this); } public async getExtensionObjectConstructor(dataTypeNodeId: NodeId): Promise { return getExtensionObjectConstructor(this, dataTypeNodeId); } /** * construct a Extension object from a DataType and a pojo * @param dataType * @param pojo */ public async constructExtensionObject(dataType: NodeId, pojo: Record): Promise { const Constructor = await this.getExtensionObjectConstructor(dataType); return new Constructor(pojo); } private _defaultRequest(requestClass: any, _responseClass: any, options: any, callback: any) { assert(typeof callback === "function"); const request = options instanceof requestClass ? options : new requestClass(options); /* c8 ignore next */ if (doDebug) { request.trace = new Error("").stack; } /* c8 ignore next */ if (this._closeEventHasBeenEmitted) { debugLog("ClientSession#_defaultRequest => session has been closed !!", request.toString()); setImmediate(() => { callback(new Error("ClientSession is closed !")); }); return; } this.performMessageTransaction(request, (err: Error | null, response?: Response) => { if (this._closeEventHasBeenEmitted) { debugLog( "ClientSession#_defaultRequest ... err =", err ? err.message : "null", response ? response.toString() : " null" ); } /* c8 ignore next */ if (err) { debugLog("Client session : performMessageTransaction error = ", err.message); // let intercept interesting error message if (err.message.match(/BadSessionClosed/)) { // the session has been closed by Server // probably due to timeout issue // let's print some statistics const now = Date.now(); /* c8 ignore next */ if (doDebug) { debugLog(chalk.bgWhite.red(" server send BadSessionClosed !")); debugLog(chalk.bgWhite.red(" request was "), request.toString()); debugLog(" timeout.................. ", this.timeout); debugLog( " lastRequestSentTime...... ", new Date(this.lastRequestSentTime).toISOString(), now - this.lastRequestSentTime.getTime() ); debugLog( " lastResponseReceivedTime. ", new Date(this.lastResponseReceivedTime).toISOString(), now - this.lastResponseReceivedTime.getTime() ); } // DO NOT TERMINATE SESSION, as we will need a publishEngine when we // reconnect this._terminatePublishEngine(); if (false) { // ER 10.2019 /** * send when the session has been closed by the server ( probably due to inactivity and timeout) * @event session_closed */ this.emitCloseEvent(StatusCodes.BadSessionClosed); } } return callback(err, response); } callback(null, response); }); } #_recreate_session_and_reperform_transaction(request: Request, callback: (err: Error | null, response?: Response) => void) { warningLog("attempt to recreate session to reperform a transaction ", request.constructor.name); if (this.recursive_repair_detector >= 1) { // tslint:disable-next-line: no-console warningLog("recreate_session_and_reperform_transaction => Already in Progress"); return callback(new Error("Cannot recreate session")); } this.recursive_repair_detector += 1; warningLog(chalk.red("----------------> Repairing Client Session as Server believes it is invalid now ")); repair_client_session(this._client!, this, (err?: Error) => { this.recursive_repair_detector -= 1; if (err) { warningLog(chalk.red("----------------> session Repaired has failed with error", err.message)); return callback(err); } warningLog(chalk.red("----------------> session Repaired, now redoing original transaction ")); this._performMessageTransaction(request, callback); }); } } async function promoteOpaqueStructureForCallMethodResult( session: IBasicSessionAsync2, callMethodResult: CallMethodResult ): Promise { if (!callMethodResult || !callMethodResult.outputArguments || callMethodResult.outputArguments.length === 0) { return; } await promoteOpaqueStructure( session, callMethodResult.outputArguments.map((a) => ({ value: a })) ); } function countOpaqueStructures(callMethodResults: CallMethodResult[]): number { const x = (a: Variant[] | null): PseudoDataValue[] => { if (a === null) return [] as PseudoDataValue[]; return a.map((value) => { return { value: value }; }); }; const opaqueStructureCount = callMethodResults.reduce((prev, callMethodResult) => { return prev + extractDataValueToPromote(x(callMethodResult.outputArguments)).length; }, 0); return opaqueStructureCount; } async function promoteOpaqueStructureForCall(session: IBasicSessionAsync2, callMethodResults: CallMethodResult[]): Promise { const opaqueStructureCount = countOpaqueStructures(callMethodResults); if (0 === opaqueStructureCount) return; // construct dataTypeManager if not already present await getExtraDataTypeManager(session); const promises: Promise[] = callMethodResults.map(async (x: CallMethodResult) => promoteOpaqueStructureForCallMethodResult(session, x) ); await Promise.all(promises); } // tslint:disable:no-var-requires // tslint:disable:max-line-length import { withCallback } from "thenify-ex"; const opts = { multiArgs: false }; ClientSessionImpl.prototype.browse = withCallback(ClientSessionImpl.prototype.browse, opts); ClientSessionImpl.prototype.browseNext = withCallback(ClientSessionImpl.prototype.browseNext, opts); ClientSessionImpl.prototype.readVariableValue = withCallback(ClientSessionImpl.prototype.readVariableValue, opts); ClientSessionImpl.prototype.readHistoryValue = withCallback(ClientSessionImpl.prototype.readHistoryValue, opts); ClientSessionImpl.prototype.readAggregateValue = withCallback(ClientSessionImpl.prototype.readAggregateValue, opts); ClientSessionImpl.prototype.historyRead = withCallback(ClientSessionImpl.prototype.historyRead, opts); ClientSessionImpl.prototype.write = withCallback(ClientSessionImpl.prototype.write, opts); ClientSessionImpl.prototype.writeSingleNode = withCallback(ClientSessionImpl.prototype.writeSingleNode, opts); ClientSessionImpl.prototype.readAllAttributes = withCallback(ClientSessionImpl.prototype.readAllAttributes, opts); ClientSessionImpl.prototype.read = withCallback(ClientSessionImpl.prototype.read, opts); ClientSessionImpl.prototype.createSubscription = withCallback(ClientSessionImpl.prototype.createSubscription, opts); ClientSessionImpl.prototype.createSubscription2 = withCallback(ClientSessionImpl.prototype.createSubscription2, opts); ClientSessionImpl.prototype.deleteSubscriptions = withCallback(ClientSessionImpl.prototype.deleteSubscriptions, opts); ClientSessionImpl.prototype.transferSubscriptions = withCallback(ClientSessionImpl.prototype.transferSubscriptions, opts); ClientSessionImpl.prototype.createMonitoredItems = withCallback(ClientSessionImpl.prototype.createMonitoredItems, opts); ClientSessionImpl.prototype.modifyMonitoredItems = withCallback(ClientSessionImpl.prototype.modifyMonitoredItems, opts); ClientSessionImpl.prototype.modifySubscription = withCallback(ClientSessionImpl.prototype.modifySubscription, opts); ClientSessionImpl.prototype.setTriggering = withCallback(ClientSessionImpl.prototype.setTriggering, opts); ClientSessionImpl.prototype.setMonitoringMode = withCallback(ClientSessionImpl.prototype.setMonitoringMode, opts); ClientSessionImpl.prototype.publish = withCallback(ClientSessionImpl.prototype.publish, opts); ClientSessionImpl.prototype.republish = withCallback(ClientSessionImpl.prototype.republish, opts); ClientSessionImpl.prototype.deleteMonitoredItems = withCallback(ClientSessionImpl.prototype.deleteMonitoredItems, opts); ClientSessionImpl.prototype.setPublishingMode = withCallback(ClientSessionImpl.prototype.setPublishingMode, opts); ClientSessionImpl.prototype.translateBrowsePath = withCallback(ClientSessionImpl.prototype.translateBrowsePath, opts); ClientSessionImpl.prototype.performMessageTransaction = withCallback(ClientSessionImpl.prototype.performMessageTransaction, opts); ClientSessionImpl.prototype.close = withCallback(ClientSessionImpl.prototype.close, opts); ClientSessionImpl.prototype.call = withCallback(ClientSessionImpl.prototype.call, opts); ClientSessionImpl.prototype.getMonitoredItems = withCallback(ClientSessionImpl.prototype.getMonitoredItems, opts); ClientSessionImpl.prototype.getArgumentDefinition = withCallback(ClientSessionImpl.prototype.getArgumentDefinition, opts); ClientSessionImpl.prototype.queryFirst = withCallback(ClientSessionImpl.prototype.queryFirst, opts); ClientSessionImpl.prototype.registerNodes = withCallback(ClientSessionImpl.prototype.registerNodes, opts); ClientSessionImpl.prototype.unregisterNodes = withCallback(ClientSessionImpl.prototype.unregisterNodes, opts); ClientSessionImpl.prototype.readNamespaceArray = withCallback(ClientSessionImpl.prototype.readNamespaceArray, opts); ClientSessionImpl.prototype.getBuiltInDataType = withCallback(ClientSessionImpl.prototype.getBuiltInDataType, opts); ClientSessionImpl.prototype.changeUser = withCallback(ClientSessionImpl.prototype.changeUser, opts);