Skip to content

Commit bb74589

Browse files
author
Pavel Petroshenko
authored
Merge pull request #13 from electricimp/develop
v1.1.0
2 parents daad2cc + 22a10ca commit bb74589

7 files changed

+345
-276
lines changed

Diff for: DevelopmentGuide.md

+2
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,8 @@ While the logs are being streamed, no other command can be called. To stop displ
284284

285285
Note, the log stream may be closed by the impCentral API: for example, when a new log stream is requested by the same account and that exceeds the per-account limit of opened streams.
286286

287+
If the logs streaming is stopped by an error (eg. due to a disconnection), *impt* tries to automatically reconnect and reestablish the stream. Note, even if the stream is restored, some log entries could be lost and not displayed.
288+
287289
**Example**
288290

289291
**Logging is started for a device from the Project’s Device Group**

Diff for: README.md

+13
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ This guide covers the basic and common usage of *impt*, and should be read first
1616
## Contents ##
1717

1818
- [Installation](#installation)
19+
- [Proxy Setup](#proxy-setup)
1920
- [Syntax and Command Groups](#syntax-and-command-groups)
2021
- [Help](#help)
2122
- [Debugging](#debugging)
@@ -39,6 +40,18 @@ This guide covers the basic and common usage of *impt*, and should be read first
3940
npm install -g imp-central-impt
4041
```
4142

43+
## Proxy Setup ##
44+
45+
If *impt* is going to connect to the impCentral API via a proxy, one or both of the following environment variables should be set to a value in URL format:
46+
- `HTTPS_PROXY` (or `https_proxy`) — for the proxy which passes HTTPs requests.
47+
- `HTTP_PROXY` (or `http_proxy`) — for the proxy which passes HTTP requests.
48+
49+
Note, the default impCentral API endpoint is working over HTTPs.
50+
51+
```
52+
HTTPS_PROXY=https://proxy.example.net
53+
```
54+
4255
## Syntax and Command Groups ##
4356

4457
All commands follow a unified format and [syntax](./CommandsManual.md#command-syntax):

Diff for: lib/Log.js

+22-1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ class Log {
4848
this._deviceIds = [];
4949
this._logStreamId = undefined;
5050
this._logsFinished = false;
51+
this._reconnecting = false;
5152
this._init();
5253
}
5354

@@ -68,6 +69,7 @@ class Log {
6869
_closeLogStream() {
6970
if (this._logStreamId) {
7071
return this._helper.closeLogStream(this._logStreamId);
72+
this._logStreamId = undefined;
7173
}
7274
return Promise.resolve();
7375
}
@@ -209,9 +211,11 @@ class Log {
209211
return this._helper.logStream(
210212
this._deviceIds,
211213
this._messageHandler.bind(this),
212-
this._stateChangeHandler.bind(this)).
214+
this._stateChangeHandler.bind(this),
215+
this._errorHandler.bind(this)).
213216
then((logStreamId) => {
214217
this._logStreamId = logStreamId;
218+
this._reconnecting = false;
215219
UserInteractor.spinnerStop();
216220
let deviceIds = this._deviceIds;
217221
if (deviceIds.length > UserInteractor.PRINTED_ENTITIES_LIMIT) {
@@ -253,6 +257,23 @@ class Log {
253257
}
254258
}
255259

260+
_errorHandler(error) {
261+
UserInteractor.printError(error);
262+
if (!this._reconnecting) {
263+
this._reconnect();
264+
}
265+
}
266+
267+
_reconnect() {
268+
UserInteractor.printMessage(UserInteractor.MESSAGES.LOG_STREAM_RECONNECT);
269+
this._reconnecting = true;
270+
this._closeLogStream().
271+
then(() => this._stream(this._devices)).
272+
catch(error => {
273+
this._exitWithStatus(error);
274+
});
275+
}
276+
256277
// Formats the single log message
257278
_formatLog(log, printDeviceId = false) {
258279
if (('device_id' in log) && printDeviceId) {

Diff for: lib/util/ImpCentralApiHelper.js

+76-36
Original file line numberDiff line numberDiff line change
@@ -179,13 +179,13 @@ class ImpCentralApiHelper {
179179
login(user, password) {
180180
const auth = this._impCentralApi.auth;
181181
return this._processImpCentralApiRequest(
182-
null, null, auth.login.bind(auth), user, password);
182+
false, null, null, auth.login.bind(auth), user, password);
183183
}
184184

185185
getAccessToken(loginKey) {
186186
const auth = this._impCentralApi.auth;
187187
return this._processImpCentralApiRequest(
188-
Identifier.ENTITY_TYPE.TYPE_LOGIN_KEY, loginKey, auth.getAccessToken.bind(auth), loginKey);
188+
false, Identifier.ENTITY_TYPE.TYPE_LOGIN_KEY, loginKey, auth.getAccessToken.bind(auth), loginKey);
189189
}
190190

191191
// Checks and refreshes access token if needed
@@ -195,23 +195,7 @@ class ImpCentralApiHelper {
195195
let expDate = new Date(this._authConfig.expiresAt);
196196
let currDate = new Date();
197197
if (expDate - currDate < ACCESS_TOKEN_RENEW_BEFORE_EXPIRY_MS) {
198-
if (!this._authConfig.refreshToken && !this._authConfig.loginKey) {
199-
return Promise.reject(new Errors.AccessTokenExpiredError(this._authConfig));
200-
}
201-
else {
202-
return this._renewAccessToken()
203-
.then(result => {
204-
this._authConfig.setAccessToken(result.access_token, result.expires_at);
205-
this._impCentralApi.auth.accessToken = this._authConfig.accessToken;
206-
return this._authConfig.save();
207-
}).catch(error => {
208-
if (error instanceof Errors.EntityNotFoundError ||
209-
this._isEntityNotFoundError(error)) {
210-
return Promise.reject(new Errors.RefreshTokenError(this._authConfig));
211-
}
212-
return Promise.reject(error);
213-
});
214-
}
198+
return this._refreshAccessToken();
215199
}
216200
else {
217201
this._impCentralApi.auth.accessToken = this._authConfig.accessToken;
@@ -220,11 +204,33 @@ class ImpCentralApiHelper {
220204
});
221205
}
222206

207+
_refreshAccessToken() {
208+
if (!this._authConfig.refreshToken && !this._authConfig.loginKey) {
209+
// --temp option was specified for impt auth login,
210+
// there is no information required to refresh access token.
211+
return Promise.reject(new Errors.AccessTokenExpiredError(this._authConfig));
212+
}
213+
else {
214+
return this._renewAccessToken()
215+
.then(result => {
216+
this._authConfig.setAccessToken(result.access_token, result.expires_at);
217+
this._impCentralApi.auth.accessToken = this._authConfig.accessToken;
218+
return this._authConfig.save();
219+
}).catch(error => {
220+
if (error instanceof Errors.EntityNotFoundError ||
221+
this._isEntityNotFoundError(error)) {
222+
return Promise.reject(new Errors.RefreshTokenError(this._authConfig));
223+
}
224+
return Promise.reject(error);
225+
});
226+
}
227+
}
228+
223229
_renewAccessToken() {
224230
const auth = this._impCentralApi.auth;
225231
if (this._authConfig.refreshToken) {
226232
return this._processImpCentralApiRequest(
227-
null, null, auth.refreshAccessToken.bind(auth), this._authConfig.refreshToken);
233+
false, null, null, auth.refreshAccessToken.bind(auth), this._authConfig.refreshToken);
228234
}
229235
else if (this._authConfig.loginKey) {
230236
return this.getAccessToken(this._authConfig.loginKey);
@@ -237,7 +243,7 @@ class ImpCentralApiHelper {
237243
[pageNumber, pageSize] :
238244
[filters, pageNumber, pageSize];
239245

240-
return this._processImpCentralApiRequest(null, null, entityApi.list.bind(entityApi), ...listArgs).
246+
return this._processImpCentralApiRequest(true, null, null, entityApi.list.bind(entityApi), ...listArgs).
241247
then(result => {
242248
let data = result.data;
243249
if ('next' in result.links) {
@@ -281,7 +287,7 @@ class ImpCentralApiHelper {
281287
const entityApi = this._getEntityApi(entityType);
282288
return this._checkAccessToken().
283289
then(() => this._entityStorage.clearList(entityType)).
284-
then(() => this._processImpCentralApiRequest(null, null, entityApi.create.bind(entityApi), ...args)).
290+
then(() => this._processImpCentralApiRequest(true, null, null, entityApi.create.bind(entityApi), ...args)).
285291
then(result => result.data);
286292
}
287293

@@ -292,7 +298,7 @@ class ImpCentralApiHelper {
292298
this._entityStorage.clearList(entityType);
293299
this._entityStorage.clearEntity(id);
294300
}).
295-
then(() => this._processImpCentralApiRequest(entityType, id, entityApi.update.bind(entityApi), id, ...args)).
301+
then(() => this._processImpCentralApiRequest(true, entityType, id, entityApi.update.bind(entityApi), id, ...args)).
296302
then(result => result.data);
297303
}
298304

@@ -304,7 +310,7 @@ class ImpCentralApiHelper {
304310
if (entity) {
305311
return entity;
306312
}
307-
const entityRequest = this._processImpCentralApiRequest(entityType, id, entityApi.get.bind(entityApi), id).
313+
const entityRequest = this._processImpCentralApiRequest(true, entityType, id, entityApi.get.bind(entityApi), id).
308314
then(result => result.data);
309315
this._entityStorage.setEntity(id, entityRequest);
310316
return entityRequest.
@@ -322,7 +328,7 @@ class ImpCentralApiHelper {
322328
this._entityStorage.clearList(entityType);
323329
this._entityStorage.clearEntity(id);
324330
}).
325-
then(() => this._processImpCentralApiRequest(entityType, id, entityApi.delete.bind(entityApi), id, ...args));
331+
then(() => this._processImpCentralApiRequest(true, entityType, id, entityApi.delete.bind(entityApi), id, ...args));
326332
}
327333

328334
restartDevices(devGroupId, conditional = false) {
@@ -333,7 +339,7 @@ class ImpCentralApiHelper {
333339
entityApi.conditionalRestartDevices :
334340
entityApi.restartDevices;
335341
return this._processImpCentralApiRequest(
336-
Identifier.ENTITY_TYPE.TYPE_DEVICE_GROUP, devGroupId, method.bind(entityApi), devGroupId);
342+
true, Identifier.ENTITY_TYPE.TYPE_DEVICE_GROUP, devGroupId, method.bind(entityApi), devGroupId);
337343
});
338344
}
339345

@@ -345,6 +351,7 @@ class ImpCentralApiHelper {
345351
this._entityStorage.clearEntity(devGroupId);
346352
}).
347353
then(() => this._processImpCentralApiRequest(
354+
true,
348355
Identifier.ENTITY_TYPE.TYPE_DEVICE_GROUP,
349356
devGroupId,
350357
entityApi.updateMinSupportedDeployment.bind(entityApi),
@@ -360,7 +367,7 @@ class ImpCentralApiHelper {
360367
entityApi.conditionalRestart :
361368
entityApi.restart;
362369
return this._processImpCentralApiRequest(
363-
Identifier.ENTITY_TYPE.TYPE_DEVICE, deviceId, method.bind(entityApi), deviceId);
370+
true, Identifier.ENTITY_TYPE.TYPE_DEVICE, deviceId, method.bind(entityApi), deviceId);
364371
});
365372
}
366373

@@ -374,7 +381,12 @@ class ImpCentralApiHelper {
374381
}
375382
}).
376383
then(() => this._processImpCentralApiRequest(
377-
Identifier.ENTITY_TYPE.TYPE_DEVICE_GROUP, deviceGroupId, entityApi.addDevices.bind(entityApi), deviceGroupId, ...deviceIds));
384+
true,
385+
Identifier.ENTITY_TYPE.TYPE_DEVICE_GROUP,
386+
deviceGroupId,
387+
entityApi.addDevices.bind(entityApi),
388+
deviceGroupId,
389+
...deviceIds));
378390
}
379391

380392
unassignDevices(deviceGroupId, unbondKey, ...deviceIds) {
@@ -387,6 +399,7 @@ class ImpCentralApiHelper {
387399
}
388400
}).
389401
then(() => this._processImpCentralApiRequest(
402+
true,
390403
Identifier.ENTITY_TYPE.TYPE_DEVICE_GROUP,
391404
deviceGroupId,
392405
entityApi.removeDevices.bind(entityApi),
@@ -395,30 +408,43 @@ class ImpCentralApiHelper {
395408
...deviceIds));
396409
}
397410

398-
logStream(deviceIds, messageHandler, stateChangeHandler = null) {
411+
logStream(deviceIds, messageHandler, stateChangeHandler = null, errorHandler = null) {
399412
let logStreamId;
400413
const entityApi = this._impCentralApi.logStreams;
401414
return this._checkAccessToken().
402415
then(() => this._processImpCentralApiRequest(
403-
null, null, entityApi.create.bind(entityApi), messageHandler, stateChangeHandler, null, LogStreams.FORMAT_JSON)).
416+
true,
417+
null,
418+
null,
419+
entityApi.create.bind(entityApi),
420+
messageHandler,
421+
stateChangeHandler,
422+
errorHandler,
423+
LogStreams.FORMAT_JSON)).
404424
then((logStream) => {
405425
logStreamId = logStream.data.id;
406426
return this.makeConcurrentOperations(deviceIds, (deviceId) =>
407-
this._processImpCentralApiRequest(null, null, entityApi.addDevice.bind(entityApi), logStreamId, deviceId));
427+
this._processImpCentralApiRequest(true, null, null, entityApi.addDevice.bind(entityApi), logStreamId, deviceId));
408428
}).
409429
then(() => logStreamId);
410430
}
411431

412432
closeLogStream(logStreamId) {
413433
const entityApi = this._impCentralApi.logStreams;
414-
return this._processImpCentralApiRequest(null, null, entityApi.close.bind(entityApi), logStreamId);
434+
return this._processImpCentralApiRequest(true, null, null, entityApi.close.bind(entityApi), logStreamId);
415435
}
416436

417437
logs(deviceId, pageNumber = null, pageSize = null) {
418438
const entityApi = this._impCentralApi.devices;
419439
return this._checkAccessToken().
420440
then(() => this._processImpCentralApiRequest(
421-
Identifier.ENTITY_TYPE.TYPE_DEVICE, deviceId, entityApi.getLogs.bind(entityApi), deviceId, pageNumber, pageSize)).
441+
true,
442+
Identifier.ENTITY_TYPE.TYPE_DEVICE,
443+
deviceId,
444+
entityApi.getLogs.bind(entityApi),
445+
deviceId,
446+
pageNumber,
447+
pageSize)).
422448
then(result => result.data);
423449
}
424450

@@ -428,15 +454,24 @@ class ImpCentralApiHelper {
428454
return result;
429455
}
430456

431-
_processImpCentralApiRequest(entityType, id, impCentralApiMethod, ...args) {
457+
_processImpCentralApiRequest(refreshAccessTokenOnAuthError, entityType, id, impCentralApiMethod, ...args) {
432458
UserInteractor.spinnerStart();
433459
return impCentralApiMethod(...args).
434460
then(result => this._resolveImpCentralApiResponse(result)).
435461
catch(error => {
436-
if (this._isRateLimitError(error)) {
462+
if (this._isAuthenticationError(error)) {
463+
if (refreshAccessTokenOnAuthError) {
464+
// try to refresh access token in case of authentication error and then restart the original request
465+
return this._refreshAccessToken().
466+
then(() => this._processImpCentralApiRequest(
467+
refreshAccessTokenOnAuthError, entityType, id, impCentralApiMethod, ...args));
468+
}
469+
}
470+
else if (this._isRateLimitError(error)) {
437471
return new Promise((resolve, reject) => {
438472
setTimeout(() => resolve(), 1000)
439-
}).then(() => this._processImpCentralApiRequest(entityType, id, impCentralApiMethod, ...args));
473+
}).then(() => this._processImpCentralApiRequest(
474+
refreshAccessTokenOnAuthError, entityType, id, impCentralApiMethod, ...args));
440475
}
441476
else if (this._isEntityNotFoundError(error) && entityType && id) {
442477
throw new Errors.EntityNotFoundError(entityType, id);
@@ -454,6 +489,11 @@ class ImpCentralApiHelper {
454489
return error instanceof ImpCentralApi.Errors.ImpCentralApiError &&
455490
error._statusCode === 429;
456491
}
492+
493+
_isAuthenticationError(error) {
494+
return error instanceof ImpCentralApi.Errors.ImpCentralApiError &&
495+
error._statusCode === 401;
496+
}
457497
}
458498

459499
module.exports = ImpCentralApiHelper;

Diff for: lib/util/UserInteractor.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ const MESSAGES = {
103103
LOG_STREAM_OPENED : `Log stream for ${_DEVICE}(s) %s is opened.`,
104104
LOG_STREAM_EXIT : 'Press <Ctrl-C> to exit.',
105105
LOG_STREAM_CLOSED : 'The log stream is closed.',
106+
LOG_STREAM_RECONNECT : 'The log stream is lost. Trying to reconnect...',
106107
DELETE_ENTITIES : 'The following entities will be deleted:',
107108
DELETE_DEVICES_UNASSIGNED : `The following ${_DEVICES} will be unassigned from ${_DEVICE_GROUPS}:`,
108109
DELETE_DEVICES_REMOVED : `The following ${_DEVICES} will be removed from the account:`,
@@ -343,7 +344,7 @@ class UserInteractor {
343344
}
344345

345346
static printImpCentralApiError(error) {
346-
if (error.body.hasOwnProperty('errors')) {
347+
if (error.body && error.body.hasOwnProperty('errors')) {
347348
UserInteractor._printErrorsAndWarnings(error.body.errors);
348349
}
349350
else {

0 commit comments

Comments
 (0)