ua_subscription_events.c 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463
  1. /* This Source Code Form is subject to the terms of the Mozilla Public
  2. * License, v. 2.0. If a copy of the MPL was not distributed with this
  3. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
  4. *
  5. * Copyright 2018 (c) Ari Breitkreuz, fortiss GmbH
  6. */
  7. #include "ua_server_internal.h"
  8. #include "ua_subscription.h"
  9. #include "ua_subscription_events.h"
  10. #ifdef UA_ENABLE_SUBSCRIPTIONS_EVENTS
  11. typedef struct Events_nodeListElement {
  12. LIST_ENTRY(Events_nodeListElement) listEntry;
  13. UA_NodeId *node;
  14. } Events_nodeListElement;
  15. typedef LIST_HEAD(Events_nodeList, Events_nodeListElement) Events_nodeList;
  16. struct getNodesHandle {
  17. UA_Server *server;
  18. Events_nodeList *nodes;
  19. };
  20. /* generates a unique event id */
  21. static UA_StatusCode UA_Event_generateEventId(UA_Server *server, UA_ByteString *generatedId) {
  22. /* EventId is a ByteString, which is basically just a string
  23. * We will use a 16-Byte ByteString as an identifier */
  24. generatedId->length = 16;
  25. generatedId->data = (UA_Byte *) UA_malloc(16 * sizeof(UA_Byte));
  26. if (!generatedId->data) {
  27. UA_LOG_WARNING(server->config.logger, UA_LOGCATEGORY_USERLAND,
  28. "Server unable to allocate memory for EventId data.");
  29. UA_free(generatedId);
  30. return UA_STATUSCODE_BADOUTOFMEMORY;
  31. }
  32. /* GUIDs are unique, have a size of 16 byte and already have
  33. * a generator so use that.
  34. * Make sure GUIDs really do have 16 byte, in case someone may
  35. * have changed that struct */
  36. UA_assert(sizeof(UA_Guid) == 16);
  37. UA_Guid tmpGuid = UA_Guid_random();
  38. memcpy(generatedId->data, &tmpGuid, 16);
  39. return UA_STATUSCODE_GOOD;
  40. }
  41. static UA_StatusCode findAllSubtypesNodeIteratorCallback(UA_NodeId parentId, UA_Boolean isInverse,
  42. UA_NodeId referenceTypeId, void *handle) {
  43. /* only subtypes of hasSubtype */
  44. UA_NodeId hasSubtypeId = UA_NODEID_NUMERIC(0, UA_NS0ID_HASSUBTYPE);
  45. if (isInverse || !UA_NodeId_equal(&referenceTypeId, &hasSubtypeId)) {
  46. return UA_STATUSCODE_GOOD;
  47. }
  48. Events_nodeListElement *entry = (Events_nodeListElement *) UA_malloc(sizeof(Events_nodeListElement));
  49. if (!entry) {
  50. return UA_STATUSCODE_BADOUTOFMEMORY;
  51. }
  52. entry->node = UA_NodeId_new();
  53. if (!entry->node) {
  54. UA_free(entry);
  55. return UA_STATUSCODE_BADOUTOFMEMORY;
  56. }
  57. UA_NodeId_copy(&parentId, entry->node);
  58. LIST_INSERT_HEAD(((struct getNodesHandle *) handle)->nodes, entry, listEntry);
  59. /* recursion */
  60. UA_Server_forEachChildNodeCall(((struct getNodesHandle *) handle)->server,
  61. parentId, findAllSubtypesNodeIteratorCallback, handle);
  62. return UA_STATUSCODE_GOOD;
  63. }
  64. /* Searches for an attribute of an event with the name 'name' and the depth from the event relativePathSize.
  65. * Returns the browsePathResult of searching for that node */
  66. static void UA_Event_findVariableNode(UA_Server *server, UA_QualifiedName *name, size_t relativePathSize,
  67. const UA_NodeId *event, UA_BrowsePathResult *out) {
  68. /* get a list with all subtypes of aggregates */
  69. struct getNodesHandle handle;
  70. Events_nodeList list;
  71. LIST_INIT(&list);
  72. handle.server = server;
  73. handle.nodes = &list;
  74. UA_StatusCode retval = UA_Server_forEachChildNodeCall(server, UA_NODEID_NUMERIC(0, UA_NS0ID_AGGREGATES),
  75. findAllSubtypesNodeIteratorCallback, &handle);
  76. if (retval != UA_STATUSCODE_GOOD) {
  77. out->statusCode = retval;
  78. }
  79. /* check if you can find the node with any of the subtypes of aggregates */
  80. UA_Boolean nodeFound = UA_FALSE;
  81. Events_nodeListElement *iter, *tmp_iter;
  82. LIST_FOREACH_SAFE(iter, &list, listEntry, tmp_iter) {
  83. if (!nodeFound) {
  84. UA_RelativePathElement rpe;
  85. UA_RelativePathElement_init(&rpe);
  86. rpe.referenceTypeId = *iter->node;
  87. rpe.isInverse = false;
  88. rpe.includeSubtypes = false;
  89. rpe.targetName = *name;
  90. /* TODO: test larger browsepath perhaps put browsepath in a loop */
  91. UA_BrowsePath bp;
  92. UA_BrowsePath_init(&bp);
  93. bp.relativePath.elementsSize = relativePathSize;
  94. bp.startingNode = *event;
  95. bp.relativePath.elements = &rpe;
  96. *out = UA_Server_translateBrowsePathToNodeIds(server, &bp);
  97. if (out->statusCode == UA_STATUSCODE_GOOD) {
  98. nodeFound = UA_TRUE;
  99. }
  100. }
  101. LIST_REMOVE(iter, listEntry);
  102. UA_NodeId_delete(iter->node);
  103. UA_free(iter);
  104. }
  105. }
  106. UA_StatusCode UA_EXPORT
  107. UA_Server_createEvent(UA_Server *server, const UA_NodeId eventType, UA_NodeId *outNodeId) {
  108. if (!outNodeId) {
  109. UA_LOG_ERROR(server->config.logger, UA_LOGCATEGORY_USERLAND, "outNodeId cannot be NULL!");
  110. return UA_STATUSCODE_BADINVALIDARGUMENT;
  111. }
  112. UA_StatusCode retval;
  113. /* make sure the eventType is a subtype of BaseEventType */
  114. UA_NodeId hasSubtypeId = UA_NODEID_NUMERIC(0, UA_NS0ID_HASSUBTYPE);
  115. UA_NodeId baseEventTypeId = UA_NODEID_NUMERIC(0, UA_NS0ID_BASEEVENTTYPE);
  116. if (!isNodeInTree(&server->config.nodestore, &eventType, &baseEventTypeId, &hasSubtypeId, 1)) {
  117. UA_LOG_ERROR(server->config.logger, UA_LOGCATEGORY_USERLAND, "Event type must be a subtype of BaseEventType!");
  118. return UA_STATUSCODE_BADINVALIDARGUMENT;
  119. }
  120. UA_ObjectAttributes oAttr = UA_ObjectAttributes_default;
  121. oAttr.displayName.locale = UA_STRING_NULL;
  122. oAttr.displayName.text = UA_STRING_NULL;
  123. oAttr.description.locale = UA_STRING_NULL;
  124. oAttr.description.text = UA_STRING_NULL;
  125. UA_QualifiedName name;
  126. UA_QualifiedName_init(&name);
  127. /* create an ObjectNode which represents the event */
  128. retval = UA_Server_addObjectNode(server,
  129. UA_NODEID_NULL, /* the user may not have control over the nodeId */
  130. UA_NODEID_NULL, /* an event does not have a parent */
  131. UA_NODEID_NULL, /* an event does not have any references */
  132. name, /* an event does not have a name */
  133. eventType, /* the type of the event */
  134. oAttr, /* default attributes are fine */
  135. NULL, /* no node context */
  136. outNodeId);
  137. if (retval != UA_STATUSCODE_GOOD) {
  138. UA_LOG_ERROR(server->config.logger, UA_LOGCATEGORY_USERLAND,
  139. "Adding event failed. StatusCode %s", UA_StatusCode_name(retval));
  140. return retval;
  141. }
  142. /* find the eventType variableNode */
  143. name = UA_QUALIFIEDNAME(0, "EventType");
  144. UA_BrowsePathResult bpr;
  145. UA_BrowsePathResult_init(&bpr);
  146. UA_Event_findVariableNode(server, &name, 1, outNodeId, &bpr);
  147. if (bpr.statusCode != UA_STATUSCODE_GOOD || bpr.targetsSize < 1) {
  148. return bpr.statusCode;
  149. }
  150. UA_Variant value;
  151. UA_Variant_init(&value);
  152. UA_Variant_setScalarCopy(&value, &eventType, &UA_TYPES[UA_TYPES_NODEID]);
  153. UA_Server_writeValue(server, bpr.targets[0].targetId.nodeId, value);
  154. UA_Variant_deleteMembers(&value);
  155. UA_BrowsePathResult_deleteMembers(&bpr);
  156. /* the object is not put in any queues until it is triggered */
  157. return retval;
  158. }
  159. static UA_Boolean isValidEvent(UA_Server *server, const UA_NodeId *validEventParent, const UA_NodeId *eventId) {
  160. /* find the eventType variableNode */
  161. UA_BrowsePathResult bpr;
  162. UA_BrowsePathResult_init(&bpr);
  163. UA_QualifiedName findName = UA_QUALIFIEDNAME(0, "EventType");
  164. UA_Event_findVariableNode(server, &findName, 1, eventId, &bpr);
  165. if (bpr.statusCode != UA_STATUSCODE_GOOD || bpr.targetsSize < 1) {
  166. return UA_FALSE;
  167. }
  168. UA_NodeId hasSubtypeId = UA_NODEID_NUMERIC(0, UA_NS0ID_HASSUBTYPE);
  169. UA_Boolean tmp = isNodeInTree(&server->config.nodestore, &bpr.targets[0].targetId.nodeId, validEventParent,
  170. &hasSubtypeId, 1);
  171. UA_BrowsePathResult_deleteMembers(&bpr);
  172. return tmp;
  173. }
  174. static UA_StatusCode whereClausesApply(UA_Server *server, const UA_ContentFilter whereClause, UA_EventFieldList *efl,
  175. UA_Boolean *result) {
  176. /* if the where clauses aren't specified leave everything as is */
  177. if (whereClause.elementsSize == 0) {
  178. *result = UA_TRUE;
  179. return UA_STATUSCODE_GOOD;
  180. }
  181. /* where clauses were specified */
  182. UA_LOG_WARNING(server->config.logger, UA_LOGCATEGORY_USERLAND, "Where clauses are not supported by the server.");
  183. *result = UA_TRUE;
  184. return UA_STATUSCODE_BADNOTSUPPORTED;
  185. }
  186. /* filters the given event with the given filter and writes the results into a notification */
  187. static UA_StatusCode UA_Server_filterEvent(UA_Server *server, const UA_NodeId *eventNode, UA_EventFilter *filter,
  188. UA_EventNotification *notification) {
  189. if (filter->selectClausesSize == 0) {
  190. return UA_STATUSCODE_BADEVENTFILTERINVALID;
  191. }
  192. UA_StatusCode retval;
  193. /* setup */
  194. UA_EventFieldList_init(&notification->fields);
  195. /* EventFilterResult isn't being used currently
  196. UA_EventFilterResult_init(&notification->result); */
  197. notification->fields.eventFieldsSize = filter->selectClausesSize;
  198. notification->fields.eventFields = (UA_Variant *) UA_Array_new(notification->fields.eventFieldsSize,
  199. &UA_TYPES[UA_TYPES_VARIANT]);
  200. if (!notification->fields.eventFields) {
  201. /* EventFilterResult currently isn't being used
  202. UA_EventFiterResult_deleteMembers(&notification->result); */
  203. return UA_STATUSCODE_BADOUTOFMEMORY;
  204. }
  205. /* EventFilterResult currently isn't being used
  206. notification->result.selectClauseResultsSize = filter->selectClausesSize;
  207. notification->result.selectClauseResults = (UA_StatusCode *) UA_Array_new(filter->selectClausesSize,
  208. &UA_TYPES[UA_TYPES_VARIANT]);
  209. if (!notification->result->selectClauseResults) {
  210. UA_EventFieldList_deleteMembers(&notification->fields);
  211. UA_EventFilterResult_deleteMembers(&notification->result);
  212. return UA_STATUSCODE_BADOUTOFMEMORY;
  213. }
  214. */
  215. /* ================ apply the filter ===================== */
  216. /* check if the browsePath is BaseEventType, in which case nothing more needs to be checked */
  217. UA_NodeId baseEventTypeId = UA_NODEID_NUMERIC(0, UA_NS0ID_BASEEVENTTYPE);
  218. /* iterate over the selectClauses */
  219. for (size_t i = 0; i < filter->selectClausesSize; i++) {
  220. if (!UA_NodeId_equal(&filter->selectClauses[i].typeDefinitionId, &baseEventTypeId)
  221. && !isValidEvent(server, &filter->selectClauses[0].typeDefinitionId, eventNode)) {
  222. UA_Variant_init(&notification->fields.eventFields[i]);
  223. /* EventFilterResult currently isn't being used
  224. notification->result.selectClauseResults[i] = UA_STATUSCODE_BADTYPEDEFINITIONINVALID; */
  225. continue;
  226. }
  227. /* type is correct */
  228. /* find the variable node with the data being looked for */
  229. UA_BrowsePathResult bpr;
  230. UA_BrowsePathResult_init(&bpr);
  231. UA_Event_findVariableNode(server, filter->selectClauses[i].browsePath, filter->selectClauses[i].browsePathSize,
  232. eventNode, &bpr);
  233. if (bpr.statusCode != UA_STATUSCODE_GOOD || bpr.targetsSize < 1) {
  234. UA_Variant_init(&notification->fields.eventFields[i]);
  235. continue;
  236. }
  237. /* copy the value */
  238. UA_Boolean whereClauseResult = UA_TRUE;
  239. UA_Boolean whereClausesUsed = UA_FALSE; /* placeholder until whereClauses are implemented */
  240. retval = whereClausesApply(server, filter->whereClause, &notification->fields, &whereClauseResult);
  241. if (retval == UA_STATUSCODE_BADNOTSUPPORTED) {
  242. whereClausesUsed = UA_TRUE;
  243. }
  244. if (whereClauseResult) {
  245. retval = UA_Server_readValue(server, bpr.targets[0].targetId.nodeId, &notification->fields.eventFields[i]);
  246. if (retval != UA_STATUSCODE_GOOD) {
  247. UA_Variant_init(&notification->fields.eventFields[i]);
  248. }
  249. if (whereClausesUsed) {
  250. return UA_STATUSCODE_BADNOTSUPPORTED;
  251. }
  252. } else {
  253. UA_Variant_init(&notification->fields.eventFields[i]);
  254. /* TODO: better statuscode for failing at where clauses */
  255. /* EventFilterResult currently isn't being used
  256. notification->result.selectClauseResults[i] = UA_STATUSCODE_BADDATAUNAVAILABLE; */
  257. }
  258. UA_BrowsePathResult_deleteMembers(&bpr);
  259. }
  260. return UA_STATUSCODE_GOOD;
  261. }
  262. static UA_StatusCode eventSetConstants(UA_Server *server, const UA_NodeId *event, const UA_NodeId *origin,
  263. UA_ByteString *outEventId) {
  264. UA_BrowsePathResult bpr;
  265. UA_BrowsePathResult_init(&bpr);
  266. /* set the source */
  267. UA_QualifiedName name = UA_QUALIFIEDNAME(0, "SourceNode");
  268. UA_Event_findVariableNode(server, &name, 1, event, &bpr);
  269. if (bpr.statusCode != UA_STATUSCODE_GOOD || bpr.targetsSize < 1) {
  270. UA_StatusCode tmp = bpr.statusCode;
  271. UA_BrowsePathResult_deleteMembers(&bpr);
  272. return tmp;
  273. }
  274. UA_Variant value;
  275. UA_Variant_init(&value);
  276. UA_Variant_setScalarCopy(&value, origin, &UA_TYPES[UA_TYPES_NODEID]);
  277. UA_Server_writeValue(server, bpr.targets[0].targetId.nodeId, value);
  278. UA_Variant_deleteMembers(&value);
  279. UA_BrowsePathResult_deleteMembers(&bpr);
  280. /* set the receive time */
  281. name = UA_QUALIFIEDNAME(0, "ReceiveTime");
  282. UA_Event_findVariableNode(server, &name, 1, event, &bpr);
  283. if (bpr.statusCode != UA_STATUSCODE_GOOD || bpr.targetsSize < 1) {
  284. UA_StatusCode tmp = bpr.statusCode;
  285. UA_BrowsePathResult_deleteMembers(&bpr);
  286. return tmp;
  287. }
  288. UA_DateTime time = UA_DateTime_now();
  289. UA_Variant_setScalar(&value, &time, &UA_TYPES[UA_TYPES_DATETIME]);
  290. UA_Server_writeValue(server, bpr.targets[0].targetId.nodeId, value);
  291. UA_BrowsePathResult_deleteMembers(&bpr);
  292. /* set the eventId attribute */
  293. UA_ByteString eventId;
  294. UA_ByteString_init(&eventId);
  295. UA_StatusCode retval = UA_Event_generateEventId(server, &eventId);
  296. if (retval != UA_STATUSCODE_GOOD) {
  297. UA_ByteString_deleteMembers(&eventId);
  298. return retval;
  299. }
  300. if (outEventId) {
  301. UA_ByteString_copy(&eventId, outEventId);
  302. }
  303. name = UA_QUALIFIEDNAME(0, "EventId");
  304. UA_Event_findVariableNode(server, &name, 1, event, &bpr);
  305. if (bpr.statusCode != UA_STATUSCODE_GOOD || bpr.targetsSize < 1) {
  306. UA_StatusCode tmp = bpr.statusCode;
  307. UA_BrowsePathResult_deleteMembers(&bpr);
  308. return tmp;
  309. }
  310. UA_Variant_init(&value);
  311. UA_Variant_setScalar(&value, &eventId, &UA_TYPES[UA_TYPES_BYTESTRING]);
  312. UA_Server_writeValue(server, bpr.targets[0].targetId.nodeId, value);
  313. UA_ByteString_deleteMembers(&eventId);
  314. UA_BrowsePathResult_deleteMembers(&bpr);
  315. return UA_STATUSCODE_GOOD;
  316. }
  317. /* insert each node into the list (passed as handle) */
  318. static UA_StatusCode getParentsNodeIteratorCallback(UA_NodeId parentId, UA_Boolean isInverse,
  319. UA_NodeId referenceTypeId, void *handle) {
  320. /* parents have an inverse reference */
  321. if (!isInverse) {
  322. return UA_STATUSCODE_GOOD;
  323. }
  324. Events_nodeListElement *entry = (Events_nodeListElement *) UA_malloc(sizeof(Events_nodeListElement));
  325. if (!entry) {
  326. return UA_STATUSCODE_BADOUTOFMEMORY;
  327. }
  328. entry->node = UA_NodeId_new();
  329. if (!entry->node) {
  330. return UA_STATUSCODE_BADOUTOFMEMORY;
  331. }
  332. UA_NodeId_copy(&parentId, entry->node);
  333. LIST_INSERT_HEAD(((struct getNodesHandle *) handle)->nodes, entry, listEntry);
  334. /* recursion */
  335. UA_Server_forEachChildNodeCall(((struct getNodesHandle *) handle)->server,
  336. parentId, getParentsNodeIteratorCallback, handle);
  337. return UA_STATUSCODE_GOOD;
  338. }
  339. /* filters an event according to the filter specified by mon and then adds it to mons notification queue */
  340. static UA_StatusCode
  341. UA_Event_addEventToMonitoredItem(UA_Server *server, const UA_NodeId *event, UA_MonitoredItem *mon) {
  342. UA_Notification *notification = (UA_Notification *) UA_malloc(sizeof(UA_Notification));
  343. if (!notification) {
  344. return UA_STATUSCODE_BADOUTOFMEMORY;
  345. }
  346. /* apply the filter */
  347. UA_StatusCode retval = UA_Server_filterEvent(server, event, &mon->filter.eventFilter, &notification->data.event);
  348. if (retval != UA_STATUSCODE_GOOD) {
  349. UA_free(notification);
  350. return retval;
  351. }
  352. notification->mon = mon;
  353. /* add to the monitored item queue */
  354. MonitoredItem_ensureQueueSpace(server, mon);
  355. TAILQ_INSERT_TAIL(&mon->queue, notification, listEntry);
  356. ++mon->queueSize;
  357. /* add to the subscription queue */
  358. TAILQ_INSERT_TAIL(&mon->subscription->notificationQueue, notification, globalEntry);
  359. ++mon->subscription->notificationQueueSize;
  360. return UA_STATUSCODE_GOOD;
  361. }
  362. UA_StatusCode UA_EXPORT
  363. UA_Server_triggerEvent(UA_Server *server, const UA_NodeId eventNodeId, const UA_NodeId origin,
  364. UA_ByteString *outEventId, const UA_Boolean deleteEventNode) {
  365. /* make sure the origin is in the ObjectsFolder (TODO: or in the ViewsFolder) */
  366. UA_NodeId objectsFolderId = UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER);
  367. UA_NodeId references[2] = {
  368. {0, UA_NODEIDTYPE_NUMERIC, {UA_NS0ID_ORGANIZES}},
  369. {0, UA_NODEIDTYPE_NUMERIC, {UA_NS0ID_HASCOMPONENT}}
  370. };
  371. if (!isNodeInTree(&server->config.nodestore, &origin, &objectsFolderId, references, 2)) {
  372. UA_LOG_ERROR(server->config.logger, UA_LOGCATEGORY_USERLAND, "Node for event must be in ObjectsFolder!");
  373. return UA_STATUSCODE_BADINVALIDARGUMENT;
  374. }
  375. UA_StatusCode retval = eventSetConstants(server, &eventNodeId, &origin, outEventId);
  376. if (retval != UA_STATUSCODE_GOOD) {
  377. return retval;
  378. }
  379. /* get an array with all parents */
  380. struct getNodesHandle parentHandle;
  381. Events_nodeList parentList;
  382. LIST_INIT(&parentList);
  383. parentHandle.server = server;
  384. parentHandle.nodes = &parentList;
  385. retval = getParentsNodeIteratorCallback(origin, UA_TRUE, UA_NODEID_NULL, &parentHandle);
  386. if (retval != UA_STATUSCODE_GOOD) {
  387. return retval;
  388. }
  389. /* add the event to each node's monitored items */
  390. Events_nodeListElement *parentIter, *tmp_parentIter;
  391. LIST_FOREACH_SAFE(parentIter, &parentList, listEntry, tmp_parentIter) {
  392. const UA_ObjectNode *node = (const UA_ObjectNode *) UA_Nodestore_get(server, parentIter->node);
  393. /* SLIST_FOREACH */
  394. for (UA_MonitoredItem *monIter = node->monitoredItemQueue; monIter != NULL; monIter = monIter->next) {
  395. retval = UA_Event_addEventToMonitoredItem(server, &eventNodeId, monIter);
  396. if (retval != UA_STATUSCODE_GOOD) {
  397. UA_Nodestore_release(server, (const UA_Node *) node);
  398. return retval;
  399. }
  400. }
  401. UA_Nodestore_release(server, (const UA_Node *) node);
  402. LIST_REMOVE(parentIter, listEntry);
  403. UA_NodeId_delete(parentIter->node);
  404. UA_free(parentIter);
  405. }
  406. /* delete the node representation of the event */
  407. if (deleteEventNode) {
  408. retval = UA_Server_deleteNode(server, eventNodeId, UA_TRUE);
  409. if (retval != UA_STATUSCODE_GOOD) {
  410. UA_LOG_WARNING(server->config.logger,
  411. UA_LOGCATEGORY_SERVER,
  412. "Attempt to remove event using deleteNode failed. StatusCode %s",
  413. UA_StatusCode_name(retval));
  414. return retval;
  415. }
  416. }
  417. return UA_STATUSCODE_GOOD;
  418. }
  419. #endif /* UA_ENABLE_SUBSCRIPTIONS_EVENTS */