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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,8 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
package-lock.json

# build files
release/**
bun.lockb
149 changes: 149 additions & 0 deletions electron/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# Electron Desktop App

This document explains how to build and run the flash.comma.ai app as a cross-platform desktop application using Electron and Bun.

## Development

### Prerequisites
- Node.js >= 20.11.0
- Bun (latest version)

### Installation
```bash
bun install
```

### Development Mode
To run the app in development mode with hot reload:

```bash
bun run electron-dev
```

This will:
1. Start the Vite dev server with Bun
2. Wait for it to be ready
3. Launch Electron pointing to the dev server

### Building for Production

#### Build for current platform
```bash
bun run electron-build
```

#### Build for specific platforms
```bash
bun run electron-build-win # Windows
bun run electron-build-mac # macOS
bun run electron-build-linux # Linux
```

#### Build for all platforms
```bash
bun run electron-build-all
```

## Platform-Specific Notes

### Windows
- The app will be packaged as an NSIS installer
- USB drivers may need to be installed separately using Zadig
- Admin privileges may be required for USB device access

### macOS
- The app will be packaged as a DMG
- Code signing may be required for distribution
- USB device access should work out of the box

### Linux
- The app will be packaged as both AppImage and .deb
- USB device permissions may need to be configured via udev rules
- Run with `sudo` if USB access fails

## USB Device Access

The Electron app automatically handles USB device permissions for QDL devices (VID: 0x05C6, PID: 0x9008). However, some platforms may require additional setup:

### Linux USB Permissions
Create a udev rule to allow access to QDL devices:

```bash
# Create udev rule
sudo tee /etc/udev/rules.d/51-comma-qdl.rules > /dev/null <<EOF
SUBSYSTEM=="usb", ATTR{idVendor}=="05c6", ATTR{idProduct}=="9008", MODE="0666", GROUP="plugdev"
EOF

# Reload udev rules
sudo udevadm control --reload-rules
sudo udevadm trigger
```

Add your user to the plugdev group:
```bash
sudo usermod -a -G plugdev $USER
```

Log out and back in for the group change to take effect.

## ES Modules Support

The project uses ES modules throughout, including the main Electron process. The preload script uses CommonJS (`.cjs` extension) as required by Electron.

## Security Features

The Electron app implements several security best practices:

- Context isolation enabled
- Node integration disabled
- Preload script for secure IPC
- Content Security Policy
- External link handling
- USB device filtering

## Auto-Updates

The app includes electron-updater for automatic updates. Configure your update server in the build configuration to enable this feature.

## Troubleshooting

### USB Device Not Detected
1. Ensure the device is in QDL mode
2. Check that drivers are installed (Windows)
3. Verify USB permissions (Linux)
4. Try a different USB cable or port

### Build Issues
1. Clear node_modules and reinstall: `bun install --force`
2. Ensure you have the latest version of electron-builder
3. Check that all required build tools are installed

### App Won't Start
1. Check the console for error messages
2. Ensure the dist folder exists and contains built files
3. Try running in development mode first

### ES Module Issues
If you encounter module-related errors:
1. Ensure all `.js` files in `electron/` use ES module syntax
2. The preload script should use `.cjs` extension and CommonJS syntax
3. Check that `"type": "module"` is set in package.json

## File Structure

```
electron/
├── main.js # Main Electron process (ES modules)
├── preload.cjs # Preload script (CommonJS)
└── (old files) # Can be removed

dist/ # Built web app (created by Vite)
release/ # Electron build output
```

## Bun-Specific Notes

- All scripts use `bun run` instead of `npm run`
- Dependencies are installed with `bun add` instead of `npm install`
- Bun provides faster installation and execution compared to npm
- The development server starts faster with Bun
172 changes: 172 additions & 0 deletions electron/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import { app, BrowserWindow, shell, dialog, ipcMain } from 'electron'
import { fileURLToPath } from 'url'
import { dirname, join } from 'path'

const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
const isDev = process.env.NODE_ENV === 'development'

// Handle creating/removing shortcuts on Windows when installing/uninstalling.
if (process.platform === 'win32') {
try {
const { default: setupEvents } = await import('electron-squirrel-startup')
if (setupEvents) {
app.quit()
}
} catch (e) {
// electron-squirrel-startup is optional
}
}

let mainWindow

// IPC handlers
ipcMain.handle('get-app-version', () => {
return app.getVersion()
})

ipcMain.handle('show-open-dialog', async (event, options) => {
const { canceled, filePaths } = await dialog.showOpenDialog(mainWindow, options)
if (canceled) {
return { canceled: true }
}
return { canceled: false, filePaths }
})

ipcMain.handle('show-save-dialog', async (event, options) => {
const { canceled, filePath } = await dialog.showSaveDialog(mainWindow, options)
if (canceled) {
return { canceled: true }
}
return { canceled: false, filePath }
})

const createWindow = () => {
// Create the browser window.
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
minWidth: 800,
minHeight: 600,
icon: join(__dirname, '../src/app/icon.png'),
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
enableRemoteModule: false,
preload: join(__dirname, 'preload.cjs'),
webSecurity: true,
},
titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : 'default',
show: false,
})

// Load the app
if (isDev) {
mainWindow.loadURL('http://localhost:5173')
// Open DevTools in development
mainWindow.webContents.openDevTools()
} else {
mainWindow.loadFile(join(__dirname, '../dist/index.html'))
}

// Show window when ready to prevent visual flash
mainWindow.once('ready-to-show', () => {
mainWindow.show()
})

// Handle external links
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
shell.openExternal(url)
return { action: 'deny' }
})

// Prevent navigation to external websites
mainWindow.webContents.on('will-navigate', (event, navigationUrl) => {
const parsedUrl = new URL(navigationUrl)
if (parsedUrl.origin !== 'http://localhost:5173' && !navigationUrl.startsWith('file://')) {
event.preventDefault()
}
})
}

// This method will be called when Electron has finished initialization
app.whenReady().then(() => {
// Set app user model ID for Windows
if (process.platform === 'win32') {
app.setAppUserModelId('ai.comma.flash')
}

createWindow()

// Handle USB device permissions
mainWindow.webContents.session.on('select-usb-device', (event, details, callback) => {
// Add USB device selection logic here
event.preventDefault()

// Look for QDL devices (vendor ID 0x05C6, product ID 0x9008)
const qdlDevice = details.deviceList.find(device =>
device.vendorId === 0x05C6 && device.productId === 0x9008
)

if (qdlDevice) {
callback(qdlDevice.deviceId)
} else {
callback()
}
})

// Handle USB device access requests
mainWindow.webContents.session.on('usb-device-added', (event, device) => {
console.log('USB device added:', device)
mainWindow.webContents.send('usb-device-added', device)
})

mainWindow.webContents.session.on('usb-device-removed', (event, device) => {
console.log('USB device removed:', device)
mainWindow.webContents.send('usb-device-removed', device)
})

// macOS specific behavior
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
}
})

})

// Quit when all windows are closed, except on macOS
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})

// Security: Prevent new window creation
app.on('web-contents-created', (event, contents) => {
contents.on('new-window', (event, navigationUrl) => {
event.preventDefault()
shell.openExternal(navigationUrl)
})
})

// Handle certificate errors
app.on('certificate-error', (event, webContents, url, error, certificate, callback) => {
if (isDev) {
// In development, ignore certificate errors
event.preventDefault()
callback(true)
} else {
// In production, use default behavior
callback(false)
}
})

// Handle app protocol for auto-updater
if (process.defaultApp) {
if (process.argv.length >= 2) {
app.setAsDefaultProtocolClient('flash-comma', process.execPath, [process.argv[1]])
}
} else {
app.setAsDefaultProtocolClient('flash-comma')
}
29 changes: 29 additions & 0 deletions electron/preload.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
const { contextBridge, ipcRenderer } = require('electron')

// Expose protected methods that allow the renderer process to use
// the ipcRenderer without exposing the entire object
contextBridge.exposeInMainWorld('electronAPI', {
platform: process.platform,
versions: process.versions,

// USB device management
onUsbDeviceAdded: (callback) => {
ipcRenderer.on('usb-device-added', callback)
},

onUsbDeviceRemoved: (callback) => {
ipcRenderer.on('usb-device-removed', callback)
},

// App management
getAppVersion: () => ipcRenderer.invoke('get-app-version'),

// File system operations (if needed)
showOpenDialog: (options) => ipcRenderer.invoke('show-open-dialog', options),
showSaveDialog: (options) => ipcRenderer.invoke('show-save-dialog', options),
})

// Security: Remove global Node.js APIs
delete window.require
delete window.exports
delete window.module
Loading