Skip to content

Commit 522c98c

Browse files
authored
fix: crashing dmg (#604)
1 parent c6a094c commit 522c98c

6 files changed

Lines changed: 124 additions & 99 deletions

File tree

.changeset/yummy-pandas-stand.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"kiji-privacy-proxy": patch
3+
---
4+
5+
build fix

Makefile

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
.PHONY: format lint lint-go lint-frontend lint-frontend-fix lint-all typecheck typecheck-frontend check check-all ruff-fix ruff-all
33
.PHONY: test test-go test-all test-e2e test-e2e-dataset
44
.PHONY: clean clean-venv clean-all
5-
.PHONY: build-dmg build-linux verify-linux
5+
.PHONY: build-dmg build-linux verify-linux build-go build-go-embed
66
.PHONY: setup-onnx setup-tokenizers
77
.PHONY: electron-build electron-run electron electron-dev electron-install
88
.PHONY: list show shell jupyter info quickstart
@@ -274,6 +274,32 @@ build-go: ## Build Go binary for development
274274
./src/backend
275275
@echo "$(GREEN)✅ Go binary built at build/kiji-proxy (version $(VERSION))$(NC)"
276276

277+
build-go-embed: ## Build PRODUCTION Go binary with embedded UI + model (-tags embed)
278+
@echo "$(BLUE)Building production Go binary (embedded UI + model)...$(NC)"
279+
@echo "$(BLUE) → Building frontend (webpack production)...$(NC)"
280+
@cd src/frontend && npm run build:electron
281+
@if [ ! -d "src/frontend/dist" ]; then \
282+
echo "$(YELLOW)❌ src/frontend/dist not found after build — aborting$(NC)"; \
283+
exit 1; \
284+
fi
285+
@echo "$(BLUE) → Staging embed assets into src/backend/ (go:embed can't use ../ paths)...$(NC)"
286+
@rm -rf src/backend/frontend/dist && mkdir -p src/backend/frontend/dist
287+
@cp -R src/frontend/dist/. src/backend/frontend/dist/
288+
@rm -rf src/backend/model/quantized && mkdir -p src/backend/model/quantized
289+
@cp -R model/quantized/. src/backend/model/quantized/
290+
@echo "$(BLUE) → Compiling with -tags embed...$(NC)"
291+
@mkdir -p build src/frontend/resources
292+
@CGO_ENABLED=1 \
293+
CGO_LDFLAGS="$(CGO_LDFLAGS)" \
294+
go build \
295+
-tags embed \
296+
-ldflags="-s -w -X main.version=$(VERSION) -extldflags '$(CGO_LDFLAGS)'" \
297+
-o build/kiji-proxy \
298+
./src/backend
299+
@cp build/kiji-proxy src/frontend/resources/kiji-proxy
300+
@chmod +x src/frontend/resources/kiji-proxy
301+
@echo "$(GREEN)✅ Embedded Go binary built (version $(VERSION)) and copied to src/frontend/resources/$(NC)"
302+
277303
electron-build: ## Build Electron app for production
278304
@echo "$(BLUE)Building Electron app...$(NC)"
279305
@cd src/frontend && npm run build:electron

src/frontend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"start": "node server.js",
1414
"electron": "npm run build:electron && electron .",
1515
"electron:dev": "NODE_ENV=development electron .",
16-
"electron:pack": "npm run build:electron && electron-builder",
16+
"electron:pack": "(cd ../.. && make build-go-embed) && electron-builder",
1717
"lint": "eslint .",
1818
"lint:fix": "eslint . --fix",
1919
"type-check": "tsc --noEmit",

src/frontend/src/electron/electron-main.js

Lines changed: 57 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,23 @@ const { registerIpcHandlers } = require("./ipc-handlers");
1414
const { setMenuLanguage, mt } = require("./menu-i18n");
1515
const isDev = process.env.NODE_ENV === "development";
1616

17+
// Global safety net: log (and, if telemetry is on, report) any promise
18+
// rejection or exception that escaped a local handler instead of letting Node
19+
// crash the process. Registered before any async startup work runs. Notably
20+
// this catches rejections from the auto-updater (Squirrel.Mac) code paths.
21+
process.on("unhandledRejection", (reason) => {
22+
console.error("[Main] Unhandled promise rejection:", reason);
23+
if (typeof Sentry !== "undefined" && isTelemetryEnabled()) {
24+
Sentry.captureException(reason);
25+
}
26+
});
27+
process.on("uncaughtException", (error) => {
28+
console.error("[Main] Uncaught exception:", error);
29+
if (typeof Sentry !== "undefined" && isTelemetryEnabled()) {
30+
Sentry.captureException(error);
31+
}
32+
});
33+
1734
// Telemetry (Sentry error reporting) is OPT-IN. It is only initialized when the
1835
// user turned on "Crash & error reporting" in Settings (persisted as
1936
// `telemetryEnabled` in the Electron config). See initTelemetryMain().
@@ -79,6 +96,16 @@ autoUpdater.on("error", (err) => {
7996
console.error("[AutoUpdater] Error:", err);
8097
});
8198

99+
// Trigger the installer relaunch. This is the Squirrel.Mac crash site, so guard
100+
// it: a synchronous throw here must not take down the click handler / process.
101+
const quitAndInstall = () => {
102+
try {
103+
autoUpdater.quitAndInstall();
104+
} catch (err) {
105+
console.error("[AutoUpdater] quitAndInstall failed:", err);
106+
}
107+
};
108+
82109
let mainWindow;
83110
let splashWindow = null;
84111
let goProcess = null;
@@ -756,7 +783,7 @@ function updateTrayMenu() {
756783
? [
757784
{
758785
label: mt("restartToUpdate"),
759-
click: () => autoUpdater.quitAndInstall(),
786+
click: () => quitAndInstall(),
760787
},
761788
]
762789
: []),
@@ -1033,7 +1060,7 @@ function createMenu() {
10331060
? [
10341061
{
10351062
label: mt("restartToUpdate"),
1036-
click: () => autoUpdater.quitAndInstall(),
1063+
click: () => quitAndInstall(),
10371064
},
10381065
]
10391066
: []),
@@ -1087,28 +1114,41 @@ app.whenReady().then(async () => {
10871114
createWindow();
10881115

10891116
// Check for updates after launch
1090-
autoUpdater.checkForUpdatesAndNotify();
1117+
autoUpdater.checkForUpdatesAndNotify().catch((err) => {
1118+
console.error("[AutoUpdater] checkForUpdatesAndNotify failed:", err);
1119+
});
10911120

10921121
// Re-check for updates every hour for long-running sessions
1093-
setInterval(() => autoUpdater.checkForUpdates(), 60 * 60 * 1000);
1122+
setInterval(() => {
1123+
autoUpdater.checkForUpdates().catch((err) => {
1124+
console.error("[AutoUpdater] periodic checkForUpdates failed:", err);
1125+
});
1126+
}, 60 * 60 * 1000);
10941127

10951128
app.on("activate", async () => {
1096-
// On macOS, re-create a window when the dock icon is clicked
1097-
if (BrowserWindow.getAllWindows().length === 0) {
1098-
// Ensure backend is running
1099-
if (!goProcess) {
1100-
launchGoBinary();
1101-
await waitForBackend();
1102-
} else {
1103-
// Process exists but might not be listening yet
1104-
await waitForBackend(10, 500);
1129+
// On macOS, re-create a window when the dock icon is clicked. Electron
1130+
// discards the promise returned by this async listener, so guard it here.
1131+
try {
1132+
if (BrowserWindow.getAllWindows().length === 0) {
1133+
// Ensure backend is running
1134+
if (!goProcess) {
1135+
launchGoBinary();
1136+
await waitForBackend();
1137+
} else {
1138+
// Process exists but might not be listening yet
1139+
await waitForBackend(10, 500);
1140+
}
1141+
createWindow();
1142+
} else if (mainWindow) {
1143+
// If window exists but is hidden, show it
1144+
showMainWindow();
11051145
}
1106-
createWindow();
1107-
} else if (mainWindow) {
1108-
// If window exists but is hidden, show it
1109-
showMainWindow();
1146+
} catch (err) {
1147+
console.error("[Main] activate handler failed:", err);
11101148
}
11111149
});
1150+
}).catch((err) => {
1151+
console.error("[Main] Startup (whenReady) failed:", err);
11121152
});
11131153

11141154
// Keep app running in menu bar even when all windows are closed

src/frontend/src/electron/ipc-handlers.js

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -453,18 +453,23 @@ const registerIpcHandlers = ({
453453
// ---- Model directory management ----
454454

455455
ipcMain.handle("select-model-directory", async () => {
456-
const { dialog } = require("electron");
457-
const result = await dialog.showOpenDialog(getMainWindow(), {
458-
properties: ["openDirectory"],
459-
title: "Select Model Directory",
460-
message: "Choose the directory containing your PII model files",
461-
});
456+
try {
457+
const { dialog } = require("electron");
458+
const result = await dialog.showOpenDialog(getMainWindow(), {
459+
properties: ["openDirectory"],
460+
title: "Select Model Directory",
461+
message: "Choose the directory containing your PII model files",
462+
});
463+
464+
if (result.canceled || result.filePaths.length === 0) {
465+
return null;
466+
}
462467

463-
if (result.canceled || result.filePaths.length === 0) {
468+
return result.filePaths[0];
469+
} catch (error) {
470+
console.error("[IPC] select-model-directory failed:", error);
464471
return null;
465472
}
466-
467-
return result.filePaths[0];
468473
});
469474

470475
defineConfigField(

src/scripts/build_dmg.sh

Lines changed: 20 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -154,18 +154,12 @@ else
154154
fi
155155

156156
echo ""
157-
echo "📦 Step 5: Building Electron app..."
158-
echo "-----------------------------------"
159-
160-
npm run build:electron
161-
162-
if [ $? -ne 0 ]; then
163-
echo "❌ Electron app build failed!"
164-
exit 1
165-
fi
166-
167-
echo "✅ Electron app built successfully"
168-
157+
echo "📦 Step 5: Returning to project root..."
158+
echo "---------------------------------------"
159+
# NOTE: the frontend (webpack) build now happens inside `make build-go-embed`
160+
# (Step 7-8 below) so the exact same command produces the UI that gets embedded
161+
# into the Go binary. This keeps the embed build and `npm run electron:pack`
162+
# from drifting. A second frontend build for the asar still runs in Step 10.
169163
cd "$PROJECT_ROOT"
170164

171165
echo ""
@@ -227,71 +221,26 @@ for file in model.onnx.data tokenizer.json vocab.txt model_manifest.json; do
227221
done
228222

229223
echo ""
230-
echo "📦 Step 7: Preparing files for Go embedding..."
231-
echo "----------------------------------------------"
232-
233-
# Copy frontend/dist files to src/backend/frontend/dist/ for embedding
234-
# Go embed cannot use ../ paths, so we need the files under src/backend/
235-
if [ -d "src/frontend/dist" ]; then
236-
mkdir -p src/backend/frontend/dist
237-
# Use rsync for faster copying (handles incremental updates)
238-
if command -v rsync >/dev/null 2>&1; then
239-
rsync -a --delete src/frontend/dist/ src/backend/frontend/dist/
240-
echo "✅ Frontend files synced to src/backend/frontend/dist/ for embedding (rsync)"
241-
else
242-
cp -r src/frontend/dist/* src/backend/frontend/dist/
243-
echo "✅ Frontend files copied to src/backend/frontend/dist/ for embedding"
244-
fi
245-
else
246-
echo "❌ Frontend dist directory not found: src/frontend/dist"
247-
exit 1
248-
fi
249-
250-
# Copy model files to src/backend/model/quantized/ for embedding
251-
if [ -d "model/quantized" ]; then
252-
mkdir -p src/backend/model/quantized
253-
# Use rsync for faster copying if available
254-
if command -v rsync >/dev/null 2>&1; then
255-
rsync -a --delete model/quantized/ src/backend/model/quantized/
256-
else
257-
cp -r model/quantized/* src/backend/model/quantized/
258-
fi
259-
260-
# Verify model files after copying
261-
COPIED_MODEL_SIZE=$(stat -f%z "src/backend/model/quantized/model.onnx" 2>/dev/null || stat -c%s "src/backend/model/quantized/model.onnx" 2>/dev/null || wc -c < "src/backend/model/quantized/model.onnx" 2>/dev/null || echo "0")
262-
echo "✅ Model files copied to src/backend/model/quantized/ for embedding (${COPIED_MODEL_SIZE} bytes)"
263-
else
264-
echo "❌ Model directory not found: model/quantized"
265-
echo " This will cause runtime errors - the app needs the model files"
266-
exit 1
267-
fi
268-
269-
echo ""
270-
echo "📦 Step 8: Building Go binary..."
271-
echo "--------------------------------"
272-
273-
# Extract version from package.json
274-
VERSION=$(cd src/frontend && node -p "require('./package.json').version" 2>/dev/null || echo "0.0.0")
275-
echo "Building version: $VERSION"
276-
277-
# Build the Go binary with embedded files and version injection
278-
mkdir -p build
224+
echo "📦 Step 7-8: Building embedded Go binary (UI + model)..."
225+
echo "--------------------------------------------------------"
279226

280-
# Use parallel compilation with version injection
281-
CGO_ENABLED=1 \
282-
GOMAXPROCS=$PARALLEL_JOBS \
283-
go build \
284-
-tags embed \
285-
-ldflags="-s -w -X main.version=$VERSION -extldflags '-L./build/tokenizers'" \
286-
-o build/kiji-proxy \
287-
./src/backend
227+
# Single source of truth for the embedded backend binary: `make build-go-embed`
228+
# builds the frontend, stages src/frontend/dist + model/quantized into
229+
# src/backend/ for //go:embed (Go embed can't use ../ paths), compiles with
230+
# `-tags embed`, and copies the binary into src/frontend/resources/. The same
231+
# target backs `npm run electron:pack`, so the DMG/CI build and local packing
232+
# can never diverge on the embed step (the root cause of the white-screen bug).
233+
#
234+
# NOTE: model LFS verification (Step 6 above) must run before this so the staged
235+
# model files are the real binaries, not LFS pointers.
236+
make build-go-embed
288237

289238
if [ $? -ne 0 ]; then
290-
echo "❌ Go binary build failed!"
239+
echo "Embedded Go binary build failed!"
291240
exit 1
292241
fi
293242

294-
echo "✅ Go binary created: build/kiji-proxy"
243+
echo "✅ Go binary created: build/kiji-proxy (embedded UI + model)"
295244

296245
echo ""
297246
echo "📦 Step 9: Preparing Electron resources..."

0 commit comments

Comments
 (0)