From 4360672b78149c2e069993a63cc57ea3c207d9d7 Mon Sep 17 00:00:00 2001 From: zlshames <zlshames@gmail.com> Date: Sat, 6 Jul 2024 14:47:26 -0400 Subject: [PATCH 01/11] adds raw sigterm/sigint handlers --- packages/server/.gitignore | 2 +- packages/server/src/main.ts | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/server/.gitignore b/packages/server/.gitignore index ec05802f..9b093bb4 100644 --- a/packages/server/.gitignore +++ b/packages/server/.gitignore @@ -2,7 +2,7 @@ node_modules/ !dist/ dist/* !dist/dev-app-update.yml -release/ +releases/ .vscode .DS_Store **/*.log diff --git a/packages/server/src/main.ts b/packages/server/src/main.ts index b82c9944..69f6bcac 100644 --- a/packages/server/src/main.ts +++ b/packages/server/src/main.ts @@ -111,6 +111,15 @@ app.on("window-all-closed", () => { */ app.on("before-quit", event => handleExit(event)); +process.on("SIGTERM", async () => { + log.debug("Received SIGTERM, exiting..."); + await handleExit(null, { exit: false }); +}); +process.on("SIGINT", async () => { + log.debug("Received SIGINT, exiting..."); + await handleExit(null, { exit: false }); +}); + /** * All code below this point has to do with the command-line functionality. * This is when you run the app via terminal, we want to give users the ability From f6044d774b3309decac37ce640391989c3a01324 Mon Sep 17 00:00:00 2001 From: zlshames <zlshames@gmail.com> Date: Sat, 6 Jul 2024 19:39:20 -0400 Subject: [PATCH 02/11] improved error logging --- packages/server/src/server/index.ts | 36 ++++++++++++++--------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/server/src/server/index.ts b/packages/server/src/server/index.ts index 04857b70..f9dd75dc 100644 --- a/packages/server/src/server/index.ts +++ b/packages/server/src/server/index.ts @@ -298,7 +298,7 @@ class BlueBubblesServer extends EventEmitter { setNotificationCount(count: number) { this.notificationCount = count; - if (this.repo.getConfig("dock_badge")) { + if (this.repo?.getConfig("dock_badge")) { app.setBadgeCount(this.notificationCount); } } @@ -358,7 +358,7 @@ class BlueBubblesServer extends EventEmitter { this.logger.info("Initializing filesystem..."); FileSystem.setup(); } catch (ex: any) { - this.logger.error(`Failed to setup Filesystem! ${ex.message}`); + this.logger.error(`Failed to setup Filesystem! ${ex?.message ?? String(ex)}}`); } // Initialize and connect to the server database @@ -432,7 +432,7 @@ class BlueBubblesServer extends EventEmitter { this.logger.info("Initializing connection to Google FCM..."); this.fcm = new FCMService(); } catch (ex: any) { - this.logger.error(`Failed to setup Google FCM service! ${ex.message}`); + this.logger.error(`Failed to setup Google FCM service! ${ex?.message ?? String(ex)}}`); } } @@ -441,7 +441,7 @@ class BlueBubblesServer extends EventEmitter { this.logger.info("Initializing OAuth service..."); this.oauthService = new OauthService(); } catch (ex: any) { - this.logger.error(`Failed to setup OAuth service! ${ex.message}`); + this.logger.error(`Failed to setup OAuth service! ${ex?.message ?? String(ex)}}`); } } @@ -452,7 +452,7 @@ class BlueBubblesServer extends EventEmitter { this.logger.info("Initializing up sockets..."); this.httpService = new HttpService(); } catch (ex: any) { - this.logger.error(`Failed to setup socket service! ${ex.message}`); + this.logger.error(`Failed to setup socket service! ${ex?.message ?? String(ex)}}`); } this.initOauthService(); @@ -461,7 +461,7 @@ class BlueBubblesServer extends EventEmitter { this.logger.info("Initializing helper service..."); this.privateApi = new PrivateApiService(); } catch (ex: any) { - this.logger.error(`Failed to setup helper service! ${ex.message}`); + this.logger.error(`Failed to setup helper service! ${ex?.message ?? String(ex)}}`); } try { @@ -472,28 +472,28 @@ class BlueBubblesServer extends EventEmitter { new ZrokService() ]; } catch (ex: any) { - this.logger.error(`Failed to initialize proxy services! ${ex.message}`); + this.logger.error(`Failed to initialize proxy services! ${ex?.message ?? String(ex)}}`); } try { this.logger.info("Initializing Message Manager..."); this.messageManager = new OutgoingMessageManager(); } catch (ex: any) { - this.logger.error(`Failed to start Message Manager service! ${ex.message}`); + this.logger.error(`Failed to start Message Manager service! ${ex?.message ?? String(ex)}}`); } try { this.logger.info("Initializing Webhook Service..."); this.webhookService = new WebhookService(); } catch (ex: any) { - this.logger.error(`Failed to start Webhook service! ${ex.message}`); + this.logger.error(`Failed to start Webhook service! ${ex?.message ?? String(ex)}}`); } try { this.logger.info("Initializing Scheduled Messages Service..."); this.scheduledMessages = new ScheduledMessagesService(); } catch (ex: any) { - this.logger.error(`Failed to start Scheduled Message service! ${ex.message}`); + this.logger.error(`Failed to start Scheduled Message service! ${ex?.message ?? String(ex)}}`); } } @@ -507,7 +507,7 @@ class BlueBubblesServer extends EventEmitter { this.httpService.initialize(); this.httpService.start(); } catch (ex: any) { - this.logger.error(`Failed to start HTTP service! ${ex.message}`); + this.logger.error(`Failed to start HTTP service! ${ex?.message ?? String(ex)}}`); } // Only start the oauth service if the tutorial isn't done @@ -527,14 +527,14 @@ class BlueBubblesServer extends EventEmitter { try { await this.startProxyServices(); } catch (ex: any) { - this.logger.error(`Failed to connect to proxy service! ${ex.message}`); + this.logger.error(`Failed to connect to proxy service! ${ex?.message ?? String(ex)}`); } try { this.logger.info("Starting Scheduled Messages service..."); await this.scheduledMessages.start(); } catch (ex: any) { - this.logger.error(`Failed to start Scheduled Messages service! ${ex.message}`); + this.logger.error(`Failed to start Scheduled Messages service! ${ex?.message ?? String(ex)}}`); } const privateApiEnabled = this.repo.getConfig("enable_private_api") as boolean; @@ -553,7 +553,7 @@ class BlueBubblesServer extends EventEmitter { this.logger.info("Starting FCM service..."); await this.fcm.start(); } catch (ex: any) { - this.logger.error(`Failed to start FCM service! ${ex.message}`); + this.logger.error(`Failed to start FCM service! ${ex?.message ?? String(ex)}}`); } } @@ -711,14 +711,14 @@ class BlueBubblesServer extends EventEmitter { this.caffeinate.start(); } } catch (ex: any) { - this.logger.error(`Failed to setup caffeinate service! ${ex.message}`); + this.logger.error(`Failed to setup caffeinate service! ${ex?.message ?? String(ex)}}`); } try { this.logger.info("Initializing queue service..."); this.queue = new QueueService(); } catch (ex: any) { - this.logger.error(`Failed to setup queue service! ${ex.message}`); + this.logger.error(`Failed to setup queue service! ${ex?.message ?? String(ex)}}`); } try { @@ -743,7 +743,7 @@ class BlueBubblesServer extends EventEmitter { this.networkChecker.start(); } catch (ex: any) { - this.logger.error(`Failed to setup network service! ${ex.message}`); + this.logger.error(`Failed to setup network service! ${ex?.message ?? String(ex)}}`); } } @@ -950,7 +950,7 @@ class BlueBubblesServer extends EventEmitter { await MacOsInterface.lock(); } } catch (ex: any) { - this.logger.debug(`Failed to auto-lock Mac! ${ex.message}`); + this.logger.debug(`Failed to auto-lock Mac! ${ex?.message ?? String(ex)}}`); } this.logger.info("Finished post-start checks..."); From c531215a618cb1cd745668f795076523f718e8f2 Mon Sep 17 00:00:00 2001 From: zlshames <zlshames@gmail.com> Date: Sat, 6 Jul 2024 19:39:36 -0400 Subject: [PATCH 03/11] time sync errors are now debug level --- packages/server/src/server/fileSystem/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server/src/server/fileSystem/index.ts b/packages/server/src/server/fileSystem/index.ts index fb3c9b12..cc3f299e 100644 --- a/packages/server/src/server/fileSystem/index.ts +++ b/packages/server/src/server/fileSystem/index.ts @@ -689,8 +689,8 @@ export class FileSystem { return Number.parseFloat(time); } } catch (ex) { - Server().log("Failed to sync time with time servers!", "warn"); - Server().log(ex); + Server().log("Failed to sync time with time servers!", "debug"); + Server().log(ex, 'debug'); } return null; From 55dc7ba0b62d5f9e593908e92963114bfb42b9ad Mon Sep 17 00:00:00 2001 From: zlshames <zlshames@gmail.com> Date: Sat, 6 Jul 2024 19:40:40 -0400 Subject: [PATCH 04/11] switches away from zx for cloudflare manager --- .../managers/cloudflareManager/index.ts | 69 +++++++++++-------- 1 file changed, 40 insertions(+), 29 deletions(-) diff --git a/packages/server/src/server/managers/cloudflareManager/index.ts b/packages/server/src/server/managers/cloudflareManager/index.ts index 8230de84..7386bc30 100644 --- a/packages/server/src/server/managers/cloudflareManager/index.ts +++ b/packages/server/src/server/managers/cloudflareManager/index.ts @@ -1,10 +1,9 @@ -import { ProcessOutput, ProcessPromise } from "zx"; -import * as zx from "zx"; import * as path from "path"; import { FileSystem } from "@server/fileSystem"; import { Server } from "@server"; import { isEmpty, isNotEmpty } from "@server/helpers/utils"; import { Loggable } from "@server/lib/logging/Loggable"; +import { ChildProcess, spawn } from "child_process"; export class CloudflareManager extends Loggable { tag = "CloudflareManager"; @@ -17,7 +16,7 @@ export class CloudflareManager extends Loggable { pidPath = path.join(FileSystem.resources, "macos", "daemons", "cloudflare", "cloudflare.pid"); - proc: ProcessPromise<ProcessOutput>; + proc: ChildProcess; currentProxyUrl: string; @@ -39,34 +38,37 @@ export class CloudflareManager extends Loggable { private async connectHandler(): Promise<string> { return new Promise((resolve, reject) => { - const port = Server().repo.getConfig("socket_port") as string; - // eslint-disable-next-line max-len - this.proc = zx.$`${this.daemonPath} tunnel --url localhost:${port} --config ${this.cfgPath} --pidfile ${this.pidPath}`; - - // If there is an error with the command, throw the error - this.proc.catch(reason => { - reject(reason); - }); - - // Configure handlers for all the output events - this.proc.stdout.on("data", chunk => this.handleData(chunk)); - this.proc.stdout.on("error", chunk => this.handleError(chunk)); - this.proc.stderr.on("data", chunk => this.handleData(chunk)); - this.proc.stderr.on("error", chunk => this.handleError(chunk)); - - this.on("new-url", url => resolve(url)); - this.on("error", err => { - // Ignore certain errors - if (typeof err === "string") { - if (err.includes("Thank you for trying Cloudflare Tunnel.")) return; - } + try { + const port = Server().repo.getConfig("socket_port") as string; + this.proc = spawn(this.daemonPath, [ + 'tunnel', + '--url', `localhost:${port}`, + '--config', this.cfgPath, + '--pidfile', this.pidPath + ]); + + // Configure handlers for all the output events + this.proc.stdout.on("data", chunk => this.handleData(chunk)); + this.proc.stdout.on("error", chunk => this.handleError(chunk)); + this.proc.stderr.on("data", chunk => this.handleData(chunk)); + this.proc.stderr.on("error", chunk => this.handleError(chunk)); + + this.on("new-url", url => resolve(url)); + this.on("error", err => { + // Ignore certain errors + if (typeof err === "string") { + if (err.includes("Thank you for trying Cloudflare Tunnel.")) return; + } - reject(err); - }); + reject(err); + }); - setTimeout(() => { - reject(new Error("Failed to connect to Cloudflare after 2 minutes...")); - }, 1000 * 60 * 2); // 2 minutes + setTimeout(() => { + reject(new Error("Failed to connect to Cloudflare after 2 minutes...")); + }, 1000 * 60 * 2); // 2 minutes + } catch (ex) { + reject(ex); + } }); } @@ -90,10 +92,19 @@ export class CloudflareManager extends Loggable { return; } + this.detectErrors(data); this.detectNewUrl(data); this.detectMaxConnectionRetries(data); } + private detectErrors(data: string) { + if (data.includes('no such host')) { + this.handleError('Unable to resolve api.trycloudflare.com! Ensure that your Mac has internet access and that any networking tools you use are not blocking the hostname.') + } else if (data.includes('failed to request quick Tunnel: ')) { + this.handleError(data.split('failed to request quick Tunnel: ')[1]); + } + } + private detectNewUrl(data: string) { const urlMatches = data.match(this.proxyUrlRegex); if (isNotEmpty(urlMatches)) this.setProxyUrl(urlMatches[1]); From baeedb3cdd98ac9c1b4dbd65ca79e1c14d612fa8 Mon Sep 17 00:00:00 2001 From: zlshames <zlshames@gmail.com> Date: Sat, 6 Jul 2024 19:40:54 -0400 Subject: [PATCH 05/11] graceful error handling for update service --- .../server/services/updateService/index.ts | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/packages/server/src/server/services/updateService/index.ts b/packages/server/src/server/services/updateService/index.ts index 4927fd6c..f04dda71 100644 --- a/packages/server/src/server/services/updateService/index.ts +++ b/packages/server/src/server/services/updateService/index.ts @@ -4,7 +4,7 @@ import { Server } from "@server"; import { SERVER_UPDATE } from "@server/events"; import { ScheduledService } from "@server/lib/ScheduledService"; import { Loggable } from "@server/lib/logging/Loggable"; -import axios from "axios"; +import axios, { AxiosResponse } from "axios"; export class UpdateService extends Loggable { tag = "UpdateService"; @@ -52,14 +52,21 @@ export class UpdateService extends Loggable { } async checkForUpdate({ showNoUpdateDialog = false, showUpdateDialog = true } = {}): Promise<boolean> { - const releasesRes = await axios.get( - "https://api.github.com/repos/BlueBubblesApp/bluebubbles-server/releases", - { - headers: { - Accept: "application/vnd.github.v3+json" + let releasesRes: AxiosResponse<any, any>; + + try { + releasesRes = await axios.get( + "https://api.github.com/repos/BlueBubblesApp/bluebubbles-server/releases", + { + headers: { + Accept: "application/vnd.github.v3+json" + } } - } - ); + ); + } catch (ex: any) { + this.log.error(`Failed to fetch release information from GitHub! Error: ${ex?.message ?? String(ex)}`); + return false; + } const releases = (releasesRes.data as any[]).filter((x) => !x.prerelease && From 9a32a3f54b71f83e9cede7ee1e49b0b45867ab79 Mon Sep 17 00:00:00 2001 From: zlshames <zlshames@gmail.com> Date: Sat, 6 Jul 2024 19:44:13 -0400 Subject: [PATCH 06/11] start delay only occurs when uptime is < 5 minutes now --- packages/server/src/server/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/server/src/server/index.ts b/packages/server/src/server/index.ts index f9dd75dc..2b1820d2 100644 --- a/packages/server/src/server/index.ts +++ b/packages/server/src/server/index.ts @@ -370,8 +370,10 @@ class BlueBubblesServer extends EventEmitter { this.logger.info("Starting IPC Listeners.."); IPCService.startIpcListeners(); + // Delay will only occur if your Mac started up within the last 5 minutes const startDelay: number = getStartDelay(); - if (startDelay > 0) { + const uptimeSeconds = os.uptime(); + if (startDelay > 0 && uptimeSeconds < 300) { this.logger.info(`Delaying server startup by ${startDelay} seconds`); await waitMs(startDelay * 1000); } From da787ab868d65433a836d7272c77198424575dc9 Mon Sep 17 00:00:00 2001 From: zlshames <zlshames@gmail.com> Date: Sat, 6 Jul 2024 19:49:24 -0400 Subject: [PATCH 07/11] better error handling for cloudflare manager --- .../managers/cloudflareManager/index.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/server/src/server/managers/cloudflareManager/index.ts b/packages/server/src/server/managers/cloudflareManager/index.ts index 7386bc30..4ce7b88b 100644 --- a/packages/server/src/server/managers/cloudflareManager/index.ts +++ b/packages/server/src/server/managers/cloudflareManager/index.ts @@ -92,17 +92,21 @@ export class CloudflareManager extends Loggable { return; } - this.detectErrors(data); + const error = this.detectError(data); + if (error) this.emitError(error); + this.detectNewUrl(data); this.detectMaxConnectionRetries(data); } - private detectErrors(data: string) { + private detectError(data: string): string | null { if (data.includes('no such host')) { - this.handleError('Unable to resolve api.trycloudflare.com! Ensure that your Mac has internet access and that any networking tools you use are not blocking the hostname.') + return 'Unable to resolve api.trycloudflare.com! Ensure that your Mac has internet access and that any networking tools you use are not blocking the hostname.'; } else if (data.includes('failed to request quick Tunnel: ')) { - this.handleError(data.split('failed to request quick Tunnel: ')[1]); + return data.split('failed to request quick Tunnel: ')[1]; } + + return null; } private detectNewUrl(data: string) { @@ -136,6 +140,11 @@ export class CloudflareManager extends Loggable { } handleError(chunk: any) { - this.emit("error", chunk); + const error = this.detectError(chunk); + if (error) this.emitError(error); + } + + emitError(err: any) { + this.emit("error", err); } } From 6fe1b5363d45079a8601c787eda2bd8e867d8456 Mon Sep 17 00:00:00 2001 From: zlshames <zlshames@gmail.com> Date: Tue, 9 Jul 2024 09:59:36 -0400 Subject: [PATCH 08/11] fix: improves firebase setup slightly --- packages/server/src/server/services/oauthService/index.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/server/src/server/services/oauthService/index.ts b/packages/server/src/server/services/oauthService/index.ts index b337dd43..bec69166 100644 --- a/packages/server/src/server/services/oauthService/index.ts +++ b/packages/server/src/server/services/oauthService/index.ts @@ -513,7 +513,10 @@ export class OauthService extends Loggable { try { const url = `https://firebase.googleapis.com/v1beta1/projects/${projectId}/androidApps`; const data = { displayName: this.projectName, packageName: this.packageName }; - const createRes = await this.sendRequest("POST", url, data); + const createRes = await this.tryUntilNoError("POST", url, data, 3, 10000); + if (!createRes?.data?.name) { + throw new Error(`Failed to provision Android App: ${getObjectAsString(createRes)}`); + } // Wait for the app to be created const operationName = createRes.data.name; From 4e03f6338f7e7845d3d157c72dda81c1f32034e4 Mon Sep 17 00:00:00 2001 From: zlshames <zlshames@gmail.com> Date: Tue, 9 Jul 2024 10:00:13 -0400 Subject: [PATCH 09/11] fix fcm start condition --- packages/server/src/server/services/fcmService/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server/src/server/services/fcmService/index.ts b/packages/server/src/server/services/fcmService/index.ts index 288d23b0..3baedb2e 100644 --- a/packages/server/src/server/services/fcmService/index.ts +++ b/packages/server/src/server/services/fcmService/index.ts @@ -257,7 +257,7 @@ export class FCMService extends Loggable { if (isEmpty(serverUrl)) return; // Make sure that if we haven't initialized, we do so - if (!this.hasInitialized || !(await this.start())) return; + if (!this.hasInitialized && !(await this.start())) return; this.log.debug(`Attempting to write server URL to database...`); await this.saveUrlToDb(serverUrl); @@ -449,7 +449,7 @@ export class FCMService extends Loggable { priority: "normal" | "high" = "normal" ): Promise<admin.messaging.BatchResponse> { try { - if (!this.hasInitialized || !(await this.start())) return null; + if (!this.hasInitialized && !(await this.start())) return null; // Build out the notification message const payload: admin.messaging.MulticastMessage = { From 8ee08bafbd1a3a82769f5ab7780b38f2928957a6 Mon Sep 17 00:00:00 2001 From: zlshames <zlshames@gmail.com> Date: Tue, 9 Jul 2024 10:05:45 -0400 Subject: [PATCH 10/11] migrated away from zx due to leak --- package-lock.json | 172 +---------- packages/server/package.json | 3 +- packages/server/src/server/api/http/index.ts | 14 +- .../privateApi/modes/dylibPlugins/index.ts | 40 ++- .../server/src/server/lib/ProcessSpawner.ts | 291 ++++++++++++++++++ .../managers/cloudflareManager/index.ts | 46 +-- .../src/server/managers/zrokManager/index.ts | 83 ++--- 7 files changed, 388 insertions(+), 261 deletions(-) create mode 100644 packages/server/src/server/lib/ProcessSpawner.ts diff --git a/package-lock.json b/package-lock.json index 45aedc86..39c48214 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6139,6 +6139,7 @@ "version": "9.0.13", "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dev": true, "dependencies": { "@types/node": "*" } @@ -6369,11 +6370,6 @@ "integrity": "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==", "dev": true }, - "node_modules/@types/minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==" - }, "node_modules/@types/ms": { "version": "0.7.34", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", @@ -6385,15 +6381,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.52.tgz", "integrity": "sha512-s3nugnZumCC//n4moGGe6tkNMyYEdaDBitVjwPxXmR5lnMG5dHePinH2EdxkG3Rh1ghFHHixAG4NJhpJW1rthQ==" }, - "node_modules/@types/node-fetch": { - "version": "2.6.11", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz", - "integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==", - "dependencies": { - "@types/node": "*", - "form-data": "^4.0.0" - } - }, "node_modules/@types/node-forge": { "version": "1.3.11", "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", @@ -11449,7 +11436,8 @@ "node_modules/duplexer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", - "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==" + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "peer": true }, "node_modules/duplexify": { "version": "4.1.3", @@ -13132,20 +13120,6 @@ "node": ">= 0.6" } }, - "node_modules/event-stream": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", - "integrity": "sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==", - "dependencies": { - "duplexer": "~0.1.1", - "from": "~0", - "map-stream": "~0.1.0", - "pause-stream": "0.0.11", - "split": "0.3", - "stream-combiner": "~0.0.4", - "through": "~2.3.1" - } - }, "node_modules/event-target-shim": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", @@ -14062,11 +14036,6 @@ "node": ">= 0.6" } }, - "node_modules/from": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", - "integrity": "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==" - }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -18437,11 +18406,6 @@ "tmpl": "1.0.5" } }, - "node_modules/map-stream": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", - "integrity": "sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==" - }, "node_modules/matcher": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", @@ -19765,14 +19729,6 @@ "node": ">=8" } }, - "node_modules/pause-stream": { - "version": "0.0.11", - "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", - "integrity": "sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==", - "dependencies": { - "through": "~2.3" - } - }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -21533,20 +21489,6 @@ "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==" }, - "node_modules/ps-tree": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/ps-tree/-/ps-tree-1.2.0.tgz", - "integrity": "sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA==", - "dependencies": { - "event-stream": "=3.3.4" - }, - "bin": { - "ps-tree": "bin/ps-tree.js" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", @@ -23943,17 +23885,6 @@ "wbuf": "^1.7.3" } }, - "node_modules/split": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", - "integrity": "sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA==", - "dependencies": { - "through": "2" - }, - "engines": { - "node": "*" - } - }, "node_modules/sprintf-js": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", @@ -24135,14 +24066,6 @@ "node": ">= 0.4" } }, - "node_modules/stream-combiner": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", - "integrity": "sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==", - "dependencies": { - "duplexer": "~0.1.1" - } - }, "node_modules/stream-events": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", @@ -25157,7 +25080,8 @@ "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true }, "node_modules/thunky": { "version": "1.1.0", @@ -27327,89 +27251,6 @@ "node": ">= 10" } }, - "node_modules/zx": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/zx/-/zx-4.3.0.tgz", - "integrity": "sha512-KuEjpu5QFIMx0wWfzknDRhY98s7a3tWNRmYt19XNmB7AfOmz5zISA4+3Q8vlJc2qguxMn89uSxhPDCldPa3YLA==", - "dependencies": { - "@types/fs-extra": "^9.0.12", - "@types/minimist": "^1.2.2", - "@types/node": "^16.6", - "@types/node-fetch": "^2.5.12", - "chalk": "^4.1.2", - "fs-extra": "^10.0.0", - "globby": "^12.0.1", - "minimist": "^1.2.5", - "node-fetch": "^2.6.1", - "ps-tree": "^1.2.0", - "which": "^2.0.2" - }, - "bin": { - "zx": "zx.mjs" - }, - "engines": { - "node": ">= 14.13.1" - } - }, - "node_modules/zx/node_modules/@types/node": { - "version": "16.18.101", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.101.tgz", - "integrity": "sha512-AAsx9Rgz2IzG8KJ6tXd6ndNkVcu+GYB6U/SnFAaokSPNx2N7dcIIfnighYUNumvj6YS2q39Dejz5tT0NCV7CWA==" - }, - "node_modules/zx/node_modules/array-union": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-3.0.1.tgz", - "integrity": "sha512-1OvF9IbWwaeiM9VhzYXVQacMibxpXOMYVNIvMtKRyX9SImBXpKcFr8XvFDeEslCyuH/t6KRt7HEO94AlP8Iatw==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zx/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/zx/node_modules/globby": { - "version": "12.2.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-12.2.0.tgz", - "integrity": "sha512-wiSuFQLZ+urS9x2gGPl1H5drc5twabmm4m2gTR27XDFyjUHJUNsS8o/2aKyIF6IoBaR630atdher0XJ5g6OMmA==", - "dependencies": { - "array-union": "^3.0.1", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.7", - "ignore": "^5.1.9", - "merge2": "^1.4.1", - "slash": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zx/node_modules/slash": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", - "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "packages/server": { "name": "@bluebubbles/server", "version": "1.9.8", @@ -27459,8 +27300,7 @@ "typeorm": "^0.3.20", "uuid": "^9.0.1", "validatorjs": "^3.22.1", - "vcf": "^2.1.1", - "zx": "^4.3.0" + "vcf": "^2.1.1" }, "devDependencies": { "@babel/core": "^7.16.12", diff --git a/packages/server/package.json b/packages/server/package.json index 59a9a729..918a4ad9 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -125,7 +125,6 @@ "typeorm": "^0.3.20", "uuid": "^9.0.1", "validatorjs": "^3.22.1", - "vcf": "^2.1.1", - "zx": "^4.3.0" + "vcf": "^2.1.1" } } diff --git a/packages/server/src/server/api/http/index.ts b/packages/server/src/server/api/http/index.ts index a2223027..f28a2d90 100644 --- a/packages/server/src/server/api/http/index.ts +++ b/packages/server/src/server/api/http/index.ts @@ -10,7 +10,6 @@ import koaCors from "koa-cors"; import * as https from "https"; import * as http from "http"; import * as fs from "fs"; -import * as zx from "zx"; // Internal libraries import { Server } from "@server"; @@ -26,6 +25,7 @@ import { HELLO_WORLD } from "@server/events"; import { ScheduledService } from "../../lib/ScheduledService"; import { Loggable } from "../../lib/logging/Loggable"; import { ProxyServices } from "@server/databases/server/constants"; +import { ProcessSpawner } from "@server/lib/ProcessSpawner"; /** * This service class handles all routing for incoming socket @@ -152,9 +152,15 @@ export class HttpService extends Loggable { async checkIfPortInUse(port: number) { try { // Check if there are any listening services - zx.$.verbose = false; - const output = await zx.$`lsof -nP -iTCP -sTCP:LISTEN | grep ${port}`; - if (output.toString().includes(`:${port} (LISTEN)`)) return true; + const output = await ProcessSpawner.executeCommand('lsof', [ + '-nP', + '-iTCP', + '-sTCP:LISTEN', + '|', + 'grep', + `${port}` + ], {}, "PortChecker"); + if (output.includes(`:${port} (LISTEN)`)) return true; } catch { // Don't show an error, I believe this throws a "false error". // For instance, if the proxy service doesn't start, and the command returns diff --git a/packages/server/src/server/api/privateApi/modes/dylibPlugins/index.ts b/packages/server/src/server/api/privateApi/modes/dylibPlugins/index.ts index 1054a697..483367ea 100644 --- a/packages/server/src/server/api/privateApi/modes/dylibPlugins/index.ts +++ b/packages/server/src/server/api/privateApi/modes/dylibPlugins/index.ts @@ -1,11 +1,10 @@ import fs from "fs"; import { waitMs } from "@server/helpers/utils"; import { Server } from "@server"; -import * as zx from "zx"; -import { ProcessPromise, $ } from "zx"; import { FileSystem } from "@server/fileSystem"; import { hideApp } from "@server/api/apple/scripts"; import { Loggable } from "@server/lib/logging/Loggable"; +import { ProcessSpawner } from "@server/lib/ProcessSpawner"; export abstract class DylibPlugin extends Loggable { tag = "DylibPlugin"; @@ -20,8 +19,6 @@ export abstract class DylibPlugin extends Loggable { isInjecting = false; - dylibProcess: ProcessPromise<any>; - dylibFailureCounter = 0; dylibLastErrorTime = 0; @@ -93,9 +90,6 @@ export abstract class DylibPlugin extends Loggable { throw new Error(`Unable to locate ${this.name} parent process!`); } - // Don't log the output to stdout - zx.$.verbose = false; - while (this.dylibFailureCounter < 5) { try { // Stop the running Messages app @@ -109,14 +103,22 @@ export abstract class DylibPlugin extends Loggable { return; } - this.dylibProcess = zx.$`DYLD_INSERT_LIBRARIES=${this.dylibPath} ${this.parentProcessPath}`; - this.dylibProcess.stdout.on("data", (data: string) => { - this.log.debug(`DYLIB: ${data}`); - }); - - this.dylibProcess.stderr.on("data", (data: string) => { - this.log.debug(`DYLIB: ${data}`); - }); + const spawner = new ProcessSpawner({ + command: this.parentProcessPath, + args: [], + verbose: true, + logTag: this.parentApp, + storeOutput: false, + waitForExit: true, + restartOnNonZeroExit: false, + options: { + env: { + DYLD_INSERT_LIBRARIES: this.dylibPath + } + }, + }) + + const promise = spawner.execute(); // Hide the app after 5 seconds setTimeout(() => { @@ -128,9 +130,13 @@ export abstract class DylibPlugin extends Loggable { onSuccessfulStart(); }); - await this.dylibProcess; + await promise; + + // If it gets here, the dylib exited on its own (code: 0) + this.log.debug(`DYLIB exited on its own. Restarting...`); + this.dylibFailureCounter = 0; } catch (ex: any) { - this.log.debug(`Detected DYLIB crash for App ${this.parentApp}. Error: ${ex}`); + this.log.debug(`Detected DYLIB crash for App ${this.parentApp}. Error: ${ex?.message ?? String(ex)}`); if (this.isStopping) { this.isInjecting = false; return; diff --git a/packages/server/src/server/lib/ProcessSpawner.ts b/packages/server/src/server/lib/ProcessSpawner.ts new file mode 100644 index 00000000..870b8b53 --- /dev/null +++ b/packages/server/src/server/lib/ProcessSpawner.ts @@ -0,0 +1,291 @@ +import { ChildProcess, SpawnOptionsWithoutStdio, spawn } from "child_process"; +import { Loggable } from "./logging/Loggable"; + + +export type ProcessSpawnerConstructorArgs = { + command: string; + args?: string[]; + options?: SpawnOptionsWithoutStdio; + verbose?: boolean; + onOutput?: ((data: any) => void) | null; + onExit?: ((code: number) => void) | null; + logTag?: string | null; + restartOnNonZeroExit?: boolean; + storeOutput?: boolean; + waitForExit?: boolean; + errorOnStderr?: boolean; +}; + +export type ProcessSpawnerError = { + message: string; + output: string; + process: ChildProcess; + wasForceQuit: boolean; +}; + +type ProcessSpawnerOutput = { + type: "stdout" | "stderr"; + data: string; +}; + + +export class ProcessSpawner extends Loggable { + tag = "ProcessSpawner"; + + command: string; + + args: string[]; + + options: SpawnOptionsWithoutStdio; + + verbose: boolean; + + process: ChildProcess; + + restartOnNonZeroExit: boolean; + + storeOutput: boolean; + + waitForExit: boolean; + + errorOnStderr: boolean; + + onOutput: ((data: any) => void) | null; + + onExit: ((code: number) => void) | null; + + private _output: ProcessSpawnerOutput[] = []; + + get output() { + // Consecutive log outputs of the same type should be appended to the same line. + // If the type changes, we should add a newline. + let result = ""; + let prevType = null; + for (const item of this._output) { + if (prevType !== null && prevType != item.type) { + result += `\n${item.data}`; + prevType = item.type; + continue; + } + + result += item.data; + prevType = item.type; + } + + return result.trim(); + } + + get stdout() { + return this._output.filter(x => x.type === "stdout").map(x => x.data).join("").trim(); + } + + get stderr() { + return this._output.filter(x => x.type === "stderr").map(x => x.data).join("").trim(); + } + + constructor({ + command, + args = [], + options = {}, + verbose = false, + onOutput = null, + onExit = null, + logTag = null, + restartOnNonZeroExit = false, + storeOutput = true, + waitForExit = true, + errorOnStderr = false + }: ProcessSpawnerConstructorArgs) { + super(); + + this.command = command; + this.args = args; + this.options = options; + this.verbose = verbose; + this.onOutput = onOutput; + this.onExit = onExit; + this.restartOnNonZeroExit = restartOnNonZeroExit; + this.storeOutput = storeOutput; + this.waitForExit = waitForExit; + this.errorOnStderr = errorOnStderr; + + if (logTag) { + this.tag = logTag; + } + + if (this.restartOnNonZeroExit && this.waitForExit) { + throw new Error("Cannot use 'restartOnNonZeroExit' and 'waitForExit' together!"); + } + } + + async execute(): Promise<ProcessSpawner> { + return new Promise((resolve: (spawner: ProcessSpawner) => void, reject: (err: ProcessSpawnerError) => void) => { + try { + this.process = this.spawnProcesses(); + this.process.stdout.on("data", chunk => this.handleOutput(chunk, "stdout")); + this.process.stderr.on("data", chunk => { + this.handleOutput(chunk, "stderr"); + + if (this.errorOnStderr) { + reject({ + message: chunk.toString(), + output: this.output, + process: this.process, + wasForceQuit: false + }); + } + }); + this.process.on("exit", (code) => { + let msg = `Process was force quit`; + let wasForceQuit = true; + if (code != null) { + msg = `Process exited with code: ${code}`; + wasForceQuit = false; + } + + this.handleLog(msg); + this.handleExit(code); + + if (this.waitForExit) { + if (!this.restartOnNonZeroExit && code !== 0) { + reject({ + message: msg, + output: this.output, + process: this.process, + wasForceQuit + }); + } else { + resolve(this); + } + } + }); + + if (!this.waitForExit) { + resolve(this); + } + } catch (ex: any) { + reject({ + message: ex?.message ?? String(ex), + output: this.output, + process: this.process, + wasForceQuit: false + }); + } + }); + } + + private spawnProcesses() { + // If the args contain a pipe character, we need to split the command and args into separate processes. + // The separate processes should dynamically pipe the result into the next, returning the last process + // as the final result. + if (this.args.some(arg => arg.includes("|"))) { + // Combine the command and args into a single string + const commandStr = `${this.command} ${this.args.join(" ")}`; + + // Split by the pipe character, and trim any whitespace + const commands = commandStr.split("|").map(x => x.trim()); + if (commands.length < 2) { + throw new Error(`Invalid pipe command! Input: ${commandStr}`); + } + + // Iterate over the commands, executing them and piping the + // output to the next process. Then return the last process. + let lastProcess: ChildProcess = null; + for (let i = 0; i < commands.length; i++) { + const command = commands[i].trim(); + + // Get the command + if (!command) { + throw new Error(`Invalid command! Input: ${command}`); + } + + // Pull the first command off the list + const commandParts = command.split(" "); + const program = commandParts[0]; + const args = commandParts.slice(1); + + // Spawn the process and pipe the output to the next process + const proc = spawn(program, this.quoteArgs(args), { + stdio: [ + // If there is a previous process, pipe the output to the next process + (lastProcess) ? lastProcess.stdout : "pipe", + "pipe", + "pipe" + ] + }); + + lastProcess = proc; + } + + return lastProcess; + } + + return spawn(this.command, this.quoteArgs(this.args), this.options); + } + + private handleLog(log: string) { + if (this.verbose) { + this.log.debug(log); + } + } + + async handleOutput(chunk: any, type: "stdout" | "stderr") { + const chunkStr = chunk.toString(); + this.handleLog(`[${type}] ${chunkStr}`); + + if (this.storeOutput) { + this._output.push({ type, data: chunkStr }); + } + + if (this.onOutput) { + this.onOutput(chunkStr); + } + } + + async handleExit(code: number) { + if (this.onExit) { + this.onExit(code); + } + + if (code !== 0 && this.restartOnNonZeroExit) { + this.execute(); + } + } + + async kill() { + if (this.process) { + this.process.kill(); + } + } + + private quoteArgs(args: string[]): string[] { + return args.map(arg => { + if (arg.includes(" ")) { + return `"${arg.replace(/"/g, '\\"')}"`; + } else { + return arg; + } + }); + } + + static async executeCommand( + command: string, + args: string[] = [], + options: SpawnOptionsWithoutStdio = {}, + tag = 'CommandExecutor' + ): Promise<string> { + const spawner = new ProcessSpawner({ + command, + args, + logTag: tag, + options, + verbose: false, + restartOnNonZeroExit: false, + storeOutput: true, + waitForExit: true + }); + + await spawner.execute(); + return spawner.output; + } +} \ No newline at end of file diff --git a/packages/server/src/server/managers/cloudflareManager/index.ts b/packages/server/src/server/managers/cloudflareManager/index.ts index 4ce7b88b..13a89efb 100644 --- a/packages/server/src/server/managers/cloudflareManager/index.ts +++ b/packages/server/src/server/managers/cloudflareManager/index.ts @@ -3,7 +3,7 @@ import { FileSystem } from "@server/fileSystem"; import { Server } from "@server"; import { isEmpty, isNotEmpty } from "@server/helpers/utils"; import { Loggable } from "@server/lib/logging/Loggable"; -import { ChildProcess, spawn } from "child_process"; +import { ProcessSpawner } from "@server/lib/ProcessSpawner"; export class CloudflareManager extends Loggable { tag = "CloudflareManager"; @@ -16,7 +16,7 @@ export class CloudflareManager extends Loggable { pidPath = path.join(FileSystem.resources, "macos", "daemons", "cloudflare", "cloudflare.pid"); - proc: ChildProcess; + proc: ProcessSpawner; currentProxyUrl: string; @@ -37,21 +37,26 @@ export class CloudflareManager extends Loggable { } private async connectHandler(): Promise<string> { - return new Promise((resolve, reject) => { + return new Promise(async (resolve, reject) => { try { const port = Server().repo.getConfig("socket_port") as string; - this.proc = spawn(this.daemonPath, [ - 'tunnel', - '--url', `localhost:${port}`, - '--config', this.cfgPath, - '--pidfile', this.pidPath - ]); - - // Configure handlers for all the output events - this.proc.stdout.on("data", chunk => this.handleData(chunk)); - this.proc.stdout.on("error", chunk => this.handleError(chunk)); - this.proc.stderr.on("data", chunk => this.handleData(chunk)); - this.proc.stderr.on("error", chunk => this.handleError(chunk)); + + this.log.debug("Starting Cloudflare Tunnel..."); + this.proc = new ProcessSpawner({ + command: this.daemonPath, + args: [ + 'tunnel', + '--url', `localhost:${port}`, + '--config', this.cfgPath, + '--pidfile', this.pidPath + ], + verbose: true, + logTag: "CloudflareDaemon", + onOutput: (data) => this.handleData(data), + restartOnNonZeroExit: true, + waitForExit: false, + storeOutput: false + }); this.on("new-url", url => resolve(url)); this.on("error", err => { @@ -66,6 +71,8 @@ export class CloudflareManager extends Loggable { setTimeout(() => { reject(new Error("Failed to connect to Cloudflare after 2 minutes...")); }, 1000 * 60 * 2); // 2 minutes + + await this.proc.execute(); } catch (ex) { reject(ex); } @@ -102,6 +109,10 @@ export class CloudflareManager extends Loggable { private detectError(data: string): string | null { if (data.includes('no such host')) { return 'Unable to resolve api.trycloudflare.com! Ensure that your Mac has internet access and that any networking tools you use are not blocking the hostname.'; + } else if (data.includes("context deadline exceeded")) { + return "Failed to connect to Cloudflare's servers! Connection timed out. Please check your internet connection and try again."; + } else if (data.includes("connect: bad file descriptor")) { + return "Failed to connect to Cloudflare's servers! Please make sure your Mac is up to date"; } else if (data.includes('failed to request quick Tunnel: ')) { return data.split('failed to request quick Tunnel: ')[1]; } @@ -139,11 +150,6 @@ export class CloudflareManager extends Loggable { } } - handleError(chunk: any) { - const error = this.detectError(chunk); - if (error) this.emitError(error); - } - emitError(err: any) { this.emit("error", err); } diff --git a/packages/server/src/server/managers/zrokManager/index.ts b/packages/server/src/server/managers/zrokManager/index.ts index 10dd4e92..3ed36090 100644 --- a/packages/server/src/server/managers/zrokManager/index.ts +++ b/packages/server/src/server/managers/zrokManager/index.ts @@ -1,13 +1,12 @@ import { isNotEmpty, isEmpty } from "@server/helpers/utils"; import { Loggable, getLogger } from "@server/lib/logging/Loggable"; import { FileSystem } from "@server/fileSystem"; -import * as zx from "zx"; import axios from "axios"; import { app } from "electron"; import { Server } from "@server"; -import { ProcessOutput } from "zx"; -import { spawn, ChildProcessWithoutNullStreams } from "child_process"; +import { spawn, ChildProcess } from "child_process"; import path from "path"; +import { ProcessSpawner, ProcessSpawnerError } from "@server/lib/ProcessSpawner"; export class ZrokManager extends Loggable { tag = "ZrokManager"; @@ -16,7 +15,7 @@ export class ZrokManager extends Loggable { return path.join(FileSystem.resources, "macos", "daemons", "zrok", (process.arch === "arm64") ? "arm64" : "x86", "zrok"); } - proc: ChildProcessWithoutNullStreams; + proc: ChildProcess; currentProxyUrl: string; @@ -140,24 +139,18 @@ export class ZrokManager extends Loggable { } } - static async setToken(token: string): Promise<ProcessOutput> { + static async setToken(token: string): Promise<string> { try { - return await zx.$`${this.daemonPath} enable ${token}`; - } catch (ex: any | ProcessOutput) { - if (ex.stderr) { - if (ex.stderr.includes("you already have an enabled environment")) { - return ex; - } - - throw new Error(ex.stderr.trim()); - } - - const err = ex.stdout; - if (err.includes("enableUnauthorized")) { + return await ProcessSpawner.executeCommand(this.daemonPath, ["enable", token], {}, "ZrokManager"); + } catch (ex: any | ProcessSpawnerError) { + const output = ex?.output ?? ex?.message ?? String(ex); + if (output.includes("you already have an enabled environment")) { + return output; + } else if (output.includes("enableUnauthorized")) { throw new Error("Invalid Zrok token!"); } else { const logger = getLogger("ZrokManager"); - logger.error(`Failed to set Zrok token! Error: ${err}`); + logger.error(`Failed to set Zrok token! Error: ${output}`); throw new Error("Failed to set Zrok token! Please check your server logs for more information."); } } @@ -194,8 +187,7 @@ export class ZrokManager extends Loggable { flags.push(name); } - const result = await zx.$`${this.daemonPath} reserve public ${flags}`; - const output = result.toString().trim(); + const output = await ProcessSpawner.executeCommand(this.daemonPath, ["reserve", "public", ...flags], {}, "ZrokManager"); const urlMatches = output.match(ZrokManager.proxyUrlRegex); if (isEmpty(urlMatches)) { logger.debug(`Failed to reserve Zrok tunnel! Unable to find URL in output. Output: ${output}`); @@ -217,34 +209,25 @@ export class ZrokManager extends Loggable { await Server().repo.setConfig("zrok_reserved_token", token); return token; - } catch (ex: any | ProcessOutput) { - if (ex.stderr) { - throw new Error(ex.stderr.trim()); - } - - const err = ex.stdout; - if (err) { - const logger = getLogger("ZrokManager"); - if (err.includes("shareInternalServerError")) { - throw new Error("Failed to reserve Zrok share! Internal server error! Share may be in use."); - } else { - logger.error(`Failed to set Zrok token! Error: ${err}`); - throw new Error("Failed to set Zrok token! Please check your server logs for more information."); - } + } catch (ex: any | ProcessSpawnerError) { + const output = ex?.output ?? ex?.message ?? String(ex); + const logger = getLogger("ZrokManager"); + if (output.includes("shareInternalServerError")) { + throw new Error("Failed to reserve Zrok share! Internal server error! Share may be in use."); + } else { + logger.error(`Failed to set Zrok token! Error: ${output}`); + throw new Error("Failed to set Zrok token! Please check your server logs for more information."); } - - throw ex; } } - static async disable(): Promise<ProcessOutput> { - return await zx.$`${this.daemonPath} disable`; + static async disable(): Promise<string> { + return await ProcessSpawner.executeCommand(this.daemonPath, ["disable"], {}, "ZrokManager"); } static async getExistingReservedShareToken(name?: string): Promise<string | null> { // Run the overview command and parse the output - const result = await zx.$`${this.daemonPath} overview`; - const output = result.toString().trim(); + const output = await ProcessSpawner.executeCommand(this.daemonPath, ["overview"], {}, "ZrokManager"); const json = JSON.parse(output); const host = Server().computerIdentifier; @@ -268,25 +251,21 @@ export class ZrokManager extends Loggable { return reserved?.token ?? null; } - static async release(token: string): Promise<ProcessOutput> { + static async release(token: string): Promise<string> { try { - const result = await zx.$`${this.daemonPath} release ${token}`; + const result = await ProcessSpawner.executeCommand(this.daemonPath, ["release", token], {}, "ZrokManager"); // Clear the token from the config await Server().repo.setConfig("zrok_reserved_token", ""); return result; - } catch (ex: any | ProcessOutput) { - if (ex.stderr) { - if (ex.stderr.includes("unshareNotFound")) { - return ex; - } - - throw new Error(ex.stderr.trim()); + } catch (ex: any | ProcessSpawnerError) { + const output = ex?.output ?? ex?.message ?? String(ex); + const logger = getLogger("ZrokManager"); + if (output.includes("unshareNotFound")) { + return output; } - const err = ex.stdout ?? "Unknown"; - const logger = getLogger("ZrokManager"); - logger.error(`Failed to release Zrok share! Error: ${err}`); + logger.error(`Failed to release Zrok share! Error: ${output}`); throw new Error("Failed to release Zrok share! Please check your server logs for more information."); } } From 1720a2050f903987c31736c6b809941e6fda0568 Mon Sep 17 00:00:00 2001 From: zlshames <zlshames@gmail.com> Date: Tue, 9 Jul 2024 11:21:38 -0400 Subject: [PATCH 11/11] fix: process spawner escaping --- packages/server/src/server/lib/ProcessSpawner.ts | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/packages/server/src/server/lib/ProcessSpawner.ts b/packages/server/src/server/lib/ProcessSpawner.ts index 870b8b53..dc5e8dd2 100644 --- a/packages/server/src/server/lib/ProcessSpawner.ts +++ b/packages/server/src/server/lib/ProcessSpawner.ts @@ -205,7 +205,7 @@ export class ProcessSpawner extends Loggable { const args = commandParts.slice(1); // Spawn the process and pipe the output to the next process - const proc = spawn(program, this.quoteArgs(args), { + const proc = spawn(program, args, { stdio: [ // If there is a previous process, pipe the output to the next process (lastProcess) ? lastProcess.stdout : "pipe", @@ -220,7 +220,7 @@ export class ProcessSpawner extends Loggable { return lastProcess; } - return spawn(this.command, this.quoteArgs(this.args), this.options); + return spawn(this.command, this.args, this.options); } private handleLog(log: string) { @@ -258,16 +258,6 @@ export class ProcessSpawner extends Loggable { } } - private quoteArgs(args: string[]): string[] { - return args.map(arg => { - if (arg.includes(" ")) { - return `"${arg.replace(/"/g, '\\"')}"`; - } else { - return arg; - } - }); - } - static async executeCommand( command: string, args: string[] = [],