/* global require */
"use strict";
/**
* @module opcua.server
*/
var _ = require("underscore");
var assert = require("node-opcua-assert");
var async = require("async");
var util = require("util");
var EventEmitter = require("events").EventEmitter;
var SessionContext = require("node-opcua-address-space").SessionContext;
var NodeId = require("node-opcua-nodeid").NodeId;
var resolveNodeId = require("node-opcua-nodeid").resolveNodeId;
var makeNodeId = require("node-opcua-nodeid").makeNodeId;
var NodeIdType = require("node-opcua-nodeid").NodeIdType;
var NumericRange = require("node-opcua-numeric-range").NumericRange;
var BrowseDirection = require("node-opcua-data-model").BrowseDirection;
var BrowseResult = require("node-opcua-service-browse").BrowseResult;
var ReadRequest = require("node-opcua-service-read").ReadRequest;
require("node-opcua-common");
var read_service = require("node-opcua-service-read");
var AttributeIds = require("node-opcua-data-model").AttributeIds;
var TimestampsToReturn = read_service.TimestampsToReturn;
var UAVariable = require("node-opcua-address-space").UAVariable;
var historizing_service = require("node-opcua-service-history");
var HistoryReadRequest = historizing_service.HistoryReadRequest;
var HistoryReadDetails = historizing_service.HistoryReadDetails;
var HistoryReadResult = historizing_service.HistoryReadResult;
var DataValue = require("node-opcua-data-value").DataValue;
var Variant = require("node-opcua-variant").Variant;
var DataType = require("node-opcua-variant").DataType;
var VariantArrayType = require("node-opcua-variant").VariantArrayType;
var isValidVariant = require("node-opcua-variant").isValidVariant;
var StatusCodes = require("node-opcua-status-code").StatusCodes;
var StatusCode = require("node-opcua-status-code").StatusCode;
require("node-opcua-common");
var address_space = require("node-opcua-address-space");
var AddressSpace = address_space.AddressSpace;
var generate_address_space = require("node-opcua-address-space").generate_address_space;
var ServerSession = require("./server_session").ServerSession;
var VariableIds = require("node-opcua-constants").VariableIds;
var MethodIds = require("node-opcua-constants").MethodIds;
var ReferenceType = require("node-opcua-address-space").ReferenceType;
var ServerState = require("node-opcua-common").ServerState;
var ServerStatus = require("node-opcua-common").ServerStatus;
var ServerDiagnosticsSummary = require("node-opcua-common").ServerDiagnosticsSummary;
var endpoints_service = require("node-opcua-service-endpoints");
var ApplicationDescription = endpoints_service.ApplicationDescription;
var nodesets = require("node-opcua-nodesets");
exports.standard_nodeset_file = nodesets.standard_nodeset_file;
exports.di_nodeset_filename = nodesets.di_nodeset_filename;
exports.adi_nodeset_filename = nodesets.adi_nodeset_filename;
var mini_nodeset_filename = require("node-opcua-address-space/test_helpers/get_mini_address_space").mini_nodeset_filename;
exports.mini_nodeset_filename = mini_nodeset_filename;
var debugLog = require("node-opcua-debug").make_debugLog(__filename);
var doDebug = require("node-opcua-debug").checkDebugFlag(__filename);
var ServerCapabilities = require("./server_capabilities").ServerCapabilities;
var HistoryServerCapabilities = require("./history_server_capabilities").HistoryServerCapabilities;
var eoan = require("node-opcua-address-space");
var ServerSidePublishEngine = require("./server_publish_engine").ServerSidePublishEngine;
/**
* @class ServerEngine
* @extends EventEmitter
* @uses ServerSidePublishEngine
* @param options {Object}
* @param options.buildInfo
* @param [options.isAuditing = false ] {Boolean}
* @param [options.serverCapabilities]
* @param [options.serverCapabilities.serverProfileArray]
* @param [options.serverCapabilities.localeIdArray]
* @param options.applicationUri {String} the application URI.
* @param [options.historyServerCapabilities]
* @constructor
*/
function ServerEngine(options) {
options = options || {};
options.buildInfo = options.buildInfo || {};
EventEmitter.apply(this, arguments);
var self = this;
this._sessions = {};
this._closedSessions = {};
this.isAuditing = _.isBoolean(options.isAuditing) ? options.isAuditing : false;
options.buildInfo.buildDate = options.buildInfo.buildDate || new Date();
// ---------------------------------------------------- ServerStatus
this.serverStatus = new ServerStatus({
startTime: new Date(),
currentTime: new Date(),
state: ServerState.NoConfiguration,
buildInfo: options.buildInfo,
secondsTillShutdown: 0,
shutdownReason: {text: ""}
});
// --------------------------------------------------- ServerCapabilities
options.serverCapabilities = options.serverCapabilities || {};
options.serverCapabilities.serverProfileArray = options.serverCapabilities.serverProfileArray || [];
options.serverCapabilities.localeIdArray = options.serverCapabilities.localeIdArray || ["en-EN", "fr-FR"];
this.serverCapabilities = new ServerCapabilities(options.serverCapabilities);
this.historyServerCapabilities = new HistoryServerCapabilities(options.historyServerCapabilities);
// --------------------------------------------------- serverDiagnosticsSummary extension Object
this.serverDiagnosticsSummary = new ServerDiagnosticsSummary({});
assert(this.serverDiagnosticsSummary.hasOwnProperty("currentSessionCount"));
this.serverDiagnosticsSummary.__defineGetter__("currentSessionCount", function () {
return Object.keys(self._sessions).length;
});
// note spelling is different for serverDiagnosticsSummary.currentSubscriptionCount
// and sessionDiagnostics.currentSubscriptionsCount ( with an s)
assert(this.serverDiagnosticsSummary.hasOwnProperty("currentSubscriptionCount"));
this.serverDiagnosticsSummary.__defineGetter__("currentSubscriptionCount", function () {
// currentSubscriptionCount returns the total number of subscriptions
// that are currently active on all sessions
var counter = 0;
_.values(self._sessions).forEach(function (session) {
counter += session.currentSubscriptionCount;
});
return counter;
});
this.status = "creating";
this.setServerState(ServerState.NoConfiguration);
this.addressSpace = null;
this._shutdownTask = [];
this._applicationUri = options.applicationUri || "<unset _applicationUri>";
}
util.inherits(ServerEngine, EventEmitter);
ServerEngine.prototype.__defineGetter__("startTime", function () {
return this.serverStatus.startTime;
});
ServerEngine.prototype.__defineGetter__("currentTime", function () {
return this.serverStatus.currentTime;
});
ServerEngine.prototype.__defineGetter__("buildInfo", function () {
return this.serverStatus.buildInfo;
});
/**
* register a function that will be called when the server will perform its shut down.
* @method registerShutdownTask
*/
ServerEngine.prototype.registerShutdownTask = function (task) {
assert(_.isFunction(task));
this._shutdownTask.push(task);
};
function shutdownAndDisposeAddressSpace() {
/* jshint validthis:true */
if (this.addressSpace) {
assert(this.addressSpace instanceof AddressSpace);
this.addressSpace.shutdown();
this.addressSpace.dispose();
delete this.addressSpace;
}
}
/**
* @method shutdown
*/
ServerEngine.prototype.shutdown = function () {
var self = this;
self.status = "shutdown";
self.setServerState(ServerState.Shutdown);
// delete any existing sessions
var tokens = Object.keys(self._sessions).map(function (key) {
var session = self._sessions[key];
return session.authenticationToken;
});
//xx console.log("xxxxxxxxx ServerEngine.shutdown must terminate "+ tokens.length," sessions");
tokens.forEach(function (token) {
self.closeSession(token, true, "Terminated");
});
// all sessions must have been terminated
assert(self.currentSessionCount === 0);
self._shutdownTask.push(shutdownAndDisposeAddressSpace);
// perform registerShutdownTask
self._shutdownTask.forEach(function (task) {
task.call(self);
});
};
/**
* the number of active sessions
* @property currentSessionCount
* @type {Number}
*/
ServerEngine.prototype.__defineGetter__("currentSessionCount", function () {
return this.serverDiagnosticsSummary.currentSessionCount;
});
/**
* the cumulated number of sessions that have been opened since this object exists
* @property cumulatedSessionCount
* @type {Number}
*/
ServerEngine.prototype.__defineGetter__("cumulatedSessionCount", function () {
return this.serverDiagnosticsSummary.cumulatedSessionCount;
});
/**
* the number of active subscriptions.
* @property currentSubscriptionCount
* @type {Number}
*/
ServerEngine.prototype.__defineGetter__("currentSubscriptionCount", function () {
return this.serverDiagnosticsSummary.currentSubscriptionCount;
});
/**
* the cumulated number of subscriptions that have been created since this object exists
* @property cumulatedSubscriptionCount
* @type {Number}
*/
ServerEngine.prototype.__defineGetter__("cumulatedSubscriptionCount", function () {
return this.serverDiagnosticsSummary.cumulatedSubscriptionCount;
});
ServerEngine.prototype.__defineGetter__("rejectedSessionCount", function () {
return this.serverDiagnosticsSummary.rejectedSessionCount;
});
ServerEngine.prototype.__defineGetter__("rejectedRequestsCount", function () {
return this.serverDiagnosticsSummary.rejectedRequestsCount;
});
ServerEngine.prototype.__defineGetter__("sessionAbortCount", function () {
return this.serverDiagnosticsSummary.sessionAbortCount;
});
ServerEngine.prototype.__defineGetter__("sessionTimeoutCount", function () {
return this.serverDiagnosticsSummary.sessionTimeoutCount;
});
ServerEngine.prototype.__defineGetter__("publishingIntervalCount", function () {
return this.serverDiagnosticsSummary.publishingIntervalCount;
});
/**
* @method secondsTillShutdown
* @return {UInt32} the approximate number of seconds until the server will be shut down. The
* value is only relevant once the state changes into SHUTDOWN.
*/
ServerEngine.prototype.secondsTillShutdown = function () {
// ToDo: implement a correct solution here
return 0;
};
var CallMethodResult = require("node-opcua-service-call").CallMethodResult;
// binding methods
function getMonitoredItemsId(inputArguments, context, callback) {
assert(_.isArray(inputArguments));
assert(_.isFunction(callback));
assert(context.hasOwnProperty("session"), " expecting a session id in the context object");
var session = context.session;
if (!session) {
return callback(null, {statusCode: StatusCodes.BadInternalError});
}
var subscriptionId = inputArguments[0].value;
var subscription = session.getSubscription(subscriptionId);
if (!subscription) {
return callback(null, {statusCode: StatusCodes.BadSubscriptionIdInvalid});
}
var result = subscription.getMonitoredItems();
assert(result.statusCode);
assert(_.isArray(result.serverHandles));
assert(_.isArray(result.clientHandles));
assert(result.serverHandles.length === result.clientHandles.length);
var 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);
}
/**
* the name of the server
* @property serverName
* @type String
*/
ServerEngine.prototype.__defineGetter__("serverName", function () {
return this.serverStatus.buildInfo.productName;
});
/**
* the server urn
* @property serverNameUrn
* @type String
*/
ServerEngine.prototype.__defineGetter__("serverNameUrn", function () {
return this._applicationUri;
});
/**
* the urn of the server namespace
* @property serverNamespaceName
* @type String
*/
ServerEngine.prototype.__defineGetter__("serverNamespaceUrn", function () {
return this._applicationUri; // "urn:" + this.serverName;
});
ServerEngine.prototype.setServerState = function (serverState) {
assert(serverState !== null && serverState !== undefined);
this.serverStatus.state = serverState;
};
/**
* @method initialize
* @async
*
* @param options {Object}
* @param options.nodeset_filename {String} - [option](default : 'mini.Node.Set2.xml' )
* @param callback
*/
ServerEngine.prototype.initialize = function (options, callback) {
var self = this;
assert(!self.addressSpace); // check that 'initialize' has not been already called
self.status = "initializing";
options = options || {};
assert(_.isFunction(callback));
options.nodeset_filename = options.nodeset_filename || nodesets.standard_nodeset_file;
var startTime = new Date();
debugLog("Loading ", options.nodeset_filename, "...");
self.addressSpace = new AddressSpace();
// register namespace 1 (our namespace);
var serverNamespaceIndex = self.addressSpace.registerNamespace(self.serverNamespaceUrn);
assert(serverNamespaceIndex === 1);
generate_address_space(self.addressSpace, options.nodeset_filename, function () {
var endTime = new Date();
debugLog("Loading ", options.nodeset_filename, " done : ", endTime - startTime, " ms");
function findObjectNodeId(name) {
var obj = self.addressSpace.findNode(name);
return obj ? obj.nodeId : null;
}
self.FolderTypeId = findObjectNodeId("FolderType");
self.BaseObjectTypeId = findObjectNodeId("BaseObjectType");
self.BaseDataVariableTypeId = findObjectNodeId("BaseDataVariableType");
self.rootFolder = self.addressSpace.findNode("RootFolder");
assert(self.rootFolder && self.rootFolder.readAttribute, " must provide a root folder and expose a readAttribute method");
self.setServerState(ServerState.Running);
function bindVariableIfPresent(nodeId, opts) {
assert(nodeId instanceof NodeId);
assert(!nodeId.isEmpty());
var obj = self.addressSpace.findNode(nodeId);
if (obj) {
__bindVariable(self, nodeId, opts);
}
return obj;
}
// -------------------------------------------- install default get/put handler
var server_NamespaceArray_Id = makeNodeId(VariableIds.Server_NamespaceArray); // ns=0;i=2255
bindVariableIfPresent(server_NamespaceArray_Id, {
get: function () {
return new Variant({
dataType: DataType.String,
arrayType: VariantArrayType.Array,
value: self.addressSpace.getNamespaceArray()
});
},
set: null // read only
});
var server_NameUrn_var = new Variant({
dataType: DataType.String,
arrayType: VariantArrayType.Array,
value: [
self.serverNameUrn // this is us !
]
});
var server_ServerArray_Id = makeNodeId(VariableIds.Server_ServerArray); // ns=0;i=2254
bindVariableIfPresent(server_ServerArray_Id, {
get: function () {
return server_NameUrn_var;
},
set: null // read only
});
function bindStandardScalar(id, dataType, func, setter_func) {
assert(_.isNumber(id));
assert(_.isFunction(func));
assert(_.isFunction(setter_func) || !setter_func);
assert(dataType !== null); // check invalid dataType
var setter_func2 = null;
if (setter_func) {
setter_func2 = function (variant) {
var variable2 = !!variant.value;
setter_func(variable2);
return StatusCodes.Good;
};
}
var 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.Scalar, dataType, func()));
return bindVariableIfPresent(nodeId, {
get: function () {
return new Variant({
dataType: dataType,
arrayType: VariantArrayType.Scalar,
value: func()
});
},
set: setter_func2
});
}
function bindStandardArray(id, variantDataType, dataType, func) {
assert(_.isFunction(func));
assert(variantDataType !== null); // check invalid dataType
var 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, dataType, func()));
bindVariableIfPresent(nodeId, {
get: function () {
var value = func();
assert(_.isArray(value));
return new Variant({
dataType: variantDataType,
arrayType: VariantArrayType.Array,
value: value
});
},
set: null // read only
});
}
bindStandardScalar(VariableIds.Server_ServiceLevel,
DataType.Byte, function () {
return 255;
});
bindStandardScalar(VariableIds.Server_Auditing,
DataType.Boolean, function () {
return self.isAuditing;
});
function bindServerDiagnostics() {
var serverDiagnostics_Enabled = false;
bindStandardScalar(VariableIds.Server_ServerDiagnostics_EnabledFlag,
DataType.Boolean, function () {
return serverDiagnostics_Enabled;
}, function (newFlag) {
serverDiagnostics_Enabled = newFlag;
});
var serverDiagnosticsSummary = self.addressSpace.findNode(makeNodeId(VariableIds.Server_ServerDiagnostics_ServerDiagnosticsSummary));
if (serverDiagnosticsSummary) {
serverDiagnosticsSummary.bindExtensionObject(self.serverDiagnosticsSummary);
}
}
function bindServerStatus() {
var serverStatusNode = self.addressSpace.findNode(makeNodeId(VariableIds.Server_ServerStatus));
if (!serverStatusNode) {
return;
}
if (serverStatusNode) {
serverStatusNode.bindExtensionObject(self.serverStatus);
//xx serverStatusNode.updateExtensionObjectPartial(self.serverStatus);
//xx self.serverStatus = serverStatusNode.$extensionObject;
serverStatusNode.minimumSamplingInterval = 1000;
}
var currentTimeNode = self.addressSpace.findNode(makeNodeId(VariableIds.Server_ServerStatus_CurrentTime));
if (currentTimeNode) {
currentTimeNode.minimumSamplingInterval = 1000;
}
var secondsTillShutdown = self.addressSpace.findNode(makeNodeId(VariableIds.Server_ServerStatus_SecondsTillShutdown));
if (secondsTillShutdown) {
secondsTillShutdown.minimumSamplingInterval = 1000;
}
serverStatusNode.$extensionObject = new Proxy(serverStatusNode.$extensionObject, {
get: function (target, prop) {
if (prop === "currentTime") {
serverStatusNode.currentTime.touchValue();
return new Date();
} else if (prop === "secondsTillShutdown") {
serverStatusNode.secondsTillShutdown.touchValue();
return self.secondsTillShutdown();
}
return target[prop];
}
});
}
function bindServerCapabilities() {
bindStandardArray(VariableIds.Server_ServerCapabilities_ServerProfileArray,
DataType.String, "String", function () {
return self.serverCapabilities.serverProfileArray;
});
bindStandardArray(VariableIds.Server_ServerCapabilities_LocaleIdArray,
DataType.String, "LocaleId", function () {
return self.serverCapabilities.localeIdArray;
});
bindStandardScalar(VariableIds.Server_ServerCapabilities_MinSupportedSampleRate,
DataType.Double, function () {
return self.serverCapabilities.minSupportedSampleRate;
});
bindStandardScalar(VariableIds.Server_ServerCapabilities_MaxBrowseContinuationPoints,
DataType.UInt16, function () {
return self.serverCapabilities.maxBrowseContinuationPoints;
});
bindStandardScalar(VariableIds.Server_ServerCapabilities_MaxQueryContinuationPoints,
DataType.UInt16, function () {
return self.serverCapabilities.maxQueryContinuationPoints;
});
bindStandardScalar(VariableIds.Server_ServerCapabilities_MaxHistoryContinuationPoints,
DataType.UInt16, function () {
return self.serverCapabilities.maxHistoryContinuationPoints;
});
bindStandardArray(VariableIds.Server_ServerCapabilities_SoftwareCertificates,
DataType.ByteString, "SoftwareCertificates", function () {
return self.serverCapabilities.softwareCertificates;
});
bindStandardScalar(VariableIds.Server_ServerCapabilities_MaxArrayLength,
DataType.UInt32, function () {
return self.serverCapabilities.maxArrayLength;
});
bindStandardScalar(VariableIds.Server_ServerCapabilities_MaxStringLength,
DataType.UInt32, function () {
return self.serverCapabilities.maxStringLength;
});
function bindOperationLimits(operationLimits) {
assert(_.isObject(operationLimits));
function upperCaseFirst(str) {
return str.slice(0, 1).toUpperCase() + str.slice(1);
}
//Xx bindStandardArray(VariableIds.Server_ServerCapabilities_OperationLimits_MaxNodesPerWrite,
//Xx DataType.UInt32, "UInt32", function () { return operationLimits.maxNodesPerWrite; });
var keys = Object.keys(operationLimits);
keys.forEach(function (key) {
var uid = "Server_ServerCapabilities_OperationLimits_" + upperCaseFirst(key);
var nodeId = makeNodeId(VariableIds[uid]);
//xx console.log("xxx Binding ".bgCyan,uid,nodeId.toString());
assert(!nodeId.isEmpty());
bindStandardScalar(VariableIds[uid],
DataType.UInt32, function () {
return operationLimits[key];
});
});
}
bindOperationLimits(self.serverCapabilities.operationLimits);
}
function bindHistoryServerCapabilities() {
bindStandardScalar(VariableIds.HistoryServerCapabilities_MaxReturnDataValues,
DataType.UInt32, function () {
return self.historyServerCapabilities.maxReturnDataValues;
});
bindStandardScalar(VariableIds.HistoryServerCapabilities_MaxReturnEventValues,
DataType.UInt32, function () {
return self.historyServerCapabilities.maxReturnEventValues;
});
bindStandardScalar(VariableIds.HistoryServerCapabilities_AccessHistoryDataCapability,
DataType.Boolean, function () {
return self.historyServerCapabilities.accessHistoryDataCapability;
});
bindStandardScalar(VariableIds.HistoryServerCapabilities_AccessHistoryEventsCapability,
DataType.Boolean, function () {
return self.historyServerCapabilities.accessHistoryEventsCapability;
});
bindStandardScalar(VariableIds.HistoryServerCapabilities_InsertDataCapability,
DataType.Boolean, function () {
return self.historyServerCapabilities.insertDataCapability;
});
bindStandardScalar(VariableIds.HistoryServerCapabilities_ReplaceDataCapability,
DataType.Boolean, function () {
return self.historyServerCapabilities.replaceDataCapability;
});
bindStandardScalar(VariableIds.HistoryServerCapabilities_UpdateDataCapability,
DataType.Boolean, function () {
return self.historyServerCapabilities.updateDataCapability;
});
bindStandardScalar(VariableIds.HistoryServerCapabilities_InsertEventCapability,
DataType.Boolean, function () {
return self.historyServerCapabilities.insertEventCapability;
});
bindStandardScalar(VariableIds.HistoryServerCapabilities_ReplaceEventCapability,
DataType.Boolean, function () {
return self.historyServerCapabilities.replaceEventCapability;
});
bindStandardScalar(VariableIds.HistoryServerCapabilities_UpdateEventCapability,
DataType.Boolean, function () {
return self.historyServerCapabilities.updateEventCapability;
});
bindStandardScalar(VariableIds.HistoryServerCapabilities_DeleteEventCapability,
DataType.Boolean, function () {
return self.historyServerCapabilities.deleteEventCapability;
});
bindStandardScalar(VariableIds.HistoryServerCapabilities_DeleteRawCapability,
DataType.Boolean, function () {
return self.historyServerCapabilities.deleteRawCapability;
});
bindStandardScalar(VariableIds.HistoryServerCapabilities_DeleteAtTimeCapability,
DataType.Boolean, function () {
return self.historyServerCapabilities.deleteAtTimeCapability;
});
bindStandardScalar(VariableIds.HistoryServerCapabilities_InsertAnnotationCapability,
DataType.Boolean, function () {
return self.historyServerCapabilities.insertAnnotationCapability;
});
}
bindServerDiagnostics();
bindServerStatus();
bindServerCapabilities();
bindHistoryServerCapabilities();
function bindExtraStuff() {
// mainly for compliance
//The version number for the data type description. i=104
bindStandardScalar(VariableIds.DataTypeDescriptionType_DataTypeVersion,
DataType.UInt16, function () {
return 0.0;
});
// i=111
bindStandardScalar(VariableIds.ModellingRuleType_NamingRule,
DataType.UInt16, function () {
return 0.0;
});
// i=112
bindStandardScalar(VariableIds.ModellingRule_Mandatory_NamingRule,
DataType.UInt16, function () {
return 0.0;
});
// i=113
bindStandardScalar(VariableIds.ModellingRule_Optional_NamingRule,
DataType.UInt16, function () {
return 0.0;
});
}
bindExtraStuff();
self.__internal_bindMethod(makeNodeId(MethodIds.Server_GetMonitoredItems), getMonitoredItemsId.bind(self));
function prepareServerDiagnostics() {
var addressSpace = self.addressSpace;
if (!addressSpace.rootFolder.objects) {
return;
}
var server = addressSpace.rootFolder.objects.server;
if (!server) {
return;
}
// create SessionsDiagnosticsSummary
var serverDiagnostics = server.getComponentByName("ServerDiagnostics");
if (!serverDiagnostics) {
return;
}
var subscriptionDiagnosticsArray = serverDiagnostics.getComponentByName("SubscriptionDiagnosticsArray");
assert(subscriptionDiagnosticsArray instanceof UAVariable);
eoan.bindExtObjArrayNode(subscriptionDiagnosticsArray, "SubscriptionDiagnosticsType", "subscriptionId");
}
prepareServerDiagnostics();
self.status = "initialized";
setImmediate(callback);
});
};
var UAVariable = require("node-opcua-address-space").UAVariable;
require("node-opcua-address-space");
ServerEngine.prototype.__findObject = function (nodeId) {
// coerce nodeToBrowse to NodeId
try {
nodeId = resolveNodeId(nodeId);
}
catch (err) {
return null;
}
assert(nodeId instanceof NodeId);
return this.addressSpace.findNode(nodeId);
};
/**
*
* @method browseSingleNode
* @param nodeId {NodeId|String} : the nodeid of the element to browse
* @param browseDescription
* @param browseDescription.browseDirection {BrowseDirection} :
* @param browseDescription.referenceTypeId {String|NodeId}
* @param [session] {ServerSession}
* @return {BrowseResult}
*/
ServerEngine.prototype.browseSingleNode = function (nodeId, browseDescription, session) {
var self = this;
assert(self.addressSpace instanceof AddressSpace); // initialize not called
var addressSpace = self.addressSpace;
return addressSpace.browseSingleNode(nodeId, browseDescription, session);
};
/**
*
* @method browse
* @param nodesToBrowse {BrowseDescription[]}
* @param [session] {ServerSession}
* @return {BrowseResult[]}
*/
ServerEngine.prototype.browse = function (nodesToBrowse, session) {
var self = this;
assert(self.addressSpace instanceof AddressSpace); // initialize not called
assert(_.isArray(nodesToBrowse));
var results = [];
nodesToBrowse.forEach(function (browseDescription) {
var nodeId = resolveNodeId(browseDescription.nodeId);
var r = self.browseSingleNode(nodeId, browseDescription, session);
results.push(r);
});
return results;
};
var apply_timestamps = require("node-opcua-data-value").apply_timestamps;
/**
*
* @method readSingleNode
* @param context {SessionContext}
* @param nodeId
* @param attributeId {AttributeId}
* @param [timestampsToReturn=TimestampsToReturn.Neither]
* @return {DataValue}
*/
ServerEngine.prototype.readSingleNode = function (context, nodeId, attributeId, timestampsToReturn) {
return this._readSingleNode(context,
{
nodeId: nodeId,
attributeId: attributeId
},
timestampsToReturn);
};
ServerEngine.prototype._readSingleNode = function (context, nodeToRead, timestampsToReturn) {
assert(context instanceof SessionContext);
var self = this;
var nodeId = nodeToRead.nodeId;
var attributeId = nodeToRead.attributeId;
var indexRange = nodeToRead.indexRange;
var dataEncoding = nodeToRead.dataEncoding;
assert(self.addressSpace instanceof AddressSpace); // initialize not called
if (timestampsToReturn === TimestampsToReturn.Invalid) {
return new DataValue({statusCode: StatusCodes.BadTimestampsToReturnInvalid});
}
timestampsToReturn = (_.isObject(timestampsToReturn)) ? timestampsToReturn : TimestampsToReturn.Neither;
var obj = self.__findObject(nodeId);
var dataValue;
if (!obj) {
// may be return BadNodeIdUnknown in dataValue instead ?
// Object Not Found
return new DataValue({statusCode: StatusCodes.BadNodeIdUnknown});
} else {
// check access
// BadUserAccessDenied
// BadNotReadable
// invalid attributes : BadNodeAttributesInvalid
// invalid range : BadIndexRangeInvalid
try {
dataValue = obj.readAttribute(context, attributeId, indexRange, dataEncoding);
assert(dataValue.statusCode instanceof StatusCode);
assert(dataValue.isValid());
}
catch (err) {
console.log(" Internal error reading NodeId ", obj.nodeId.toString());
console.log(" AttributeId ", attributeId.toString());
console.log(" ", err.message);
console.log(" ", err.stack);
return new DataValue({statusCode: StatusCodes.BadInternalError});
}
dataValue = apply_timestamps(dataValue, timestampsToReturn, attributeId);
return dataValue;
}
};
/**
*
* @method read
* @param readRequest {ReadRequest}
* @param readRequest.timestampsToReturn {TimestampsToReturn}
* @param readRequest.nodesToRead
* @param readRequest.maxAge {Number} maxAge - must be greater or equal than zero.
*
* Maximum age of the value to be read in milliseconds. The age of the value is based on the difference between the
* ServerTimestamp and the time when the Server starts processing the request. For example if the Client specifies a
* maxAge of 500 milliseconds and it takes 100 milliseconds until the Server starts processing the request, the age
* of the returned value could be 600 milliseconds prior to the time it was requested.
* If the Server has one or more values of an Attribute that are within the maximum age, it can return any one of the
* values or it can read a new value from the data source. The number of values of an Attribute that a Server has
* depends on the number of MonitoredItems that are defined for the Attribute. In any case, the Client can make no
* assumption about which copy of the data will be returned.
* If the Server does not have a value that is within the maximum age, it shall attempt to read a new value from the
* data source.
* If the Server cannot meet the requested maxAge, it returns its 'best effort' value rather than rejecting the
* request.
* This may occur when the time it takes the Server to process and return the new data value after it has been
* accessed is greater than the specified maximum age.
* If maxAge is set to 0, the Server shall attempt to read a new value from the data source.
* If maxAge is set to the max Int32 value or greater, the Server shall attempt to geta cached value. Negative values
* are invalid for maxAge.
*
* @return {DataValue[]}
*/
ServerEngine.prototype.read = function (context, readRequest) {
assert(context instanceof SessionContext);
assert(readRequest instanceof ReadRequest);
assert(readRequest.maxAge >= 0);
var self = this;
var timestampsToReturn = readRequest.timestampsToReturn;
var nodesToRead = readRequest.nodesToRead;
assert(self.addressSpace instanceof AddressSpace); // initialize not called
assert(_.isArray(nodesToRead));
var dataValues = nodesToRead.map(function (readValueId) {
assert(readValueId.indexRange instanceof NumericRange);
return self._readSingleNode(context, readValueId, timestampsToReturn);
});
assert(dataValues.length === readRequest.nodesToRead.length);
return dataValues;
};
/**
*
* @method writeSingleNode
* @param context {Context}
* @param writeValue {DataValue}
* @param callback {Function}
* @param callback.err {Error|null}
* @param callback.statusCode {StatusCode}
* @async
*/
ServerEngine.prototype.writeSingleNode = function (context, writeValue, callback) {
var self = this;
assert(context instanceof SessionContext);
assert(_.isFunction(callback));
assert(writeValue._schema.name === "WriteValue");
assert(writeValue.value instanceof DataValue);
if (writeValue.value.value === null) {
return callback(null, StatusCodes.BadTypeMismatch);
}
assert(writeValue.value.value instanceof Variant);
assert(self.addressSpace instanceof AddressSpace); // initialize not called
var nodeId = writeValue.nodeId;
var obj = self.__findObject(nodeId);
if (!obj) {
callback(null, StatusCodes.BadNodeIdUnknown);
} else {
obj.writeAttribute(context, writeValue, callback);
}
};
var WriteValue = require("node-opcua-service-write").WriteValue;
/**
* write a collection of nodes
* @method write
* @param context {Object}
* @param nodesToWrite {Object[]}
* @param callback {Function}
* @param callback.err
* @param callback.results {StatusCode[]}
* @async
*/
ServerEngine.prototype.write = function (context, nodesToWrite, callback) {
assert(context instanceof SessionContext);
assert(_.isFunction(callback));
var self = this;
assert(self.addressSpace instanceof AddressSpace); // initialize not called
function performWrite(writeValue, inner_callback) {
assert(writeValue instanceof WriteValue);
self.writeSingleNode(context, writeValue, inner_callback);
}
async.map(nodesToWrite, performWrite, function (err, statusCodes) {
assert(_.isArray(statusCodes));
callback(err, statusCodes);
});
};
/**
*
* @method historyReadSingleNode
* @param context {SessionContext}
* @param nodeId {NodeId}
* @param attributeId {AttributeId}
* @param historyReadDetails {HistoryReadDetails}
* @param [timestampsToReturn=TimestampsToReturn.Neither]
* @param callback {Function}
* @param callback.err
* @param callback.results {HistoryReadResult}
*/
ServerEngine.prototype.historyReadSingleNode = function (context, nodeId, attributeId, historyReadDetails, timestampsToReturn, callback) {
assert(context instanceof SessionContext);
this._historyReadSingleNode(context,
{
nodeId: nodeId,
attributeId: attributeId
}, historyReadDetails, timestampsToReturn, callback);
};
ServerEngine.prototype._historyReadSingleNode = function (context, nodeToRead, historyReadDetails, timestampsToReturn, callback) {
assert(context instanceof SessionContext);
assert(callback instanceof Function);
var self = this;
var nodeId = nodeToRead.nodeId;
var indexRange = nodeToRead.indexRange;
var dataEncoding = nodeToRead.dataEncoding;
var continuationPoint = nodeToRead.continuationPoint;
assert(self.addressSpace instanceof AddressSpace); // initialize not called
if (timestampsToReturn === TimestampsToReturn.Invalid) {
return new DataValue({statusCode: StatusCodes.BadTimestampsToReturnInvalid});
}
timestampsToReturn = (_.isObject(timestampsToReturn)) ? timestampsToReturn : TimestampsToReturn.Neither;
var obj = self.__findObject(nodeId);
if (!obj) {
// may be return BadNodeIdUnknown in dataValue instead ?
// Object Not Found
callback(null, new HistoryReadResult({statusCode: StatusCodes.BadNodeIdUnknown}));
} else {
if (!obj.historyRead) {
// note : Object and View may also support historyRead to provide Event historical data
// todo implement historyRead for Object and View
var msg = " this node doesn't provide historyRead! probably not a UAVariable\n "
+ obj.nodeId.toString() + " " + obj.browseName.toString() + "\n"
+ "with " + nodeToRead.toString() + "\n"
+ "HistoryReadDetails " + historyReadDetails.toString();
if (doDebug) {
console.log("ServerEngine#_historyReadSingleNode ".cyan, msg.white.bold);
}
var err = new Error(msg);
// object has no historyRead method
return setImmediate(callback.bind(null, err));
}
// check access
// BadUserAccessDenied
// BadNotReadable
// invalid attributes : BadNodeAttributesInvalid
// invalid range : BadIndexRangeInvalid
obj.historyRead(context, historyReadDetails, indexRange, dataEncoding, continuationPoint, function (err, result) {
assert(result.statusCode instanceof StatusCode);
assert(result.isValid());
//result = apply_timestamps(result, timestampsToReturn, attributeId);
callback(err, result);
});
}
};
/**
*
* @method historyRead
* @param context {SessionContext}
* @param historyReadRequest {HistoryReadRequest}
* @param historyReadRequest.requestHeader {RequestHeader}
* @param historyReadRequest.historyReadDetails {HistoryReadDetails}
* @param historyReadRequest.timestampsToReturn {TimestampsToReturn}
* @param historyReadRequest.releaseContinuationPoints {Boolean}
* @param historyReadRequest.nodesToRead {HistoryReadValueId[]}
* @param callback {Function}
* @param callback.err
* @param callback.results {HistoryReadResult[]}
*/
ServerEngine.prototype.historyRead = function (context, historyReadRequest, callback) {
assert(context instanceof SessionContext);
assert(historyReadRequest instanceof HistoryReadRequest);
assert(_.isFunction(callback));
var self = this;
var timestampsToReturn = historyReadRequest.timestampsToReturn;
var historyReadDetails = historyReadRequest.historyReadDetails;
var nodesToRead = historyReadRequest.nodesToRead;
assert(historyReadDetails instanceof HistoryReadDetails);
assert(self.addressSpace instanceof AddressSpace); // initialize not called
assert(_.isArray(nodesToRead));
var historyData = [];
async.eachSeries(nodesToRead, function (readValueId, cbNode) {
self._historyReadSingleNode(context, readValueId, historyReadDetails, timestampsToReturn, function (err, result) {
if (err && !result) {
result = new HistoryReadResult({statusCode: StatusCodes.BadInternalError});
}
historyData.push(result);
async.setImmediate(cbNode); //it's not guaranteed that the historical read process is really asynchronous
});
}, function (err) {
assert(historyData.length === nodesToRead.length);
callback(err, historyData);
});
};
function __bindVariable(self, nodeId, options) {
options = options || {};
// must have a get and a set property
assert(_.difference(["get", "set"], _.keys(options)).length === 0);
var obj = self.addressSpace.findNode(nodeId);
if (obj && obj.bindVariable) {
obj.bindVariable(options);
assert(_.isFunction(obj.asyncRefresh));
assert(_.isFunction(obj.refreshFunc));
} else {
//xx console.log((new Error()).stack);
console.log("Warning: cannot bind object with id ", nodeId.toString(), " please check your nodeset.xml file or add this node programmatically");
}
}
/**
* @method bindMethod
* @param nodeId {NodeId} the node id of the method
* @param func
* @param func.inputArguments {Array<Variant>}
* @param func.context {Object}
* @param func.callback {Function}
* @param func.callback.err {Error}
* @param func.callback.callMethodResult {CallMethodResult}
*/
ServerEngine.prototype.__internal_bindMethod = function (nodeId, func) {
var self = this;
assert(_.isFunction(func));
assert(nodeId instanceof NodeId);
var methodNode = self.addressSpace.findNode(nodeId);
if (!methodNode) {
return;
}
if (methodNode && methodNode.bindMethod) {
methodNode.bindMethod(func);
} else {
//xx console.log((new Error()).stack);
console.log("WARNING: cannot bind a method with id ".yellow + nodeId.toString().cyan + ". please check your nodeset.xml file or add this node programmatically".yellow);
console.log(require("node-opcua-debug").trace_from_this_projet_only(new Error()))
}
};
ServerEngine.prototype.getOldestUnactivatedSession = function () {
var self = this;
var tmp = _.filter(self._sessions, function (session) {
return session.status === "new";
});
if (tmp.length === 0) {
return null;
}
var session = tmp[0];
for (var i = 1; i < session.length; i++) {
var c = tmp[i];
if (session.creationDate.getTime() < c.creationDate.getTime()) {
session = c;
}
}
return session;
};
/**
* create a new server session object.
* @class ServerEngine
* @method createSession
* @param [options] {Object}
* @param [options.sessionTimeout = 1000] {Number} sessionTimeout
* @param [options.clientDescription] {ApplicationDescription}
* @return {ServerSession}
*/
ServerEngine.prototype.createSession = function (options) {
options = options || {};
var self = this;
self.serverDiagnosticsSummary.cumulatedSessionCount += 1;
self.clientDescription = options.clientDescription || new ApplicationDescription({});
var sessionTimeout = options.sessionTimeout || 1000;
assert(_.isNumber(sessionTimeout));
var session = new ServerSession(self, self.cumulatedSessionCount, sessionTimeout);
var key = session.authenticationToken.toString();
self._sessions[key] = session;
// see spec OPC Unified Architecture, Part 2 page 26 Release 1.02
// TODO : When a Session is created, the Server adds an entry for the Client
// in its SessionDiagnosticsArray Variable
session.on("new_subscription", function (subscription) {
self.serverDiagnosticsSummary.cumulatedSubscriptionCount += 1;
// add the subscription diagnostics in our subscriptions diagnostics array
});
session.on("subscription_terminated", function (subscription) {
// remove the subscription diagnostics in our subscriptions diagnostics array
});
// OPC Unified Architecture, Part 4 23 Release 1.03
// Sessions are terminated by the Server automatically if the Client fails to issue a Service request on the Session
// within the timeout period negotiated by the Server in the CreateSession Service response. This protects the
// Server against Client failures and against situations where a failed underlying connection cannot be
// re-established. Clients shall be prepared to submit requests in a timely manner to prevent the Session from
// closing automatically. Clients may explicitly terminate sessions using the CloseSession Service.
session.on("timeout", function () {
// the session hasn't been active for a while , probably because the client has disconnected abruptly
// it is now time to close the session completely
self.serverDiagnosticsSummary.sessionTimeoutCount += 1;
session.sessionName = session.sessionName || "";
console.log("Server: closing SESSION ".cyan, session.status, session.sessionName.yellow, " because of timeout = ".cyan, session.sessionTimeout, " has expired without a keep alive".cyan);
var channel = session.channel;
if (channel) {
console.log("channel = ".bgCyan, channel.remoteAddress, " port = ", channel.remotePort);
}
// If a Server terminates a Session for any other reason, Subscriptions associated with the Session,
// are not deleted. => deleteSubscription= false
self.closeSession(session.authenticationToken, /*deleteSubscription=*/false, /* reason =*/"Timeout");
});
return session;
};
/**
* @method closeSession
* @param authenticationToken
* @param deleteSubscriptions {Boolean} : true if sessions's subscription shall be deleted
* @param {String} [reason = "CloseSession"] the reason for closing the session (shall be "Timeout", "Terminated" or "CloseSession")
*
*
* what the specs say:
* -------------------
*
* If a Client invokes the CloseSession Service then all Subscriptions associated with the Session are also deleted
* if the deleteSubscriptions flag is set to TRUE. If a Server terminates a Session for any other reason, Subscriptions
* associated with the Session, are not deleted. Each Subscription has its own lifetime to protect against data loss
* in the case of a Session termination. In these cases, the Subscription can be reassigned to another Client before
* its lifetime expires.
*/
ServerEngine.prototype.closeSession = function (authenticationToken, deleteSubscriptions, reason) {
var self = this;
reason = reason || "CloseSession";
assert(_.isString(reason));
assert(reason === "Timeout" || reason === "Terminated" || reason === "CloseSession" || reason === "Forcing");
debugLog("ServerEngine.closeSession ", authenticationToken.toString(), deleteSubscriptions);
var key = authenticationToken.toString();
var session = self.getSession(key);
assert(session);
if (!deleteSubscriptions) {
if (!self._orphanPublishEngine) {
self._orphanPublishEngine = new ServerSidePublishEngine({maxPublishRequestInQueue: 0});
}
ServerSidePublishEngine.transferSubscriptions(session.publishEngine, self._orphanPublishEngine);
}
session.close(deleteSubscriptions, reason);
assert(session.status === "closed");
//TODO make sure _closedSessions gets cleaned at some point
self._closedSessions[key] = session;
delete self._sessions[key];
};
ServerEngine.prototype.findSubscription = function (subscriptionId) {
var self = this;
console.log("findSubscription ", subscriptionId);
var subscriptions = [];
_.map(self._sessions, function (session) {
if (subscriptions.length) return;
var subscription = session.publishEngine.getSubscriptionById(subscriptionId);
if (subscription) {
console.log("foundSubscription ", subscriptionId, " in session", session.sessionName);
subscriptions.push(subscription)
}
});
if (subscriptions.length) {
assert(subscriptions.length === 1);
return subscriptions[0];
}
return self.findOrphanSubscription(subscriptionId);
};
ServerEngine.prototype.findOrphanSubscription = function (subscriptionId) {
var self = this;
if (!self._orphanPublishEngine) {
return null;
}
return self._orphanPublishEngine.getSubscriptionById(subscriptionId);
};
ServerEngine.prototype.deleteOrphanSubscription = function (subscription) {
var self = this;
assert(self.findSubscription(subscription.id));
var c = self._orphanPublishEngine.subscriptionCount;
subscription.terminate();
assert(self._orphanPublishEngine.subscriptionCount === c - 1);
return StatusCodes.Good;
};
var TransferResult = require("node-opcua-service-subscription").TransferResult;
/**
*
* @param session {ServerSession}
* @param subscriptionId {IntegerId}
* @param sendInitialValues {Boolean}
* @return {TransferResult}
*/
ServerEngine.prototype.transferSubscription = function (session, subscriptionId, sendInitialValues) {
var self = this;
assert(session instanceof ServerSession);
assert(_.isNumber(subscriptionId));
assert(_.isBoolean(sendInitialValues));
if (subscriptionId <= 0) {
return new TransferResult({statusCode: StatusCodes.BadSubscriptionIdInvalid});
}
var subscription = self.findSubscription(subscriptionId);
if (!subscription) {
return new TransferResult({statusCode: StatusCodes.BadSubscriptionIdInvalid});
}
// // now check that new session has sufficient right
// if (session.authenticationToken.toString() !== subscription.authenticationToken.toString()) {
// console.log("ServerEngine#transferSubscription => BadUserAccessDenied");
// return new TransferResult({ statusCode: StatusCodes.BadUserAccessDenied });
// }
if (session.publishEngine === subscription.publishEngine) {
// subscription is already in this session !!
return new TransferResult({statusCode: StatusCodes.BadNothingToDo});
}
var nbSubscriptionBefore = session.publishEngine.subscriptionCount;
ServerSidePublishEngine.transferSubscription(subscription, session.publishEngine, sendInitialValues);
assert(subscription.publishEngine === session.publishEngine);
assert(session.publishEngine.subscriptionCount === nbSubscriptionBefore + 1);
// TODO If the Server transfers the Subscription to the new Session, the Server shall issue a
// TODO StatusChangeNotification notificationMessage with the status code Good_SubscriptionTransferred
// TODO to the old Session.
var result = new TransferResult({
statusCode: StatusCodes.Good,
availableSequenceNumbers: subscription.getAvailableSequenceNumbers()
});
// istanbul ignore next
if (doDebug) {
debugLog("TransferResult",result.toString());
}
return result;
};
/**
* retrieve a session by its authenticationToken.
*
* @method getSession
* @param authenticationToken
* @param activeOnly
* @return {ServerSession}
*/
ServerEngine.prototype.getSession = function (authenticationToken, activeOnly) {
var self = this;
if (!authenticationToken || (authenticationToken.identifierType && (authenticationToken.identifierType.value !== NodeIdType.BYTESTRING.value))) {
return null; // wrong type !
}
var key = authenticationToken.toString();
var session = self._sessions[key];
if (!activeOnly && !session) {
session = self._closedSessions[key];
}
return session;
};
/**
* @method browsePath
* @param browsePath
* @return {BrowsePathResult}
*/
ServerEngine.prototype.browsePath = function (browsePath) {
return this.addressSpace.browsePath(browsePath);
};
/**
*
* performs a call to ```asyncRefresh``` on all variable nodes that provide an async refresh func.
*
* @method refreshValues
* @param nodesToRefresh {Array<Object>} an array containing the node to consider
* Each element of the array shall be of the form { nodeId: <xxx>, attributeIds: <value> }.
*
* @param callback {Function}
* @param callback.err {null|Error}
* @param callback.data {Array} : an array containing value read
* The array length matches the number of nodeIds that are candidate for an async refresh (i.e: nodes that are of type
* Variable with asyncRefresh func }
*
* @async
*/
ServerEngine.prototype.refreshValues = function (nodesToRefresh, callback) {
assert(callback instanceof Function);
var self = this;
var objs = {};
for (var i = 0; i < nodesToRefresh.length; i++) {
var nodeToRefresh = nodesToRefresh[i];
// only consider node for which the caller wants to read the Value attribute
// assuming that Value is requested if attributeId is missing,
if (nodeToRefresh.attributeId && nodeToRefresh.attributeId !== AttributeIds.Value) {
continue;
}
// ... and that are valid object and instances of Variables ...
var obj = self.addressSpace.findNode(nodeToRefresh.nodeId);
if (!obj || !(obj instanceof UAVariable)) {
continue;
}
// ... and that have been declared as asynchronously updating
if (!_.isFunction(obj.refreshFunc)) {
continue;
}
var key = obj.nodeId.toString();
if (objs[key]) {
continue;
}
objs[key] = obj;
}
if (Object.keys(objs).length === 0) {
// nothing to do
return callback(null, []);
}
// perform all asyncRefresh in parallel
async.map(objs, function (obj, inner_callback) {
obj.asyncRefresh(inner_callback);
}, function (err, arrResult) {
callback(err, arrResult);
});
};
exports.ServerEngine = ServerEngine;