homebridge-smartsystem 6.8.4 → 6.8.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -307,6 +307,10 @@ Homebridge UI version
307
307
  - 1: max/min values for hbValues
308
308
  - 2: HB up/down -> don't respond to motor unit's, only macro
309
309
  - 3: remember request before the login kicked in (and redirect to that one, once the login was succesful)
310
+ - 4: now using only TCP sockets to the server
311
+ - 5: making it more robust / inline with repo-service version
312
+ - 6: removed double error handlers on cloudSocket
313
+ - 7: restart if no free connections
310
314
 
311
315
  ## Hardware
312
316
  A Raspberry Pi that connects to a Duotecno IP Node
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "homebridge-smartsystem",
3
3
  "displayname": "Duotecno Bridge",
4
- "version": "6.8.4",
5
- "description": "SmartServer (Proxy Websockets to TCP sockets, Smappee MQTT, Duotecno IP Nodes, Homekit interface)",
4
+ "version": "6.8.7",
5
+ "description": "SmartServer (Proxy TCP sockets to the cloud, Smappee MQTT, Duotecno IP Nodes, Homekit interface)",
6
6
  "main": "index.js",
7
7
  "author": "Johan Coppieters",
8
8
  "homepage": "http://www.duotecno.be",
package/server/proxy.js CHANGED
@@ -38,6 +38,7 @@ catch (e) {
38
38
  setProxyConfig();
39
39
  console.log("No config-proxy.json found, using default values from homebridge or other.");
40
40
  }
41
+ let connectionChecker = null;
41
42
  const gCloudConnections = [];
42
43
  const kDebug = config.debug || false; // enable debug mode from config
43
44
  /////////////
