@tier0/opcua-client 0.4.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BENCHMARK.md +27 -0
- package/README.md +48 -6
- package/lib/client.js +61 -10
- package/lib/compatibility.js +28 -0
- package/lib/points.js +40 -5
- package/lib/security.js +182 -2
- package/nodes/browse.html +13 -2
- package/nodes/browse.js +29 -2
- package/nodes/connection.html +19 -9
- package/nodes/connection.js +71 -0
- package/nodes/read.html +2 -0
- package/nodes/read.js +10 -0
- package/nodes/subscribe.html +165 -5
- package/nodes/subscribe.js +218 -23
- package/nodes/write.html +2 -0
- package/nodes/write.js +10 -0
- package/package.json +3 -2
- package/prebuilds/linux-x64-musl/opcua_open62541.node +0 -0
- package/scripts/verify-offline-package.js +21 -1
- package/src/addon.cc +343 -49
package/src/addon.cc
CHANGED
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
|
|
9
9
|
#include <algorithm>
|
|
10
10
|
#include <atomic>
|
|
11
|
+
#include <charconv>
|
|
11
12
|
#include <chrono>
|
|
13
|
+
#include <cmath>
|
|
12
14
|
#include <condition_variable>
|
|
13
15
|
#include <cstdint>
|
|
14
16
|
#include <deque>
|
|
@@ -47,6 +49,14 @@ std::string SecurityPolicyUri(const std::string& policy) {
|
|
|
47
49
|
policy == "http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256") {
|
|
48
50
|
return "http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256";
|
|
49
51
|
}
|
|
52
|
+
if (policy == "Aes128_Sha256_RsaOaep" ||
|
|
53
|
+
policy == "http://opcfoundation.org/UA/SecurityPolicy#Aes128_Sha256_RsaOaep") {
|
|
54
|
+
return "http://opcfoundation.org/UA/SecurityPolicy#Aes128_Sha256_RsaOaep";
|
|
55
|
+
}
|
|
56
|
+
if (policy == "Aes256_Sha256_RsaPss" ||
|
|
57
|
+
policy == "http://opcfoundation.org/UA/SecurityPolicy#Aes256_Sha256_RsaPss") {
|
|
58
|
+
return "http://opcfoundation.org/UA/SecurityPolicy#Aes256_Sha256_RsaPss";
|
|
59
|
+
}
|
|
50
60
|
return "";
|
|
51
61
|
}
|
|
52
62
|
|
|
@@ -201,6 +211,94 @@ std::string BuiltinTypeName(const UA_NodeId& node_id) {
|
|
|
201
211
|
}
|
|
202
212
|
}
|
|
203
213
|
|
|
214
|
+
constexpr UA_Int64 kJsSafeInteger = 9007199254740991LL;
|
|
215
|
+
|
|
216
|
+
Napi::Value Int64Value(Napi::Env env, UA_Int64 value) {
|
|
217
|
+
if (value >= -kJsSafeInteger && value <= kJsSafeInteger) {
|
|
218
|
+
return Napi::Number::New(env, static_cast<double>(value));
|
|
219
|
+
}
|
|
220
|
+
return Napi::String::New(env, std::to_string(value));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
Napi::Value UInt64Value(Napi::Env env, UA_UInt64 value) {
|
|
224
|
+
if (value <= static_cast<UA_UInt64>(kJsSafeInteger)) {
|
|
225
|
+
return Napi::Number::New(env, static_cast<double>(value));
|
|
226
|
+
}
|
|
227
|
+
return Napi::String::New(env, std::to_string(value));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
bool InvalidInteger(Napi::Env env, const std::string& name,
|
|
231
|
+
const std::string& detail) {
|
|
232
|
+
Napi::RangeError::New(env, "Invalid " + name + " value: " + detail)
|
|
233
|
+
.ThrowAsJavaScriptException();
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
bool ParseIntegerNumber(Napi::Env env, const Napi::Value& input,
|
|
238
|
+
const std::string& name, double minimum,
|
|
239
|
+
double maximum, double* output) {
|
|
240
|
+
if (!input.IsNumber() && !input.IsString()) {
|
|
241
|
+
return InvalidInteger(env, name, "expected an integer number");
|
|
242
|
+
}
|
|
243
|
+
if (input.IsString() && input.As<Napi::String>().Utf8Value().empty()) {
|
|
244
|
+
return InvalidInteger(env, name, "empty string");
|
|
245
|
+
}
|
|
246
|
+
const double value = input.ToNumber().DoubleValue();
|
|
247
|
+
if (!std::isfinite(value) || std::trunc(value) != value ||
|
|
248
|
+
value < minimum || value > maximum) {
|
|
249
|
+
return InvalidInteger(env, name, input.ToString().Utf8Value());
|
|
250
|
+
}
|
|
251
|
+
*output = value;
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
bool ParseInt64(Napi::Env env, const Napi::Value& input, UA_Int64* output) {
|
|
256
|
+
if (input.IsBigInt()) {
|
|
257
|
+
bool lossless = false;
|
|
258
|
+
*output = input.As<Napi::BigInt>().Int64Value(&lossless);
|
|
259
|
+
return lossless || InvalidInteger(env, "Int64", "out of range");
|
|
260
|
+
}
|
|
261
|
+
if (input.IsString()) {
|
|
262
|
+
const std::string text = input.As<Napi::String>().Utf8Value();
|
|
263
|
+
const auto result = std::from_chars(
|
|
264
|
+
text.data(), text.data() + text.size(), *output, 10);
|
|
265
|
+
if (!text.empty() && result.ec == std::errc() &&
|
|
266
|
+
result.ptr == text.data() + text.size()) return true;
|
|
267
|
+
return InvalidInteger(env, "Int64", text);
|
|
268
|
+
}
|
|
269
|
+
double value = 0;
|
|
270
|
+
if (!ParseIntegerNumber(env, input, "Int64",
|
|
271
|
+
-static_cast<double>(kJsSafeInteger),
|
|
272
|
+
static_cast<double>(kJsSafeInteger), &value)) {
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
*output = static_cast<UA_Int64>(value);
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
bool ParseUInt64(Napi::Env env, const Napi::Value& input, UA_UInt64* output) {
|
|
280
|
+
if (input.IsBigInt()) {
|
|
281
|
+
bool lossless = false;
|
|
282
|
+
*output = input.As<Napi::BigInt>().Uint64Value(&lossless);
|
|
283
|
+
return lossless || InvalidInteger(env, "UInt64", "out of range");
|
|
284
|
+
}
|
|
285
|
+
if (input.IsString()) {
|
|
286
|
+
const std::string text = input.As<Napi::String>().Utf8Value();
|
|
287
|
+
const auto result = std::from_chars(
|
|
288
|
+
text.data(), text.data() + text.size(), *output, 10);
|
|
289
|
+
if (!text.empty() && result.ec == std::errc() &&
|
|
290
|
+
result.ptr == text.data() + text.size()) return true;
|
|
291
|
+
return InvalidInteger(env, "UInt64", text);
|
|
292
|
+
}
|
|
293
|
+
double value = 0;
|
|
294
|
+
if (!ParseIntegerNumber(env, input, "UInt64", 0,
|
|
295
|
+
static_cast<double>(kJsSafeInteger), &value)) {
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
*output = static_cast<UA_UInt64>(value);
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
|
|
204
302
|
Napi::Value VariantValue(Napi::Env env, const UA_Variant* variant) {
|
|
205
303
|
if (!variant || !variant->type || !variant->data) return env.Null();
|
|
206
304
|
const UA_DataType* type = variant->type;
|
|
@@ -213,8 +311,8 @@ Napi::Value VariantValue(Napi::Env env, const UA_Variant* variant) {
|
|
|
213
311
|
if (type == &UA_TYPES[UA_TYPES_UINT16]) return Napi::Number::New(env, *static_cast<const UA_UInt16*>(data));
|
|
214
312
|
if (type == &UA_TYPES[UA_TYPES_INT32]) return Napi::Number::New(env, *static_cast<const UA_Int32*>(data));
|
|
215
313
|
if (type == &UA_TYPES[UA_TYPES_UINT32]) return Napi::Number::New(env, *static_cast<const UA_UInt32*>(data));
|
|
216
|
-
if (type == &UA_TYPES[UA_TYPES_INT64]) return
|
|
217
|
-
if (type == &UA_TYPES[UA_TYPES_UINT64]) return
|
|
314
|
+
if (type == &UA_TYPES[UA_TYPES_INT64]) return Int64Value(env, *static_cast<const UA_Int64*>(data));
|
|
315
|
+
if (type == &UA_TYPES[UA_TYPES_UINT64]) return UInt64Value(env, *static_cast<const UA_UInt64*>(data));
|
|
218
316
|
if (type == &UA_TYPES[UA_TYPES_FLOAT]) return Napi::Number::New(env, *static_cast<const UA_Float*>(data));
|
|
219
317
|
if (type == &UA_TYPES[UA_TYPES_DOUBLE]) return Napi::Number::New(env, *static_cast<const UA_Double*>(data));
|
|
220
318
|
if (type == &UA_TYPES[UA_TYPES_STRING]) return Napi::String::New(env, UaString(*static_cast<const UA_String*>(data)));
|
|
@@ -260,14 +358,14 @@ bool JsScalarToVariant(Napi::Env env, const Napi::Value& input, const std::strin
|
|
|
260
358
|
UA_Boolean value = input.ToBoolean();
|
|
261
359
|
return UA_StatusCode_isGood(UA_Variant_setScalarCopy(output, &value, type));
|
|
262
360
|
}
|
|
263
|
-
if (name == "SByte") { UA_SByte value = static_cast<UA_SByte>(
|
|
264
|
-
if (name == "Byte") { UA_Byte value = static_cast<UA_Byte>(
|
|
265
|
-
if (name == "Int16") { UA_Int16 value = static_cast<UA_Int16>(
|
|
266
|
-
if (name == "UInt16") { UA_UInt16 value = static_cast<UA_UInt16>(
|
|
267
|
-
if (name == "Int32") { UA_Int32 value =
|
|
268
|
-
if (name == "UInt32") { UA_UInt32 value =
|
|
269
|
-
if (name == "Int64") {
|
|
270
|
-
if (name == "UInt64") {
|
|
361
|
+
if (name == "SByte") { double parsed; if (!ParseIntegerNumber(env, input, name, -128, 127, &parsed)) return false; UA_SByte value = static_cast<UA_SByte>(parsed); return UA_StatusCode_isGood(UA_Variant_setScalarCopy(output, &value, type)); }
|
|
362
|
+
if (name == "Byte") { double parsed; if (!ParseIntegerNumber(env, input, name, 0, 255, &parsed)) return false; UA_Byte value = static_cast<UA_Byte>(parsed); return UA_StatusCode_isGood(UA_Variant_setScalarCopy(output, &value, type)); }
|
|
363
|
+
if (name == "Int16") { double parsed; if (!ParseIntegerNumber(env, input, name, -32768, 32767, &parsed)) return false; UA_Int16 value = static_cast<UA_Int16>(parsed); return UA_StatusCode_isGood(UA_Variant_setScalarCopy(output, &value, type)); }
|
|
364
|
+
if (name == "UInt16") { double parsed; if (!ParseIntegerNumber(env, input, name, 0, 65535, &parsed)) return false; UA_UInt16 value = static_cast<UA_UInt16>(parsed); return UA_StatusCode_isGood(UA_Variant_setScalarCopy(output, &value, type)); }
|
|
365
|
+
if (name == "Int32") { double parsed; if (!ParseIntegerNumber(env, input, name, -2147483648.0, 2147483647.0, &parsed)) return false; UA_Int32 value = static_cast<UA_Int32>(parsed); return UA_StatusCode_isGood(UA_Variant_setScalarCopy(output, &value, type)); }
|
|
366
|
+
if (name == "UInt32") { double parsed; if (!ParseIntegerNumber(env, input, name, 0, 4294967295.0, &parsed)) return false; UA_UInt32 value = static_cast<UA_UInt32>(parsed); return UA_StatusCode_isGood(UA_Variant_setScalarCopy(output, &value, type)); }
|
|
367
|
+
if (name == "Int64") { UA_Int64 value; if (!ParseInt64(env, input, &value)) return false; return UA_StatusCode_isGood(UA_Variant_setScalarCopy(output, &value, type)); }
|
|
368
|
+
if (name == "UInt64") { UA_UInt64 value; if (!ParseUInt64(env, input, &value)) return false; return UA_StatusCode_isGood(UA_Variant_setScalarCopy(output, &value, type)); }
|
|
271
369
|
if (name == "Float") { UA_Float value = input.ToNumber().FloatValue(); return UA_StatusCode_isGood(UA_Variant_setScalarCopy(output, &value, type)); }
|
|
272
370
|
if (name == "Double") { UA_Double value = input.ToNumber().DoubleValue(); return UA_StatusCode_isGood(UA_Variant_setScalarCopy(output, &value, type)); }
|
|
273
371
|
if (name == "String") { std::string text = input.ToString().Utf8Value(); UA_String value = UA_STRING(const_cast<char*>(text.c_str())); return UA_StatusCode_isGood(UA_Variant_setScalarCopy(output, &value, type)); }
|
|
@@ -1481,6 +1579,8 @@ struct BrowseOptions {
|
|
|
1481
1579
|
bool variables_only = true;
|
|
1482
1580
|
uint32_t max_depth = 10;
|
|
1483
1581
|
uint32_t max_nodes = 10000;
|
|
1582
|
+
uint32_t browse_batch_size = 100;
|
|
1583
|
+
uint32_t references_per_node = 1000;
|
|
1484
1584
|
uint32_t attribute_batch_size = 200;
|
|
1485
1585
|
std::set<uint16_t> namespaces;
|
|
1486
1586
|
};
|
|
@@ -1512,16 +1612,51 @@ class BrowseWorker : public ClientCommandWorker {
|
|
|
1512
1612
|
item.Set("userAccessLevel", point.user_access_level); item.Set("minimumSamplingInterval", point.minimum_sampling_interval);
|
|
1513
1613
|
output.Set(index, item);
|
|
1514
1614
|
}
|
|
1615
|
+
output.Set("truncated", truncated_);
|
|
1616
|
+
if (!truncation_reason_.empty()) output.Set("truncationReason", truncation_reason_);
|
|
1617
|
+
output.Set("serviceCalls", Napi::Number::New(
|
|
1618
|
+
Env(), static_cast<double>(service_calls_)));
|
|
1619
|
+
output.Set("browseBatchSize", browse_batch_size_);
|
|
1620
|
+
output.Set("releasedContinuationPoints", Napi::Number::New(
|
|
1621
|
+
Env(), static_cast<double>(released_continuation_points_)));
|
|
1622
|
+
Napi::Array failures = Napi::Array::New(Env(), browse_failures_.size());
|
|
1623
|
+
for (size_t index = 0; index < browse_failures_.size(); ++index) {
|
|
1624
|
+
Napi::Object failure = Napi::Object::New(Env());
|
|
1625
|
+
failure.Set("nodeId", browse_failures_[index].first);
|
|
1626
|
+
failure.Set("statusCode", browse_failures_[index].second);
|
|
1627
|
+
failures.Set(index, failure);
|
|
1628
|
+
}
|
|
1629
|
+
output.Set("browseFailures", failures);
|
|
1515
1630
|
deferred_.Resolve(output);
|
|
1516
1631
|
}
|
|
1517
1632
|
private:
|
|
1518
1633
|
struct QueueItem { std::string node_id; std::string path; uint32_t depth; };
|
|
1634
|
+
struct PendingContinuation {
|
|
1635
|
+
QueueItem parent;
|
|
1636
|
+
std::vector<UA_Byte> token;
|
|
1637
|
+
};
|
|
1519
1638
|
|
|
1520
1639
|
bool NamespaceAllowed(uint16_t value) const { return options_.namespaces.empty() || options_.namespaces.count(value) > 0; }
|
|
1521
1640
|
|
|
1641
|
+
void MarkTruncated(const std::string& reason) {
|
|
1642
|
+
truncated_ = true;
|
|
1643
|
+
if (truncation_reason_.empty() || truncation_reason_ == "maxDepth") {
|
|
1644
|
+
truncation_reason_ = reason;
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
void MarkDepthLimited() {
|
|
1649
|
+
depth_limited_ = true;
|
|
1650
|
+
if (truncation_reason_.empty()) truncation_reason_ = "maxDepth";
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1522
1653
|
void ProcessBrowseResult(const QueueItem& parent, const UA_BrowseResult& result,
|
|
1523
1654
|
std::deque<QueueItem>* queue, std::set<std::string>* visited) {
|
|
1524
|
-
for (size_t index = 0; index < result.referencesSize
|
|
1655
|
+
for (size_t index = 0; index < result.referencesSize; ++index) {
|
|
1656
|
+
if (points_.size() >= options_.max_nodes) {
|
|
1657
|
+
MarkTruncated("maxNodes");
|
|
1658
|
+
return;
|
|
1659
|
+
}
|
|
1525
1660
|
const UA_ReferenceDescription& reference = result.references[index];
|
|
1526
1661
|
if (reference.nodeId.serverIndex != 0 || reference.nodeId.namespaceUri.length != 0) continue;
|
|
1527
1662
|
const UA_NodeId& node_id = reference.nodeId.nodeId;
|
|
@@ -1539,58 +1674,206 @@ class BrowseWorker : public ClientCommandWorker {
|
|
|
1539
1674
|
point.node_class = reference.nodeClass; point.type_definition = NodeIdString(reference.typeDefinition.nodeId);
|
|
1540
1675
|
points_.push_back(std::move(point));
|
|
1541
1676
|
}
|
|
1542
|
-
if (options_.recursive
|
|
1543
|
-
|
|
1677
|
+
if (options_.recursive) {
|
|
1678
|
+
if (parent.depth < options_.max_depth) {
|
|
1679
|
+
if (visited->insert(node_id_text).second) {
|
|
1680
|
+
queue->push_back({node_id_text, path, parent.depth + 1});
|
|
1681
|
+
}
|
|
1682
|
+
} else if (reference.nodeClass == UA_NODECLASS_OBJECT) {
|
|
1683
|
+
MarkDepthLimited();
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
static PendingContinuation Continuation(
|
|
1690
|
+
const QueueItem& parent, const UA_ByteString& point) {
|
|
1691
|
+
PendingContinuation output{parent, {}};
|
|
1692
|
+
if (point.length && point.data) {
|
|
1693
|
+
output.token.assign(point.data, point.data + point.length);
|
|
1694
|
+
}
|
|
1695
|
+
return output;
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
bool FillContinuationPoints(
|
|
1699
|
+
const std::vector<PendingContinuation>& pending, size_t start, size_t count,
|
|
1700
|
+
UA_BrowseNextRequest* request) {
|
|
1701
|
+
request->continuationPointsSize = count;
|
|
1702
|
+
request->continuationPoints = static_cast<UA_ByteString*>(
|
|
1703
|
+
UA_Array_new(count, &UA_TYPES[UA_TYPES_BYTESTRING]));
|
|
1704
|
+
if (!request->continuationPoints) return false;
|
|
1705
|
+
for (size_t index = 0; index < count; ++index) {
|
|
1706
|
+
const auto& token = pending[start + index].token;
|
|
1707
|
+
UA_ByteString source;
|
|
1708
|
+
source.length = token.size();
|
|
1709
|
+
source.data = const_cast<UA_Byte*>(token.data());
|
|
1710
|
+
if (UA_ByteString_copy(&source, &request->continuationPoints[index]) !=
|
|
1711
|
+
UA_STATUSCODE_GOOD) {
|
|
1712
|
+
return false;
|
|
1544
1713
|
}
|
|
1545
1714
|
}
|
|
1715
|
+
return true;
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
void ReleaseContinuations(const std::vector<PendingContinuation>& pending) {
|
|
1719
|
+
for (size_t start = 0; start < pending.size(); start += browse_batch_size_) {
|
|
1720
|
+
const size_t count = std::min<size_t>(
|
|
1721
|
+
browse_batch_size_, pending.size() - start);
|
|
1722
|
+
UA_BrowseNextRequest request;
|
|
1723
|
+
UA_BrowseNextRequest_init(&request);
|
|
1724
|
+
request.releaseContinuationPoints = true;
|
|
1725
|
+
if (!FillContinuationPoints(pending, start, count, &request)) {
|
|
1726
|
+
UA_BrowseNextRequest_clear(&request);
|
|
1727
|
+
continue;
|
|
1728
|
+
}
|
|
1729
|
+
UA_BrowseNextResponse response =
|
|
1730
|
+
UA_Client_Service_browseNext(context_->client, request);
|
|
1731
|
+
++service_calls_;
|
|
1732
|
+
UA_BrowseNextRequest_clear(&request);
|
|
1733
|
+
UA_BrowseNextResponse_clear(&response);
|
|
1734
|
+
released_continuation_points_ += count;
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
bool BrowseContinuations(
|
|
1739
|
+
std::vector<PendingContinuation>* pending, std::deque<QueueItem>* queue,
|
|
1740
|
+
std::set<std::string>* visited) {
|
|
1741
|
+
while (!pending->empty()) {
|
|
1742
|
+
std::vector<PendingContinuation> next_pending;
|
|
1743
|
+
for (size_t start = 0; start < pending->size(); start += browse_batch_size_) {
|
|
1744
|
+
const size_t count = std::min<size_t>(
|
|
1745
|
+
browse_batch_size_, pending->size() - start);
|
|
1746
|
+
UA_BrowseNextRequest request;
|
|
1747
|
+
UA_BrowseNextRequest_init(&request);
|
|
1748
|
+
if (!FillContinuationPoints(*pending, start, count, &request)) {
|
|
1749
|
+
UA_BrowseNextRequest_clear(&request);
|
|
1750
|
+
ReleaseContinuations(*pending);
|
|
1751
|
+
SetError("Failed to allocate BrowseNext continuation points");
|
|
1752
|
+
return false;
|
|
1753
|
+
}
|
|
1754
|
+
UA_BrowseNextResponse response =
|
|
1755
|
+
UA_Client_Service_browseNext(context_->client, request);
|
|
1756
|
+
++service_calls_;
|
|
1757
|
+
UA_BrowseNextRequest_clear(&request);
|
|
1758
|
+
if (!UA_StatusCode_isGood(response.responseHeader.serviceResult)) {
|
|
1759
|
+
const std::string status = StatusName(
|
|
1760
|
+
response.responseHeader.serviceResult);
|
|
1761
|
+
UA_BrowseNextResponse_clear(&response);
|
|
1762
|
+
ReleaseContinuations(*pending);
|
|
1763
|
+
SetError("BrowseNext failed: " + status);
|
|
1764
|
+
return false;
|
|
1765
|
+
}
|
|
1766
|
+
for (size_t index = 0; index < count; ++index) {
|
|
1767
|
+
const auto& parent = (*pending)[start + index].parent;
|
|
1768
|
+
if (index < response.resultsSize) {
|
|
1769
|
+
const UA_BrowseResult& result = response.results[index];
|
|
1770
|
+
if (UA_StatusCode_isGood(result.statusCode) && !truncated_) {
|
|
1771
|
+
ProcessBrowseResult(parent, result, queue, visited);
|
|
1772
|
+
} else if (!UA_StatusCode_isGood(result.statusCode)) {
|
|
1773
|
+
browse_failures_.push_back(
|
|
1774
|
+
{parent.node_id, StatusName(result.statusCode)});
|
|
1775
|
+
}
|
|
1776
|
+
if (result.continuationPoint.length) {
|
|
1777
|
+
next_pending.push_back(
|
|
1778
|
+
Continuation(parent, result.continuationPoint));
|
|
1779
|
+
}
|
|
1780
|
+
} else {
|
|
1781
|
+
browse_failures_.push_back(
|
|
1782
|
+
{parent.node_id, "BadUnexpectedError"});
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
UA_BrowseNextResponse_clear(&response);
|
|
1786
|
+
}
|
|
1787
|
+
if (truncated_) {
|
|
1788
|
+
ReleaseContinuations(next_pending);
|
|
1789
|
+
return true;
|
|
1790
|
+
}
|
|
1791
|
+
*pending = std::move(next_pending);
|
|
1792
|
+
}
|
|
1793
|
+
return true;
|
|
1546
1794
|
}
|
|
1547
1795
|
|
|
1548
1796
|
bool BrowseTree() {
|
|
1549
1797
|
std::deque<QueueItem> queue{{options_.root_node_id, "", 0}};
|
|
1550
1798
|
std::set<std::string> visited{options_.root_node_id};
|
|
1551
|
-
|
|
1552
|
-
|
|
1799
|
+
browse_batch_size_ = std::max<uint32_t>(1, options_.browse_batch_size);
|
|
1800
|
+
const uint32_t server_limit =
|
|
1801
|
+
context_->operation_limits.max_nodes_per_browse.load();
|
|
1802
|
+
if (server_limit > 0) {
|
|
1803
|
+
browse_batch_size_ = std::min(browse_batch_size_, server_limit);
|
|
1804
|
+
}
|
|
1805
|
+
while (!queue.empty() && !truncated_) {
|
|
1806
|
+
std::vector<QueueItem> batch;
|
|
1807
|
+
batch.reserve(browse_batch_size_);
|
|
1808
|
+
while (!queue.empty() && batch.size() < browse_batch_size_) {
|
|
1809
|
+
batch.push_back(std::move(queue.front()));
|
|
1810
|
+
queue.pop_front();
|
|
1811
|
+
}
|
|
1553
1812
|
UA_BrowseRequest request; UA_BrowseRequest_init(&request);
|
|
1554
|
-
request.requestedMaxReferencesPerNode =
|
|
1555
|
-
request.nodesToBrowseSize =
|
|
1556
|
-
request.nodesToBrowse = static_cast<UA_BrowseDescription*>(
|
|
1557
|
-
|
|
1558
|
-
|
|
1813
|
+
request.requestedMaxReferencesPerNode = options_.references_per_node;
|
|
1814
|
+
request.nodesToBrowseSize = batch.size();
|
|
1815
|
+
request.nodesToBrowse = static_cast<UA_BrowseDescription*>(
|
|
1816
|
+
UA_Array_new(batch.size(), &UA_TYPES[UA_TYPES_BROWSEDESCRIPTION]));
|
|
1817
|
+
if (!request.nodesToBrowse) {
|
|
1818
|
+
SetError("Failed to allocate Browse request");
|
|
1819
|
+
return false;
|
|
1820
|
+
}
|
|
1821
|
+
for (size_t index = 0; index < batch.size(); ++index) {
|
|
1822
|
+
if (!CopyCachedNodeId(context_.get(), batch[index].node_id,
|
|
1823
|
+
&request.nodesToBrowse[index].nodeId)) {
|
|
1824
|
+
const std::string invalid = batch[index].node_id;
|
|
1825
|
+
UA_BrowseRequest_clear(&request);
|
|
1826
|
+
SetError("Invalid browse NodeId: " + invalid);
|
|
1827
|
+
return false;
|
|
1828
|
+
}
|
|
1829
|
+
auto& description = request.nodesToBrowse[index];
|
|
1830
|
+
description.browseDirection = UA_BROWSEDIRECTION_FORWARD;
|
|
1831
|
+
description.referenceTypeId =
|
|
1832
|
+
UA_NODEID_NUMERIC(0, UA_NS0ID_HIERARCHICALREFERENCES);
|
|
1833
|
+
description.includeSubtypes = true;
|
|
1834
|
+
description.nodeClassMask = UA_NODECLASS_OBJECT | UA_NODECLASS_VARIABLE;
|
|
1835
|
+
description.resultMask = UA_BROWSERESULTMASK_ALL;
|
|
1559
1836
|
}
|
|
1560
|
-
auto& description = request.nodesToBrowse[0];
|
|
1561
|
-
description.browseDirection = UA_BROWSEDIRECTION_FORWARD;
|
|
1562
|
-
description.referenceTypeId = UA_NODEID_NUMERIC(0, UA_NS0ID_HIERARCHICALREFERENCES);
|
|
1563
|
-
description.includeSubtypes = true;
|
|
1564
|
-
description.nodeClassMask = UA_NODECLASS_OBJECT | UA_NODECLASS_VARIABLE;
|
|
1565
|
-
description.resultMask = UA_BROWSERESULTMASK_ALL;
|
|
1566
1837
|
UA_BrowseResponse response = UA_Client_Service_browse(context_->client, request);
|
|
1838
|
+
++service_calls_;
|
|
1567
1839
|
UA_BrowseRequest_clear(&request);
|
|
1568
1840
|
if (!UA_StatusCode_isGood(response.responseHeader.serviceResult)) {
|
|
1569
1841
|
UA_BrowseResponse_clear(&response); SetError("Browse failed: " + StatusName(response.responseHeader.serviceResult)); return false;
|
|
1570
1842
|
}
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
UA_BrowseNextResponse_clear(&next_response); break;
|
|
1843
|
+
std::vector<PendingContinuation> pending;
|
|
1844
|
+
for (size_t index = 0; index < batch.size(); ++index) {
|
|
1845
|
+
if (index < response.resultsSize) {
|
|
1846
|
+
const UA_BrowseResult& result = response.results[index];
|
|
1847
|
+
if (UA_StatusCode_isGood(result.statusCode) && !truncated_) {
|
|
1848
|
+
ProcessBrowseResult(batch[index], result, &queue, &visited);
|
|
1849
|
+
} else if (!UA_StatusCode_isGood(result.statusCode)) {
|
|
1850
|
+
browse_failures_.push_back(
|
|
1851
|
+
{batch[index].node_id, StatusName(result.statusCode)});
|
|
1852
|
+
}
|
|
1853
|
+
if (result.continuationPoint.length) {
|
|
1854
|
+
pending.push_back(
|
|
1855
|
+
Continuation(batch[index], result.continuationPoint));
|
|
1585
1856
|
}
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1857
|
+
} else {
|
|
1858
|
+
browse_failures_.push_back(
|
|
1859
|
+
{batch[index].node_id, "BadUnexpectedError"});
|
|
1589
1860
|
}
|
|
1590
|
-
UA_ByteString_clear(&continuation);
|
|
1591
1861
|
}
|
|
1592
1862
|
UA_BrowseResponse_clear(&response);
|
|
1863
|
+
if (points_.size() >= options_.max_nodes &&
|
|
1864
|
+
(!pending.empty() || !queue.empty())) {
|
|
1865
|
+
MarkTruncated("maxNodes");
|
|
1866
|
+
}
|
|
1867
|
+
if (truncated_) {
|
|
1868
|
+
ReleaseContinuations(pending);
|
|
1869
|
+
break;
|
|
1870
|
+
}
|
|
1871
|
+
if (!BrowseContinuations(&pending, &queue, &visited)) return false;
|
|
1872
|
+
if (points_.size() >= options_.max_nodes && !queue.empty()) {
|
|
1873
|
+
MarkTruncated("maxNodes");
|
|
1874
|
+
}
|
|
1593
1875
|
}
|
|
1876
|
+
if (depth_limited_) truncated_ = true;
|
|
1594
1877
|
return true;
|
|
1595
1878
|
}
|
|
1596
1879
|
|
|
@@ -1647,6 +1930,13 @@ class BrowseWorker : public ClientCommandWorker {
|
|
|
1647
1930
|
|
|
1648
1931
|
BrowseOptions options_;
|
|
1649
1932
|
std::vector<BrowsePoint> points_;
|
|
1933
|
+
uint32_t browse_batch_size_ = 1;
|
|
1934
|
+
uint64_t service_calls_ = 0;
|
|
1935
|
+
uint64_t released_continuation_points_ = 0;
|
|
1936
|
+
bool truncated_ = false;
|
|
1937
|
+
bool depth_limited_ = false;
|
|
1938
|
+
std::string truncation_reason_;
|
|
1939
|
+
std::vector<std::pair<std::string, std::string>> browse_failures_;
|
|
1650
1940
|
};
|
|
1651
1941
|
|
|
1652
1942
|
class CreateSubscriptionWorker : public ClientCommandWorker {
|
|
@@ -1673,6 +1963,9 @@ class CreateSubscriptionWorker : public ClientCommandWorker {
|
|
|
1673
1963
|
if (!context_->client) { SetError("Client disconnected"); return; }
|
|
1674
1964
|
UA_CreateSubscriptionResponse response = UA_Client_Subscriptions_create(context_->client, request, nullptr, nullptr, nullptr);
|
|
1675
1965
|
status_ = response.responseHeader.serviceResult; subscription_id_ = response.subscriptionId;
|
|
1966
|
+
revised_publishing_interval_ = response.revisedPublishingInterval;
|
|
1967
|
+
revised_lifetime_ = response.revisedLifetimeCount;
|
|
1968
|
+
revised_keepalive_ = response.revisedMaxKeepAliveCount;
|
|
1676
1969
|
UA_CreateSubscriptionResponse_clear(&response);
|
|
1677
1970
|
if (!UA_StatusCode_isGood(status_)) { SetError("Create subscription failed: " + StatusName(status_)); return; }
|
|
1678
1971
|
auto subscription = std::make_unique<SubscriptionContext>();
|
|
@@ -1685,17 +1978,18 @@ class CreateSubscriptionWorker : public ClientCommandWorker {
|
|
|
1685
1978
|
subscription->notifications->overflow_policy = overflow_policy_;
|
|
1686
1979
|
context_->subscriptions[subscription_id_] = std::move(subscription); transferred_ = true;
|
|
1687
1980
|
}
|
|
1688
|
-
void OnOK() override { Napi::Object result = Napi::Object::New(Env()); result.Set("subscriptionId", subscription_id_); result.Set("statusCode", StatusName(status_)); deferred_.Resolve(result); }
|
|
1981
|
+
void OnOK() override { Napi::Object result = Napi::Object::New(Env()); result.Set("subscriptionId", subscription_id_); result.Set("statusCode", StatusName(status_)); result.Set("revisedPublishingInterval", revised_publishing_interval_); result.Set("revisedLifetimeCount", revised_lifetime_); result.Set("revisedMaxKeepAliveCount", revised_keepalive_); deferred_.Resolve(result); }
|
|
1689
1982
|
void OnError(const Napi::Error& error) override { if (!transferred_) callback_.Release(); PromiseWorker::OnError(error); }
|
|
1690
1983
|
private:
|
|
1691
1984
|
Napi::ThreadSafeFunction callback_; bool transferred_ = false;
|
|
1692
1985
|
double publishing_interval_; uint32_t lifetime_; uint32_t keepalive_; uint32_t max_notifications_;
|
|
1693
1986
|
uint32_t queue_size_; uint32_t callback_batch_size_; std::string queue_mode_; std::string overflow_policy_;
|
|
1694
1987
|
uint32_t subscription_id_ = 0; UA_StatusCode status_ = UA_STATUSCODE_GOOD;
|
|
1988
|
+
double revised_publishing_interval_ = 0; uint32_t revised_lifetime_ = 0; uint32_t revised_keepalive_ = 0;
|
|
1695
1989
|
};
|
|
1696
1990
|
|
|
1697
1991
|
struct MonitorInput { std::string node_id; double sampling_interval; uint32_t queue_size; bool discard_oldest; std::string deadband_type; double deadband_value; };
|
|
1698
|
-
struct MonitorResult { std::string node_id; uint32_t monitored_item_id = 0; uint32_t client_handle = 0; UA_StatusCode status = UA_STATUSCODE_BADUNEXPECTEDERROR; };
|
|
1992
|
+
struct MonitorResult { std::string node_id; uint32_t monitored_item_id = 0; uint32_t client_handle = 0; double revised_sampling_interval = 0; uint32_t revised_queue_size = 0; UA_StatusCode status = UA_STATUSCODE_BADUNEXPECTEDERROR; };
|
|
1699
1993
|
|
|
1700
1994
|
class AddMonitoredWorker : public ClientCommandWorker {
|
|
1701
1995
|
public:
|
|
@@ -1750,14 +2044,14 @@ class AddMonitoredWorker : public ClientCommandWorker {
|
|
|
1750
2044
|
for (size_t index = 0; index < inputs_.size(); ++index) {
|
|
1751
2045
|
MonitorResult result; result.node_id = inputs_[index].node_id; result.client_handle = contexts[index]->client_handle;
|
|
1752
2046
|
if (index < response.resultsSize) {
|
|
1753
|
-
result.status = response.results[index].statusCode; result.monitored_item_id = response.results[index].monitoredItemId;
|
|
2047
|
+
result.status = response.results[index].statusCode; result.monitored_item_id = response.results[index].monitoredItemId; result.revised_sampling_interval = response.results[index].revisedSamplingInterval; result.revised_queue_size = response.results[index].revisedQueueSize;
|
|
1754
2048
|
if (UA_StatusCode_isGood(result.status)) { contexts[index]->monitored_item_id = result.monitored_item_id; auto* monitored_context = contexts[index].get(); subscription->monitored_by_client_handle[monitored_context->client_handle] = monitored_context; subscription->monitored[result.monitored_item_id] = std::move(contexts[index]); }
|
|
1755
2049
|
}
|
|
1756
2050
|
results_.push_back(result);
|
|
1757
2051
|
}
|
|
1758
2052
|
UA_CreateMonitoredItemsResponse_clear(&response);
|
|
1759
2053
|
}
|
|
1760
|
-
void OnOK() override { Napi::Array output = Napi::Array::New(Env(), results_.size()); for (size_t index = 0; index < results_.size(); ++index) { Napi::Object item = Napi::Object::New(Env()); item.Set("nodeId", results_[index].node_id); item.Set("monitoredItemId", results_[index].monitored_item_id); item.Set("clientHandle", results_[index].client_handle); item.Set("statusCode", StatusName(results_[index].status)); output.Set(index, item); } deferred_.Resolve(output); }
|
|
2054
|
+
void OnOK() override { Napi::Array output = Napi::Array::New(Env(), results_.size()); for (size_t index = 0; index < results_.size(); ++index) { Napi::Object item = Napi::Object::New(Env()); item.Set("nodeId", results_[index].node_id); item.Set("monitoredItemId", results_[index].monitored_item_id); item.Set("clientHandle", results_[index].client_handle); item.Set("revisedSamplingInterval", results_[index].revised_sampling_interval); item.Set("revisedQueueSize", results_[index].revised_queue_size); item.Set("statusCode", StatusName(results_[index].status)); output.Set(index, item); } deferred_.Resolve(output); }
|
|
1761
2055
|
private:
|
|
1762
2056
|
uint32_t subscription_id_; std::vector<MonitorInput> inputs_; std::vector<MonitorResult> results_;
|
|
1763
2057
|
UA_TimestampsToReturn timestamps_to_return_;
|
|
@@ -1956,7 +2250,7 @@ Napi::Value WriteManyAsync(const Napi::CallbackInfo& info) {
|
|
|
1956
2250
|
|
|
1957
2251
|
Napi::Value BrowseAsync(const Napi::CallbackInfo& info) {
|
|
1958
2252
|
Napi::Env env = info.Env(); auto context = RequireClient(env, info[0]); if (!context || !info[1].IsObject()) return env.Undefined(); Napi::Object object = info[1].As<Napi::Object>(); BrowseOptions options;
|
|
1959
|
-
options.root_node_id = GetString(object, "rootNodeId", "ns=0;i=85"); options.recursive = GetBool(object, "recursive", true); options.variables_only = GetBool(object, "variablesOnly", true); options.max_depth = GetUInt(object, "maxDepth", 10); options.max_nodes = GetUInt(object, "maxNodes", 10000); options.attribute_batch_size = GetUInt(object, "attributeBatchSize", 200);
|
|
2253
|
+
options.root_node_id = GetString(object, "rootNodeId", "ns=0;i=85"); options.recursive = GetBool(object, "recursive", true); options.variables_only = GetBool(object, "variablesOnly", true); options.max_depth = GetUInt(object, "maxDepth", 10); options.max_nodes = GetUInt(object, "maxNodes", 10000); options.browse_batch_size = GetUInt(object, "browseBatchSize", 100); options.references_per_node = GetUInt(object, "referencesPerNode", 1000); options.attribute_batch_size = GetUInt(object, "attributeBatchSize", 200);
|
|
1960
2254
|
if (object.Has("namespaceIndexes") && object.Get("namespaceIndexes").IsArray()) { Napi::Array values = object.Get("namespaceIndexes").As<Napi::Array>(); for (uint32_t index = 0; index < values.Length(); ++index) options.namespaces.insert(static_cast<uint16_t>(values.Get(index).ToNumber().Uint32Value())); }
|
|
1961
2255
|
auto deferred = Napi::Promise::Deferred::New(env); (new BrowseWorker(deferred, context, std::move(options)))->Queue(); return deferred.Promise();
|
|
1962
2256
|
}
|