Skip to content

Commit 4f7125f

Browse files
authored
Main fix for the crash of node-red due to unhandled exception. (#79)
* fix: watchdog causing crashes in some cases * chore. bumped version to 1.1.0-beta.3 * fix: fixed the error handling of the Websocket * chore: added debug logging
1 parent cdbca0b commit 4f7125f

File tree

3 files changed

+113
-39
lines changed

3 files changed

+113
-39
lines changed

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "node-red-contrib-unifi-os",
3-
"version": "1.1.0-beta.2",
3+
"version": "1.1.0-beta.3",
44
"description": "Nodes to access UniFi data using endpoints and websockets",
55
"main": "build/nodes/unifi.js",
66
"scripts": {
@@ -56,9 +56,9 @@
5656
"abortcontroller-polyfill": "^1.7.5",
5757
"axios": "^1.3.5",
5858
"cookie": "^0.5.0",
59-
"ws": "^8.13.0",
59+
"ws": "8.18.0",
6060
"lodash": "^4.17.21",
61-
"async-mutex":"0.5.0"
61+
"async-mutex": "0.5.0"
6262
},
6363
"devDependencies": {
6464
"@types/lodash": "^4.14.192",
@@ -95,4 +95,4 @@
9595
"bufferutil": "^4.0.7",
9696
"utf-8-validate": "^5.0.10"
9797
}
98-
}
98+
}

src/SharedProtectWebSocket.ts