@@ -60,13 +61,23 @@ function error(str) {
60
61
  ///////////////////////////////
61
62
  // Start up the proxy server //
62
63
  ///////////////////////////////
63
- if (config.uniqueId) {
64
- makeNewCloudConnection(config.masterAddress, config.masterPort, config.cloudServer, config.cloudPort, config.uniqueId);
65
- }
66
- function makeNewCloudConnection(master, masterPort, server, serverPort, uniqueId) {
64
+ cleanStart(true);
65
+ function makeNewCloudConnection(master, masterPort, server, serverPort, uniqueId, count = 0) {
66
+ // retry making a cloud connection
67
+ function reconnect(err) {
68
+ error(`[PROXY] → [CLOUD] Failed to connect ${count + 1} times to the Cloud server: ${err.message}`);
69
+ if (count < 3) {
70
+ setTimeout(() => {
71
+ makeNewCloudConnection(master, masterPort, server, serverPort, uniqueId, count + 1);
72
+ }, 1000);
73
+ }
74
+ }
67
75
  const cloudSocket = new net.Socket();
76
+ cloudSocket.once('error', reconnect);
77
+ log(`[PROXY] → [CLOUD] Attempt ${count + 1} to start a new free connection for ${config.uniqueId} to the Cloud at ${server}:${serverPort}`);
68
78
  cloudSocket.connect(serverPort, server, () => {
69
79
  log(`[PROXY] → [CLOUD] Connected to Cloud at ${server}:${serverPort}`);
80
+ cloudSocket.removeListener('error', reconnect);
70
81
  // Send unique ID as the first message to identify this proxy/device
71
82
  cloudSocket.write(`[${uniqueId}]`);
72
83
  debug(`[PROXY] → [CLOUD] Sent unique ID: ${uniqueId}`);
@@ -74,10 +85,6 @@ function makeNewCloudConnection(master, masterPort, server, serverPort, uniqueId
74
85
  gCloudConnections.push(context);
75
86
  log(`[PROXY] → [CLOUD] New free connection #${gCloudConnections.length} to the Cloud at ${server}:${serverPort}`);
76
87
  });
77
- cloudSocket.on('error', (err) => {
78
- error(`[PROXY] → [CLOUD] Failed to connect to Cloud: ${err.message}`);
79
- });
80
- log(`[PROXY] → [CLOUD] Attempting to start a new free connection for ${config.uniqueId} to the Cloud at ${server}:${serverPort}`);
81
88
  }
82
89
  exports.makeNewCloudConnection = makeNewCloudConnection;
83
90
  ////////////////
@@ -85,20 +92,25 @@ exports.makeNewCloudConnection = makeNewCloudConnection;
85
92
  ////////////////
86
93
  function cleanStart(restart = false) {
87
94
  if (gCloudConnections.length) {
88
- gCloudConnections.forEach(context => {
89
- if (context.deviceSocket) {
90
- context.deviceSocket.end();
91
- context.deviceSocket = null;
92
- }
93
- context.cloudSocket.end();
94
- context.cloudSocket = null;
95
- });
95
+ gCloudConnections.forEach(ctx => ctx.cleanupSockets());
96
+ log(`[PROXY] → [CLOUD] Cleaned up all ${gCloudConnections.length} cloud connections.`);
96
97
  gCloudConnections.splice(0, gCloudConnections.length); // clear the array
97
- log(`[PROXY] → [CLOUD] Cleaned up all cloud connections.`);
98
+ }
99
+ if (connectionChecker) {
100
+ clearInterval(connectionChecker);
101
+ connectionChecker = null;
98
102
  }
99
103
  if (restart) {
100
104
  if (config.uniqueId) {
101
105
  makeNewCloudConnection(config.masterAddress, config.masterPort, config.cloudServer, config.cloudPort, config.uniqueId);
106
+ // check every 5 second for a free connection
107
+ connectionChecker = setInterval(() => {
108
+ const freeConnection = gCloudConnections.find((ctx) => (!ctx.deviceSocket) && ctx.cloudSocket && (ctx.cloudSocket.readyState === 'open'));
109
+ if (!freeConnection) {
110
+ error(`[PROXY] → [CLOUD] No free connections available, restarting proxy. ***************************`);
111
+ cleanStart(true);
112
+ }
113
+ }, 5000);
102
114
  }
103
115
  else {
104
116
  log(`[PROXY] → [CLOUD] Not restarting, no unique ID configured, not starting the proxy.`);
@@ -122,7 +134,7 @@ class Context {
122
134
  }
123
135
  setupCloudSocket() {
124
136
  // set up handlers for the cloud socket
125
- this.cloudSocket.on('data', data => {
137
+ this.cloudSocket.on('data', (data) => {
126
138
  this.handleDataFromCloud(data);
127
139
  });
128
140
  this.cloudSocket.on('close', () => {
@@ -165,8 +177,8 @@ class Context {
165
177
  }
166
178
  }
167
179
  else if (this.deviceSocket.readyState === 'open') {
168
- this.deviceSocket.write(new Uint8Array(data));
169
- debug(`[PROXY] → [DEVICE] forwarding data to device: ${data.toString().slice(0, -1)}`);
180
+ this.deviceSocket.write(data);
181
+ debug(`[PROXY] → [DEVICE] forwarding data to device: ${data.toString().trim()}`);
170
182
  }
171
183
  else {
172
184
  warning(`[PROXY] → [DEVICE] Device socket is not open yet, waiting for connection... what to do with the data??`);
@@ -185,13 +197,13 @@ class Context {
185
197
  }
186
198
  log(`[PROXY] → [DEVICE] Connected to local device at ${address}:${port}`);
187
199
  this.setUpDeviceSocket();
188
- log(`[PROXY] → [DEVICE] Sending initial message: ${data.toString().slice(0, -1)}`);
189
- this.deviceSocket.write(new Uint8Array(data));
200
+ log(`[PROXY] → [DEVICE] Sending initial message: ${data.toString().trim()}`);
201
+ this.deviceSocket.write(data);
190
202
  });
191
203
  }
192
204
  else if (this.deviceSocket.readyState === 'open') {
193
205
  warning(`[PROXY] → [DEVICE] Device socket already open, sending data directly -- STRANGE !!`);
194
- this.deviceSocket.write(new Uint8Array(data));
206
+ this.deviceSocket.write(data);
195
207
  }
196
208
  else {
197
209
  error(`[PROXY] → [DEVICE] Device socket exists, but is not open yet !!`);
@@ -204,8 +216,8 @@ class Context {
204
216
  }
205
217
  this.deviceSocket.on('data', (data) => {
206
218
  const message = data.toString('utf-8');
207
- debug(`[DEVICE] → [PROXY] forwarding data to Cloud: ${message.slice(0, -1)}`);
208
- this.cloudSocket.write(message);
219
+ debug(`[DEVICE] → [PROXY] forwarding data to Cloud: ${message.trim()}`);
220
+ this.cloudSocket.write(data);
209
221
  });
210
222
  this.deviceSocket.on('close', () => {
211
223
  log(`[DEVICE] → [PROXY] Device socket closed`);
@@ -216,33 +228,43 @@ class Context {
216
228
  this.removeConnection();
217
229
  });
218
230
  }
219
- removeConnection() {
220
- // Clean up device socket
231
+ cleanupSockets() {
232
+ // Clean up the device socket
221
233
  if (this.deviceSocket) {
222
234
  if (!this.deviceSocket.destroyed) {
235
+ this.deviceSocket.removeAllListeners();
223
236
  this.deviceSocket.end();
237
+ this.deviceSocket.destroy();
224
238
  }
225
239
  this.deviceSocket = null;
226
240
  }
227
- // remove this context and close the cloud socket
228
- const index = gCloudConnections.indexOf(this);
229
- if (index !== -1) {
230
- this.cloudSocket.end();
241
+ // Clean up the cloud socket
242
+ if (this.cloudSocket) {
243
+ if (!this.cloudSocket.destroyed) {
244
+ this.cloudSocket.removeAllListeners();
245
+ this.cloudSocket.end();
246
+ this.cloudSocket.destroy();
247
+ }
231
248
  this.cloudSocket = null;
232
- gCloudConnections.splice(index, 1);
233
- log(`[PROXY] → [CLOUD] Closed socket to Cloud and removed context for device ${this.master}`);
234
249
  }
250
+ }
251
+ removeConnection() {
252
+ const index = gCloudConnections.indexOf(this);
253
+ if (index !== -1)
254
+ gCloudConnections.splice(index, 1);
255
+ this.cleanupSockets();
256
+ log(`[PROXY] → [CLOUD] Closed socket to Cloud and removed context for device ${this.master}`);
235
257
  if (gCloudConnections.length === 0) {
236
258
  log(`[PROXY] → [CLOUD] No more cloud connections, restarting proxy.`);
237
259
  // just to be sure: restart...
238
260
  if (config.kind === "pm2") {
239
261
  // PM2 should restart us.
240
262
  log(`[PROXY] → [CLOUD] PM2 should restart us, exiting now.`);
241
- process.exit();
263
+ setTimeout(() => process.exit(0), 100);
242
264
  }
243
265
  else if (config.kind === "solo") {
244
266
  log(`[PROXY] → [CLOUD] Running solo, restarting proxy.`);
245
- this.restart();
267
+ restart();
246
268
  }
247
269
  else {
248
270
  log(`[PROXY] → [CLOUD] Running under gateway/homebridge, not restarting... not sure what to do here.`);
@@ -252,25 +274,18 @@ class Context {
252
274
  ///////////////
253
275
  // Utilities //
254
276
  ///////////////
255
- restart() {
256
- console.log('🔄 Restarting...');
257
- (0, child_process_1.spawn)(process_1.execPath, process_1.argv.slice(1), {
258
- cwd: (0, process_1.cwd)(),
259
- detached: true,
260
- stdio: 'inherit',
261
- });
262
- process.exit();
263
- }
264
277
  // check if it's a heartbeat request from the server
265
278
  isHeartbeatRequest(data) {
279
+ const msg = data.toString('utf-8').trim();
280
+ return msg === '[215,3]';
266
281
  // hearbeatKind = (poll = 1, poll_old = 2, serve = 3)
267
- return ((data[0] === 91) && // [
268
- (data[1] === 50) && // 2
269
- (data[2] === 49) && // 1
270
- (data[3] === 53) && // 5
271
- (data[4] === 44) && // ,
272
- (data[5] === 51) && // 3
273
- (data[6] === 93)); // ]
282
+ // return ((data[0] === 91) && // [
283
+ // (data[1] === 50) && // 2
284
+ // (data[2] === 49) && // 1
285
+ // (data[3] === 53) && // 5
286
+ // (data[4] === 44) && // ,
287
+ // (data[5] === 51) && // 3
288
+ // (data[6] === 93)); // ]
274
289
  }
275
290
  // check if it's a server response after connecting and sending our uniqueID
276
291
  isConnectionResponse(data) {
@@ -280,4 +295,52 @@ class Context {
280
295
  }
281
296
  }
282
297
  exports.Context = Context;
298
+ function restart() {
299
+ console.log('🔄 Restarting...');
300
+ setTimeout(() => {
301
+ (0, child_process_1.spawn)(process_1.execPath, process_1.argv.slice(1), {
302
+ cwd: (0, process_1.cwd)(),
303
+ detached: true,
304
+ stdio: 'inherit',
305
+ });
306
+ process.exit(0);
307
+ }, 100);
308
+ }
309
+ // Graceful shutdown handler
310
+ function handleShutdown(signal) {
311
+ log(`[PROXY] → [SYSTEM] Received ${signal}, shutting down gracefully...`);
312
+ if (connectionChecker)
313
+ clearInterval(connectionChecker);
314
+ // Close all cloud connections
315
+ gCloudConnections.forEach(ctx => ctx.cleanupSockets());
316
+ gCloudConnections.splice(0, gCloudConnections.length);
317
+ log(`[PROXY] → [SYSTEM] All connections closed. Exiting.`);
318
+ if (config.kind === "solo") {
319
+ log(`[PROXY] → [SYSTEM] Restarting proxy in solo mode...`);
320
+ restart();
321
+ }
322
+ else {
323
+ log(`[PROXY] → [SYSTEM] Exiting without restart.`);
324
+ setTimeout(() => process.exit(0), 100);
325
+ }
326
+ }
327
+ // Listen for termination signals
328
+ process.on('SIGINT', () => handleShutdown('SIGINT'));
329
+ process.on('SIGTERM', () => handleShutdown('SIGTERM'));
330
+ process.on('SIGINT', (err) => {
331
+ error(`[SYSTEM] received SIGINT`);
332
+ handleShutdown('SIGINT');
333
+ });
334
+ process.on('SIGTERM', (err) => {
335
+ error(`[SYSTEM] received SIGTERM`);
336
+ handleShutdown('SIGTERM');
337
+ });
338
+ process.on('uncaughtException', (err) => {
339
+ error(`[SYSTEM] Uncaught Exception: ${err.message}`);
340
+ handleShutdown('uncaughtException');
341
+ });
342
+ process.on('unhandledRejection', (reason) => {
343
+ error(`[SYSTEM] Unhandled Rejection: ${reason}`);
344
+ handleShutdown('unhandledRejection');
345
+ });
283
346
  //# sourceMappingURL=proxy.js.map
package/server/proxy.ts CHANGED
@@ -44,13 +44,14 @@ export function setProxyConfig(c?: ProxyConfig): ProxyConfig {
44
44
 
45
45
  let config: ProxyConfig;
46
46
  try {
47
- setProxyConfig(require('../config-proxy.json'))
47
+ setProxyConfig(require('../config-proxy.json'));
48
48
  } catch (e) {
49
49
  setProxyConfig();
50
50
  console.log("No config-proxy.json found, using default values from homebridge or other.");
51
51
  }
52
52
 
53
53
 
54
+ let connectionChecker: NodeJS.Timer | null = null;
54
55
 
55
56
  const gCloudConnections: Array<Context> = [];
56
57
  const kDebug = config.debug || false; // enable debug mode from config
@@ -77,16 +78,28 @@ function error(str: string) {
77
78
  ///////////////////////////////
78
79
  // Start up the proxy server //
79
80
  ///////////////////////////////
80
- if (config.uniqueId) {
81
- makeNewCloudConnection(config.masterAddress, config.masterPort, config.cloudServer, config.cloudPort, config.uniqueId);
82
- }
81
+ cleanStart(true);
82
+
83
83
 
84
+ export function makeNewCloudConnection(master: string, masterPort: number, server: string, serverPort: number, uniqueId: string, count = 0) {
85
+ // retry making a cloud connection
86
+ function reconnect(err: Error) {
87
+ error(`[PROXY] → [CLOUD] Failed to connect ${count+1} times to the Cloud server: ${err.message}`);
88
+ if (count < 3) {
89
+ setTimeout(() => {
90
+ makeNewCloudConnection(master, masterPort, server, serverPort, uniqueId, count + 1);
91
+ }, 1000);
92
+ }
93
+ }
84
94
 
85
- export function makeNewCloudConnection(master: string, masterPort: number, server: string, serverPort: number, uniqueId: string) {
86
95
  const cloudSocket = new net.Socket();
87
-
96
+ cloudSocket.once('error', reconnect);
97
+
98
+ log(`[PROXY] → [CLOUD] Attempt ${count+1} to start a new free connection for ${config.uniqueId} to the Cloud at ${server}:${serverPort}`);
99
+
88
100
  cloudSocket.connect(serverPort, server, () => {
89
101
  log(`[PROXY] → [CLOUD] Connected to Cloud at ${server}:${serverPort}`);
102
+ cloudSocket.removeListener('error', reconnect);
90
103
 
91
104
  // Send unique ID as the first message to identify this proxy/device
92
105
  cloudSocket.write(`[${uniqueId}]`);
@@ -96,12 +109,6 @@ export function makeNewCloudConnection(master: string, masterPort: number, serve
96
109
  gCloudConnections.push(context);
97
110
  log(`[PROXY] → [CLOUD] New free connection #${gCloudConnections.length} to the Cloud at ${server}:${serverPort}`);
98
111
  });
99
-
100
- cloudSocket.on('error', (err) => {
101
- error(`[PROXY] → [CLOUD] Failed to connect to Cloud: ${err.message}`);
102
- });
103
-
104
- log(`[PROXY] → [CLOUD] Attempting to start a new free connection for ${config.uniqueId} to the Cloud at ${server}:${serverPort}`);
105
112
  }
106
113
 
107
114
 
@@ -110,21 +117,32 @@ export function makeNewCloudConnection(master: string, masterPort: number, serve
110
117
  ////////////////
111
118
  export function cleanStart(restart: boolean = false) {
112
119
  if (gCloudConnections.length) {
113
- gCloudConnections.forEach(context => {
114
- if (context.deviceSocket) {
115
- context.deviceSocket.end();
116
- context.deviceSocket = null;
117
- }
118
- context.cloudSocket.end();
119
- context.cloudSocket = null;
120
- });
120
+ gCloudConnections.forEach(ctx => ctx.cleanupSockets());
121
+ log(`[PROXY] → [CLOUD] Cleaned up all ${gCloudConnections.length} cloud connections.`);
122
+
121
123
  gCloudConnections.splice(0, gCloudConnections.length); // clear the array
122
- log(`[PROXY] → [CLOUD] Cleaned up all cloud connections.`);
124
+ }
125
+
126
+ if (connectionChecker) {
127
+ clearInterval(connectionChecker);
128
+ connectionChecker = null;
123
129
  }
124
130
 
125
131
  if (restart) {
126
132
  if (config.uniqueId) {
127
133
  makeNewCloudConnection(config.masterAddress, config.masterPort, config.cloudServer, config.cloudPort, config.uniqueId);
134
+
135
+ // check every 5 second for a free connection
136
+ connectionChecker = setInterval(() => {
137
+ const freeConnection = gCloudConnections.find((ctx: Context) =>
138
+ (!ctx.deviceSocket) && ctx.cloudSocket && (ctx.cloudSocket.readyState === 'open')
139
+ );
140
+ if (!freeConnection) {
141
+ error(`[PROXY] → [CLOUD] No free connections available, restarting proxy. ***************************`);
142
+ cleanStart(true);
143
+ }
144
+ }, 5000);
145
+
128
146
  } else {
129
147
  log(`[PROXY] → [CLOUD] Not restarting, no unique ID configured, not starting the proxy.`);
130
148
  }
@@ -135,11 +153,11 @@ export function cleanStart(restart: boolean = false) {
135
153
 
136
154
 
137
155
  export class Context {
138
- deviceSocket: net.Socket;
156
+ deviceSocket: net.Socket | null;
139
157
  master: string;
140
158
  masterPort: number;
141
159
 
142
- cloudSocket: net.Socket;
160
+ cloudSocket: net.Socket | null;
143
161
  server: string;
144
162
  serverPort: number;
145
163
 
@@ -162,8 +180,8 @@ export class Context {
162
180
 
163
181
  setupCloudSocket() {
164
182
  // set up handlers for the cloud socket
165
- this.cloudSocket.on('data', data => {
166
- this.handleDataFromCloud(data as Buffer);
183
+ this.cloudSocket.on('data', (data: Buffer) => {
184
+ this.handleDataFromCloud(data);
167
185
  });
168
186
 
169
187
  this.cloudSocket.on('close', () => {
@@ -210,8 +228,8 @@ export class Context {
210
228
  }
211
229
 
212
230
  } else if (this.deviceSocket.readyState === 'open') {
213
- this.deviceSocket.write(new Uint8Array(data));
214
- debug(`[PROXY] → [DEVICE] forwarding data to device: ${data.toString().slice(0, -1)}`);
231
+ this.deviceSocket.write(data);
232
+ debug(`[PROXY] → [DEVICE] forwarding data to device: ${data.toString().trim()}`);
215
233
 
216
234
  } else {
217
235
  warning(`[PROXY] → [DEVICE] Device socket is not open yet, waiting for connection... what to do with the data??`);
@@ -236,14 +254,14 @@ export class Context {
236
254
  log(`[PROXY] → [DEVICE] Connected to local device at ${address}:${port}`);
237
255
 
238
256
  this.setUpDeviceSocket();
239
-
240
- log(`[PROXY] → [DEVICE] Sending initial message: ${data.toString().slice(0, -1)}`);
241
- this.deviceSocket.write(new Uint8Array(data));
257
+
258
+ log(`[PROXY] → [DEVICE] Sending initial message: ${data.toString().trim()}`);
259
+ this.deviceSocket.write(data);
242
260
  });
243
261
 
244
262
  } else if (this.deviceSocket.readyState === 'open') {
245
263
  warning(`[PROXY] → [DEVICE] Device socket already open, sending data directly -- STRANGE !!`);
246
- this.deviceSocket.write(new Uint8Array(data));
264
+ this.deviceSocket.write(data);
247
265
 
248
266
  } else {
249
267
  error(`[PROXY] → [DEVICE] Device socket exists, but is not open yet !!`);
@@ -258,8 +276,8 @@ export class Context {
258
276
 
259
277
  this.deviceSocket.on('data', (data: Buffer) => {
260
278
  const message = data.toString('utf-8');
261
- debug(`[DEVICE] → [PROXY] forwarding data to Cloud: ${message.slice(0, -1)}`);
262
- this.cloudSocket.write(message);
279
+ debug(`[DEVICE] → [PROXY] forwarding data to Cloud: ${message.trim()}`);
280
+ this.cloudSocket.write(data);
263
281
  });
264
282
 
265
283
  this.deviceSocket.on('close', () => {
@@ -273,23 +291,34 @@ export class Context {
273
291
  });
274
292
  }
275
293
 
276
- removeConnection() {
277
- // Clean up device socket
294
+ cleanupSockets() {
295
+ // Clean up the device socket
278
296
  if (this.deviceSocket) {
279
297
  if (!this.deviceSocket.destroyed) {
298
+ this.deviceSocket.removeAllListeners();
280
299
  this.deviceSocket.end();
300
+ this.deviceSocket.destroy();
281
301
  }
282
302
  this.deviceSocket = null;
283
303
  }
284
304
 
285
- // remove this context and close the cloud socket
286
- const index = gCloudConnections.indexOf(this);
287
- if (index !== -1) {
288
- this.cloudSocket.end();
305
+ // Clean up the cloud socket
306
+ if (this.cloudSocket) {
307
+ if (!this.cloudSocket.destroyed) {
308
+ this.cloudSocket.removeAllListeners();
309
+ this.cloudSocket.end();
310
+ this.cloudSocket.destroy();
311
+ }
289
312
  this.cloudSocket = null;
290
- gCloudConnections.splice(index, 1);
291
- log(`[PROXY] → [CLOUD] Closed socket to Cloud and removed context for device ${this.master}`);
292
313
  }
314
+ }
315
+
316
+ removeConnection() {
317
+ const index = gCloudConnections.indexOf(this);
318
+ if (index !== -1) gCloudConnections.splice(index, 1);
319
+
320
+ this.cleanupSockets();
321
+ log(`[PROXY] → [CLOUD] Closed socket to Cloud and removed context for device ${this.master}`);
293
322
 
294
323
  if (gCloudConnections.length === 0) {
295
324
  log(`[PROXY] → [CLOUD] No more cloud connections, restarting proxy.`);
@@ -297,11 +326,11 @@ export class Context {
297
326
  if (config.kind === "pm2") {
298
327
  // PM2 should restart us.
299
328
  log(`[PROXY] → [CLOUD] PM2 should restart us, exiting now.`);
300
- process.exit();
329
+ setTimeout(() => process.exit(0), 100);
301
330
 
302
331
  } else if (config.kind === "solo"){
303
332
  log(`[PROXY] → [CLOUD] Running solo, restarting proxy.`);
304
- this.restart();
333
+ restart();
305
334
 
306
335
  } else {
307
336
  log(`[PROXY] → [CLOUD] Running under gateway/homebridge, not restarting... not sure what to do here.`);
@@ -314,28 +343,18 @@ export class Context {
314
343
  // Utilities //
315
344
  ///////////////
316
345
 
317
- restart() {
318
- console.log('🔄 Restarting...');
319
-
320
- spawn(execPath, argv.slice(1), {
321
- cwd: cwd(),
322
- detached: true,
323
- stdio: 'inherit',
324
- });
325
-
326
- process.exit();
327
- }
328
-
329
346
  // check if it's a heartbeat request from the server
330
347
  isHeartbeatRequest(data: Buffer): boolean {
348
+ const msg = data.toString('utf-8').trim();
349
+ return msg === '[215,3]';
331
350
  // hearbeatKind = (poll = 1, poll_old = 2, serve = 3)
332
- return ((data[0] === 91) && // [
333
- (data[1] === 50) && // 2
334
- (data[2] === 49) && // 1
335
- (data[3] === 53) && // 5
336
- (data[4] === 44) && // ,
337
- (data[5] === 51) && // 3
338
- (data[6] === 93)); // ]
351
+ // return ((data[0] === 91) && // [
352
+ // (data[1] === 50) && // 2
353
+ // (data[2] === 49) && // 1
354
+ // (data[3] === 53) && // 5
355
+ // (data[4] === 44) && // ,
356
+ // (data[5] === 51) && // 3
357
+ // (data[6] === 93)); // ]
339
358
  }
340
359
 
341
360
  // check if it's a server response after connecting and sending our uniqueID
@@ -346,3 +365,64 @@ export class Context {
346
365
  return (message.startsWith('[OK')) || (message.startsWith('[ERROR'));
347
366
  }
348
367
  }
368
+
369
+ function restart() {
370
+ console.log('🔄 Restarting...');
371
+
372
+ setTimeout(() => {
373
+ spawn(execPath, argv.slice(1), {
374
+ cwd: cwd(),
375
+ detached: true,
376
+ stdio: 'inherit',
377
+ });
378
+
379
+ process.exit(0);
380
+ }, 100);
381
+ }
382
+
383
+
384
+ // Graceful shutdown handler
385
+ function handleShutdown(signal: string) {
386
+ log(`[PROXY] → [SYSTEM] Received ${signal}, shutting down gracefully...`);
387
+
388
+ if (connectionChecker)clearInterval(connectionChecker);
389
+
390
+ // Close all cloud connections
391
+ gCloudConnections.forEach(ctx => ctx.cleanupSockets());
392
+ gCloudConnections.splice(0, gCloudConnections.length);
393
+
394
+ log(`[PROXY] → [SYSTEM] All connections closed. Exiting.`);
395
+
396
+ if (config.kind === "solo") {
397
+ log(`[PROXY] → [SYSTEM] Restarting proxy in solo mode...`);
398
+ restart();
399
+ } else {
400
+ log(`[PROXY] → [SYSTEM] Exiting without restart.`);
401
+ setTimeout(() => process.exit(0), 100);
402
+ }
403
+ }
404
+
405
+ // Listen for termination signals
406
+ process.on('SIGINT', () => handleShutdown('SIGINT'));
407
+ process.on('SIGTERM', () => handleShutdown('SIGTERM'));
408
+
409
+
410
+ process.on('SIGINT', (err) => {
411
+ error(`[SYSTEM] received SIGINT`);
412
+ handleShutdown('SIGINT');
413
+ });
414
+
415
+ process.on('SIGTERM', (err) => {
416
+ error(`[SYSTEM] received SIGTERM`);
417
+ handleShutdown('SIGTERM');
418
+ });
419
+
420
+ process.on('uncaughtException', (err) => {
421
+ error(`[SYSTEM] Uncaught Exception: ${err.message}`);
422
+ handleShutdown('uncaughtException');
423
+ });
424
+
425
+ process.on('unhandledRejection', (reason) => {
426
+ error(`[SYSTEM] Unhandled Rejection: ${reason}`);
427
+ handleShutdown('unhandledRejection');
428
+ });