specter-desktop/pyinstaller/electron/main.js

338 lines
10 KiB
JavaScript

// Modules to control application life and create native browser window
const fs = require('fs')
const { spawn, exec } = require('child_process')
const { app, nativeImage, BrowserWindow, Menu, screen, shell, dialog, ipcMain } = require('electron')
const defaultMenu = require('electron-default-menu')
const contextMenu = require('electron-context-menu')
const { appSettingsPath, specterdDirPath, appSettings, platformName, appNameLower } = require('./src/config.js')
const { logger } = require('./src/logging.js')
const downloadloc = require('./downloadloc')
const { downloadSpecterd, destroyProgressbar } = require('./src/download.js')
const { startSpecterd, quitSpecterd } = require('./src/specterd.js')
const { getFileHash, versionData, isDev, devFolder, isMac } = require('./src/helpers.js')
const { getAppSettings } = require('./src/config.js')
const { showError, updatingLoaderMsg, updateSpecterdStatus, initMainWindow, loadUrl, initTray } = require('./src/uiHelpers.js')
const { hashesMatch, missingHashMessage, synchronizeSpecterdSettings } = require('./src/version-data.js')
// Quit again if there is no version-data in dev
if (isDev && versionData === undefined) {
console.log(
`You need to create a version-data.json in your dev folder (${devFolder}) to run the app. Check helpers.js for the format. Quitting ...`
)
app.quit()
return
}
ipcMain.handle('showMessageBoxSync', (e, message, buttons) => {
dialog.showMessageBoxSync(mainWindow, { message, buttons })
})
if (isDev) {
logger.info('Running the Electron app in dev mode.')
}
// Register "specter" protocol. This feature is for Mac only as of now.
// This will launch the app for URLs like this: specter://?action=importWallet&data=someData
if (isMac) {
const isDefaultProtocolClient = app.isDefaultProtocolClient('specter')
if (!isDefaultProtocolClient) {
app.setAsDefaultProtocolClient('specter')
}
}
// Set the dock icon (MacOS and for development only)
if (isMac && isDev) {
const dockIcon = nativeImage.createFromPath(app.getAppPath() + '/assets-dev/dock_icon_macos.png')
app.dock.setIcon(dockIcon)
}
let dimensions = { width: 1500, height: 1000 }
// Modify the context menu
contextMenu({
menu: (actions) => [
{
label: 'Reload',
click: () => {
mainWindow.reload()
},
},
{
label: 'Back',
click: () => {
mainWindow.webContents.goBack()
},
},
actions.separator(),
actions.copy(),
actions.cut(),
actions.paste(),
],
})
let specterdProcess
let automaticWalletImport = false
let mainWindow
let prefWindow
// Flag the app was quitted
let quitted = false
app.commandLine.appendSwitch('ignore-certificate-errors')
logger.info('Using version ' + appSettings.specterdVersion)
logger.info('Using platformName ' + platformName)
let trySavedAuth = true
app.on('login', function (event, webContents, request, authInfo, callback) {
event.preventDefault()
appSettings = getAppSettings() // ensure latest settings are used
if (appSettings.basicAuth && trySavedAuth) {
callback(appSettings.basicAuthUser, appSettings.basicAuthPass)
} else {
let user = ''
let pass = ''
let win = createNewWindow('basic_auth.html', 800, 600, mainWindow, true)
win.show()
win.on('close', (event) => {
win = null
callback(user, pass)
})
ipcMain.once('basic-auth', (event, creds) => {
if (win != null) {
user = creds.username
pass = creds.password
win.close()
}
})
}
// if we are prompted for auth again show the auth dialog
trySavedAuth = false
})
// 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.whenReady().then(() => {
// Create the tray icon
logger.info('Framework ready! Starting tray icon ...')
initTray(openPreferences, quitSpecterd)
dimensions = screen.getPrimaryDisplay().size
// create a new `splash`-Window
logger.info('Framework Ready! Initializing Main-Window, populating Menu ...')
mainWindow = initMainWindow(dimensions)
mainWindow.on('close', function (event) {
if (platformName == 'win64') {
quitSpecterd()
app.quit()
} else {
event.preventDefault()
mainWindow.hide()
}
})
setMainMenu()
loadUrl(`file://${__dirname}/src/splash.html`)
if (!fs.existsSync(specterdDirPath)) {
logger.info('Creating specterd-binaries folder:' + specterdDirPath)
fs.mkdirSync(specterdDirPath, { recursive: true })
}
const settingsUpdate = synchronizeSpecterdSettings(appSettings, versionData, process.arch)
if (!settingsUpdate.hash) {
const errorMessage = missingHashMessage(versionData, process.arch)
logger.error(errorMessage)
updatingLoaderMsg(errorMessage)
updateSpecterdStatus('Failed to validate this release...')
return
}
if (settingsUpdate.changed) {
logger.info(`Updating ${appSettingsPath} : ${JSON.stringify(appSettings)}`)
fs.writeFileSync(appSettingsPath, JSON.stringify(appSettings))
}
const specterdPath = specterdDirPath + '/' + appNameLower + 'd'
if (fs.existsSync(specterdPath + (platformName == 'win64' ? '.exe' : ''))) {
getFileHash(specterdPath + (platformName == 'win64' ? '.exe' : ''), function (specterdHash) {
if (hashesMatch(appSettings.specterdHash, specterdHash)) {
startSpecterd(specterdPath)
} else if (appSettings.specterdVersion != '') {
updatingLoaderMsg('Specterd version could not be validated. Trying again to download the Specter binary ...')
downloadSpecterd(specterdPath)
} else {
updatingLoaderMsg(
'Specterd file could not be validated and no version is configured in the settings<br>Please go to Preferences and set version to fetch or add an executable manually...',
false,
{ isHtml: true }
)
updateSpecterdStatus('Failed to locate specterd...')
}
})
} else {
if (appSettings.specterdVersion) {
downloadSpecterd(specterdPath)
} else {
// NB: smoketest workflow (.github/workflows/electron-smoketest.yml) greps
// for the leading "Specterd was not found and no version is configured"
// substring to assert splash reached this branch. Keep both in sync.
updatingLoaderMsg(
'Specterd was not found and no version is configured in the settings<br>Please go to Preferences and set version to fetch or add an executable manually...',
false,
{ isHtml: true }
)
updateSpecterdStatus('Failed to locate specterd...')
}
}
})
app.on('window-all-closed', function () {
if (platformName == 'win64') {
quitSpecterd()
app.quit()
}
})
// Cleanup before quitting (helps prevent memory leaks)
app.on('before-quit', (event) => {
if (!quitted) {
logger.info('Quitting ...')
quitted = true
quitSpecterd()
if (mainWindow && !mainWindow.isDestroyed()) {
destroyProgressbar()
mainWindow.destroy()
mainWindow = null
prefWindow = null
tray = null
}
}
})
ipcMain.on('open-settings', (event) => {
const senderUrl = event.senderFrame?.url || ''
if (!senderUrl.startsWith('file://') || !senderUrl.endsWith('/splash.html')) {
logger.warn(`Rejected open-settings IPC from untrusted sender: ${senderUrl}`)
return
}
openPreferences()
})
ipcMain.on('request-mainprocess-action', (event, arg) => {
switch (arg.message) {
case 'save-preferences':
// Child process already closed
if (!specterdProcess || specterdProcess.exitCode != null) {
prefWindow.webContents.executeJavaScript(`savePreferences()`)
} else {
specterdProcess.on('close', (code) => {
logger.info(`child process exited with code ${code}`)
prefWindow.webContents.executeJavaScript(`savePreferences()`)
})
quitSpecterd()
}
break
case 'quit-app':
quitSpecterd()
app.quit()
break
}
})
function setMainMenu() {
const menu = defaultMenu(app, shell)
// Add custom menu
if (platformName == 'osx') {
menu[0].submenu.splice(1, 0, {
label: 'Settings...', // This is a naming convention on MacOS. If you use just "Preferences", it gets translated to "Settings..." on MacOS.
click: openPreferences,
accelerator: 'CmdOrCtrl+,',
})
menu[0].submenu.splice(1, 0, {
label: 'Specter Logs',
click: openErrorLog,
accelerator: 'CmdOrCtrl+L',
})
} else {
menu.unshift({
label: 'Specter',
submenu: [
{
label: 'Settings',
click: openPreferences,
accelerator: 'CmdOrCtrl+,',
},
{
label: 'Specter Logs',
click: openErrorLog,
accelerator: 'CmdOrCtrl+L',
},
],
})
}
Menu.setApplicationMenu(Menu.buildFromTemplate(menu))
}
function createNewWindow(htmlContentFile, width, height, parent, modal) {
if (!width) {
width = 700
}
if (!height) {
height = 750
}
if (!parent) {
parent = null
}
if (!modal) {
modal = false
}
prefWindow = new BrowserWindow({
width: width,
height: height,
parent: parent,
modal: modal,
autoHideMenuBar: true,
webPreferences: {
nodeIntegration: true,
contextIsolation: false, // acceptable as this is not the mainwindow. No remote content!
enableRemoteModule: true,
},
})
prefWindow.webContents.on('new-window', function (e, url) {
e.preventDefault()
shell.openExternal(url)
})
prefWindow.loadURL(`file://${__dirname}/${htmlContentFile}`)
return prefWindow
}
function openPreferences() {
createNewWindow('settings.html', 800, 750, mainWindow).show()
}
function openErrorLog() {
;(width = parseInt(dimensions.width * 0.7)), (height = parseInt(dimensions.height * 0.7))
createNewWindow('error_logs.html', width, height).show()
}
process.on('unhandledRejection', (error) => {
showError(error)
logger.error(error.stack)
})
process.on('uncaughtException', (error) => {
showError(error)
// I would love to rethrow the error here as this would create a stacktrace in the logs
// but this will terminate the whole process even though i've set
// exitOnError: false in the winstonOptions above.
// Unacceptable for the folks which can't use a commandline, clicking an icon
//throw(error)
logger.error(error.stack)
})