From 04bc7f7055b6fbe0377970dd0b423ae5b1d2f250 Mon Sep 17 00:00:00 2001 From: Ryan Ward Date: Sun, 16 Aug 2026 00:57:08 -0500 Subject: [PATCH] fix: stop Galaxy before replacing app --- .../src/index.tsx | 49 ++++++++++++++----- 1 file changed, 37 insertions(+), 12 deletions(-) diff --git a/script/build-and-install-to-applications/src/index.tsx b/script/build-and-install-to-applications/src/index.tsx index defca002..de06abdd 100644 --- a/script/build-and-install-to-applications/src/index.tsx +++ b/script/build-and-install-to-applications/src/index.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect, useCallback } from "react"; import { render, Text, Box } from "ink"; import Spinner from "ink-spinner"; -import { spawn, execSync } from "child_process"; +import { spawn, execFileSync, execSync } from "child_process"; import path from "path"; import fs from "fs"; @@ -59,15 +59,48 @@ function runCommandStreaming( }); } -function isAppRunning(appName: string): boolean { +function isAppRunning(processName: string): boolean { try { - execSync(`pgrep -x "${appName}"`, { stdio: "ignore" }); + execFileSync("pgrep", ["-x", processName], { stdio: "ignore" }); return true; } catch { return false; } } +async function waitForAppToExit(processName: string, timeoutMs: number): Promise { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + if (!isAppRunning(processName)) return true; + await new Promise((resolve) => setTimeout(resolve, 100)); + } + return !isAppRunning(processName); +} + +async function stopApp(processName: string, onLog: (line: string) => void): Promise { + if (!isAppRunning(processName)) return; + + onLog(`${processName} is running. Stopping it before replacing the app...`); + try { + execFileSync("pkill", ["-TERM", "-x", processName], { stdio: "ignore" }); + } catch { + // The process may exit between the running check and the signal. + } + + if (await waitForAppToExit(processName, 2000)) return; + + onLog(`${processName} did not exit after SIGTERM. Force killing it...`); + try { + execFileSync("pkill", ["-KILL", "-x", processName], { stdio: "ignore" }); + } catch { + // Verify the process state below instead of relying on pkill's exit status. + } + + if (!(await waitForAppToExit(processName, 1000))) { + throw new Error(`Unable to stop ${processName} before updating /Applications`); + } +} + // ─── UI Component ──────────────────────────────────────────────────────────── function StepLine({ step }: { step: Step }) { @@ -158,15 +191,7 @@ function App() { throw new Error(`Built app not found at ${appPath}`); } - if (isAppRunning("Galaxy")) { - appendLog(1, "Galaxy is running. Quitting it before replacing..."); - try { - execSync(`osascript -e 'quit app "Galaxy"'`, { stdio: "ignore" }); - await new Promise((r) => setTimeout(r, 2000)); - } catch { - // ignore - } - } + await stopApp("Galaxy", (line) => appendLog(1, line)); if (fs.existsSync(destPath)) { appendLog(1, `Removing existing ${destPath}`);