From 7903189674830d104b20fe6f8c6f06b94ebccae1 Mon Sep 17 00:00:00 2001 From: "Andrew D.Laptev" Date: Sun, 19 May 2024 02:53:06 +0300 Subject: [PATCH 1/3] feat(onvif-events): Restart event requests after ECONNRESET error --- .github/workflows/pr.yml | 6 +- README.md | 1 - lib/cam.js | 1 - lib/events.js | 43 ++++--- package-lock.json | 253 ++++++++++++++++++++++++++++++++++++--- test/events.js | 27 +++++ test/serverMockup.js | 46 +++---- 7 files changed, 320 insertions(+), 57 deletions(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 2a8b9852..7fe51c38 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -9,12 +9,12 @@ jobs: strategy: fail-fast: false matrix: - node-version: [14.x,16.x,18.x] + node-version: [16.x,18.x,20.x] steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} cache: 'npm' diff --git a/README.md b/README.md index 187610b0..e2ea18d5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ # ONVIF -[![Build Status](https://travis-ci.org/agsh/onvif.png)](https://travis-ci.org/agsh/onvif) [![Coverage Status](https://img.shields.io/coveralls/agsh/onvif.svg)](https://coveralls.io/r/agsh/onvif?branch=master) [![NPM version](https://img.shields.io/npm/v/onvif.svg)](https://www.npmjs.com/package/onvif) diff --git a/lib/cam.js b/lib/cam.js index 57b46c47..7dda802d 100755 --- a/lib/cam.js +++ b/lib/cam.js @@ -86,7 +86,6 @@ var Cam = function(options, callback) { this.path = options.path || '/onvif/device_service'; this.timeout = options.timeout || 120000; this.agent = options.agent || false; - this.eventReconnectms = options.eventReconnectms; /** * Force using hostname and port from constructor for the services * @type {boolean} diff --git a/lib/events.js b/lib/events.js index 2a91f400..a917297e 100644 --- a/lib/events.js +++ b/lib/events.js @@ -266,18 +266,14 @@ module.exports = function(Cam) { if (!err) { var data = linerase(res).pullMessagesResponse; } - else if (typeof err === "object" && err.code === "ECONNRESET") { - // connection reset - restart Event loop for pullMessages request - this._eventRequest(); - return; - } callback.call(this, err, data, xml); }.bind(this)); }; /** * Unsubscribe from pull-point subscription - * @param {Cam~PullMessagesResponse} callback + * @param {Cam~PullMessagesResponse} [callback] + * @param {boolean} [preserveListeners=false] Don't remove listeners on 'event' * @throws {Error} {@link Cam#events.subscription} must exists */ Cam.prototype.unsubscribe = function(callback, preserveListeners) { @@ -354,8 +350,9 @@ module.exports = function(Cam) { this.createPullPointSubscription(function(error) { if (!error) { this._eventPull(); - } else if (this.eventReconnectms) { - setTimeout(() => this._eventRequest(), this.eventReconnectms); + } else if (typeof error === 'object' && error.code === 'ECONNRESET') { + // connection reset on creation - restart Event loop for pullMessages request + this._restartEventRequest(); } }.bind(this)); } else { @@ -363,7 +360,6 @@ module.exports = function(Cam) { } } else { delete this.events.terminationTime; - this.unsubscribe(); } }; @@ -402,22 +398,39 @@ module.exports = function(Cam) { this._eventRequest(); // go around the loop again, once the RENEW has completed (and terminationTime updated) }); } else { - // there was an error pulling the message - this.unsubscribe(function(_err,_data,_xml) { - // once the unsubsribe has completed (even if it failed), go around the loop again - this._eventRequest(); - }, true); + if (typeof err === 'object' && err.code === 'ECONNRESET') { + // connection reset - restart Event loop for pullMessages request + this._restartEventRequest(); + } else { + // there was an error pulling the message + this.unsubscribe(function(_err, _data, _xml) { + // once the unsubsribe has completed (even if it failed), go around the loop again + this._eventRequest(); + }, true); + } } }.bind(this)); } else { delete this.events.terminationTime; - if (this.events.subscription) { this.unsubscribe(); } } }; + /** + * Restart the event request with an increasing interval when the connection to the device is refused + * @private + */ + Cam.prototype._restartEventRequest = function() { + // TODO maybe stop trying to connect after some time + if (!this._eventReconnectms || this._eventReconnectms > 2 * 60 * 1000) { + this._eventReconnectms = 10; + } + setTimeout(this._eventRequest.bind(this), this._eventReconnectms); + this._eventReconnectms = 1.111 * this._eventReconnectms; + }; + /** * Helper Function to Parse XML Event data received by an external TCP port and * a camera in Event PUSH mode (ie not in subscribe mode) diff --git a/package-lock.json b/package-lock.json index 640487c1..b0e8d68e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "eslint": "^8.3.0", "eslint-plugin-node": "^11.1.0", "ip": "^1.1.5", + "jsdoc": "^4.0.2", "keypress": "^0.2.1", "mocha": "^10.0.0", "mocha-lcov-reporter": "^1.3.0", @@ -359,9 +360,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.16.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.16.4.tgz", - "integrity": "sha512-6V0qdPUaiVHH3RtZeLIsc+6pDhbYzHR8ogA8w+f+Wc77DuXto19g2QUwveINoS34Uw+W8/hQDGJCx+i4n7xcng==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz", + "integrity": "sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -493,18 +494,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/traverse/node_modules/@babel/parser": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", - "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", - "dev": true, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@babel/traverse/node_modules/@babel/template": { "version": "7.22.15", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", @@ -827,6 +816,40 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@jsdoc/salty": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.8.tgz", + "integrity": "sha512-5e+SFVavj1ORKlKaKr2BmTOekmXbelU7dC0cDkQLqag7xfuTPuGMUFx7KWJuv4bYZrTsoL2Z18VVCOKYxzoHcg==", + "dev": true, + "dependencies": { + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=v12.0.0" + } + }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "dev": true + }, + "node_modules/@types/markdown-it": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.1.tgz", + "integrity": "sha512-4NpsnpYl2Gt1ljyBGrKMxFYAYvpqbnnkgP/i/g+NLpjEUa3obn1XJCur9YbEXKDAkaXqsR1LbDnGEJ0MmKFxfg==", + "dev": true, + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "dev": true + }, "node_modules/acorn": { "version": "8.6.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.6.0.tgz", @@ -1013,6 +1036,12 @@ "node": ">=8" } }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -1116,6 +1145,18 @@ "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", "dev": true }, + "node_modules/catharsis": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz", + "integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==", + "dev": true, + "dependencies": { + "lodash": "^4.17.15" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -1422,6 +1463,18 @@ "node": ">=8.6" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es6-error": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", @@ -2459,12 +2512,59 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/js2xmlparser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz", + "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==", + "dev": true, + "dependencies": { + "xmlcreate": "^2.0.4" + } + }, "node_modules/jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", "dev": true }, + "node_modules/jsdoc": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.3.tgz", + "integrity": "sha512-Nu7Sf35kXJ1MWDZIMAuATRQTg1iIPdzh7tqJ6jjvaU/GfDf+qi5UV8zJR3Mo+/pYFvm8mzay4+6O5EWigaQBQw==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.15", + "@jsdoc/salty": "^0.2.1", + "@types/markdown-it": "^14.1.1", + "bluebird": "^3.7.2", + "catharsis": "^0.9.0", + "escape-string-regexp": "^2.0.0", + "js2xmlparser": "^4.0.2", + "klaw": "^3.0.0", + "markdown-it": "^14.1.0", + "markdown-it-anchor": "^8.6.7", + "marked": "^4.0.10", + "mkdirp": "^1.0.4", + "requizzle": "^0.2.3", + "strip-json-comments": "^3.1.0", + "underscore": "~1.13.2" + }, + "bin": { + "jsdoc": "jsdoc.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/jsdoc/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -2534,6 +2634,15 @@ "integrity": "sha1-HoBFQlABjbrUw/6USX1uZ7YmnHc=", "dev": true }, + "node_modules/klaw": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", + "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.9" + } + }, "node_modules/lcov-parse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/lcov-parse/-/lcov-parse-1.0.0.tgz", @@ -2556,6 +2665,15 @@ "node": ">= 0.8.0" } }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "dev": true, + "dependencies": { + "uc.micro": "^2.0.0" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -2571,6 +2689,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, "node_modules/lodash.flattendeep": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", @@ -2649,6 +2773,57 @@ "semver": "bin/semver.js" } }, + "node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/markdown-it-anchor": { + "version": "8.6.7", + "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz", + "integrity": "sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==", + "dev": true, + "peerDependencies": { + "@types/markdown-it": "*", + "markdown-it": "*" + } + }, + "node_modules/markdown-it/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/marked": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "dev": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "dev": true + }, "node_modules/mime-db": { "version": "1.51.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", @@ -2688,6 +2863,18 @@ "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", "dev": true }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/mocha": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.1.0.tgz", @@ -3322,6 +3509,15 @@ "node": ">=6" } }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/qs": { "version": "6.5.3", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", @@ -3423,6 +3619,15 @@ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", "dev": true }, + "node_modules/requizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz", + "integrity": "sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw==", + "dev": true, + "dependencies": { + "lodash": "^4.17.21" + } + }, "node_modules/resolve": { "version": "1.20.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", @@ -3769,6 +3974,18 @@ "is-typedarray": "^1.0.0" } }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true + }, + "node_modules/underscore": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", + "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==", + "dev": true + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -3899,6 +4116,12 @@ "node": ">=4.0" } }, + "node_modules/xmlcreate": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", + "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==", + "dev": true + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/test/events.js b/test/events.js index a894d323..a3f51e97 100644 --- a/test/events.js +++ b/test/events.js @@ -1,5 +1,6 @@ const assert = require('assert'); const onvif = require('../lib/onvif'); +const serverMockup = require('../test/serverMockup'); describe('Events', () => { let cam = null; @@ -75,4 +76,30 @@ describe('Events', () => { done(); }, 1000); }); + it('should resume long-pulling when connection with server fails', (done) => { + // wait 1 second for any Pull requests still running when we removed the listener to complete + let gotMessage = 0; + let pullMessagesCallCount = 0; + const onEvent = () => { + if (gotMessage === 10) { + // after the tenth message, the next requests will reset the connection + serverMockup.connectionBreaker.break = true; + } + gotMessage += 1; + }; + const pullMessages = cam.pullMessages; + cam.pullMessages = function(options, callback) { + pullMessagesCallCount += 1; + pullMessages.call(cam, options, callback); + }; + cam.on('event', onEvent); + setTimeout(() => { + serverMockup.connectionBreaker.break = false; + cam.pullMessages = pullMessages; + assert.ok(gotMessage === 11 || gotMessage === 12); + assert.ok(pullMessagesCallCount > gotMessage && pullMessagesCallCount > 20); + cam.removeListener('event', onEvent); + done(); + }, 1.5 * 1000); + }); }); diff --git a/test/serverMockup.js b/test/serverMockup.js index be463082..5edc5343 100644 --- a/test/serverMockup.js +++ b/test/serverMockup.js @@ -16,6 +16,14 @@ const conf = { }; const verbose = process.env.VERBOSE || false; +const log = (...msgs) => { + if (verbose) { + console.log(...msgs); + } +}; +let connectionBreaker = { + break: false +}; const listener = (req, res) => { req.setEncoding('utf8'); @@ -44,9 +52,7 @@ const listener = (req, res) => { if (onvifNamespaces) { ns = onvifNamespaces[1]; } - if (verbose) { - console.log('received', ns, command); - } + log('received', ns, command); if (fs.existsSync(__xmldir + ns + '.' + command + '.xml')) { command = ns + '.' + command; } @@ -54,10 +60,13 @@ const listener = (req, res) => { command = 'Error'; } const fileName = __xmldir + command + '.xml'; - if (verbose) { - console.log('serving', fileName); - } + log('serving', fileName); res.setHeader('Content-Type', 'application/soap+xml;charset=UTF-8'); + if (connectionBreaker.break) { + log('break connection'); + res.destroy(); + return; + } res.end(template(fs.readFileSync(fileName))(conf)); }); }; @@ -67,9 +76,7 @@ const discoverReply = dgram.createSocket('udp4'); const discover = dgram.createSocket({ type: 'udp4', reuseAddr: true }); discover.on('error', (err) => { throw err; }); discover.on('message', (msg, rinfo) => { - if (verbose) { - console.log('Discovery received'); - } + log('Discovery received'); // Extract MessageTo from the XML. xml2ns options remove the namespace tags and ensure element character content is accessed with '_' xml2js.parseString(msg.toString(), { explicitCharkey: true, tagNameProcessors: [xml2js.processors.stripPrefix]}, (err, result) => { const msgId = result.Envelope.Header[0].MessageID[0]._; @@ -93,32 +100,27 @@ discover.on('message', (msg, rinfo) => { }); }); -if (verbose) { - console.log('Listening for Discovery Messages on Port 3702'); -} +log('Listening for Discovery Messages on Port 3702'); discover.bind(3702, () => discover.addMembership('239.255.255.250')); const server = http.createServer(listener).listen(conf.port, (err) => { if (err) { throw err; } - if (verbose) { - console.log('Listening on port', conf.port); - } + log('Listening on port', conf.port); }); const close = () => { discover.close(); discoverReply.close(); server.close(); - if (verbose) { - console.log('Closing ServerMockup'); - } + log('Closing ServerMockup'); }; module.exports = { - server: server, - conf: conf, - discover: discover, - close: close, + server, + conf, + discover, + close, + connectionBreaker }; From cfe19cace896aae2b798659722169104482e3cd6 Mon Sep 17 00:00:00 2001 From: Harry Martland Date: Sun, 19 May 2024 16:31:55 +0100 Subject: [PATCH 2/3] retry on other error codes and sets max retry delay --- lib/events.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/events.js b/lib/events.js index a917297e..b44d7ea6 100644 --- a/lib/events.js +++ b/lib/events.js @@ -26,6 +26,8 @@ module.exports = function(Cam) { const linerase = require('./utils').linerase; const parseSOAPString = require('./utils').parseSOAPString; + const retryErrorCodes = ['ECONNREFUSED','ECONNRESET','ETIMEDOUT', 'ENETUNREACH']; + const maxEventReconnectMs = 2 * 60 * 1000; /** @@ -350,10 +352,11 @@ module.exports = function(Cam) { this.createPullPointSubscription(function(error) { if (!error) { this._eventPull(); - } else if (typeof error === 'object' && error.code === 'ECONNRESET') { + } else if (typeof error === 'object' && retryErrorCodes.includes( error.code)) { // connection reset on creation - restart Event loop for pullMessages request this._restartEventRequest(); } + console.log("Error: " + error.code); }.bind(this)); } else { this._eventPull(); @@ -376,6 +379,7 @@ module.exports = function(Cam) { }, function(err, data, xml) { if (!err) { if (data.notificationMessage) { + this._eventReconnectms = 0; if (!Array.isArray(data.notificationMessage)) { data.notificationMessage = [data.notificationMessage]; } @@ -424,11 +428,13 @@ module.exports = function(Cam) { */ Cam.prototype._restartEventRequest = function() { // TODO maybe stop trying to connect after some time - if (!this._eventReconnectms || this._eventReconnectms > 2 * 60 * 1000) { + if (!this._eventReconnectms) { this._eventReconnectms = 10; } setTimeout(this._eventRequest.bind(this), this._eventReconnectms); - this._eventReconnectms = 1.111 * this._eventReconnectms; + if (this._eventReconnectms < maxEventReconnectMs) { + this._eventReconnectms = 1.111 * this._eventReconnectms; + } }; /** From d04eae05d8a83e0e4747d788ed8fd4657cde6221 Mon Sep 17 00:00:00 2001 From: "Andrew D.Laptev" Date: Sun, 19 May 2024 21:26:37 +0300 Subject: [PATCH 3/3] feat(onvif-events): Add `eventsError` event --- .github/workflows/npm.yml | 30 +++++++++++++++++++++++++++++ .github/workflows/publish.yml | 18 ------------------ lib/events.js | 36 +++++++++++++++++++---------------- test/events.js | 8 +++++++- 4 files changed, 57 insertions(+), 35 deletions(-) create mode 100644 .github/workflows/npm.yml delete mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/npm.yml b/.github/workflows/npm.yml new file mode 100644 index 00000000..15ea352a --- /dev/null +++ b/.github/workflows/npm.yml @@ -0,0 +1,30 @@ +name: Node.js Package + +on: + release: + types: [created] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + - run: npm ci + - run: npm test + + publish-npm: + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + registry-url: https://registry.npmjs.org/ + - run: npm ci + - run: npm publish + env: + NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index 6c332684..00000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: Test & Publish -on: - push: - branches: - - master -jobs: - publish: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - uses: actions/setup-node@v1 - with: - node-version: 16 - - run: npm install - - run: npm test - - uses: JS-DevTools/npm-publish@v1 - with: - token: ${{ secrets.NPM_TOKEN }} diff --git a/lib/events.js b/lib/events.js index b44d7ea6..a203d739 100644 --- a/lib/events.js +++ b/lib/events.js @@ -146,11 +146,13 @@ module.exports = function(Cam) { /** * Renew pull-point subscription - * @param {options} callback - * @param {function} callback + * @param {Object|Function} [options] + * @param {Function} callback */ - Cam.prototype.renew = function(options, callback) { + if (!callback) { + callback = options; + } let urlAddress = null; let subscriptionId = null; try { @@ -335,9 +337,7 @@ module.exports = function(Cam) { * @private */ function _terminationTime(response) { - let result = new Date(Date.now() - response.currentTime.getTime() + response.terminationTime.getTime()); - // console.log("Events: Termination Time is " + result); - return result; + return new Date(Date.now() - response.currentTime.getTime() + response.terminationTime.getTime()); } /** @@ -351,12 +351,15 @@ module.exports = function(Cam) { // if there is no pull-point subscription or it has expired, create new subscription this.createPullPointSubscription(function(error) { if (!error) { + delete this._eventReconnectms; this._eventPull(); - } else if (typeof error === 'object' && retryErrorCodes.includes( error.code)) { - // connection reset on creation - restart Event loop for pullMessages request - this._restartEventRequest(); + } else { + this.emit('eventsError', error); + if (typeof error === 'object' && retryErrorCodes.includes(error.code)) { + // connection reset on creation - restart Event loop for pullMessages request + this._restartEventRequest(); + } } - console.log("Error: " + error.code); }.bind(this)); } else { this._eventPull(); @@ -376,10 +379,10 @@ module.exports = function(Cam) { if (this.listeners('event').length && this.events.subscription) { // check for event listeners, if zero, or no subscription then stop pulling this.pullMessages({ messageLimit: this.events.messageLimit - }, function(err, data, xml) { - if (!err) { + }, function(error, data, xml) { + if (!error) { + delete this._eventReconnectms; if (data.notificationMessage) { - this._eventReconnectms = 0; if (!Array.isArray(data.notificationMessage)) { data.notificationMessage = [data.notificationMessage]; } @@ -395,14 +398,15 @@ module.exports = function(Cam) { this.events.terminationTime = _terminationTime(data); // Axis does not increment the termination time. Use RENEW. Vista returns a termination time with the time now (ie we have expired) even if there was still time left over. Use RENEW // Axis cameras require us to Rewew the Pull Point Subscription - this.renew({},function(err,data) { - if (!err) { + this.renew({},function(error, data) { + if (!error) { this.events.terminationTime = _terminationTime(data); } this._eventRequest(); // go around the loop again, once the RENEW has completed (and terminationTime updated) }); } else { - if (typeof err === 'object' && err.code === 'ECONNRESET') { + this.emit('eventsError', error); + if (typeof error === 'object' && retryErrorCodes.includes(error.code)) { // connection reset - restart Event loop for pullMessages request this._restartEventRequest(); } else { diff --git a/test/events.js b/test/events.js index a3f51e97..07a1a7bf 100644 --- a/test/events.js +++ b/test/events.js @@ -99,7 +99,13 @@ describe('Events', () => { assert.ok(gotMessage === 11 || gotMessage === 12); assert.ok(pullMessagesCallCount > gotMessage && pullMessagesCallCount > 20); cam.removeListener('event', onEvent); - done(); + cam.unsubscribe(done); }, 1.5 * 1000); }); + it('should return an error when calling renew without subscription', (done) => { + cam.renew({}, (err) => { + assert.ok(err instanceof Error); + done(); + }); + }); });