Lines changed: 105 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export class SharedProtectWebSocket {
3434
private accessController: AccessControllerNodeType
3535
private wsLogger: Loggers
3636
private RECONNECT_TIMEOUT = 15000
37-
private HEARTBEAT_INTERVAL = 10000
37+
private HEARTBEAT_INTERVAL = 30000
3838
private INITIAL_CONNECT_ERROR_THRESHOLD = 1000
3939
private reconnectAttempts = 0
4040
private currentStatus: SocketStatus = SocketStatus.UNKNOWN
@@ -67,22 +67,38 @@ export class SharedProtectWebSocket {
6767
}
6868

6969
shutdown(): void {
70+
this.wsLogger?.debug(
71+
'shutdown()'
72+
)
7073
this.disconnect()
7174
this.callbacks = {}
7275
}
7376

74-
private disconnect(): void {
77+
private async disconnect(): Promise<void> {
78+
79+
this.wsLogger?.debug(
80+
'Disconnecting websocket'
81+
)
7582
if (this.reconnectTimer) {
7683
clearTimeout(this.reconnectTimer)
7784
this.reconnectTimer = undefined
7885
}
79-
this.ws?.removeAllListeners()
80-
if (this.ws?.readyState === OPEN) {
81-
this.ws?.close()
82-
this.ws?.terminate()
83-
}
86+
87+
try {
88+
this.ws?.removeAllListeners()
89+
if (this.ws?.readyState === OPEN) {
90+
//this.ws?.close()
91+
//this.ws?.terminate()
92+
}
93+
this.ws?.terminate() // Terminate anyway
94+
this.ws = undefined
95+
} catch (error) {
96+
this.wsLogger?.debug(
97+
'Disconnecting websocket error '+ (error as Error).stack
98+
)
99+
}
84100

85-
this.ws = undefined
101+
86102
}
87103

88104
private updateStatusForNodes = (Status: SocketStatus): Promise<void> => {
@@ -97,37 +113,71 @@ export class SharedProtectWebSocket {
97113
}
98114

99115
private reconnectTimer: NodeJS.Timeout | undefined
116+
private heartBeatTimer: NodeJS.Timeout | undefined
100117
private mutex = new Mutex()
101118
private async reset(): Promise<void> {
119+
this.wsLogger?.debug(
120+
'PONG received'
121+
)
102122
await this.mutex.runExclusive(async () => {
103123
if (this.reconnectTimer) {
104124
clearTimeout(this.reconnectTimer)
105125
this.reconnectTimer = undefined
106126
await this.updateStatusForNodes(SocketStatus.CONNECTED)
107-
this.watchDog()
127+
try {
128+
this.watchDog()
129+
} catch (error) {
130+
this.wsLogger?.error(
131+
'reset watchdog error: ' + (error as Error).stack
132+
)
133+
}
108134
}
109135
})
110136
}
111137

112138
private async watchDog(): Promise<void> {
113-
setTimeout(async () => {
139+
140+
if (this.heartBeatTimer!==undefined) clearTimeout(this.heartBeatTimer)
141+
this.heartBeatTimer = setTimeout(async () => {
142+
this.wsLogger?.debug(
143+
'heartBeatTimer kicked in'
144+
)
114145
await this.updateStatusForNodes(SocketStatus.HEARTBEAT)
115146
if (!this.ws || this.ws?.readyState !== WebSocket.OPEN) {
116147
return
117148
}
118-
this.ws?.ping()
119-
149+
try {
150+
this.wsLogger?.debug(
151+
'gonna PING the server...'
152+
)
153+
this.ws?.ping()
154+
} catch (error) {
155+
this.wsLogger?.error(
156+
'PING error: ' + (error as Error).stack
157+
)
158+
}
159+
160+
if (this.reconnectTimer!==undefined) clearTimeout(this.reconnectTimer)
120161
this.reconnectTimer = setTimeout(async () => {
162+
this.wsLogger?.debug(
163+
'reconnectTimer kicked in'
164+
)
121165
await this.mutex.runExclusive(async () => {
122-
this.disconnect()
166+
await this.disconnect()
123167
await this.updateStatusForNodes(
124168
SocketStatus.RECOVERING_CONNECTION
125169
)
126-
this.connect()
170+
try {
171+
await this.connect()
172+
} catch (error) {
173+
this.wsLogger?.error(
174+
'connect into reconnectTimer error: ' + (error as Error).stack
175+
)
176+
}
177+
127178
})
128179
}, this.RECONNECT_TIMEOUT)
129-
130-
this.ws?.once('pong', this.reset.bind(this))
180+
131181
}, this.HEARTBEAT_INTERVAL)
132182
}
133183

@@ -149,14 +199,13 @@ export class SharedProtectWebSocket {
149199
})
150200
}
151201

152-
private processError(): void {
153-
// This needs improving, but the watchDog is kind of taking care of stuff
154-
}
202+
155203

156204
private connectCheckInterval: NodeJS.Timeout | undefined
157205
private connectMutex = new Mutex()
158206

159207
private async connect(): Promise<void> {
208+
160209
await this.mutex.runExclusive(async () => {
161210
if (this.currentStatus !== SocketStatus.RECOVERING_CONNECTION) {
162211
await this.updateStatusForNodes(SocketStatus.CONNECTING)
@@ -166,15 +215,33 @@ export class SharedProtectWebSocket {
166215
this.accessControllerConfig.wsPort ||
167216
endpoints[this.accessController.controllerType].wsport
168217
const url = `${endpoints.protocol.webSocket}${this.accessControllerConfig.controllerIp}:${wsPort}/proxy/protect/ws/updates?lastUpdateId=${this.bootstrap.lastUpdateId}`
169-
170-
this.ws = new WebSocket(url, {
171-
rejectUnauthorized: false,
172-
headers: {
173-
Cookie: await this.accessController.getAuthCookie(),
174-
},
175-
})
176-
177-
this.ws.on('error', this.processError.bind(this))
218+
219+
this.disconnect()
220+
221+
try {
222+
this.ws = new WebSocket(url, {
223+
rejectUnauthorized: false,
224+
headers: {
225+
Cookie: await this.accessController.getAuthCookie(),
226+
},
227+
})
228+
this.ws.on('error', (error) => {
229+
this.wsLogger?.error(
230+
'connect(): this.ws.on(error: ' + (error as Error).stack
231+
)
232+
})
233+
this.ws.on('pong', this.reset.bind(this))
234+
this.ws.on('message', this.processData.bind(this))
235+
} catch (error) {
236+
this.wsLogger.error(
237+
'Error instantiating websocket ' + (error as Error).stack
238+
)
239+
clearInterval(this.connectCheckInterval!)
240+
this.connectCheckInterval = undefined
241+
this.reconnectAttempts = 0
242+
this.watchDog()
243+
}
244+
178245

179246
this.connectCheckInterval = setInterval(async () => {
180247
await this.connectMutex.runExclusive(async () => {
@@ -186,8 +253,7 @@ export class SharedProtectWebSocket {
186253
SocketStatus.CONNECTED
187254
)
188255
this.reconnectAttempts = 0
189-
this.watchDog()
190-
this.ws.on('message', this.processData.bind(this))
256+
this.watchDog()
191257
break
192258

193259
case WebSocket.CONNECTING:
@@ -210,7 +276,15 @@ export class SharedProtectWebSocket {
210276
this.connectCheckInterval = undefined
211277
this.reconnectAttempts++
212278
setTimeout(async () => {
213-
await this.connect()
279+
try {
280+
await this.disconnect()
281+
await this.connect()
282+
} catch (error) {
283+
this.wsLogger?.error(
284+
'Websocket disconnecting error ' + (error as Error).stack
285+
)
286+
}
287+
214288
}, this.RECONNECT_TIMEOUT)
215289
}
216290
break

0 commit comments

Comments
 (0)