import { app, ipcMain, IpcMainEvent, Tray, Menu, shell, dialog } from 'electron'; import path from 'path'; import fse from 'fs-extra'; import semver from 'semver'; import os from 'os'; import AutoLaunch from 'auto-launch'; import { AppWindow } from './app/AppWindow'; import { now } from './util/now'; import { showUncaughtException } from './crash/show-uncaught-exception'; import { UpdaterChannel, FileChannel, IdleStateChannel, NotificationChannel, ChatChannel, MessengerChannel, MessageChannel } from '@ucap-webmessenger/native-electron'; import { ElectronNotificationService } from '@ucap-webmessenger/electron-notification'; import { ElectronUpdateWindowService } from '@ucap-webmessenger/electron-update-window'; import { root } from './util/root'; import { FileUtil } from './lib/file-util'; import { IdleChecker } from './lib/idle-checker'; import { NotificationRequest, NotificationType, NativePathName } from '@ucap-webmessenger/native'; import { ElectronAppChannel } from '@ucap-webmessenger/electron-core'; import { autoUpdater, CancellationToken } from 'electron-updater'; import log from 'electron-log'; import { RendererUpdater } from './lib/renderer-updater'; import { Storage } from './lib/storage'; const appIconPath = __LINUX__ ? path.join(__dirname, 'resources/linuxicon', '256x256.png') : path.join(__dirname, 'resources/image', '64_64.png'); let appWindow: AppWindow | null = null; let appTray: Tray | null = null; const launchTime = now(); let readyTime: number | null = null; type OnDidLoadFn = (window: AppWindow) => void; let onDidLoadFns: Array | null = []; let preventQuit = false; let notificationService: ElectronNotificationService | null; let updateWindowService: ElectronUpdateWindowService | null; const appStorage: Storage = new Storage(); function handleUncaughtException(error: Error) { preventQuit = true; // If we haven't got a window we'll assume it's because // we've just launched and haven't created it yet. // It could also be because we're encountering an unhandled // exception on shutdown but that's less likely and since // this only affects the presentation of the crash dialog // it's a safe assumption to make. const isLaunchError = appWindow === null; if (appWindow) { appWindow.destroy(); appWindow = null; } showUncaughtException(isLaunchError, error); } function getUptimeInSeconds() { return (now() - launchTime) / 1000; } process.on('uncaughtException', (error: Error) => { // error = withSourceMappedStack(error); // reportError(error, getExtraErrorContext()); handleUncaughtException(error); }); let isDuplicateInstance = false; const gotSingleInstanceLock = app.requestSingleInstanceLock(); isDuplicateInstance = !gotSingleInstanceLock; let idle: IdleChecker | null; let rendererUpdater: RendererUpdater | undefined; log.transports.file.level = 'debug'; let autoUpdaterCancellationToken: CancellationToken; autoUpdater.autoDownload = false; autoUpdater.logger = log; const ucapMessengerLauncher = new AutoLaunch({ name: app.name }); app.on(ElectronAppChannel.SecondInstance, (event, args, workingDirectory) => { // Someone tried to run a second instance, we should focus our window. if (appWindow) { if (appWindow.isMinimized()) { appWindow.restore(); } if (!appWindow.isVisible()) { appWindow.show(); } appWindow.focus(); } }); if (isDuplicateInstance) { app.quit(); } function createWindow() { const window = new AppWindow(appIconPath); if (__DEV__) { // const { // default: installExtension, // REDUX_DEVTOOLS // } = require('electron-devtools-installer'); import('electron-debug').then(ed => { ed.default({ showDevTools: true }); }); import('electron-devtools-installer').then(edi => { const ChromeLens = { id: 'idikgljglpfilbhaboonnpnnincjhjkd', electron: '>=1.2.1' }; const extensions = [edi.REDUX_DEVTOOLS, ChromeLens]; for (const extension of extensions) { try { edi.default(extension); } catch (e) { log.error(e); } } }); } window.onClose(() => { appWindow = null; if (!__DARWIN__ && !preventQuit) { app.quit(); } }); window.onDidLoad(() => { if (!appStorage.startupHideWindow) { window.show(); } const fns = onDidLoadFns; onDidLoadFns = null; for (const fn of fns) { fn(window); } }); window.load(); appWindow = window; } function createTray() { appTray = new Tray(appIconPath); const contextMenu = Menu.buildFromTemplate([ { label: '로그아웃', // accelerator: 'Q', // selector: 'terminate:', click: () => { appWindow.show(); appWindow.browserWindow.webContents.send(MessengerChannel.Logout); } }, { label: '설정', // accelerator: 'Q', // selector: 'terminate:', click: () => { appWindow.show(); appWindow.browserWindow.webContents.send(MessengerChannel.ShowSetting); } }, { label: '버전', submenu: [{ label: 'Ver. ' + app.getVersion() }] }, { label: '종료', // accelerator: 'Q', // selector: 'terminate:', click: () => { // 메신저에 로그아웃 후 종료 appWindow = null; app.exit(); } } ]); appTray.setToolTip('DS Talk'); appTray.setContextMenu(contextMenu); appTray.on('click', () => { appWindow.isVisible() ? appWindow.hide() : appWindow.show(); }); } // This method will be called when Electron has finished // initialization and is ready to create browser windows. // Some APIs can only be used after this event occurs. app.on(ElectronAppChannel.Ready, () => { if (isDuplicateInstance) { return; } readyTime = now() - launchTime; createWindow(); createTray(); notificationService = new ElectronNotificationService({ width: 340, height: 100, padding: 0, borderRadius: 0, // appIcon: iconPath, displayTime: 5000, defaultStyleContainer: {}, defaultStyleAppIcon: { display: 'none' }, defaultStyleImage: {}, defaultStyleClose: {}, defaultStyleText: {} }); notificationService.options.defaultWindow.webPreferences.preload = path.join( __dirname, 'resources/notification/preload.js' ); notificationService.templatePath = path.join( __dirname, 'resources/notification/template.html' ); updateWindowService = new ElectronUpdateWindowService({ width: 500, height: 160, frame: false, skipTaskbar: true, alwaysOnTop: true, maximizable: false, onReady: () => {}, onAcceptUpdate: () => { log.info('OnAcceptUpdate'); autoUpdaterCancellationToken = new CancellationToken(); autoUpdater.downloadUpdate(autoUpdaterCancellationToken); }, onDenyUpdate: () => { log.info('OnDenyUpdate'); updateWindowService.close(); }, onCancelDownload: () => { autoUpdaterCancellationToken.cancel(); updateWindowService.close(); } }); updateWindowService.templatePath = path.join( __dirname, 'resources/update-window/template.html' ); // updateWindowService.show(); ipcMain.on('uncaught-exception', (event: IpcMainEvent, error: Error) => { handleUncaughtException(error); }); ipcMain.on( 'send-error-report', ( event: IpcMainEvent, { error, extra }: { error: Error; extra: { [key: string]: string } } ) => {} ); }); // Quit when all windows are closed. app.on(ElectronAppChannel.WindowAllClosed, () => { // On OS X it is common for applications and their menu bar // to stay active until the user quits explicitly with Cmd + Q if (process.platform !== 'darwin') { app.quit(); } }); app.on(ElectronAppChannel.Activate, () => { onDidLoad(window => { window.show(); }); }); function onDidLoad(fn: OnDidLoadFn) { if (onDidLoadFns) { onDidLoadFns.push(fn); } else { if (appWindow) { fn(appWindow); } } } ipcMain.on(UpdaterChannel.Check, (event: IpcMainEvent, ...args: any[]) => { // if (__DEV__) { // event.returnValue = false; // return; // } const ver = args[0]; if (semver.lt(app.getVersion(), ver)) { autoUpdater .checkForUpdatesAndNotify() .then(result => { if (!result) { event.returnValue = false; } else { event.returnValue = true; } }) .catch(reason => { event.returnValue = false; }); } else { event.returnValue = false; } }); ipcMain.on( MessengerChannel.GetNetworkInfo, (event: IpcMainEvent, ...args: any[]) => { const interfaces = os.networkInterfaces(); const addresses: { ip: string; mac: string }[] = []; // tslint:disable-next-line: forin for (const k in interfaces) { // tslint:disable-next-line: forin for (const k2 in interfaces[k]) { const address = interfaces[k][k2]; if (address.family === 'IPv4' && !address.internal) { addresses.push({ ip: address.address, mac: address.mac }); } } } event.returnValue = addresses; } ); ipcMain.on( MessengerChannel.ChangeAutoLaunch, (event: IpcMainEvent, ...args: any[]) => { const isAutoLaunch = args[0] as boolean; if (isAutoLaunch) { ucapMessengerLauncher .enable() .then(() => { event.returnValue = true; log.info('AutoLaunch is enabled'); }) .catch(reason => { event.returnValue = false; }); } else { ucapMessengerLauncher .disable() .then(() => { event.returnValue = true; log.info('AutoLaunch is disabled'); }) .catch(reason => { event.returnValue = false; }); } } ); ipcMain.on( MessengerChannel.ChangeStartupHideWindow, (event: IpcMainEvent, ...args: any[]) => { const isStartupHideWindow = args[0] as boolean; appStorage.startupHideWindow = isStartupHideWindow; log.info( 'StartupHideWindow is changed from ', !appStorage.startupHideWindow, ' to ', appStorage.startupHideWindow ); event.returnValue = true; } ); ipcMain.on( UpdaterChannel.StartCheckInstant, (event: IpcMainEvent, ...args: any[]) => { // const config = args[0] as UpdateCheckConfig; // if (!!rendererUpdater) { // rendererUpdater.stopCheck(); // rendererUpdater = null; // } // rendererUpdater = new RendererUpdater(appWindow.browserWindow, config); // default 10min // rendererUpdater.startCheck(); } ); ipcMain.on( UpdaterChannel.StopCheckInstant, (event: IpcMainEvent, ...args: any[]) => { // if (!!rendererUpdater) { // rendererUpdater.stopCheck(); // rendererUpdater = null; // } } ); ipcMain.on( UpdaterChannel.ApplyInstant, (event: IpcMainEvent, ...args: any[]) => { // if (!!rendererUpdater) { // rendererUpdater.apply(); // } } ); ipcMain.on(FileChannel.ReadFile, (event: IpcMainEvent, ...args: any[]) => { const filePath = root(args[0]); try { fse.readFile(filePath, (err, data) => { if (!!err) { event.returnValue = err; log.error(`File read failed path[${filePath}] err:`, err); } else { event.returnValue = data; } }); } catch (error) { log.error(`File read failed path[${filePath}] err:`, error); event.returnValue = null; } }); ipcMain.on( FileChannel.SaveFile, async (event: IpcMainEvent, ...args: any[]) => { try { const buffer: Buffer = args[0]; const fileName: string = args[1]; const mimeType: string = args[2]; let savePath: string = path.join( !!args[3] ? args[3] : app.getPath('downloads'), fileName ); savePath = await FileUtil.uniqueFileName(savePath); fse.writeFile(savePath, buffer, err => { if (!err) { event.returnValue = savePath; } else { event.returnValue = undefined; } }); } catch (error) { event.returnValue = undefined; } } ); ipcMain.on( FileChannel.OpenFolderItem, async (event: IpcMainEvent, ...args: any[]) => { try { let folderItem: string = args[0]; const make: boolean = args[1]; if (!folderItem) { folderItem = app.getPath('downloads'); } let isSuccess = true; if (make) { fse.ensureDirSync(folderItem); } if (isSuccess && fse.existsSync(folderItem)) { shell.openItem(folderItem); } else { isSuccess = false; } if (isSuccess) { event.returnValue = true; } else { event.returnValue = false; } } catch (error) { event.returnValue = false; } } ); ipcMain.on(FileChannel.GetPath, async (event: IpcMainEvent, ...args: any[]) => { try { const name: NativePathName = args[0]; event.returnValue = app.getPath(name); } catch (error) { event.returnValue = undefined; } }); ipcMain.on( FileChannel.SelectDirectory, (event: IpcMainEvent, ...args: any[]) => { dialog .showOpenDialog(appWindow.browserWindow, { defaultPath: app.getPath('home'), properties: ['openDirectory'] }) .then(value => { event.returnValue = value.filePaths[0]; }) .catch(reason => { event.returnValue = undefined; }); } ); ipcMain.on( IdleStateChannel.StartCheck, (event: IpcMainEvent, ...args: any[]) => { if (!!idle) { idle.destoryChecker(); idle = null; } idle = new IdleChecker(appWindow.browserWindow); // default 10min idle.startChecker(); } ); ipcMain.on( NotificationChannel.Notify, (event: IpcMainEvent, ...args: any[]) => { const noti: NotificationRequest = args[0]; notificationService.notify({ title: noti.title, text: noti.contents, image: noti.image || path.join( __dirname, 'resources/notification/images/img_nophoto_50.png' ), sound: noti.useSound ? path.join( 'file://', __dirname, 'resources/notification/sounds/messageAlarm.mp3' ) : '', displayTime: !!noti.displayTime ? noti.displayTime : undefined, onClick: e => { appWindow.browserWindow.flashFrame(false); if (noti.type === NotificationType.Event) { appWindow.browserWindow.webContents.send( ChatChannel.OpenRoom, noti.seq ); } else if (noti.type === NotificationType.Message) { appWindow.browserWindow.webContents.send( MessageChannel.OpenMessage, noti.seq ); } appWindow.show(); e.close(); } }); appWindow.browserWindow.flashFrame(true); } ); ipcMain.on( NotificationChannel.CloseAllNotify, (event: IpcMainEvent, ...args: any[]) => { appWindow.browserWindow.flashFrame(false); } ); autoUpdater.on('checking-for-update', () => { log.info('Checking for update...'); }); autoUpdater.on('update-available', info => { log.info(info); log.info('Update available.'); updateWindowService.show(); }); autoUpdater.on('update-not-available', info => { log.info('Update not available.'); }); autoUpdater.on('error', err => { log.info('Error in auto-updater. ' + err); }); autoUpdater.on('download-progress', progressObj => { let logMessage = 'Download speed: ' + progressObj.bytesPerSecond; logMessage = logMessage + ' - Downloaded ' + progressObj.percent + '%'; logMessage = logMessage + ' (' + progressObj.transferred + '/' + progressObj.total + ')'; log.info(logMessage); updateWindowService.setDownloadValue( progressObj.transferred, progressObj.total ); }); autoUpdater.on('update-downloaded', info => { log.info('Update downloaded'); updateWindowService.setDownloadComplete(); autoUpdater.quitAndInstall(true, true); });