UNPKG

node-opcua-client

Version:

pure nodejs OPCUA SDK - module client

218 lines 11.5 kB
"use strict"; /** * @module node-opcua-client */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ClientSessionKeepAliveManager = void 0; const chalk_1 = __importDefault(require("chalk")); const events_1 = require("events"); const node_opcua_assert_1 = require("node-opcua-assert"); const node_opcua_basic_types_1 = require("node-opcua-basic-types"); const node_opcua_common_1 = require("node-opcua-common"); const node_opcua_constants_1 = require("node-opcua-constants"); const node_opcua_debug_1 = require("node-opcua-debug"); const node_opcua_nodeid_1 = require("node-opcua-nodeid"); const node_opcua_secure_channel_1 = require("node-opcua-secure-channel"); const node_opcua_status_code_1 = require("node-opcua-status-code"); const serverStatusStateNodeId = (0, node_opcua_nodeid_1.coerceNodeId)(node_opcua_constants_1.VariableIds.Server_ServerStatus_State); const debugLog = (0, node_opcua_debug_1.make_debugLog)(__filename); const doDebug = (0, node_opcua_debug_1.checkDebugFlag)(__filename); const warningLog = (0, node_opcua_debug_1.make_warningLog)(__filename); const maxBackoffInterval = 60_000; class ClientSessionKeepAliveManager extends events_1.EventEmitter { session; timerId; pingTimeout; lastKnownState; transactionInProgress = false; consecutiveFailures = 0; count = 0; checkInterval; constructor(session) { super(); this.session = session; this.timerId = undefined; this.pingTimeout = 0; this.checkInterval = 0; this.count = 0; } start(keepAliveInterval) { (0, node_opcua_assert_1.assert)(!this.timerId); /* c8 ignore next*/ if (this.session.timeout < 600) { warningLog(`[NODE-OPCUA-W13] ClientSessionKeepAliveManager detected that the session timeout (${this.session.timeout} ms) is really too small: please adjust it to a greater value ( at least 1000))`); } /* c8 ignore next*/ if (this.session.timeout < 100) { throw new Error(`ClientSessionKeepAliveManager detected that the session timeout (${this.session.timeout} ms) is really too small: please adjust it to a greater value ( at least 1000))`); } const selectedCheckInterval = keepAliveInterval || Math.min(Math.floor(Math.min((this.session.timeout * 2) / 3, 20000)), node_opcua_secure_channel_1.ClientSecureChannelLayer.defaultTransportTimeout); this.checkInterval = selectedCheckInterval; this.pingTimeout = Math.floor(Math.min(Math.max(50, selectedCheckInterval / 2), 20000)); // make sure first one is almost immediate this.timerId = setTimeout(() => this.ping_server(), this.pingTimeout); } stop() { if (this.timerId) { debugLog("ClientSessionKeepAliveManager#stop"); clearTimeout(this.timerId); this.timerId = undefined; } else { debugLog("warning ClientSessionKeepAliveManager#stop ignore (already stopped)"); } } ping_server() { this._ping_server().then((delta) => { if (!this.session || this.session.hasBeenClosed()) { return; // stop here } if (this.timerId) { // When delta exceeds checkInterval it is an explicit backoff requested by _ping_server; // otherwise delta is the time already consumed this cycle. const timeout = delta > this.checkInterval ? delta : Math.max(1, this.checkInterval - delta); this.timerId = setTimeout(() => this.ping_server(), timeout); } }); } /** * @private * when a session is opened on a server, the client shall send request on a regular basis otherwise the server * session object might time out. * start_ping make sure that ping_server is called on a regular basis to prevent session to timeout. * */ async _ping_server() { const session = this.session; if (!session || session.isReconnecting) { debugLog("ClientSessionKeepAliveManager#ping_server => no session available"); return 0; } if (!this.timerId) { return 0; // keep-alive has been canceled .... } const now = Date.now(); const timeSinceLastServerContact = now - session.lastResponseReceivedTime.getTime(); if (timeSinceLastServerContact < this.pingTimeout) { debugLog("ClientSessionKeepAliveManager#ping_server skipped because last communication with server was not that long ago ping timeout=", Math.round(this.pingTimeout), "timeSinceLastServerContact = ", timeSinceLastServerContact); // no need to send a ping yet return timeSinceLastServerContact - this.pingTimeout; } if (session.isReconnecting) { debugLog("ClientSessionKeepAliveManager#ping_server skipped because client is reconnecting"); return 0; } if (session.hasBeenClosed()) { debugLog("ClientSessionKeepAliveManager#ping_server skipped because client is reconnecting"); return 0; } debugLog("ClientSessionKeepAliveManager#ping_server timeSinceLastServerContact=", timeSinceLastServerContact, "timeout", this.session.timeout); if (this.transactionInProgress) { // readVariable already taking place ! Ignore return 0; } this.transactionInProgress = true; // Server_ServerStatus_State return new Promise((resolve) => { session.read({ nodeId: serverStatusStateNodeId, attributeId: node_opcua_basic_types_1.AttributeIds.Value }, (err, dataValue) => { this.transactionInProgress = false; if (err) { warningLog(chalk_1.default.cyan(" warning : ClientSessionKeepAliveManager#ping_server "), chalk_1.default.yellow(err.message)); const serviceFaultResponse = err.response; if (serviceFaultResponse) { const sc = serviceFaultResponse.responseHeader?.serviceResult; if (sc?.equals(node_opcua_status_code_1.StatusCodes.BadSessionIdInvalid) || sc?.equals(node_opcua_status_code_1.StatusCodes.BadSessionClosed)) { this.emit("failure"); warningLog("Keep alive has failed, considering a network outage is in place, forcing a reconnection"); terminateConnection(session._client); resolve(0); } else { if (sc?.equals(node_opcua_status_code_1.StatusCodes.BadInvalidTimestamp)) { // BadInvalidTimestamp (OPC UA Part 4 7.38.2, Table 178: // "The timestamp is outside the range allowed by the Server") // refers to the timestamp field of the RequestHeader // (OPC UA Part 4 7.32), which the spec states is used // "only for diagnostic and logging purposes in the Server". // // The server responded at the OPC UA application layer: // the SecureChannel and Session are intact. The cause is // clock skew between client and server; this is an // infrastructure concern outside the scope of the keepalive // manager. // // Treating this as a keepalive failure is semantically // incorrect: the round-trip succeeded. Incrementing // consecutiveFailures leads to unbounded exponential backoff // and eventual session expiry server-side, triggering an // unnecessary reconnect loop. // // See: https://reference.opcfoundation.org/Core/Part4/v105/docs/7.38.2 // https://reference.opcfoundation.org/Core/Part4/v105/docs/7.32 this.consecutiveFailures = 0; debugLog("emit keepalive (BadInvalidTimestamp: session alive, clock skew on request timestamp)"); this.emit("keepalive", this.lastKnownState ?? node_opcua_common_1.ServerState.Unknown, this.count); resolve(0); return; } this.consecutiveFailures++; warningLog("Keep alive received ServiceFault from server (session intact):", sc?.toString()); this.emit("keepalive_failure"); resolve(Math.min(this.checkInterval * 2 ** this.consecutiveFailures, maxBackoffInterval)); } } else { this.emit("failure"); warningLog("Keep alive has failed, considering a network outage is in place, forcing a reconnection"); terminateConnection(session._client); resolve(0); } return; } if (!dataValue || !dataValue.value) { /** * @event failure * raised when the server is not responding or is responding with en error to * the keep alive read Variable value transaction */ this.emit("failure"); warningLog("Keep alive has failed, considering a network outage is in place, forcing a reconnection"); terminateConnection(session._client); resolve(0); return; } if (dataValue.statusCode.isGood()) { const newState = dataValue.value.value; // c8 ignore next if (newState !== this.lastKnownState && this.lastKnownState) { warningLog("ClientSessionKeepAliveManager#Server state has changed = ", node_opcua_common_1.ServerState[newState], " was ", node_opcua_common_1.ServerState[this.lastKnownState]); } this.lastKnownState = newState; this.count++; // increase successful counter } this.consecutiveFailures = 0; debugLog("emit keepalive"); this.emit("keepalive", this.lastKnownState, this.count); resolve(0); }); }); } } exports.ClientSessionKeepAliveManager = ClientSessionKeepAliveManager; function terminateConnection(client) { if (!client) return; const channel = client._secureChannel; if (!channel) { return; } channel.forceConnectionBreak(); } //# sourceMappingURL=client_session_keepalive_manager.js.map