Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions electron/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,17 @@ import {
shell,
} from 'electron';
import log from 'electron-log';

// ==================== EPIPE error prevention ====================
// During app shutdown, the stdout/stderr pipes may close before all log
// writes complete. This causes uncaught EPIPE errors that crash the app.
// Handle them gracefully by ignoring broken pipe writes.
for (const stream of [process.stdout, process.stderr]) {
stream?.on?.('error', (err: NodeJS.ErrnoException) => {
if (err.code === 'EPIPE') return; // Silently ignore broken pipe
});
}

import FormData from 'form-data';
import fsp from 'fs/promises';
import mime from 'mime';
Expand Down Expand Up @@ -199,6 +210,25 @@ log.transports.file.level = 'info';
log.transports.console.format = '[{level}]{text}';
log.transports.file.format = '[{level}]{text}';

// Catch any uncaught exceptions from broken pipes during shutdown
// to prevent the app from crashing with EPIPE errors
process.on('uncaughtException', (error: NodeJS.ErrnoException) => {
if (error.code === 'EPIPE') {
// Broken pipe during shutdown - safe to ignore.
// This happens when electron-log's console transport tries to write
// after the renderer process or stdout pipe has been closed.
return;
}
// For non-EPIPE errors, log to file (not console to avoid recursion) and re-throw
log.transports.file?.({
data: [`[UNCAUGHT] ${error.stack || error.message}`],
level: 'error',
date: new Date(),
variables: {},
} as any);
throw error;
});

// Disable GPU Acceleration for Windows 7
if (os.release().startsWith('6.1')) app.disableHardwareAcceleration();

Expand Down Expand Up @@ -2234,6 +2264,9 @@ app.whenReady().then(async () => {
app.on('window-all-closed', () => {
log.info('window-all-closed');

// Disable console transport - all windows are closed so stdout pipe may break
log.transports.console.level = false;

// Clean up WebView manager
if (webViewManager) {
webViewManager.destroy();
Expand Down Expand Up @@ -2271,6 +2304,12 @@ app.on('before-quit', async (event) => {
// Prevent default quit to ensure cleanup completes
event.preventDefault();

// Disable console transport BEFORE destroying the window.
// Once the window/renderer is destroyed, stdout pipe may close,
// causing EPIPE errors on any subsequent console.info/log writes.
// File transport remains active for debugging shutdown issues.
log.transports.console.level = false;

try {
// NOTE: Profile sync removed - we now use app userData directly for all partitions
// No need to sync between different profile directories
Expand Down
2 changes: 2 additions & 0 deletions electron/main/install-deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,8 @@ class InstallLogs {
'--no-dev',
'--cache-dir',
getCachePath('uv_cache'),
'--no-build-isolation-package',
'llvmlite',
...extraArgs,
],
{
Expand Down
58 changes: 57 additions & 1 deletion electron/main/utils/process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -631,12 +631,68 @@ export function getUvEnv(version: string): Record<string, string> {
const prebuiltPython = getPrebuiltPythonDir();
const pythonInstallDir = prebuiltPython || getCachePath('uv_python');

return {
// Ensure PATH includes common tool directories (cmake, brew, etc.)
// Electron GUI apps on macOS don't inherit the full shell PATH
const currentPath = process.env.PATH || '';
const extraPaths = [
'/usr/local/bin',
'/opt/homebrew/bin',
'/usr/bin',
'/bin',
'/usr/sbin',
'/sbin',
];

// Add LLVM directories so llvmlite can find the correct version during builds.
// Prefer LLVM 20 (required by llvmlite >=0.46) over older versions.
const llvmPaths = [
'/usr/local/opt/llvm@20/bin',
'/opt/homebrew/opt/llvm@20/bin',
];

const pathParts = currentPath.split(':');
// Prepend LLVM paths so they take priority over older LLVM versions
const allExtraPaths = [...llvmPaths, ...extraPaths];
const missingPaths = allExtraPaths.filter(
(p) => !pathParts.includes(p) && fs.existsSync(p)
);
const enhancedPath =
missingPaths.length > 0
? [...missingPaths, ...pathParts].join(':')
: currentPath;

// Set LLVM_CONFIG explicitly for llvmlite builds
const env: Record<string, string> = {
UV_PYTHON_INSTALL_DIR: pythonInstallDir,
UV_TOOL_DIR: getCachePath('uv_tool'),
UV_PROJECT_ENVIRONMENT: getVenvPath(version),
UV_HTTP_TIMEOUT: '300',
PATH: enhancedPath,
};

// Point llvmlite/cmake to the correct LLVM 20 installation.
// llvmlite >=0.46 requires LLVM 20. Without explicit LLVM_DIR, cmake may
// find an older LLVM version (e.g. 14) via system paths and fail the build.
// CMAKE_ARGS with -DLLVM_DIR forces cmake's find_package(LLVM) to use LLVM 20.
// LLVM_CONFIG provides the direct path for llvmlite's build.py.
const llvmPrefixes = [
'/usr/local/opt/llvm@20', // Intel Mac (Homebrew)
'/opt/homebrew/opt/llvm@20', // Apple Silicon (Homebrew)
];

for (const llvmPrefix of llvmPrefixes) {
const llvmCmakeDir = path.join(llvmPrefix, 'lib', 'cmake', 'llvm');
if (fs.existsSync(llvmCmakeDir)) {
env.CMAKE_ARGS = `-DLLVM_DIR=${llvmCmakeDir}`;
const llvmConfigPath = path.join(llvmPrefix, 'bin', 'llvm-config');
if (fs.existsSync(llvmConfigPath)) {
env.LLVM_CONFIG = llvmConfigPath;
}
break;
}
}

return env;
}

export async function killProcessByName(name: string): Promise<void> {
Expand Down