Skip to content

Commit

Permalink
Merge pull request #320 from agsh/feat-restart-event-request-after-co…
Browse files Browse the repository at this point in the history
…nnection-error

feat(onvif-events): Restart event requests after ECONNRESET error
  • Loading branch information
agsh authored May 19, 2024
2 parents a73b7da + d04eae0 commit a325700
Show file tree
Hide file tree
Showing 9 changed files with 376 additions and 85 deletions.
30 changes: 30 additions & 0 deletions .github/workflows/npm.yml
Original file line number Diff line number Diff line change
@@ -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}}
6 changes: 3 additions & 3 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
18 changes: 0 additions & 18 deletions .github/workflows/publish.yml

This file was deleted.

1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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)

Expand Down
1 change: 0 additions & 1 deletion lib/cam.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
73 changes: 48 additions & 25 deletions lib/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;


/**
Expand Down Expand Up @@ -144,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 {
Expand Down Expand Up @@ -266,18 +270,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) {
Expand Down Expand Up @@ -337,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());
}

/**
Expand All @@ -353,17 +351,21 @@ 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 (this.eventReconnectms) {
setTimeout(() => this._eventRequest(), this.eventReconnectms);
} 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();
}
}
}.bind(this));
} else {
this._eventPull();
}
} else {
delete this.events.terminationTime;

this.unsubscribe();
}
};
Expand All @@ -377,8 +379,9 @@ 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) {
if (!Array.isArray(data.notificationMessage)) {
data.notificationMessage = [data.notificationMessage];
Expand All @@ -395,29 +398,49 @@ 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 {
// 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);
this.emit('eventsError', error);
if (typeof error === 'object' && retryErrorCodes.includes(error.code)) {
// 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 = 10;
}
setTimeout(this._eventRequest.bind(this), this._eventReconnectms);
if (this._eventReconnectms < maxEventReconnectMs) {
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)
Expand Down
Loading

0 comments on commit a325700

Please sign in to comment.