import { app, ipcMain, IpcMainEvent, Tray, Menu, shell, dialog, BrowserWindow, clipboard } from 'electron'; import path from 'path'; import fse from 'fs-extra'; import semver from 'semver'; import os from 'os'; import ChildProcess from 'child_process'; import * as url from 'url'; import * as tmp from 'tmp'; 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, ProcessChannel, IdleStateChannel, NotificationChannel, ChatChannel, MessengerChannel, MessageChannel, AppChannel, ClipboardChannel, ExternalChannel } 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, ElectronBrowserWindowChannel } from '@ucap-webmessenger/electron-core'; import { autoUpdater, CancellationToken, UpdateCheckResult } from 'electron-updater'; import log from 'electron-log'; import { RendererUpdater } from './lib/renderer-updater'; import { appStorage } from './lib/storage'; const appIconPath = __LINUX__ ? __DEV__ ? path.join( __dirname, '../../', 'config/build/linux/icon/daesang/', '256x256.png' ) : path.join(__dirname, '..', '..', '/assets/icon/', '256x256.png') : __DEV__ ? path.join( __dirname, '../../', 'config/build/win/icon/daesang/', '16x16.ico' ) : path.join(__dirname, '..', '..', '/assets/icon/', '16x16.ico'); 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; tmp.setGracefulCleanup(); 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; let updateCheckResult: UpdateCheckResult; 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: () => { // 메신저에 로그아웃 후 종료 appExit(); } } ]); 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, 'assets/notification/preload.js' ); notificationService.templatePath = path.join( __dirname, 'assets/notification/template.html' ); updateWindowService = new ElectronUpdateWindowService({ width: 500, height: 160, frame: false, skipTaskbar: true, alwaysOnTop: true, maximizable: false, onReady: () => {}, onAcceptUpdate: () => { if (!!autoUpdaterCancellationToken) { log.info('downloadUpdate already'); return; } log.info('OnAcceptUpdate'); autoUpdaterCancellationToken = new CancellationToken(); autoUpdater.downloadUpdate(autoUpdaterCancellationToken); }, onDenyUpdate: () => { log.info('OnDenyUpdate'); updateCheckResult.cancellationToken.cancel(); updateWindowService.close(); }, onCancelDownload: () => { autoUpdaterCancellationToken.cancel(); updateWindowService.close(); } }); updateWindowService.templatePath = path.join( __dirname, 'assets/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.Apply, (event: IpcMainEvent, ...args: any[]) => { if (!autoUpdater.isUpdaterActive()) { log.info('autoUpdater is not active'); return; } const ver = args[0]; if (semver.lt(app.getVersion(), ver)) { updateCheckResult = undefined; autoUpdater .checkForUpdatesAndNotify() .then(r => { updateCheckResult = r; }) .catch(reason => { log.error(reason); }) .finally(() => {}); } }); 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.GetVersionInfo, (event: IpcMainEvent, ...args: any[]) => { event.returnValue = app.getVersion(); } ); 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( MessengerChannel.ChangeDownloadPath, (event: IpcMainEvent, ...args: any[]) => { const downloadPath = args[0] as string; if (!!downloadPath && downloadPath.length > 0) { appStorage.downloadPath = downloadPath; log.info('downloadPath is changed to ', appStorage.downloadPath); event.returnValue = appStorage.downloadPath; } else { event.returnValue = ''; } } ); 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]; const customSavePath: string = args[3]; let basePath = path.join( app.getPath('documents'), appStorage.constDefaultDownloadFolder ); if (!!appStorage.downloadPath) { basePath = appStorage.downloadPath; } try { fse.mkdirpSync(basePath); } catch (err) { log.error(err); basePath = app.getPath('downloads'); } let savePath: string = path.join(basePath, fileName); if (!!customSavePath) { savePath = customSavePath; } else { savePath = await FileUtil.uniqueFileName(savePath); } fse.writeFile(savePath, buffer, err => { if (!err) { event.returnValue = savePath; } else { log.error('SaveFile err', err); event.returnValue = undefined; } }); } catch (error) { log.error('SaveFile error', 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) { let basePath = app.getPath('downloads'); if (!!appStorage.downloadPath) { try { basePath = appStorage.downloadPath; } catch (err) { log.error(err); basePath = app.getPath('downloads'); } } folderItem = basePath; } if (make) { fse.mkdirpSync(folderItem); } let isSuccess = true; if (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( FileChannel.SelectSaveFilePath, (event: IpcMainEvent, ...args: any[]) => { const defaultPath = args[0]; const ext = path.extname(defaultPath); dialog .showSaveDialog({ defaultPath }) .then(obj => { const filePath = '' !== ext && '' === path.extname(obj.filePath) ? `${obj.filePath}${ext}` : obj.filePath; event.returnValue = { canceled: obj.canceled, filePath }; }) .catch(obj => { event.returnValue = undefined; }); } ); ipcMain.on(ProcessChannel.Execute, (event: IpcMainEvent, ...args: any[]) => { try { const executableName: string = args[0]; const binPath = __DEV__ ? path.join(__dirname, '../../', 'config/build/win/bin/') : path.join(__dirname, '..', '..', '..', '/bin/'); const executablePath = __WIN32__ ? path.join(binPath, `${executableName}.exe`) : path.join(binPath, executableName); const childProcess = ChildProcess.spawn(executablePath, [], { stdio: ['ignore', 'ignore', 'ignore'], detached: true }); event.returnValue = childProcess.pid; } catch (error) { 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( IdleStateChannel.StopCheck, (event: IpcMainEvent, ...args: any[]) => { if (!!idle) { idle.destoryChecker(); idle = null; } } ); ipcMain.on( IdleStateChannel.ChangeLimitTime, (event: IpcMainEvent, ...args: any[]) => { const limitTime: number = args[0]; if (!!idle) { idle.destoryChecker(); idle = null; } idle = new IdleChecker(appWindow.browserWindow); idle.resetIdleTime(limitTime); } ); 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, 'assets/notification/images/img_nophoto_50.png'), sound: noti.useSound ? path.join( 'file://', __dirname, 'assets/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); } ); ipcMain.on(ClipboardChannel.Read, (event: IpcMainEvent, ...args: any[]) => { try { const text = clipboard.readText('clipboard'); const rtf = clipboard.readRTF('clipboard'); const html = clipboard.readHTML('clipboard'); const image = clipboard.readImage('clipboard'); event.returnValue = { text, rtf, html, image: !image.isEmpty() ? image.toBitmap() : undefined, imageDataUrl: !image.isEmpty() ? image.toDataURL() : undefined }; } catch (error) { event.returnValue = {}; } }); ipcMain.on(AppChannel.Exit, (event: IpcMainEvent, ...args: any[]) => { appExit(); }); ipcMain.on(ExternalChannel.OpenUrl, (event: IpcMainEvent, ...args: any[]) => { const targetUrl = args[0]; const options = JSON.stringify(args[1] || {}); tmp.file({ postfix: '.html' }, (err, name, fd, cb) => { fse.writeFileSync( name, ` DS Talk Link