import { app, Menu, ipcMain, BrowserWindow, shell } from 'electron'; import * as fse from 'fs-extra'; import * as path from 'path'; import * as ChildProcess from 'child_process'; import { shellNeedsPatching, updateEnvironmentForProcess } from '@overflow/core/shell'; import { parseAppURL } from '@overflow/core/parse-app-url'; // import { // enableSourceMaps, // withSourceMappedStack, // } from '@overflow/core/source-map-support'; import { now } from '@overflow/core/now'; import { IMenuItem } from '@overflow/core/menu-item'; import { AppWindow } from './app-window'; import { handleSquirrelEvent } from './squirrel-updater'; import { openDirectorySafe } from './shell'; import { buildDefaultMenu } from './menu/build-default'; import { MenuEvent } from '../commons/type'; import { findMenuItemByID } from './menu/find-menu-item'; import { IMenuItemState } from '../commons/model/menu-update'; import * as sqlite3 from 'sqlite3'; // enableSourceMaps(); let mainWindow: AppWindow | null = null; const launchTime = now(); let preventQuit = false; let readyTime: number | null = null; let db: sqlite3.Database | null = null; let probeProcess: ChildProcess.ChildProcess | null = null; type OnDidLoadFn = (window: AppWindow) => void; /** See the `onDidLoad` function. */ let onDidLoadFns: Array | null = []; function handleUncaughtException(error: Error) { preventQuit = true; if (mainWindow) { mainWindow.destroy(); mainWindow = null; } const isLaunchError = !mainWindow; // showUncaughtException(isLaunchError, error); } // process.on('uncaughtException', (error: Error) => { // error = withSourceMappedStack(error); // // reportError(error); // handleUncaughtException(error); // }); const handlingSquirrelEvent = false; // if (__WIN32__ && process.argv.length > 1) { // const arg = process.argv[1]; // const promise = handleSquirrelEvent(arg); // if (promise) { // handlingSquirrelEvent = true; // promise // .catch(e => { // log.error(`Failed handling Squirrel event: ${arg}`, e); // }) // .then(() => { // app.quit(); // }); // } else { // handlePossibleProtocolLauncherArgs(process.argv); // } // } // function handleAppURL(url: string) { // log.info('Processing protocol url'); // const action = parseAppURL(url); // onDidLoad(window => { // // This manual focus call _shouldn't_ be necessary, but is for Chrome on // // macOS. See https://github.com/desktop/desktop/issues/973. // window.focus(); // window.sendURLAction(action); // }); // } const isDuplicateInstance = false; // If we're handling a Squirrel event we don't want to enforce single instance. // We want to let the updated instance launch and do its work. It will then quit // once it's done. // if (!handlingSquirrelEvent) { // isDuplicateInstance = app.makeSingleInstance((args, workingDirectory) => { // // Someone tried to run a second instance, we should focus our window. // if (mainWindow) { // if (mainWindow.isMinimized()) { // mainWindow.restore(); // } // if (!mainWindow.isVisible()) { // mainWindow.show(); // } // mainWindow.focus(); // } // handlePossibleProtocolLauncherArgs(args); // }); // if (isDuplicateInstance) { // app.quit(); // } // } if (shellNeedsPatching(process)) { updateEnvironmentForProcess(); } app.on('will-finish-launching', () => { // macOS only app.on('open-url', (event, url) => { event.preventDefault(); // handleAppURL(url); }); }); /** * Attempt to detect and handle any protocol handler arguments passed * either via the command line directly to the current process or through * IPC from a duplicate instance (see makeSingleInstance) * * @param args Essentially process.argv, i.e. the first element is the exec * path */ // function handlePossibleProtocolLauncherArgs(args: ReadonlyArray) { // log.info(`Received possible protocol arguments: ${args.length}`); // if (__WIN32__) { // // We register our protocol handler callback on Windows as // // [executable path] --protocol-launcher -- "%1" meaning that any // // url data comes after we've stopped processing arguments. We check // // for that exact scenario here before doing any processing. If there's // // more than 4 args because of a malformed url then we bail out. // if ( // args.length === 4 && // args[1] === '--protocol-launcher' && // args[2] === '--' // ) { // handleAppURL(args[3]); // } // } else if (args.length > 1) { // handleAppURL(args[1]); // } // } /** * Wrapper around app.setAsDefaultProtocolClient that adds our * custom prefix command line switches on Windows that prevents * command line argument parsing after the `--`. */ function setAsDefaultProtocolClient(protocol: string) { if (__WIN32__) { app.setAsDefaultProtocolClient(protocol, process.execPath, [ '--protocol-launcher', '--' ]); } else { app.setAsDefaultProtocolClient(protocol); } } // if (process.env.GITHUB_DESKTOP_DISABLE_HARDWARE_ACCELERATION) { // log.info( // `GITHUB_DESKTOP_DISABLE_HARDWARE_ACCELERATION environment variable set, disabling hardware acceleration` // ); // app.disableHardwareAcceleration(); // } app.on('ready', () => { if (isDuplicateInstance || handlingSquirrelEvent) { return; } const dbPath = __DEV__ ? path.join(__dirname, '..', '..', 'config', '_database.sqlite') : path.join(__dirname, '..', 'bin', 'database.sqlite'); db = new sqlite3.Database(dbPath, sqlite3.OPEN_READONLY, (err: Error) => { if (err) { console.error(err.message); } }); if (!__DEV__) { const probePath = __WIN32__ ? path.join(__dirname, '..', 'bin', 'probe.exe') : path.join(__dirname, '..', 'bin', 'probe'); probeProcess = ChildProcess.spawn(probePath, [], { stdio: ['ignore', 'ignore', 'ignore'], detached: true }); } readyTime = now() - launchTime; // setAsDefaultProtocolClient('x-github-client'); // if (__DEV__) { // setAsDefaultProtocolClient('x-github-desktop-dev-auth'); // } else { // setAsDefaultProtocolClient('x-github-desktop-auth'); // } // // Also support Desktop Classic's protocols. // if (__DARWIN__) { // setAsDefaultProtocolClient('github-mac'); // } else if (__WIN32__) { // setAsDefaultProtocolClient('github-windows'); // } createWindow(); let menu = buildDefaultMenu(); Menu.setApplicationMenu(menu); ipcMain.on( 'update-preferred-app-menu-item-labels', (event: Electron.IpcMessageEvent) => { menu = buildDefaultMenu(); Menu.setApplicationMenu(menu); if (mainWindow) { mainWindow.sendAppMenu(); } } ); ipcMain.on('menu-event', (event: Electron.IpcMessageEvent, args: any[]) => { const { name }: { name: MenuEvent } = event as any; if (mainWindow) { mainWindow.sendMenuEvent(name); } }); /** * An event sent by the renderer asking that the menu item with the given id * is executed (ie clicked). */ ipcMain.on( 'execute-menu-item', (event: Electron.IpcMessageEvent, { id }: { id: string }) => { const menuItem = findMenuItemByID(menu, id); if (menuItem) { const window = BrowserWindow.fromWebContents(event.sender); const fakeEvent = { preventDefault: () => { }, sender: event.sender }; menuItem.click(fakeEvent, window, event.sender); } } ); ipcMain.on( 'update-menu-state', ( event: Electron.IpcMessageEvent, items: Array<{ id: string; state: IMenuItemState }> ) => { let sendMenuChangedEvent = false; for (const item of items) { const { id, state } = item; const menuItem = findMenuItemByID(menu, id); if (menuItem) { // Only send the updated app menu when the state actually changes // or we might end up introducing a never ending loop between // the renderer and the main process if ( state.enabled !== undefined && menuItem.enabled !== state.enabled ) { menuItem.enabled = state.enabled; sendMenuChangedEvent = true; } } else { // fatalError(`Unknown menu id: ${id}`); } } if (sendMenuChangedEvent && mainWindow) { mainWindow.sendAppMenu(); } } ); // ipcMain.on( // 'show-contextual-menu', // (event: Electron.IpcMessageEvent, items: ReadonlyArray) => { // const menu = buildContextMenu(items, ix => // event.sender.send('contextual-menu-action', ix) // ); // const window = BrowserWindow.fromWebContents(event.sender); // menu.popup({ window }); // } // ); /** * An event sent by the renderer asking for a copy of the current * application menu. */ ipcMain.on('get-app-menu', () => { if (mainWindow) { mainWindow.sendAppMenu(); } }); // ipcMain.on( // 'show-certificate-trust-dialog', // ( // event: Electron.IpcMessageEvent, // { // certificate, // message, // }: { certificate: Electron.Certificate; message: string } // ) => { // // This API is only implemented for macOS and Windows right now. // if (__DARWIN__ || __WIN32__) { // onDidLoad(window => { // window.showCertificateTrustDialog(certificate, message); // }); // } // } // ); // ipcMain.on( // 'log', // (event: Electron.IpcMessageEvent, level: LogLevel, message: string) => { // writeLog(level, message); // } // ); // ipcMain.on( // 'uncaught-exception', // (event: Electron.IpcMessageEvent, error: Error) => { // handleUncaughtException(error); // } // ); // ipcMain.on( // 'send-error-report', // ( // event: Electron.IpcMessageEvent, // { error, extra }: { error: Error; extra: { [key: string]: string } } // ) => { // reportError(error, extra); // } // ); // ipcMain.on( // 'open-external', // (event: Electron.IpcMessageEvent, { path }: { path: string }) => { // const pathLowerCase = path.toLowerCase(); // if ( // pathLowerCase.startsWith('http://') || // pathLowerCase.startsWith('https://') // ) { // log.info(`opening in browser: ${path}`); // } // const result = shell.openExternal(path); // event.sender.send('open-external-result', { result }); // } // ); // ipcMain.on( // 'show-item-in-folder', // (event: Electron.IpcMessageEvent, { path }: { path: string }) => { // Fs.stat(path, (err, stats) => { // if (err) { // log.error(`Unable to find file at '${path}'`, err); // return; // } // if (stats.isDirectory()) { // openDirectorySafe(path); // } else { // shell.showItemInFolder(path); // } // }); // } // ); }); app.on('activate', () => { onDidLoad(window => { window.show(); }); }); app.on('web-contents-created', (event, contents) => { contents.on('new-window', (_event, url) => { // Prevent links or window.open from opening new windows _event.preventDefault(); log.warn(`Prevented new window to: ${url}`); }); }); app.on('before-quit', function (event) { if (null !== db) { db.close(); } if (null !== probeProcess && !probeProcess.killed) { probeProcess.kill('SIGKILL'); } }); app.on('window-all-closed', function (event) { app.quit(); }); // app.on( // 'certificate-error', // (event, webContents, url, error, certificate, callback) => { // callback(false); // onDidLoad(window => { // window.sendCertificateError(certificate, error, url); // }); // } // ); function createWindow() { const window = new AppWindow(); if (__DEV__) { const { default: installExtension, REDUX_DEVTOOLS } = require('electron-devtools-installer'); require('electron-debug')({ showDevTools: true }); const ChromeLens = { id: 'idikgljglpfilbhaboonnpnnincjhjkd', electron: '>=1.2.1' }; const extensions = [ChromeLens, REDUX_DEVTOOLS]; for (const extension of extensions) { try { installExtension(extension); } catch (e) { } } } window.onClose(() => { mainWindow = null; if (!__DARWIN__ && !preventQuit) { app.quit(); } }); window.onDidLoad((data: any) => { window.show(); window.sendLaunchTimingStats({ mainReadyTime: readyTime ? readyTime : 0, loadTime: window.loadTime ? window.loadTime : 0, rendererReadyTime: window.rendererReadyTime ? window.rendererReadyTime : 0 }); const fns = onDidLoadFns ? onDidLoadFns : null; onDidLoadFns = null; if (fns) { for (const fn of fns) { fn(window); } } }); window.load(); mainWindow = window; } /** * Register a function to be called once the window has been loaded. If the * window has already been loaded, the function will be called immediately. */ function onDidLoad(fn: OnDidLoadFn) { if (onDidLoadFns) { onDidLoadFns.push(fn); } else { if (mainWindow) { fn(mainWindow); } } }