Skip to content

Commit 6e3cfff

Browse files
committed
Handle command errors.
1 parent fa85f66 commit 6e3cfff

File tree

5 files changed

+128
-42
lines changed

5 files changed

+128
-42
lines changed

README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,21 @@ The current version `2.x` requires node.js version 4.x or newer. For older node.
2020

2121
## Usage
2222

23+
#### First establish connection
24+
25+
``` javascript
26+
let Rcon = require('srcds-rcon');
27+
let rcon = Rcon({
28+
address: '192.168.1.10',
29+
password: 'test'
30+
});
31+
rcon.connect().then(() => {
32+
console.log('connected');
33+
}).catch(console.error);
34+
```
35+
36+
#### Run commands
37+
2338
``` javascript
2439
let rcon = require('srcds-rcon')({
2540
address: '192.168.1.10',
@@ -42,3 +57,21 @@ rcon.connect().then(() => {
4257
});
4358
```
4459

60+
#### Specify command timeout
61+
62+
rcon.command('cvarlist', 1000).then(console.log, console.error);
63+
64+
## Errors
65+
66+
Some errors may contain partial command output. That indicates that the command was run, but reply packets have been lost.
67+
68+
``` javascript
69+
rcon.command('cvarlist').then(() => {}).catch(err => {
70+
console.log(`Command error: ${err.message}`);
71+
if (err.details && err.details.partialResponse) {
72+
console.log(`Got partial response: ${err.details.partialResponse}`);
73+
}
74+
});
75+
76+
When an error is returned, even if an error doesn't contain a partial output, there is no guarantee the command was not run. The protocol uses udp and the packets sometimes get lost. The only guarantee is when the error does contain a partial output, the command was definitely run, but some reply packets got lost.
77+

dev/auth.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use strict';
22

33
let rcon = require('..')({
4-
address: '192.168.1.10',
4+
address: '127.0.0.1',
55
password: 'test'
66
});
77

@@ -13,6 +13,8 @@ rcon.connect().then(() => {
1313
() => rcon.command('status').then(status => console.log(`got status ${status}`))
1414
).then(
1515
() => rcon.command('cvarlist').then(cvarlist => console.log(`cvarlist is \n${cvarlist}`))
16+
).then(
17+
() => rcon.command('cvarlist', 1).then(cvarlist => console.log(`cvarlist is \n${cvarlist}`))
1618
).then(
1719
() => rcon.command('changelevel de_dust2').then(() => console.log('changed map'))
1820
).catch(err => {

index.js

Lines changed: 83 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -28,18 +28,17 @@ module.exports = params => {
2828
});
2929
connection.send(buf);
3030
return Promise.race([
31-
util.promiseTimeout(3000).then(() => 'timeout'),
31+
util.promiseTimeout(3000).then(() => {
32+
let err = new Error('Auth timeout');
33+
return Promise.reject(err);
34+
}),
3235
connection.getData(dataHandler)
3336
]).then(data => {
3437
// TODO: data as a single type, not string/object
35-
if ('timeout' === data) {
36-
let err = new Error('Auth timeout');
37-
throw err;
38-
}
3938
let res = packet.response(data);
4039
if (res.id === -1) {
4140
let err = new Error('Wrong rcon password');
42-
throw err;
41+
return Promise.reject(err);
4342
}
4443
// Auth successful, but continue after receiving packet index
4544
return connection.getData(dataHandler).then(() => {
@@ -62,41 +61,87 @@ module.exports = params => {
6261
return nextPacketId += 1;
6362
}
6463

65-
function command(text) {
66-
return new Promise(resolve => {
67-
let responseData = new Buffer(0);
68-
let reqId = _getNextPacketId();
69-
let req = packet.request({
70-
id: reqId,
71-
type: packet.SERVERDATA_EXECCOMMAND,
72-
body: text
73-
});
74-
let ackId = _getNextPacketId();
75-
let ack = packet.request({
76-
id: ackId,
77-
type: packet.SERVERDATA_EXECCOMMAND,
78-
body: ''
79-
});
80-
_connection.send(req);
81-
_connection.send(ack);
82-
_connection.getData(dataHandler).then(done);
64+
function command(text, timeout) {
65+
return Promise.race([
66+
new Promise((resolve, reject) => {
67+
let unexpectedPackets;
68+
69+
let responseData = new Buffer(0);
70+
let reqId = _getNextPacketId();
71+
let req = packet.request({
72+
id: reqId,
73+
type: packet.SERVERDATA_EXECCOMMAND,
74+
body: text
75+
});
76+
let ackId = _getNextPacketId();
77+
let ack = packet.request({
78+
id: ackId,
79+
type: packet.SERVERDATA_EXECCOMMAND,
80+
body: ''
81+
});
82+
_connection.send(req);
83+
_connection.send(ack);
84+
_connection.getData(dataHandler).then(done);
8385

84-
function dataHandler(data) {
85-
let res = packet.response(data);
86-
if (res.id === ackId) {
87-
return false;
88-
} else if (res.id === reqId) {
89-
// More data to come
90-
responseData = Buffer.concat([responseData, res.payload], responseData.length + res.payload.length);
86+
function dataHandler(data) {
87+
let res = packet.response(data);
88+
if (res.id === ackId) {
89+
return false;
90+
} else if (res.id === reqId) {
91+
// More data to come
92+
responseData = Buffer.concat([responseData, res.payload], responseData.length + res.payload.length);
93+
return true;
94+
} else {
95+
return handleUnexpectedData(res.id);
96+
}
9197
}
92-
return true;
93-
}
9498

95-
function done() {
96-
let text = packet.convertPayload(responseData);
97-
resolve(text);
98-
}
99-
});
99+
function done() {
100+
let text = packet.convertPayload(responseData);
101+
resolve(text);
102+
}
103+
104+
function handleUnexpectedData(id) {
105+
// Unexpected res.id, possibly from other commands
106+
if (reqId > id) {
107+
// Do nothing and keep listening, packets from older
108+
// commands are still coming in
109+
return true;
110+
}
111+
if ('undefined' === typeof unexpectedPackets) {
112+
unexpectedPackets = new Map();
113+
}
114+
if (!unexpectedPackets.has(id)) {
115+
if (unexpectedPackets.size >= 2) {
116+
let err = new Error('Command lost');
117+
err.details = {
118+
reqId: reqId
119+
};
120+
if (responseData.length > 0) {
121+
err.details.partialResponse = packet.convertPayload(responseData);
122+
}
123+
reject(err);
124+
return false;
125+
}
126+
unexpectedPackets.set(id, 1);
127+
return true;
128+
}
129+
unexpectedPackets.set(id, unexpectedPackets.get(id) + 1);
130+
return true;
131+
}
132+
}),
133+
new Promise((resolve, reject) => {
134+
if ('number' === typeof timeout) {
135+
return util.promiseTimeout(timeout).then(() => {
136+
let err = new Error('Command timeout');
137+
err.details = {
138+
timeout: timeout
139+
};
140+
reject(err);
141+
});
142+
}
143+
})
144+
]);
100145
}
101146
};
102147

lib/connection.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ module.exports = address => {
3232
connection.on('error', errorHandler);
3333

3434
function errorHandler(err) {
35+
connection.removeListener('error', errorHandler);
3536
reject(err);
3637
}
3738
});
@@ -44,15 +45,20 @@ module.exports = address => {
4445

4546
function dataHandler(data) {
4647
if (!cbSync(data)) {
47-
connection.removeListener('error', errorHandler);
48-
connection.removeListener('data', dataHandler);
48+
removeListeners();
4949
resolve(data);
5050
}
5151
}
5252

5353
function errorHandler(err) {
54+
removeListeners();
5455
reject(err);
5556
}
57+
58+
function removeListeners() {
59+
connection.removeListener('error', errorHandler);
60+
connection.removeListener('data', dataHandler);
61+
}
5662
});
5763
}
5864

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "srcds-rcon",
3-
"version": "2.0.1",
3+
"version": "2.1.0",
44
"description": "A node.js driver for the SRCDS RCON",
55
"main": "index.js",
66
"scripts": {

0 commit comments

Comments
 (0)