752 lines
18 KiB
TypeScript
752 lines
18 KiB
TypeScript
import {
|
|
app,
|
|
ipcMain,
|
|
IpcMainEvent,
|
|
Tray,
|
|
Menu,
|
|
shell,
|
|
dialog,
|
|
webFrame
|
|
} 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 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
|
|
} 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__
|
|
? __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<OnDidLoadFn> | 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: () => {
|
|
// 메신저에 로그아웃 후 종료
|
|
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: () => {
|
|
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,
|
|
'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 (__DEV__) {
|
|
// event.returnValue = false;
|
|
// return;
|
|
// }
|
|
|
|
const ver = args[0];
|
|
|
|
if (semver.lt(app.getVersion(), ver)) {
|
|
updateWindowService.versionInfo = {
|
|
latest: ver,
|
|
installed: app.getVersion()
|
|
};
|
|
autoUpdater
|
|
.checkForUpdatesAndNotify()
|
|
.then(result => {})
|
|
.catch(reason => {
|
|
log.error(reason);
|
|
});
|
|
}
|
|
});
|
|
|
|
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 savePath: string = path.join(
|
|
!!appStorage.downloadPath
|
|
? appStorage.downloadPath
|
|
: app.getPath('downloads'),
|
|
fileName
|
|
);
|
|
if (!!customSavePath) {
|
|
savePath = customSavePath;
|
|
} else {
|
|
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(
|
|
FileChannel.SelectSaveFilePath,
|
|
(event: IpcMainEvent, ...args: any[]) => {
|
|
dialog
|
|
.showSaveDialog({ defaultPath: args[0] })
|
|
.then(obj => {
|
|
event.returnValue = obj.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.ChangeLimitTime,
|
|
(event: IpcMainEvent, ...args: any[]) => {
|
|
const limitTime: number = args[0];
|
|
if (!!idle) {
|
|
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(AppChannel.Exit, (event: IpcMainEvent, ...args: any[]) => {
|
|
appExit();
|
|
});
|
|
|
|
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);
|
|
});
|
|
|
|
function appExit() {
|
|
appWindow = null;
|
|
app.exit();
|
|
}
|