diff --git a/.changeset/yummy-pandas-stand.md b/.changeset/yummy-pandas-stand.md new file mode 100644 index 00000000..25909bb0 --- /dev/null +++ b/.changeset/yummy-pandas-stand.md @@ -0,0 +1,5 @@ +--- +"kiji-privacy-proxy": patch +--- + +build fix diff --git a/Makefile b/Makefile index e9cab08c..c6b10f66 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ .PHONY: format lint lint-go lint-frontend lint-frontend-fix lint-all typecheck typecheck-frontend check check-all ruff-fix ruff-all .PHONY: test test-go test-all test-e2e test-e2e-dataset .PHONY: clean clean-venv clean-all -.PHONY: build-dmg build-linux verify-linux +.PHONY: build-dmg build-linux verify-linux build-go build-go-embed .PHONY: setup-onnx setup-tokenizers .PHONY: electron-build electron-run electron electron-dev electron-install .PHONY: list show shell jupyter info quickstart @@ -274,6 +274,32 @@ build-go: ## Build Go binary for development ./src/backend @echo "$(GREEN)✅ Go binary built at build/kiji-proxy (version $(VERSION))$(NC)" +build-go-embed: ## Build PRODUCTION Go binary with embedded UI + model (-tags embed) + @echo "$(BLUE)Building production Go binary (embedded UI + model)...$(NC)" + @echo "$(BLUE) → Building frontend (webpack production)...$(NC)" + @cd src/frontend && npm run build:electron + @if [ ! -d "src/frontend/dist" ]; then \ + echo "$(YELLOW)❌ src/frontend/dist not found after build — aborting$(NC)"; \ + exit 1; \ + fi + @echo "$(BLUE) → Staging embed assets into src/backend/ (go:embed can't use ../ paths)...$(NC)" + @rm -rf src/backend/frontend/dist && mkdir -p src/backend/frontend/dist + @cp -R src/frontend/dist/. src/backend/frontend/dist/ + @rm -rf src/backend/model/quantized && mkdir -p src/backend/model/quantized + @cp -R model/quantized/. src/backend/model/quantized/ + @echo "$(BLUE) → Compiling with -tags embed...$(NC)" + @mkdir -p build src/frontend/resources + @CGO_ENABLED=1 \ + CGO_LDFLAGS="$(CGO_LDFLAGS)" \ + go build \ + -tags embed \ + -ldflags="-s -w -X main.version=$(VERSION) -extldflags '$(CGO_LDFLAGS)'" \ + -o build/kiji-proxy \ + ./src/backend + @cp build/kiji-proxy src/frontend/resources/kiji-proxy + @chmod +x src/frontend/resources/kiji-proxy + @echo "$(GREEN)✅ Embedded Go binary built (version $(VERSION)) and copied to src/frontend/resources/$(NC)" + electron-build: ## Build Electron app for production @echo "$(BLUE)Building Electron app...$(NC)" @cd src/frontend && npm run build:electron diff --git a/src/frontend/package.json b/src/frontend/package.json index 9de94d4f..49aeb14e 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -13,7 +13,7 @@ "start": "node server.js", "electron": "npm run build:electron && electron .", "electron:dev": "NODE_ENV=development electron .", - "electron:pack": "npm run build:electron && electron-builder", + "electron:pack": "(cd ../.. && make build-go-embed) && electron-builder", "lint": "eslint .", "lint:fix": "eslint . --fix", "type-check": "tsc --noEmit", diff --git a/src/frontend/src/electron/electron-main.js b/src/frontend/src/electron/electron-main.js index 9e1b3e6d..1d383712 100644 --- a/src/frontend/src/electron/electron-main.js +++ b/src/frontend/src/electron/electron-main.js @@ -14,6 +14,23 @@ const { registerIpcHandlers } = require("./ipc-handlers"); const { setMenuLanguage, mt } = require("./menu-i18n"); const isDev = process.env.NODE_ENV === "development"; +// Global safety net: log (and, if telemetry is on, report) any promise +// rejection or exception that escaped a local handler instead of letting Node +// crash the process. Registered before any async startup work runs. Notably +// this catches rejections from the auto-updater (Squirrel.Mac) code paths. +process.on("unhandledRejection", (reason) => { + console.error("[Main] Unhandled promise rejection:", reason); + if (typeof Sentry !== "undefined" && isTelemetryEnabled()) { + Sentry.captureException(reason); + } +}); +process.on("uncaughtException", (error) => { + console.error("[Main] Uncaught exception:", error); + if (typeof Sentry !== "undefined" && isTelemetryEnabled()) { + Sentry.captureException(error); + } +}); + // Telemetry (Sentry error reporting) is OPT-IN. It is only initialized when the // user turned on "Crash & error reporting" in Settings (persisted as // `telemetryEnabled` in the Electron config). See initTelemetryMain(). @@ -79,6 +96,16 @@ autoUpdater.on("error", (err) => { console.error("[AutoUpdater] Error:", err); }); +// Trigger the installer relaunch. This is the Squirrel.Mac crash site, so guard +// it: a synchronous throw here must not take down the click handler / process. +const quitAndInstall = () => { + try { + autoUpdater.quitAndInstall(); + } catch (err) { + console.error("[AutoUpdater] quitAndInstall failed:", err); + } +}; + let mainWindow; let splashWindow = null; let goProcess = null; @@ -756,7 +783,7 @@ function updateTrayMenu() { ? [ { label: mt("restartToUpdate"), - click: () => autoUpdater.quitAndInstall(), + click: () => quitAndInstall(), }, ] : []), @@ -1033,7 +1060,7 @@ function createMenu() { ? [ { label: mt("restartToUpdate"), - click: () => autoUpdater.quitAndInstall(), + click: () => quitAndInstall(), }, ] : []), @@ -1087,28 +1114,41 @@ app.whenReady().then(async () => { createWindow(); // Check for updates after launch - autoUpdater.checkForUpdatesAndNotify(); + autoUpdater.checkForUpdatesAndNotify().catch((err) => { + console.error("[AutoUpdater] checkForUpdatesAndNotify failed:", err); + }); // Re-check for updates every hour for long-running sessions - setInterval(() => autoUpdater.checkForUpdates(), 60 * 60 * 1000); + setInterval(() => { + autoUpdater.checkForUpdates().catch((err) => { + console.error("[AutoUpdater] periodic checkForUpdates failed:", err); + }); + }, 60 * 60 * 1000); app.on("activate", async () => { - // On macOS, re-create a window when the dock icon is clicked - if (BrowserWindow.getAllWindows().length === 0) { - // Ensure backend is running - if (!goProcess) { - launchGoBinary(); - await waitForBackend(); - } else { - // Process exists but might not be listening yet - await waitForBackend(10, 500); + // On macOS, re-create a window when the dock icon is clicked. Electron + // discards the promise returned by this async listener, so guard it here. + try { + if (BrowserWindow.getAllWindows().length === 0) { + // Ensure backend is running + if (!goProcess) { + launchGoBinary(); + await waitForBackend(); + } else { + // Process exists but might not be listening yet + await waitForBackend(10, 500); + } + createWindow(); + } else if (mainWindow) { + // If window exists but is hidden, show it + showMainWindow(); } - createWindow(); - } else if (mainWindow) { - // If window exists but is hidden, show it - showMainWindow(); + } catch (err) { + console.error("[Main] activate handler failed:", err); } }); +}).catch((err) => { + console.error("[Main] Startup (whenReady) failed:", err); }); // Keep app running in menu bar even when all windows are closed diff --git a/src/frontend/src/electron/ipc-handlers.js b/src/frontend/src/electron/ipc-handlers.js index 52098eec..e1520a9c 100644 --- a/src/frontend/src/electron/ipc-handlers.js +++ b/src/frontend/src/electron/ipc-handlers.js @@ -453,18 +453,23 @@ const registerIpcHandlers = ({ // ---- Model directory management ---- ipcMain.handle("select-model-directory", async () => { - const { dialog } = require("electron"); - const result = await dialog.showOpenDialog(getMainWindow(), { - properties: ["openDirectory"], - title: "Select Model Directory", - message: "Choose the directory containing your PII model files", - }); + try { + const { dialog } = require("electron"); + const result = await dialog.showOpenDialog(getMainWindow(), { + properties: ["openDirectory"], + title: "Select Model Directory", + message: "Choose the directory containing your PII model files", + }); + + if (result.canceled || result.filePaths.length === 0) { + return null; + } - if (result.canceled || result.filePaths.length === 0) { + return result.filePaths[0]; + } catch (error) { + console.error("[IPC] select-model-directory failed:", error); return null; } - - return result.filePaths[0]; }); defineConfigField( diff --git a/src/scripts/build_dmg.sh b/src/scripts/build_dmg.sh index ce5eada1..77727de9 100755 --- a/src/scripts/build_dmg.sh +++ b/src/scripts/build_dmg.sh @@ -154,18 +154,12 @@ else fi echo "" -echo "📦 Step 5: Building Electron app..." -echo "-----------------------------------" - -npm run build:electron - -if [ $? -ne 0 ]; then - echo "❌ Electron app build failed!" - exit 1 -fi - -echo "✅ Electron app built successfully" - +echo "📦 Step 5: Returning to project root..." +echo "---------------------------------------" +# NOTE: the frontend (webpack) build now happens inside `make build-go-embed` +# (Step 7-8 below) so the exact same command produces the UI that gets embedded +# into the Go binary. This keeps the embed build and `npm run electron:pack` +# from drifting. A second frontend build for the asar still runs in Step 10. cd "$PROJECT_ROOT" echo "" @@ -227,71 +221,26 @@ for file in model.onnx.data tokenizer.json vocab.txt model_manifest.json; do done echo "" -echo "📦 Step 7: Preparing files for Go embedding..." -echo "----------------------------------------------" - -# Copy frontend/dist files to src/backend/frontend/dist/ for embedding -# Go embed cannot use ../ paths, so we need the files under src/backend/ -if [ -d "src/frontend/dist" ]; then - mkdir -p src/backend/frontend/dist - # Use rsync for faster copying (handles incremental updates) - if command -v rsync >/dev/null 2>&1; then - rsync -a --delete src/frontend/dist/ src/backend/frontend/dist/ - echo "✅ Frontend files synced to src/backend/frontend/dist/ for embedding (rsync)" - else - cp -r src/frontend/dist/* src/backend/frontend/dist/ - echo "✅ Frontend files copied to src/backend/frontend/dist/ for embedding" - fi -else - echo "❌ Frontend dist directory not found: src/frontend/dist" - exit 1 -fi - -# Copy model files to src/backend/model/quantized/ for embedding -if [ -d "model/quantized" ]; then - mkdir -p src/backend/model/quantized - # Use rsync for faster copying if available - if command -v rsync >/dev/null 2>&1; then - rsync -a --delete model/quantized/ src/backend/model/quantized/ - else - cp -r model/quantized/* src/backend/model/quantized/ - fi - - # Verify model files after copying - 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") - echo "✅ Model files copied to src/backend/model/quantized/ for embedding (${COPIED_MODEL_SIZE} bytes)" -else - echo "❌ Model directory not found: model/quantized" - echo " This will cause runtime errors - the app needs the model files" - exit 1 -fi - -echo "" -echo "📦 Step 8: Building Go binary..." -echo "--------------------------------" - -# Extract version from package.json -VERSION=$(cd src/frontend && node -p "require('./package.json').version" 2>/dev/null || echo "0.0.0") -echo "Building version: $VERSION" - -# Build the Go binary with embedded files and version injection -mkdir -p build +echo "📦 Step 7-8: Building embedded Go binary (UI + model)..." +echo "--------------------------------------------------------" -# Use parallel compilation with version injection -CGO_ENABLED=1 \ -GOMAXPROCS=$PARALLEL_JOBS \ -go build \ - -tags embed \ - -ldflags="-s -w -X main.version=$VERSION -extldflags '-L./build/tokenizers'" \ - -o build/kiji-proxy \ - ./src/backend +# Single source of truth for the embedded backend binary: `make build-go-embed` +# builds the frontend, stages src/frontend/dist + model/quantized into +# src/backend/ for //go:embed (Go embed can't use ../ paths), compiles with +# `-tags embed`, and copies the binary into src/frontend/resources/. The same +# target backs `npm run electron:pack`, so the DMG/CI build and local packing +# can never diverge on the embed step (the root cause of the white-screen bug). +# +# NOTE: model LFS verification (Step 6 above) must run before this so the staged +# model files are the real binaries, not LFS pointers. +make build-go-embed if [ $? -ne 0 ]; then - echo "❌ Go binary build failed!" + echo "❌ Embedded Go binary build failed!" exit 1 fi -echo "✅ Go binary created: build/kiji-proxy" +echo "✅ Go binary created: build/kiji-proxy (embedded UI + model)" echo "" echo "📦 Step 9: Preparing Electron resources..."