diff --git a/.gitignore b/.gitignore index 1ef352c6e..2f909a404 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ pyinstaller/electron/downloadloc.js pyinstaller/electron/node_modules pyinstaller/electron/package-lock.json pyinstaller/electron/dist +pyinstaller/electron/signing_logs pyinstaller/electron/fonts pyinstaller/electron/typography.css pyinstaller/electron/output.css diff --git a/.vscode/settings.json b/.vscode/settings.json index e137fadb9..60b12ccec 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,9 @@ { "python.testing.unittestEnabled": false, - "python.testing.pytestEnabled": true + "python.testing.pytestEnabled": true, + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "[javascript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, } \ No newline at end of file diff --git a/pyinstaller/electron/build/entitlements.mac.plist b/pyinstaller/electron/build/entitlements.mac.plist index 16ee8e8fd..7e5286745 100644 --- a/pyinstaller/electron/build/entitlements.mac.plist +++ b/pyinstaller/electron/build/entitlements.mac.plist @@ -2,6 +2,8 @@ + com.apple.security.cs.allow-jit + com.apple.security.cs.allow-unsigned-executable-memory com.apple.security.device.camera diff --git a/pyinstaller/electron/error_logs.html b/pyinstaller/electron/error_logs.html index c1cc6bd41..a9fa6ac09 100644 --- a/pyinstaller/electron/error_logs.html +++ b/pyinstaller/electron/error_logs.html @@ -1,23 +1,20 @@ - - -
- This window shows you the last 700 lines Logs of the specterApp. This also includes - the Logs of specterd which are marked as such. It might give you hints on why specter is not coming up properly. - The best approach is to scroll to the bottom and then search upwards for errors. - You can find the logfile in [yourHomedirectory]/.specter/specterApp.log. -
-
- -
+ + +
+ This window shows you the last 700 lines Logs of the specterApp. This also includes the Logs of specterd which are + marked as such. It might give you hints on why specter is not coming up properly. The best approach is to scroll to the + bottom and then search upwards for errors. You can find the logfile in [yourHomedirectory]/.specter/specterApp.log. +
+
+
- - \ No newline at end of file + + diff --git a/pyinstaller/electron/helpers.js b/pyinstaller/electron/helpers.js deleted file mode 100644 index cb9c8392b..000000000 --- a/pyinstaller/electron/helpers.js +++ /dev/null @@ -1,125 +0,0 @@ -const fs = require('fs') -const os = require('os') -const path = require('path') -const crypto = require('crypto') -const readLastLines = require('read-last-lines'); -const downloadloc = require('./downloadloc'); -const appName = downloadloc.appName() -const appNameLower = appName.toLowerCase() -const isDev = process.env.NODE_ENV === "development" -const unresolvedDevFolder = process.env.SPECTER_DATA_FOLDER || "~/.specter_dev" -const devFolder = unresolvedDevFolder.replace(/^~/, os.homedir()); -const prodFolder = path.resolve(os.homedir(), `.${appNameLower}`) -const isMac = process.platform === 'darwin' - -let appSettingsPath -let specterdDirPath -let specterAppLogPath -let versionData - -// Use different version-data.jsons - -// Should look like this: -// { -// "version": "v2.0.0-pre32", -// "sha256": "aa049abf3e75199bad26fbded08ee5911ad48e325b42c43ec195136bd0736785" -// } - -if (isDev) { - let versionDataPath = `${devFolder}/version-data.json` - try { - versionData = require(versionDataPath) - } - catch { - } -} -else { - try { - versionData = require('./version-data.json') - } - catch (e) { - console.log('Could not find default version data configurations: '+e) - versionData = { - "version": "", - "sha256": "" - } - } -} - -if (isDev) { - appSettingsPath = `${devFolder}/app_settings.json` - specterdDirPath = `${devFolder}/specterd-binaries` - specterAppLogPath = `${devFolder}/specterApp.log` -} -else { - appSettingsPath = `${prodFolder}/app_settings.json` - specterdDirPath = `${prodFolder}/specterd-binaries` - specterAppLogPath = `${prodFolder}/specterApp.log` -} - -function getFileHash(filename, callback) { - let shasum = crypto.createHash('sha256') - // Updating shasum with file content - , s = fs.ReadStream(filename) - s.on('data', function(data) { - shasum.update(data) - }) - // making digest - s.on('end', function() { - var hash = shasum.digest('hex') - callback(hash) - }) -} - -function getAppSettings() { - let defaultSettings = { - mode: 'specterd', - specterURL: 'http://localhost:25441', - basicAuth: false, - basicAuthUser: '', - basicAuthPass: '', - tor: false, - proxyURL: "socks5://127.0.0.1:9050", - specterdVersion: (versionData && versionData.version !== undefined) ? versionData.version : 'unknown', - specterdHash: (versionData && versionData.sha256 !== undefined) ? versionData.sha256 : 'unknown', - specterdCLIArgs: "", - versionInitialized: false - } - - try { - if (!fs.existsSync(appSettingsPath)){ - fs.mkdirSync(path.resolve(appSettingsPath, '..'), { recursive: true }); - } - fs.writeFileSync(appSettingsPath, JSON.stringify(defaultSettings), { flag: 'wx' }); - } catch { - // settings file already exists - } - - // Make sure to add missing settings in case the format changed or new settings were added - let appSettings = require(appSettingsPath) - for (let key of Object.keys(defaultSettings)) { - if (!appSettings.hasOwnProperty(key)) { - appSettings[key] = defaultSettings[key] - } - } - - return appSettings -} - -function getSpecterAppLogs(callback) { - readLastLines.read(specterAppLogPath, 700) - .then(callback); -} - -module.exports = { - getFileHash: getFileHash, - appSettingsPath: appSettingsPath, - getAppSettings: getAppSettings, - specterdDirPath: specterdDirPath, - getSpecterAppLogs: getSpecterAppLogs, - specterAppLogPath: specterAppLogPath, - versionData, - isDev: isDev, - devFolder, - isMac, isMac -} diff --git a/pyinstaller/electron/main.js b/pyinstaller/electron/main.js index e4f59e193..3607d287c 100644 --- a/pyinstaller/electron/main.js +++ b/pyinstaller/electron/main.js @@ -1,30 +1,18 @@ // Modules to control application life and create native browser window -const { app, nativeTheme, nativeImage, BrowserWindow, Menu, Tray, screen, shell, dialog, ipcMain } = require('electron') - -const path = require('path') const fs = require('fs') -const { URL } = require('node:url') -const request = require('request') -const https = require('https') -const extract = require('extract-zip') -const defaultMenu = require('electron-default-menu') -const ProgressBar = require('electron-progressbar') const { spawn, exec } = require('child_process') -const { - getFileHash, - getAppSettings, - appSettingsPath, - specterdDirPath, - specterAppLogPath, - versionData, - isDev, - devFolder, - isMac, -} = require('./helpers') +const { app, nativeTheme, nativeImage, BrowserWindow, Menu, Tray, 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 } = require('./src/download.js') const getDownloadLocation = downloadloc.getDownloadLocation -const appName = downloadloc.appName() -const appNameLower = appName.toLowerCase() +const { startSpecterd, quitSpecterd } = require('./src/specterd.js') +const { getFileHash, getAppSettings, versionData, isDev, devFolder, isMac } = require('./src/helpers.js') +const { showError, updatingLoaderMsg, initMainWindow, loadUrl, initTray } = require('./src/uiHelpers.js') // Quit again if there is no version-data in dev if (isDev && versionData === undefined) { @@ -39,23 +27,6 @@ ipcMain.handle('showMessageBoxSync', (e, message, buttons) => { dialog.showMessageBoxSync(mainWindow, { message, buttons }) }) -// Logging -const { transports, format, createLogger } = require('winston') -const combinedLog = new transports.File({ filename: specterAppLogPath }) -const winstonOptions = { - exitOnError: false, - format: format.combine( - format.timestamp(), - format.json(), - format.printf((info) => { - return `${info.timestamp} [${info.level}] : ${info.message}` - }) - ), - transports: [new transports.Console({ json: false }), combinedLog], - exceptionHandlers: [combinedLog], -} -const logger = createLogger(winstonOptions) - if (isDev) { logger.info('Running the Electron app in dev mode.') } @@ -75,11 +46,10 @@ if (isMac && isDev) { app.dock.setIcon(dockIcon) } -let appSettings = getAppSettings() let dimensions = { width: 1500, height: 1000 } // Modify the context menu -const contextMenu = require('electron-context-menu') + contextMenu({ menu: (actions) => [ { @@ -101,136 +71,16 @@ contextMenu({ ], }) -// The standard quit item cannot be replaced / modified and it is not triggering the -// before-quit event on MacOS if a child window is open -const dockMenuWithforceQuit = Menu.buildFromTemplate([ - { - label: 'Force Quit during download', - click: () => { - // If the progress bar exists, close it - if (progressBar) { - progressBar.close() - } - // Quit the app - app.quit() - }, - }, -]) - -// Download function with progress bar -let progressBar -const download = (uri, filename, callback) => { - // HEAD request first - request.head(uri, (err, res, body) => { - if (res.statusCode != 404) { - let receivedBytes = 0 - const totalBytes = res.headers['content-length'] - logger.info(`Total size to download: ${totalBytes}`) - progressBar = new ProgressBar({ - indeterminate: false, - abortOnError: true, - text: 'Downloading the Specter binary from GitHub', - detail: - 'This can take several minutes depending on your Internet connection. Specter will start once the download is finished.', - maxValue: totalBytes, - browserWindow: { - parent: mainWindow, - }, - style: { - detail: { - 'margin-bottom': '12px', - }, - bar: { - 'background-color': '#fff', - }, - value: { - 'background-color': '#000', - }, - }, - }) - - // Add Force Quit item during download for MacOS dock - if (isMac) { - app.dock.setMenu(dockMenuWithforceQuit) - } - - progressBar.on('completed', () => { - progressBar.close() - // Remove the Force Quit dock item again for Mac - if (isMac) { - const updatedDockMenu = Menu.buildFromTemplate( - dockMenuWithforceQuit.items.filter((item) => item.label !== 'Force Quit during download') - ) - app.dock.setMenu(updatedDockMenu) - } - }) - - progressBar.on('aborted', () => { - logger.info('Download was aborted before it could finish.') - }) - - // Loggin the download progress - let lastLogTime = 0 - const logInterval = 5000 // log every 5 seconds - progressBar.on('progress', () => { - const currentTime = Date.now() - if (currentTime - lastLogTime >= logInterval) { - lastLogTime = currentTime - logger.info(`Download status: ${((receivedBytes / totalBytes) * 100).toFixed(0)}%`) - } - }) - - // GET request - request(uri) - .on('data', (chunk) => { - receivedBytes += chunk.length - if (progressBar) { - progressBar.value = receivedBytes - } - }) - .pipe(fs.createWriteStream(filename)) - .on('close', callback) - } - // If the download link was not found, call callback (updatingLoaderMsg with error feedback) - else { - logger.info(`Error while trying to download specterd: ${err}`) - callback(true) - } - }) -} - let specterdProcess let automaticWalletImport = false let mainWindow let prefWindow -let tray -let trayMenu // Flag the app was quitted let quitted = false -let webPreferences = { - worldSafeExecuteJavaScript: true, - contextIsolation: true, - preload: path.join(__dirname, 'preload.js'), -} - app.commandLine.appendSwitch('ignore-certificate-errors') -let platformName = '' -switch (process.platform) { - case 'darwin': - platformName = 'osx' - break - case 'win32': - platformName = 'win64' - break - case 'linux': - platformName = 'x86_64-linux-gnu' - break - default: - throw `Unknown platformName ${platformName}` -} logger.info('Using version ' + appSettings.specterdVersion) logger.info('Using platformName ' + platformName) @@ -261,85 +111,35 @@ app.on('login', function (event, webContents, request, authInfo, callback) { trySavedAuth = false }) -function createWindow(specterURL) { - if (!mainWindow) { - initMainWindow() - } - - // Create the browser window. - if (appSettings.tor) { - mainWindow.webContents.session.setProxy({ proxyRules: appSettings.proxyURL }) - } - mainWindow.loadURL(specterURL + '?mode=remote') -} - // 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 ...') - if (isMac) { - const trayIconPath = nativeTheme.shouldUseDarkColors ? '/assets/menu_icon_dark.png' : '/assets/menu_icon_light.png' - const createTrayIcon = (trayIconPath) => { - let trayIcon = nativeImage.createFromPath(app.getAppPath() + trayIconPath) - // Resize - trayIcon = trayIcon.resize({ width: 22, height: 22 }) - return trayIcon - } - const trayIcon = createTrayIcon(trayIconPath) - tray = new Tray(trayIcon) - - // Change the tray icon if appearance is changed in Mac settings - const updateTrayIcon = () => { - logger.info('Updating tray icon ...') - const trayIconPath = nativeTheme.shouldUseDarkColors ? '/assets/menu_icon_dark.png' : '/assets/menu_icon_light.png' - const newTrayIcon = createTrayIcon(trayIconPath) - tray.setImage(newTrayIcon) - } - nativeTheme.on('updated', updateTrayIcon) - } else { - const trayIcon = nativeImage.createFromPath(app.getAppPath() + '/assets/menu_icon.png') - tray = new Tray(trayIcon) - } - - trayMenu = [ - { label: 'Launching Specter ...', enabled: false }, - { - label: 'Show Specter', - click() { - mainWindow.show() - }, - }, - { - label: 'Settings', - click() { - openPreferences() - }, - }, - { - label: 'Quit', - click() { - quitSpecterd() - app.quit() - }, - }, - ] - tray.setToolTip('Specter') - tray.setContextMenu(Menu.buildFromTemplate(trayMenu)) + initTray(openPreferences, quitSpecterd) dimensions = screen.getPrimaryDisplay().size // create a new `splash`-Window logger.info('Framework Ready! Initializing Main-Window, populating Menu ...') - initMainWindow() + mainWindow = initMainWindow(dimensions) + mainWindow.on('close', function (event) { + if (platformName == 'win64') { + quitSpecterd() + app.quit() + } else { + event.preventDefault() + mainWindow.hide() + } + }) setMainMenu() - mainWindow.loadURL(`file://${__dirname}/splash.html`) + loadUrl(`file://${__dirname}/splash.html`) if (!fs.existsSync(specterdDirPath)) { - logger.info('Creating specterd-binaries folder') + logger.info('Creating specterd-binaries folder:' + specterdDirPath) fs.mkdirSync(specterdDirPath, { recursive: true }) } @@ -377,294 +177,6 @@ app.whenReady().then(() => { } }) -function initMainWindow() { - // In production we use the icons from the build folder - // Note: On MacOS setting an icon here as no effect - const iconPath = isDev ? path.join(__dirname, 'assets-dev/app_icon.png') : '' - mainWindow = new BrowserWindow({ - width: parseInt(dimensions.width * 0.8), - minWidth: 1120, - height: parseInt(dimensions.height * 0.8), - icon: iconPath, - webPreferences, - }) - - // Ensures that any links with target="_blank" or window.open() will be opened in the user's default browser instead of within the app - mainWindow.webContents.setWindowOpenHandler(({ url }) => { - shell.openExternal(url) - return { action: 'deny' } - }) - - mainWindow.on('close', function (event) { - if (platformName == 'win64') { - quitSpecterd() - app.quit() - } else { - event.preventDefault() - mainWindow.hide() - } - }) - - mainWindow.webContents.on('did-fail-load', function () { - mainWindow.loadURL(`file://${__dirname}/splash.html`) - updatingLoaderMsg( - `Failed to load: ${appSettings.specterURL}
Please make sure the URL is entered correctly in the settings and try again...` - ) - }) -} - -function downloadSpecterd(specterdPath) { - updatingLoaderMsg(`Starting download`) - updateSpecterdStatus(`Downloading the ${appName} binary...`) - // Some logging - logger.info('Using version ' + appSettings.specterdVersion) - logger.info('Using platformName ' + platformName) - download_location = getDownloadLocation(appSettings.specterdVersion, platformName) - logger.info('Downloading from ' + download_location) - download(download_location, specterdPath + '.zip', function (errored) { - if (errored == true) { - updatingLoaderMsg( - `Downloading the ${appNameLower} binary from GitHub failed, could not reach the server or the file wasn't found.` - ) - updateSpecterdStatus(`Downloading ${appNameLower}d failed...`) - return - } - updatingLoaderMsg('Download completed. Unpacking files...') - logger.info('Extracting ' + specterdPath) - - extract(specterdPath + '.zip', { dir: specterdPath + '-dir' }).then(function () { - let extraPath = '' - switch (process.platform) { - case 'darwin': - extraPath = appNameLower + 'd' - break - case 'win32': - extraPath = appNameLower + 'd.exe' - break - case 'linux': - extraPath = appNameLower + 'd' - } - var oldPath = specterdPath + `-dir/${extraPath}` - var newPath = specterdPath + (platformName == 'win64' ? '.exe' : '') - - fs.renameSync(oldPath, newPath) - fs.unlinkSync(specterdPath + '.zip') - fs.rmdirSync(specterdPath + '-dir', { recursive: true }) - getFileHash(specterdPath + (platformName == 'win64' ? '.exe' : ''), function (specterdHash) { - if (appSettings.specterdHash.toLowerCase() === specterdHash || appSettings.specterdHash == '') { - startSpecterd(specterdPath) - } else { - updatingLoaderMsg('Specterd version could not be validated.') - logger.error(`hash of downloaded file: ${specterdHash}`) - logger.error(`Expected hash: ${appSettings.specterdHash}`) - updateSpecterdStatus('Failed to launch specterd...') - } - }) - }) - }) -} - -function updateSpecterdStatus(status) { - trayMenu[0] = { label: status, enabled: false } - tray.setContextMenu(Menu.buildFromTemplate(trayMenu)) -} - -function updatingLoaderMsg(msg, showSpinner = false) { - if (mainWindow) { - let code = ` - var launchText = document.getElementById('launch-text'); - if (launchText) { - launchText.innerHTML = '${msg}'; - } - var spinnerElement = document.getElementById('spinner'); - if (spinnerElement) { - if (${showSpinner} === true) { - spinnerElement.classList.remove('hidden') - } - else { - spinnerElement.classList.add('hidden') - } - } - ` - mainWindow.webContents.executeJavaScript(code) - } - logger.info('Updated LoaderMsg: ' + msg) -} - -function checkSpecterd(logs, specterdStarted) { - // There doesn't seem to be another more straightforward way to check whether specterd is running: https://github.com/nodejs/help/issues/1191 - // Setting a timeout to avoid waiting for specterd endlessly - const timeout = 180000 // 3 minutes - const now = Date.now() - const timeElapsed = now - specterdStarted - if (timeElapsed > timeout) { - return 'timeout' - } - if (logs.toString().includes('Serving Flask app')) { - return 'running' - } else { - return 'not running' - } -} - -let specterIsRunning = false -function startSpecterd(specterdPath) { - if (platformName == 'win64') { - specterdPath += '.exe' - } - let appSettings = getAppSettings() - let hwiBridgeMode = appSettings.mode == 'hwibridge' - updatingLoaderMsg('Launching Specter ...', (showSpinner = true)) - updateSpecterdStatus('Launching Specter ...') - let specterdArgs = ['server'] - specterdArgs.push('--no-filelog') - if (hwiBridgeMode) specterdArgs.push('--hwibridge') - if (appSettings.specterdCLIArgs != '') { - // User has inputed cli arguments in the UI - let specterdExtraArgs = appSettings.specterdCLIArgs.split(' ') - specterdExtraArgs.forEach((arg) => { - // Ensures that whitespaces are not used as cli arguments - if (arg != '') { - specterdArgs.push(arg) - } - }) - } - // locale fix (copying from nodejs-env + adding locales) - const options = { - env: { ...process.env }, - } - options.env['LC_ALL'] = 'en_US.utf-8' - options.env['LANG'] = 'en_US.utf-8' - options.env['SPECTER_LOGFORMAT'] = 'SPECTERD: %(levelname)s in %(module)s: %(message)s' - specterdProcess = spawn(specterdPath, specterdArgs, options) - const specterdStarted = Date.now() - - // We are checking for both, stdout and stderr, to be on the save side. - specterdProcess.stdout.on('data', (data) => { - logger.info('stdout-' + data.toString()) - let serverdStatus = checkSpecterd(data, specterdStarted) - // We don't want to check the logs forever, just until specterd is up and running - if (!specterIsRunning) { - if (serverdStatus === 'running') { - logger.info(`Specter server seems to run ...`) - updateSpecterdStatus('Specter is running') - specterIsRunning = true - if (mainWindow) { - if (automaticWalletImport === true) { - logger.info('Performing automatic wallet import ...') - updatingLoaderMsg('Launching wallet importer. This will only work with a node connection.', (showSpinner = true)) - setTimeout(() => { - importWallet(walletDataFromUrl) - }, 3000) - } else { - logger.info('Normal startup of Specter.') - createWindow(appSettings.specterURL) - } - } - } else if (serverdStatus === 'timeout') { - showError('Specter does not seem to start. Check the logs in the menu for more details.') - updateSpecterdStatus('Specter does not start') - logger.error('Startup timeout for specterd exceeded') - } else { - updatingLoaderMsg('Still waiting for Specter to start ...') - updateSpecterdStatus('Specter is starting') - } - } - }) - - specterdProcess.stderr.on('data', (data) => { - logger.info('stderr-' + data.toString()) - let serverdStatus = checkSpecterd(data, specterdStarted) - if (!specterIsRunning) { - if (serverdStatus === 'running') { - logger.info(`Specter server seems to run ...`) - updateSpecterdStatus('Specter is running') - specterIsRunning = true - if (mainWindow) { - logger.info('... creating Electron window for it.') - createWindow(appSettings.specterURL) - } - } else if (serverdStatus === 'timeout') { - showError('Specter does not seem to start. Check the logs in the menu for more details.') - updateSpecterdStatus('Specter does not start') - logger.error('Startup timeout for specterd exceeded') - } else { - updatingLoaderMsg('Still waiting for Specter to start ...') - updateSpecterdStatus('Specter is starting') - } - } - }) - - specterdProcess.on('exit', (code) => { - logger.error(`specterd exited with code ${code}`) - showError(`Specter exited with exit code ${code}. Check the logs in the menu for more details.`) - }) - - specterdProcess.on('error', (err) => { - logger.error(`Error starting Specter server: ${err}`) - showError(`Specter failed to start, due to ${err.message}. Check the logs in the menu for more details.`) - }) - - app.on('activate', function () { - // On macOS it's common to re-create a window in the app when the - // dock icon is clicked and there are no other windows open. - if (BrowserWindow.getAllWindows().length === 0) createWindow(appSettings.specterURL) - }) - // since these are streams, you can pipe them elsewhere - specterdProcess.on('close', (code) => { - updateSpecterdStatus('Specter stopped...') - logger.info(`child process exited with code ${code}`) - }) -} - -let walletDataFromUrl -// Checking whether the app was opened via a Specter URL and determine whether to perform a specific startup action -app.on('open-url', (_, url) => { - logger.info('The app was opened via URL, checking the URL to decide whether to do any automatic actions ...') - // Parse the URL to extract the query parameters - const specterUrl = new URL(url) - const searchParams = specterUrl.searchParams - // Get the query parameter values - const action = searchParams.get('action') - const data = searchParams.get('data') - if (action === 'importWallet' && data !== '') { - logger.info('Automatic wallet import identified in the URL, setting automaticWalletImport to true.') - automaticWalletImport = true - walletDataFromUrl = data - // Directly import if the app and specterd is already running - if (specterIsRunning) { - logger.info('Performing automatic wallet import ...') - mainWindow.loadURL(`file://${__dirname}/splash.html`) - updatingLoaderMsg('Launching wallet importer. This will only work with a node connection.', (showSpinner = true)) - setTimeout(() => { - importWallet(walletDataFromUrl) - }, 3000) - } - } -}) - -// Automatically import the wallet json string, bring user to the final import wallet screen. -// Only proceed with the import if the importFromWalletSoftwareBtn can be found. -// If it is not, users are redirected by specterd to the configure connection screen. -function importWallet(walletData) { - mainWindow.loadURL(appSettings.specterURL + '/wallets/new_wallet/') - if (mainWindow) { - let code = ` - const importFromWalletSoftwareBtn = document.getElementById('import-from-wallet-software-btn') - if (importFromWalletSoftwareBtn) { - importFromWalletSoftwareBtn.click() - const walletDataTextArea = document.getElementById('txt') - if (walletDataTextArea) { - walletDataTextArea.value = \`${walletData}\` - } - const continueBtn = document.getElementById('continue-with-wallet-import-btn'); - continueBtn.click() - } - ` - mainWindow.webContents.executeJavaScript(code) - } -} - app.on('window-all-closed', function () { if (platformName == 'win64') { quitSpecterd() @@ -715,21 +227,6 @@ ipcMain.on('request-mainprocess-action', (event, arg) => { } }) -function quitSpecterd() { - if (specterdProcess) { - try { - if (platformName == 'win64') { - exec('taskkill /F /T /PID ' + specterdProcess.pid) - exec('taskkill /IM specterd.exe ') - process.kill(-specterdProcess.pid) - } - specterdProcess.kill('SIGINT') - } catch (e) { - logger.info('Specterd quit warning: ' + e) - } - } -} - function setMainMenu() { const menu = defaultMenu(app, shell) @@ -808,21 +305,17 @@ function openErrorLog() { createNewWindow('error_logs.html', width, height).show() } -function showError(error) { - updatingLoaderMsg('Specter encountered an error:
' + error.toString()) -} - process.on('unhandledRejection', (error) => { showError(error) - logger.error(error.toString(), error.name) + 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 wistonOptions above. + // exitOnError: false in the winstonOptions above. // Unacceptable for the folks which can't use a commandline, clicking an icon //throw(error) - logger.error(error.toString()) + logger.error(error.stack) }) diff --git a/pyinstaller/electron/package-lock.json b/pyinstaller/electron/package-lock.json index 65defe8ed..108006fbd 100644 --- a/pyinstaller/electron/package-lock.json +++ b/pyinstaller/electron/package-lock.json @@ -1,26 +1,26 @@ { "name": "Specter", - "version": "v0.0.0", + "version": "v2.0.3-pre1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "Specter", - "version": "v0.0.0", + "version": "v2.0.3-pre1", "hasInstallScript": true, "license": "MIT", "dependencies": { - "electron-context-menu": "^2.3.0", + "electron-context-menu": "^2.5.2", "electron-default-menu": "^1.0.2", - "electron-progressbar": "^2.0.1", + "electron-progressbar": "^2.1.0", "extract-zip": "^2.0.1", "read-last-lines": "^1.8.0", "request": "^2.88.2", "winston": "^3.3.3" }, "devDependencies": { - "electron": "^22.1.0", - "electron-builder": "^23.3.1" + "electron": "^22.3.12", + "electron-builder": "^23.6.0" } }, "node_modules/@dabh/diagnostics": { @@ -1421,9 +1421,9 @@ } }, "node_modules/electron": { - "version": "22.1.0", - "resolved": "https://registry.npmjs.org/electron/-/electron-22.1.0.tgz", - "integrity": "sha512-wz5s4N6V7zyKm4YQmXJImFoxO1Doai32ShYm0FzOLPBMwLMdQBV+REY+j1opRx0KJ9xJEIdjYgcA8OSw6vx3pA==", + "version": "22.3.27", + "resolved": "https://registry.npmjs.org/electron/-/electron-22.3.27.tgz", + "integrity": "sha512-7Rht21vHqj4ZFRnKuZdFqZFsvMBCmDqmjetiMqPtF+TmTBiGne1mnstVXOA/SRGhN2Qy5gY5bznJKpiqogjM8A==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -1502,13 +1502,16 @@ } }, "node_modules/electron-context-menu": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/electron-context-menu/-/electron-context-menu-2.3.0.tgz", - "integrity": "sha512-XYsYkNY+jvX4C5o09qMuZoKL6e9frnQzBFehZSIiKp6zK0u3XYowJYDyK3vDKKZxYsOIGiE/Gbx40jERC03Ctw==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/electron-context-menu/-/electron-context-menu-2.5.2.tgz", + "integrity": "sha512-1cEQR6fA9ktFsRBc+eXPwvrOgAPytUD7rUV4iBAA5zTrLAPKokJ23xeMjcK2fjrDPrlFRBxcLz0KP+GUhMrSCQ==", "dependencies": { - "cli-truncate": "^2.0.0", - "electron-dl": "^3.0.0", - "electron-is-dev": "^1.0.1" + "cli-truncate": "^2.1.0", + "electron-dl": "^3.1.0", + "electron-is-dev": "^1.2.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/electron-default-menu": { @@ -1517,13 +1520,19 @@ "integrity": "sha512-YAL/UNR3kPG58wOOlmDpTG3i6+bzwhHx6NllIOaLuVrU7uYifeYGGdk5IH2Hap4wVEx2YTA8cqQ2PGSplYwDWQ==" }, "node_modules/electron-dl": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/electron-dl/-/electron-dl-3.0.2.tgz", - "integrity": "sha512-pRgE9Jbhoo5z6Vk3qi+vIrfpMDlCp2oB1UeR96SMnsfz073jj0AZGQwp69EdIcEvlUlwBSGyJK8Jt6OB6JLn+g==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/electron-dl/-/electron-dl-3.5.2.tgz", + "integrity": "sha512-i104cl+u8yJ0lhpRAtUWfeGuWuL1PL6TBiw2gLf0MMIBjfgE485Ags2mcySx4uWU9P9uj/vsD3jd7X+w1lzZxw==", "dependencies": { "ext-name": "^5.0.0", "pupa": "^2.0.1", "unused-filename": "^2.1.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/electron-is-dev": { @@ -1581,9 +1590,9 @@ "dev": true }, "node_modules/electron-progressbar": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/electron-progressbar/-/electron-progressbar-2.0.1.tgz", - "integrity": "sha512-+N60GX2q+KH5OvZXxwtjMTZB/1AyxriFd95vOnR3sOfNpvz+30LMsM0a9SnEivZE6N8Djy7F3z4TY8pLs8aopw==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/electron-progressbar/-/electron-progressbar-2.2.1.tgz", + "integrity": "sha512-LQ9bxM3Tf5PG/1QngY8ywvht7IKvQ8tEIra8uh3RkLASqN/GYvr4r0uU9qz38r0Zn72UcouYzumKDfLwoI/rsw==", "dependencies": { "extend": "^3.0.1" } @@ -2216,7 +2225,7 @@ "node_modules/is-plain-obj": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", "engines": { "node": ">=0.10.0" } @@ -2514,7 +2523,7 @@ "node_modules/modify-filename": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/modify-filename/-/modify-filename-1.1.0.tgz", - "integrity": "sha1-mi3sg4Bvuy2XXyK+7IWcoms5OqE=", + "integrity": "sha512-EickqnKq3kVVaZisYuCxhtKbZjInCuwgwZWyAmRIp1NTMhri7r3380/uqwrUHfaDiPzLVTuoNy4whX66bxPVog==", "engines": { "node": ">=0.10.0" } @@ -2669,9 +2678,9 @@ } }, "node_modules/pupa": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.0.1.tgz", - "integrity": "sha512-hEJH0s8PXLY/cdXh66tNEQGndDrIKNqNC5xmrysZy3i5C3oEoLna7YAOad+7u125+zH1HNXUmGEkrhb3c2VriA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.1.1.tgz", + "integrity": "sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==", "dependencies": { "escape-goat": "^2.0.0" }, @@ -2968,7 +2977,7 @@ "node_modules/sort-keys": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", - "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=", + "integrity": "sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg==", "dependencies": { "is-plain-obj": "^1.0.0" }, @@ -2979,7 +2988,7 @@ "node_modules/sort-keys-length": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz", - "integrity": "sha1-nLb09OnkgVWmqgZx7dM2/xR5oYg=", + "integrity": "sha512-GRbEOUqCxemTAk/b32F2xa8wDTs+Z1QHOkbhJDQTvv/6G3ZkbJ+frYWsTcc7cBB3Fu4wy4XlLCuNtJuMn7Gsvw==", "dependencies": { "sort-keys": "^1.0.0" }, @@ -4592,9 +4601,9 @@ } }, "electron": { - "version": "22.1.0", - "resolved": "https://registry.npmjs.org/electron/-/electron-22.1.0.tgz", - "integrity": "sha512-wz5s4N6V7zyKm4YQmXJImFoxO1Doai32ShYm0FzOLPBMwLMdQBV+REY+j1opRx0KJ9xJEIdjYgcA8OSw6vx3pA==", + "version": "22.3.27", + "resolved": "https://registry.npmjs.org/electron/-/electron-22.3.27.tgz", + "integrity": "sha512-7Rht21vHqj4ZFRnKuZdFqZFsvMBCmDqmjetiMqPtF+TmTBiGne1mnstVXOA/SRGhN2Qy5gY5bznJKpiqogjM8A==", "dev": true, "requires": { "@electron/get": "^2.0.0", @@ -4652,13 +4661,13 @@ } }, "electron-context-menu": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/electron-context-menu/-/electron-context-menu-2.3.0.tgz", - "integrity": "sha512-XYsYkNY+jvX4C5o09qMuZoKL6e9frnQzBFehZSIiKp6zK0u3XYowJYDyK3vDKKZxYsOIGiE/Gbx40jERC03Ctw==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/electron-context-menu/-/electron-context-menu-2.5.2.tgz", + "integrity": "sha512-1cEQR6fA9ktFsRBc+eXPwvrOgAPytUD7rUV4iBAA5zTrLAPKokJ23xeMjcK2fjrDPrlFRBxcLz0KP+GUhMrSCQ==", "requires": { - "cli-truncate": "^2.0.0", - "electron-dl": "^3.0.0", - "electron-is-dev": "^1.0.1" + "cli-truncate": "^2.1.0", + "electron-dl": "^3.1.0", + "electron-is-dev": "^1.2.0" } }, "electron-default-menu": { @@ -4667,9 +4676,9 @@ "integrity": "sha512-YAL/UNR3kPG58wOOlmDpTG3i6+bzwhHx6NllIOaLuVrU7uYifeYGGdk5IH2Hap4wVEx2YTA8cqQ2PGSplYwDWQ==" }, "electron-dl": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/electron-dl/-/electron-dl-3.0.2.tgz", - "integrity": "sha512-pRgE9Jbhoo5z6Vk3qi+vIrfpMDlCp2oB1UeR96SMnsfz073jj0AZGQwp69EdIcEvlUlwBSGyJK8Jt6OB6JLn+g==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/electron-dl/-/electron-dl-3.5.2.tgz", + "integrity": "sha512-i104cl+u8yJ0lhpRAtUWfeGuWuL1PL6TBiw2gLf0MMIBjfgE485Ags2mcySx4uWU9P9uj/vsD3jd7X+w1lzZxw==", "requires": { "ext-name": "^5.0.0", "pupa": "^2.0.1", @@ -4722,9 +4731,9 @@ } }, "electron-progressbar": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/electron-progressbar/-/electron-progressbar-2.0.1.tgz", - "integrity": "sha512-+N60GX2q+KH5OvZXxwtjMTZB/1AyxriFd95vOnR3sOfNpvz+30LMsM0a9SnEivZE6N8Djy7F3z4TY8pLs8aopw==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/electron-progressbar/-/electron-progressbar-2.2.1.tgz", + "integrity": "sha512-LQ9bxM3Tf5PG/1QngY8ywvht7IKvQ8tEIra8uh3RkLASqN/GYvr4r0uU9qz38r0Zn72UcouYzumKDfLwoI/rsw==", "requires": { "extend": "^3.0.1" } @@ -5220,7 +5229,7 @@ "is-plain-obj": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=" + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==" }, "is-stream": { "version": "2.0.1", @@ -5445,7 +5454,7 @@ "modify-filename": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/modify-filename/-/modify-filename-1.1.0.tgz", - "integrity": "sha1-mi3sg4Bvuy2XXyK+7IWcoms5OqE=" + "integrity": "sha512-EickqnKq3kVVaZisYuCxhtKbZjInCuwgwZWyAmRIp1NTMhri7r3380/uqwrUHfaDiPzLVTuoNy4whX66bxPVog==" }, "ms": { "version": "2.1.2", @@ -5570,9 +5579,9 @@ "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" }, "pupa": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.0.1.tgz", - "integrity": "sha512-hEJH0s8PXLY/cdXh66tNEQGndDrIKNqNC5xmrysZy3i5C3oEoLna7YAOad+7u125+zH1HNXUmGEkrhb3c2VriA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.1.1.tgz", + "integrity": "sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==", "requires": { "escape-goat": "^2.0.0" } @@ -5809,7 +5818,7 @@ "sort-keys": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", - "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=", + "integrity": "sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg==", "requires": { "is-plain-obj": "^1.0.0" } @@ -5817,7 +5826,7 @@ "sort-keys-length": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz", - "integrity": "sha1-nLb09OnkgVWmqgZx7dM2/xR5oYg=", + "integrity": "sha512-GRbEOUqCxemTAk/b32F2xa8wDTs+Z1QHOkbhJDQTvv/6G3ZkbJ+frYWsTcc7cBB3Fu4wy4XlLCuNtJuMn7Gsvw==", "requires": { "sort-keys": "^1.0.0" } diff --git a/pyinstaller/electron/package.json b/pyinstaller/electron/package.json index 859ababcd..37d6b5de8 100644 --- a/pyinstaller/electron/package.json +++ b/pyinstaller/electron/package.json @@ -20,8 +20,8 @@ "author": "Specter", "license": "MIT", "devDependencies": { - "electron": "^22.1.0", - "electron-builder": "^23.3.1" + "electron": "^22.3.12", + "electron-builder": "^23.6.0" }, "build": { "productName": "Specter", @@ -30,9 +30,9 @@ "category": "public.app-category.utilities", "identity": "Kim Neunert (FWV59JHV83)", "entitlements": "./build/entitlements.mac.plist", - "entitlementsInherit": "./build/entitlements.mac.plist", "hardenedRuntime": true, - "icon": "./icons/icon.icns" + "icon": "./icons/icon.icns", + "provisioningProfile": "/Users/kim/Specter_Desktop_vanilla.provisionprofile" }, "win": { "icon": "./icons/icon.ico" @@ -45,12 +45,12 @@ } }, "dependencies": { - "electron-context-menu": "^2.3.0", + "electron-context-menu": "^2.5.2", "electron-default-menu": "^1.0.2", - "electron-progressbar": "^2.0.1", + "electron-progressbar": "^2.1.0", "extract-zip": "^2.0.1", "read-last-lines": "^1.8.0", "request": "^2.88.2", "winston": "^3.3.3" } -} \ No newline at end of file +} diff --git a/pyinstaller/electron/preload.js b/pyinstaller/electron/preload.js index c2ff9c169..9811c142c 100644 --- a/pyinstaller/electron/preload.js +++ b/pyinstaller/electron/preload.js @@ -1,11 +1,19 @@ // All of the Node.js APIs are available in the preload process. // It has the same sandbox as a Chrome extension. + +const { contextBridge, ipcRenderer } = require('electron') + +contextBridge.exposeInMainWorld('api', { + send: (channel, data) => ipcRenderer.send(channel, data), + receive: (channel, func) => ipcRenderer.on(channel, (event, ...args) => func(...args)), +}) + window.addEventListener('DOMContentLoaded', () => { + // No idea why this is here const replaceText = (selector, text) => { const element = document.getElementById(selector) if (element) element.innerText = text } - for (const type of ['chrome', 'node', 'electron']) { replaceText(`${type}-version`, process.versions[type]) } diff --git a/pyinstaller/electron/renderer.js b/pyinstaller/electron/renderer.js index d3bdade6d..47d051290 100644 --- a/pyinstaller/electron/renderer.js +++ b/pyinstaller/electron/renderer.js @@ -4,3 +4,25 @@ // `nodeIntegration` is turned off. Use `preload.js` to // selectively enable features needed in the rendering // process. + +// All of the Node.js APIs are available in the preload process. +// It has the same sandbox as a Chrome extension. +window.addEventListener('DOMContentLoaded', () => { + + + const updateSpinner = (show) => { + const spinnerElement = document.getElementById('spinner'); + if (spinnerElement) { + spinnerElement.classList.toggle('hidden', !show); + } + }; + window.api.receive('update-loader-message', (data) => { + const launchTextElement = document.getElementById('launch-text'); + if (launchTextElement) { + launchTextElement.textContent = data.msg; + updateSpinner(data.showSpinner); + } + }); + + + }) \ No newline at end of file diff --git a/pyinstaller/electron/splash.html b/pyinstaller/electron/splash.html index 51d6fad40..f56b4579b 100644 --- a/pyinstaller/electron/splash.html +++ b/pyinstaller/electron/splash.html @@ -6,4 +6,5 @@

Launching Specter Desktop...

+ diff --git a/pyinstaller/electron/src/config.js b/pyinstaller/electron/src/config.js new file mode 100644 index 000000000..b656030c4 --- /dev/null +++ b/pyinstaller/electron/src/config.js @@ -0,0 +1,109 @@ +const os = require('os') +const path = require('path') +const fs = require('fs') + + +const downloadloc = require('../downloadloc'); + + + +let platformName = '' +switch (process.platform) { + case 'darwin': + platformName = 'osx' + break + case 'win32': + platformName = 'win64' + break + case 'linux': + platformName = 'x86_64-linux-gnu' + break + default: + throw `Unknown platformName ${platformName}` +} +const appName = downloadloc.appName() +const appNameLower = appName.toLowerCase() +const isDev = process.env.NODE_ENV === "development" +const unresolvedDevFolder = process.env.SPECTER_DATA_FOLDER || "~/.specter_dev" +const devFolder = unresolvedDevFolder.replace(/^~/, os.homedir()); +const prodFolder = path.resolve(os.homedir(), `.${appNameLower}`) +let appSettingsPath +let appSettings +let specterdDirPath +let specterAppLogPath +let versionDataPath +let versionData + +function getAppSettings() { + let defaultSettings = { + mode: 'specterd', + specterURL: 'http://localhost:25441', + basicAuth: false, + basicAuthUser: '', + basicAuthPass: '', + tor: false, + proxyURL: "socks5://127.0.0.1:9050", + specterdVersion: (versionData && versionData.version !== undefined) ? versionData.version : 'unknown', + specterdHash: (versionData && versionData.sha256 !== undefined) ? versionData.sha256 : 'unknown', + specterdCLIArgs: "", + versionInitialized: false + } + + try { + if (!fs.existsSync(appSettingsPath)){ + fs.mkdirSync(path.resolve(appSettingsPath, '..'), { recursive: true }); + } + fs.writeFileSync(appSettingsPath, JSON.stringify(defaultSettings), { flag: 'wx' }); + } catch (error) { + if (error.toString().startsWith("Error: EEXIST: file already exists,")) { + //ignore + } else { + throw error + } + } + + // Make sure to add missing settings in case the format changed or new settings were added + let appSettings = require(appSettingsPath) + for (let key of Object.keys(defaultSettings)) { + if (!appSettings.hasOwnProperty(key)) { + appSettings[key] = defaultSettings[key] + } + } + + return appSettings +} + +if (isDev) { + versionDataPath = `${devFolder}/version-data.json` + +} else { + versionDataPath = `../version-data.json` +} +versionData = require(versionDataPath) + + +if (isDev) { + appSettingsPath = `${devFolder}/app_settings.json` + specterdDirPath = `${devFolder}/specterd-binaries` + specterAppLogPath = `${devFolder}/specterApp.log` +} +else { + appSettingsPath = `${prodFolder}/app_settings.json` + specterdDirPath = `${prodFolder}/specterd-binaries` + specterAppLogPath = `${prodFolder}/specterApp.log` +} +appSettings = getAppSettings() + +module.exports = { + platformName, + appSettings, + appName, + appNameLower, + appSettingsPath, + specterdDirPath, + specterAppLogPath, + versionDataPath, + versionData, + isDev: isDev, + devFolder, +} \ No newline at end of file diff --git a/pyinstaller/electron/src/download.js b/pyinstaller/electron/src/download.js new file mode 100644 index 000000000..b72a5712d --- /dev/null +++ b/pyinstaller/electron/src/download.js @@ -0,0 +1,145 @@ +const request = require('request') +const fs = require('fs') +const { app, Menu } = require('electron') +const extract = require('extract-zip') +const { getDownloadLocation } = require('../downloadloc.js') +const { appName, appSettings, platformName, appNameLower, versionData, versionDataPath } = require('./config.js') +const { isMac, getFileHash } = require('./helpers.js') +const { logger } = require('./logging.js') +const ProgressBar = require('electron-progressbar') +const { updateSpecterdStatus, updatingLoaderMsg, createProgressBar } = require('./uiHelpers.js') +const { startSpecterd } = require('./specterd.js') + +// The standard quit item cannot be replaced / modified and it is not triggering the +// before-quit event on MacOS if a child window is open +const dockMenuWithforceQuit = Menu.buildFromTemplate([ + { + label: 'Force Quit during download', + click: () => { + // If the progress bar exists, close it + if (progressBar) { + progressBar.close() + } + // Quit the app + app.quit() + }, + }, +]) + +function downloadSpecterd(specterdPath) { + updatingLoaderMsg(`Starting download`) + updateSpecterdStatus(`Downloading the ${appName} binary...`) + // Some logging + logger.info('Using version ' + appSettings.specterdVersion) + logger.info('Using platformName ' + platformName) + download_location = getDownloadLocation(appSettings.specterdVersion, platformName) + logger.info('Downloading from ' + download_location) + download(download_location, specterdPath + '.zip', function (errored) { + if (errored == true) { + updatingLoaderMsg( + `Downloading the ${appNameLower} binary from GitHub failed, could not reach the server or the file wasn't found.` + ) + updateSpecterdStatus(`Downloading ${appNameLower}d failed...`) + return + } + updatingLoaderMsg('Download completed. Unpacking files...') + logger.info('Extracting ' + specterdPath) + + extract(specterdPath + '.zip', { dir: specterdPath + '-dir' }).then(function () { + let extraPath = '' + switch (process.platform) { + case 'darwin': + extraPath = appNameLower + 'd' + break + case 'win32': + extraPath = appNameLower + 'd.exe' + break + case 'linux': + extraPath = appNameLower + 'd' + } + var oldPath = specterdPath + `-dir/${extraPath}` + var newPath = specterdPath + (platformName == 'win64' ? '.exe' : '') + + fs.renameSync(oldPath, newPath) + fs.unlinkSync(specterdPath + '.zip') + fs.rmdirSync(specterdPath + '-dir', { recursive: true }) + getFileHash(specterdPath + (platformName == 'win64' ? '.exe' : ''), function (specterdHash) { + if (appSettings.specterdHash.toLowerCase() === specterdHash || appSettings.specterdHash == '') { + startSpecterd(specterdPath) + } else { + updatingLoaderMsg('Specterd version could not be validated.') + logger.error(`hash of downloaded file: ${specterdHash}`) + logger.error(`Expected hash: ${appSettings.specterdHash} from ${versionDataPath}`) + updateSpecterdStatus('Failed to launch specterd...') + } + }) + }) + }) +} + +// Download function with progress bar +const download = (uri, filename, callback) => { + // HEAD request first + request.head(uri, (err, res, body) => { + if (res.statusCode != 404) { + let receivedBytes = 0 + const totalBytes = res.headers['content-length'] + logger.info(`Total size to download: ${totalBytes}`) + const progressBar = createProgressBar(totalBytes) + // Add Force Quit item during download for MacOS dock + if (isMac) { + app.dock.setMenu(dockMenuWithforceQuit) + } + + progressBar.on('completed', () => { + progressBar.close() + // Remove the Force Quit dock item again for Mac + if (isMac) { + const updatedDockMenu = Menu.buildFromTemplate( + dockMenuWithforceQuit.items.filter((item) => item.label !== 'Force Quit during download') + ) + app.dock.setMenu(updatedDockMenu) + } + }) + + progressBar.on('aborted', () => { + logger.warn('Download was aborted before it could finish.') + }) + // Loggin the download progress + let lastLogTime = 0 + const logInterval = 5000 // log every 5 seconds + progressBar.on('progress', () => { + const currentTime = Date.now() + if (currentTime - lastLogTime >= logInterval) { + lastLogTime = currentTime + logger.info(`Download status: ${((receivedBytes / totalBytes) * 100).toFixed(0)}%`) + } + }) + // GET request + request(uri) + .on('data', (chunk) => { + receivedBytes += chunk.length + if (progressBar) { + progressBar.value = receivedBytes + } + }) + .pipe(fs.createWriteStream(filename)) + .on('close', callback) + } + // If the download link was not found, call callback (updatingLoaderMsg with error feedback) + else { + logger.error(`Error while trying to download specterd: ${err}`) + try { + callback(true) + } catch (error) { + logger.error(error) + throw error + } + } + }) +} + +module.exports = { + downloadSpecterd: downloadSpecterd, + download: download, +} diff --git a/pyinstaller/electron/src/helpers.js b/pyinstaller/electron/src/helpers.js new file mode 100644 index 000000000..a2e87f9f2 --- /dev/null +++ b/pyinstaller/electron/src/helpers.js @@ -0,0 +1,43 @@ +const { Menu } = require('electron') +const fs = require('fs') +const path = require('path') +const crypto = require('crypto') +const readLastLines = require('read-last-lines') +const isMac = process.platform === 'darwin' + +const { versionData, appSettingsPath, specterAppLogPath } = require('./config.js') +const { logger } = require('./logging.js') + +// Use different version-data.jsons + +// Should look like this: +// { +// "version": "v2.0.0-pre32", +// "sha256": "aa049abf3e75199bad26fbded08ee5911ad48e325b42c43ec195136bd0736785" +// } + +function getFileHash(filename, callback) { + let shasum = crypto.createHash('sha256'), + // Updating shasum with file content + s = fs.ReadStream(filename) + s.on('data', function (data) { + shasum.update(data) + }) + // making digest + s.on('end', function () { + var hash = shasum.digest('hex') + callback(hash) + }) +} + +function getSpecterAppLogs(callback) { + readLastLines.read(specterAppLogPath, 700).then(callback) +} + +module.exports = { + getFileHash, + getSpecterAppLogs, + versionData, + isMac, + isMac, +} diff --git a/pyinstaller/electron/src/logging.js b/pyinstaller/electron/src/logging.js new file mode 100644 index 000000000..0715a93b2 --- /dev/null +++ b/pyinstaller/electron/src/logging.js @@ -0,0 +1,22 @@ +const { specterAppLogPath } = require('./config.js') + +// Logging +const { transports, format, createLogger } = require('winston') +const combinedLog = new transports.File({ filename: specterAppLogPath }) +const winstonOptions = { + exitOnError: false, + format: format.combine( + format.timestamp(), + format.json(), + format.printf((info) => { + return `${info.timestamp} [${info.level}] : ${info.message}` + }) + ), + transports: [new transports.Console({ json: false }), combinedLog], + exceptionHandlers: [combinedLog], +} +const logger = createLogger(winstonOptions) + +module.exports = { + logger: logger, +} \ No newline at end of file diff --git a/pyinstaller/electron/src/specterd.js b/pyinstaller/electron/src/specterd.js new file mode 100644 index 000000000..7c2a4c316 --- /dev/null +++ b/pyinstaller/electron/src/specterd.js @@ -0,0 +1,189 @@ +const { app, BrowserWindow } = require('electron') +const { appSettings, platformName } = require('./config') +const { + showError, + updateSpecterdStatus, + updatingLoaderMsg, + createWindow, + loadUrl, + executeJavaScript, +} = require('./uiHelpers') +const { logger } = require('./logging') +const { spawn } = require('child_process') +const { URL } = require('url') + +function checkSpecterd(logs, specterdStarted) { + // There doesn't seem to be another more straightforward way to check whether specterd is running: https://github.com/nodejs/help/issues/1191 + // Setting a timeout to avoid waiting for specterd endlessly + const timeout = 180000 // 3 minutes + const now = Date.now() + const timeElapsed = now - specterdStarted + if (timeElapsed > timeout) { + return 'timeout' + } + if (logs.toString().includes('Serving Flask app')) { + return 'running' + } else { + return 'not running' + } +} + +let specterIsRunning = false +function startSpecterd(specterdPath, automaticWalletImport = false) { + if (platformName == 'win64') { + specterdPath += '.exe' + } + let hwiBridgeMode = appSettings.mode == 'hwibridge' + updatingLoaderMsg('Launching Specter ...', (showSpinner = 'true')) + updateSpecterdStatus('Launching Specter ...') + let specterdArgs = ['server'] + specterdArgs.push('--no-filelog') + if (hwiBridgeMode) specterdArgs.push('--hwibridge') + if (appSettings.specterdCLIArgs != '') { + // User has inputed cli arguments in the UI + let specterdExtraArgs = appSettings.specterdCLIArgs.split(' ') + specterdExtraArgs.forEach((arg) => { + // Ensures that whitespaces are not used as cli arguments + if (arg != '') { + specterdArgs.push(arg) + } + }) + } + // locale fix (copying from nodejs-env + adding locales) + const options = { + env: { ...process.env }, + } + options.env['LC_ALL'] = 'en_US.utf-8' + options.env['LANG'] = 'en_US.utf-8' + options.env['SPECTER_LOGFORMAT'] = 'SPECTERD: %(levelname)s in %(module)s: %(message)s' + logger.info(`Starting specter: ${specterdPath} ${specterdArgs.join(' ')}`) + specterdProcess = spawn(specterdPath, specterdArgs, options) + const specterdStarted = Date.now() + + // We are checking for both, stdout and stderr, to be on the save side. + specterdProcess.stdout.on('data', (data) => { + actOnNewLogLine(data.toString(), 'stdout') + }) + + specterdProcess.stderr.on('data', (data) => { + actOnNewLogLine(data.toString(), 'stderr') + }) + + const actOnNewLogLine = (logLine, origin) => { + logger.info(`${origin}: ${logLine.replace(/(\r\n|\n|\r)/gm, '')}`) + const serverdStatus = checkSpecterd(logLine, specterdStarted) + if (!specterIsRunning) { + if (serverdStatus === 'running') { + logger.info(`Specter server seems to run ...`) + updateSpecterdStatus('Specter is running') + specterIsRunning = true + if (automaticWalletImport === true) { + logger.info('Performing automatic wallet import ...') + updatingLoaderMsg('Launching wallet importer. This will only work with a node connection.', (showSpinner = true)) + setTimeout(() => { + importWallet(walletDataFromUrl) + }, 3000) + } else { + logger.info('Normal startup of Specter.') + createWindow(appSettings.specterURL) + } + } else if (serverdStatus === 'timeout') { + showError('Specter does not seem to start. Check the logs in the menu for more details.') + updateSpecterdStatus('Specter does not start') + logger.error('Startup timeout for specterd exceeded') + } else { + updatingLoaderMsg('Still waiting for Specter to start ...') + updateSpecterdStatus('Specter is starting') + } + } + } + + specterdProcess.on('exit', (code) => { + if (code !== 0) { + logger.error(`specterd exited with code ${code}`) + showError(`Specter exited with exit code ${code}. Check the logs in the menu for more details.`) + } + }) + + specterdProcess.on('error', (err) => { + logger.error(`Error starting Specter server: ${err}`) + showError(`Specter failed to start, due to ${err.message}. Check the logs in the menu for more details.`) + }) + + app.on('activate', function () { + // On macOS it's common to re-create a window in the app when the + // dock icon is clicked and there are no other windows open. + if (BrowserWindow.getAllWindows().length === 0) createWindow(appSettings.specterURL) + }) + // since these are streams, you can pipe them elsewhere + specterdProcess.on('close', (code) => { + updateSpecterdStatus('Specter stopped...') + logger.info(`child process exited with code ${code}`) + }) +} + +function quitSpecterd() { + if (specterdProcess) { + try { + if (platformName == 'win64') { + exec('taskkill /F /T /PID ' + specterdProcess.pid) + exec('taskkill /IM specterd.exe ') + process.kill(-specterdProcess.pid) + } + specterdProcess.kill('SIGINT') + } catch (e) { + logger.info('Specterd quit warning: ' + e) + } + } +} + +let walletDataFromUrl +// Checking whether the app was opened via a Specter URL and determine whether to perform a specific startup action +app.on('open-url', (_, url) => { + logger.info('The app was opened via URL, checking the URL to decide whether to do any automatic actions ...') + // Parse the URL to extract the query parameters + const specterUrl = new URL(url) + const searchParams = specterUrl.searchParams + // Get the query parameter values + const action = searchParams.get('action') + const data = searchParams.get('data') + if (action === 'importWallet' && data !== '') { + logger.info('Automatic wallet import identified in the URL, setting automaticWalletImport to true.') + automaticWalletImport = true + walletDataFromUrl = data + // Directly import if the app and specterd is already running + if (specterIsRunning) { + logger.info('Performing automatic wallet import ...') + loadURL(`file://${__dirname}/splash.html`) + updatingLoaderMsg('Launching wallet importer. This will only work with a node connection.', (showSpinner = true)) + setTimeout(() => { + importWallet(walletDataFromUrl) + }, 3000) + } + } +}) + +// Automatically import the wallet json string, bring user to the final import wallet screen. +// Only proceed with the import if the importFromWalletSoftwareBtn can be found. +// If it is not, users are redirected by specterd to the configure connection screen. +function importWallet(walletData) { + loadUrl(appSettings.specterURL + '/wallets/new_wallet/') + let code = ` + const importFromWalletSoftwareBtn = document.getElementById('import-from-wallet-software-btn') + if (importFromWalletSoftwareBtn) { + importFromWalletSoftwareBtn.click() + const walletDataTextArea = document.getElementById('txt') + if (walletDataTextArea) { + walletDataTextArea.value = \`${walletData}\` + } + const continueBtn = document.getElementById('continue-with-wallet-import-btn'); + continueBtn.click() + } + ` + executeJavaScript(code) +} + +module.exports = { + startSpecterd, + quitSpecterd, +} diff --git a/pyinstaller/electron/src/uiHelpers.js b/pyinstaller/electron/src/uiHelpers.js new file mode 100644 index 000000000..b41ccc3ce --- /dev/null +++ b/pyinstaller/electron/src/uiHelpers.js @@ -0,0 +1,177 @@ +const path = require('path') +const { Menu, BrowserWindow, nativeTheme, app, Tray, nativeImage } = require('electron') +const { logger } = require('./logging') +const { appSettings, isDev, platformName } = require('./config') +const ProgressBar = require('electron-progressbar') +const { isMac } = require('./helpers') + +// Initialized with initMainWindow +let mainWindow +// Initialized with InitTray +let tray +let trayMenu + +const loadUrl = (url) => { + mainWindow.loadURL(url) +} + +const executeJavaScript = (code) => { + mainWindow.webContents.executeJavaScript(code) +} + +function showError(error) { + updatingLoaderMsg('Specter encountered an error:' + error.toString()) +} + +function updatingLoaderMsg(msg, showSpinner = false) { + if (mainWindow) { + // see preload.js where this is setup + mainWindow.webContents.send('update-loader-message', { + msg, + showSpinner, + }) + } else { + logger.error('mainWindow not initialized in updatingLoaderMsg') + } + logger.info('Updated LoaderMsg: ' + msg) +} + +function updateSpecterdStatus(status) { + trayMenu[0] = { label: status, enabled: false } + tray.setContextMenu(Menu.buildFromTemplate(trayMenu)) +} + +function createWindow(specterURL) { + if (!mainWindow) { + initMainWindow() + } + + // Create the browser window. + if (appSettings.tor) { + mainWindow.webContents.session.setProxy({ proxyRules: appSettings.proxyURL }) + } + mainWindow.loadURL(specterURL + '?mode=remote') +} + +let webPreferences = { + worldSafeExecuteJavaScript: true, + contextIsolation: true, + preload: path.join(__dirname, 'preload.js'), +} + +function initMainWindow(dimensions) { + // In production we use the icons from the build folder + // Note: On MacOS setting an icon here as no effect + const iconPath = isDev ? path.join(__dirname, 'assets-dev/app_icon.png') : '' + mainWindow = new BrowserWindow({ + width: parseInt(dimensions.width * 0.8), + minWidth: 1120, + height: parseInt(dimensions.height * 0.8), + icon: iconPath, + webPreferences, + }) + + // Ensures that any links with target="_blank" or window.open() will be opened in the user's default browser instead of within the app + mainWindow.webContents.setWindowOpenHandler(({ url }) => { + shell.openExternal(url) + return { action: 'deny' } + }) + + mainWindow.webContents.on('did-fail-load', function () { + mainWindow.loadURL(`file://${__dirname}/splash.html`) + updatingLoaderMsg( + `Failed to load: ${appSettings.specterURL}
Please make sure the URL is entered correctly in the settings and try again...` + ) + }) + return mainWindow +} + +const initTray = (openPreferences, quitSpecterd) => { + if (isMac) { + const trayIconPath = nativeTheme.shouldUseDarkColors ? '/assets/menu_icon_dark.png' : '/assets/menu_icon_light.png' + const createTrayIcon = (trayIconPath) => { + let trayIcon = nativeImage.createFromPath(app.getAppPath() + trayIconPath) + // Resize + trayIcon = trayIcon.resize({ width: 22, height: 22 }) + return trayIcon + } + const trayIcon = createTrayIcon(trayIconPath) + tray = new Tray(trayIcon) + + // Change the tray icon if appearance is changed in Mac settings + const updateTrayIcon = () => { + logger.info('Updating tray icon ...') + const trayIconPath = nativeTheme.shouldUseDarkColors ? '/assets/menu_icon_dark.png' : '/assets/menu_icon_light.png' + const newTrayIcon = createTrayIcon(trayIconPath) + tray.setImage(newTrayIcon) + } + nativeTheme.on('updated', updateTrayIcon) + } else { + const trayIcon = nativeImage.createFromPath(app.getAppPath() + '/assets/menu_icon.png') + tray = new Tray(trayIcon) + } + + trayMenu = [ + { label: 'Launching Specter ...', enabled: false }, + { + label: 'Show Specter', + click() { + mainWindow.show() + }, + }, + { + label: 'Settings', + click() { + openPreferences() + }, + }, + { + label: 'Quit', + click() { + quitSpecterd() + app.quit() + }, + }, + ] + tray.setToolTip('Specter') + tray.setContextMenu(Menu.buildFromTemplate(trayMenu)) +} + +const createProgressBar = (totalBytes) => { + progressBar = new ProgressBar({ + indeterminate: false, + abortOnError: true, + text: 'Downloading the Specter binary from GitHub', + detail: + 'This can take several minutes depending on your Internet connection. Specter will start once the download is finished.', + maxValue: totalBytes, + browserWindow: { + parent: mainWindow, + }, + style: { + detail: { + 'margin-bottom': '12px', + }, + bar: { + 'background-color': '#fff', + }, + value: { + 'background-color': '#000', + }, + }, + }) + return progressBar +} + +module.exports = { + loadUrl, + executeJavaScript, + showError, + updatingLoaderMsg, + updateSpecterdStatus, + createWindow, + initMainWindow, + mainWindow, + initTray, + createProgressBar, +} diff --git a/pyproject.toml b/pyproject.toml index fb795dc37..819fe9877 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,7 +77,7 @@ dependencies = {file = ["requirements.in"]} test = [ "black==22.3.0", "pre-commit==2.13.0", - "pip-tools==6.12.2", + "pip-tools==6.13", "pytest==7.1.2", "PySocks==1.7.1", "pytest-cov==2.10.1", diff --git a/requirements.txt b/requirements.txt index 81878fda4..5a9e0712d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -816,9 +816,9 @@ urllib3==1.26.14 \ --hash=sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72 \ --hash=sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1 # via requests -werkzeug==2.2.3 \ - --hash=sha256:2e1ccc9417d4da358b9de6f174e3ac094391ea1d4fbef2d667865d819dfd0afe \ - --hash=sha256:56433961bc1f12533306c624f3be5e744389ac61d722175d543e1751285da612 +werkzeug==2.2.2 \ + --hash=sha256:7ea2d48322cc7c0f8b3a215ed73eabd7b5d75d0b50e31ab006286ccff9e00b8f \ + --hash=sha256:f979ab81f58d7318e064e99c4506445d60135ac5cd2e177a2de0089bfd4c9bd5 # via # flask # flask-login @@ -832,6 +832,5 @@ wtforms==3.0.1 \ # via flask-wtf # WARNING: The following packages were not pinned, but pip requires them to be -# pinned when the requirements file includes hashes and the requirement is not -# satisfied by a package already installed. Consider using the --allow-unsafe flag. +# pinned when the requirements file includes hashes. Consider using the --allow-unsafe flag. # setuptools diff --git a/tests/install_noded.sh b/tests/install_noded.sh index 46b1a4d19..190662f83 100755 --- a/tests/install_noded.sh +++ b/tests/install_noded.sh @@ -85,7 +85,7 @@ function calc_pytestinit_nodeimpl_version { if cat ../pyproject.toml | grep -q "${node_impl}d-version" ; then # in this case, we use the expected version from the test also as the tag to be checked out # i admit that this is REALLY ugly. Happy for any recommendations to do that more easy - PINNED=$(cat ../pyproject.toml | grep "addopts = " | grep -oP "${node_impl}d-version \K\S+" | cut -d'"' -f1) + PINNED=$(cat ../pyproject.toml | grep "addopts = " | ${grep} -oP "${node_impl}d-version \K\S+" | cut -d'"' -f1) if [ "$node_impl" = "elements" ]; then # in the case of elements, the tags have a "elements-" prefix @@ -182,7 +182,14 @@ function check_compile_prerequisites { function check_binary_prerequisites { if [ $(uname) = "Darwin" ]; then - echo " --> No binary prerequisites checking for MacOS, GOOD LUCK!" + if brew list --versions grep &>/dev/null; then + grep=ggrep + else + echo "install grep via brew install grep" + exit 1 + fi + echo " --> No FURTHER binary prerequisites checking for MacOS, GOOD LUCK!" + else REQUIRED_PKGS="wget" for REQUIRED_PKG in $REQUIRED_PKGS; do @@ -194,6 +201,7 @@ function check_binary_prerequisites { apt-get --yes install $REQUIRED_PKG fi done + grep=grep fi } @@ -212,7 +220,7 @@ function sub_compile { if [ $(uname) = "Darwin" ]; then find tests/${node_impl}/src -maxdepth 1 -type f -perm +111 -exec ls -ld {} \; else - find tests/${node_impl}/src -maxdepth 1 -type f -executable -exec ls -ld {} \; + find tests/${node_impl}/src -maxdepth 1 -type f -executable -exec ls -ld {} \; fi END=$(date +%s.%N) DIFF=$(echo "$END - $START" | bc) @@ -251,6 +259,7 @@ function sub_binary { rm -rf ./"$node_impl" fi fi + rm "$node_impl" ln -s ./"$node_impl"-${version} "$node_impl" echo " --> Listing binaries" if [ $(uname) = "Darwin" ]; then diff --git a/utils/build-common.sh b/utils/build-common.sh index fdb24684e..2e7b05b91 100644 --- a/utils/build-common.sh +++ b/utils/build-common.sh @@ -11,7 +11,7 @@ function create_virtualenv_for_pyinstaller { echo " But first Delete it ..." rm -rf .buildenv fi - virtualenv --python=python3 .buildenv + virtualenv --python=python3.10 .buildenv source .buildenv/bin/activate pip3 install -e ".[test]" } @@ -102,6 +102,13 @@ function make_hash_if_necessary { } function building_electron_app { + # https://www.electron.build/ + # Prerequisites: + # * A developer Certificate (in the System keychain) + # * private and public key in the login-keychain + # * The cert needs to be referenced in pyinstaller/electron/package.json -> build.mac.identity + + platform="-- --${1}" # either linux or win (maxOS is empty) cd pyinstaller/electron echo " --> building electron-app" @@ -113,8 +120,18 @@ function building_electron_app { } function macos_code_sign { + # prerequisites for this: + # in short: + # * make sure you have a proper app-specific password on https://appleid.apple.com/account/manage + # * collect some information via scrun altool --list-providers -u "" + # * create profile via xcrun notarytool store-credentials --apple-id "" --password "app-specific-pw" --team-id "seeFromAbove" + # * Call the profile: SpecterProfile + # For details see: + # * https://www.youtube.com/watch?v=2xJcMzoi0EI + # * https://blog.dgunia.de/2022/09/01/switching-from-altool-to-notarytool/ + # * https://scriptingosx.com/2021/07/notarize-a-command-line-tool-with-notarytool/ # This creates a ZIP archive from the app package (using the ditto command). - # This ZIP archive is then used to upload the app to the Apple notarization service. + # This ZIP archive is then used to upload the app to the Apple notarization service via xcrun notarytool (formerly xcrun altool) # After the app has been uploaded to the Apple servers and notarized, the ZIP archive is not used again. # The function uses the xcrun stapler command to attach the notarization result to the app, and then exits. @@ -124,46 +141,27 @@ function macos_code_sign { cd pyinstaller/electron echo ' --> Attempting to code sign...' echo ' executing: ditto -c -k --keepParent "dist/mac/${specterimg_filename}.app" dist/${specterimg_filename}.zip' - ditto -c -k --keepParent "dist/mac/${specterimg_filename}.app" dist/${specterimg_filename}.zip + + ditto -c -k --keepParent "dist/${dist_mac_folder_name}/${specterimg_filename}.app" dist/${specterimg_filename}.zip # upload echo ' uploading ... ' - echo ' executing: xcrun altool --notarize-app -t osx -f dist/${specterimg_filename}.zip --primary-bundle-id "solutions.specter.desktop" -u "${mail}" --password "@keychain:AC_PASSWORD" --output-format json' - output_json=$(xcrun altool --notarize-app -t osx -f dist/${specterimg_filename}.zip --primary-bundle-id "solutions.specter.desktop" -u "${mail}" --password "@keychain:AC_PASSWORD" --output-format json) - echo "JSON-Output:" + + output_json=$(xcrun notarytool submit dist/${specterimg_filename}.zip --apple-id "kneunert@gmail.com" --keychain-profile "SpecterProfile" --output-format json --wait ) + + echo "Request ID: " # parsing the requestuuid which we'll need to track progress - requestuuid=$(echo $output_json | jq -r '."notarization-upload".RequestUUID') - mkdir -p signing_logs - i=1 - while [ $i -le 6 ] ; do - echo " check result in minute $i ..." - sign_result_json=$(xcrun altool --notarization-info $requestuuid -u "${mail}" --password "@keychain:AC_PASSWORD" --output-format json) - timestamp=$(date +"%Y%m%d-%H%M") - # If it's not json-parseable - if ! echo "$sign_result_json" | jq .; then - echo $sign_result_json > ./signing_logs/${app_name}_${timestamp}_${requestuuid}.log - echo "ERROR: track-json not parseable." - echo "$sign_result_json" - exit 1 - fi - # if it's no longer in progress - status=$(echo "$sign_result_json" | jq -e -r '.["notarization-info"].Status') - if [ "$status" != "in progress" ]; then - echo " Finished code sign with status $status" - echo $sign_result_json | jq . > ./signing_logs/${app_name}_${timestamp}_${requestuuid}.log - break - fi - i=$(( $i + 1 )) - sleep 60 - done - if [ "$status" != "success" ]; then - echo "ERROR: status $status" - echo $(echo $sign_result_json | jq .) - echo + requestuuid=$(echo $output_json | jq -r '.id') + status=$(echo $output_json | jq -r '.status') + if [ "$status" = "Invalid" ]; then + mkdir -p signing_logs + echo "issues with notarisation" + xcrun notarytool log ${requestuuid} --keychain-profile SpecterProfile | tee ./signing_logs/${app_name}_${timestamp}_${requestuuid}.log exit 1 fi + # The stapler somehow "staples" the result of the notarisation in to your app # see e.g. https://stackoverflow.com/questions/58817903/how-to-download-notarized-files-from-apple - xcrun stapler staple "dist/mac/${specterimg_filename}.app" + xcrun stapler staple "dist/${dist_mac_folder_name}/${specterimg_filename}.app" cd ../.. } diff --git a/utils/build-osx.sh b/utils/build-osx.sh index 32f435d0f..b066af96b 100755 --- a/utils/build-osx.sh +++ b/utils/build-osx.sh @@ -9,15 +9,37 @@ cd .. # Overriding this function function create_virtualenv_for_pyinstaller { - if [ -d .buildenv ]; then - echo " --> Deleting .buildenv" - rm -rf .buildenv - fi - # This currently assumes to be run with: Python 3.10.4 + # This currently assumes to be run with: Python 3.10.11 # Important: pyinstaller needs a Python binary with shared library files # With pyenv, for example, you get this like so: env PYTHON_CONFIGURE_OPTS="--enable-shared" pyenv install 3.10.4 - virtualenv .buildenv - source .buildenv/bin/activate + # Use pyenv if available + if command -v pyenv >/dev/null 2>&1; then + ### This is usually in .zshrc, putting it in .bashrc didn't work ### + ### + export PYENV_ROOT="$HOME/.pyenv" + command -v pyenv >/dev/null || export PATH="$PYENV_ROOT/bin:$PATH" + eval "$(pyenv init -)" + ### this needs the pyenv-virtualenv plugin. If you don't have it: + ### git clone https://github.com/pyenv/pyenv-virtualenv.git $(pyenv root)/plugins/pyenv-virtualenv + eval "$(pyenv virtualenv-init -)" + ### ------------------------------------------------------------ ### + PYTHON_VERSION=3.10.11 + export PYENV_VERSION=$PYTHON_VERSION + echo "pyenv is available. Setting PYENV_VERSION to 3.10.4, using pyenv-virtualenv to create the buildenv..." + # echo " --> Deleting .buildenv" + # pyenv uninstall -f .buildenv + # rm -rf "$HOME/.pyenv/versions/$PYTHON_VERSION/envs/.buildenv" + # pyenv virtualenv 3.10.4 .buildenv + pyenv activate .buildenv + else + echo "pyenv is not available. Using system Python version." + if [ -d .buildenv ]; then + echo " --> Deleting .buildenv" + rm -rf .buildenv + fi + virtualenv .buildenv + source .buildenv/bin/activate + fi pip3 install -e ".[test]" } @@ -41,17 +63,10 @@ END_COMMENT ### Prerequisites # brew install gmp # to prevent module 'embit.util' has no attribute 'ctypes_secp256k1' +# brew install jq # npm install --global create-dmg ### Trouble shooting -# Currently, only MacOS Catalina is supported to build the dmg-file -# Therefore we expect xcode 12.1 (according to google) -# After installation of xcode: sudo xcode-select -s /Applications/Xcode.app/Contents/Developer -# otherwise you get xcrun: error: unable to find utility "altool", not a developer tool or in PATH -# catalina might have a a too old version of bash. You need at least 4.0 or so -# 3.2 is too low definitely -# brew install bash - # If you have the common issue "errSecInternalComponent" while signing the code: # https://medium.com/@ceyhunkeklik/how-to-fix-ios-application-code-signing-error-4818bd331327 @@ -67,6 +82,15 @@ We have: * upload will upload all the binary artifacts to the github-release-page. This includes the creation of the hash-files and the gnupg signing +### Trouble shooting (Legacy) +# Currently, only MacOS Catalina is supported to build the dmg-file +# Therefore we expect xcode 12.1 (according to google) +# After installation of xcode: sudo xcode-select -s /Applications/Xcode.app/Contents/Developer +# otherwise you get xcrun: error: unable to find utility "altool", not a developer tool or in PATH +# catalina might have a a too old version of bash. You need at least 4.0 or so +# 3.2 is too low definitely +# brew install bash + # Example-call: ./utils/build-osx.sh --debug --version v1.10.0-pre23 --appleid "Kim Neunert (FWV59JHV83)" --mail "kim@specter.solutions" make-hash specterd electron sign upload EOF @@ -158,9 +182,6 @@ fi if [[ "$build_electron" = "True" ]]; then prepare_npm make_hash_if_necessary - - - npm i if [[ "${appleid}" == '' ]] then @@ -174,6 +195,10 @@ if [[ "$build_electron" = "True" ]]; then fi if [[ "$build_sign" = "True" ]]; then + dist_mac_folder_name=mac + if [ "$(uname -m)" = "arm64" ]; then + dist_mac_folder_name=${dist_mac_folder_name}-arm64 + fi if [[ "$appleid" != '' ]] then macos_code_sign @@ -183,7 +208,7 @@ if [[ "$build_sign" = "True" ]]; then mkdir -p release rm -rf release/* - create-dmg pyinstaller/electron/dist/mac/${specterimg_filename}.app --identity="Developer ID Application: ${appleid}" dist + create-dmg pyinstaller/electron/dist/${dist_mac_folder_name}/${specterimg_filename}.app --identity="Developer ID Application: ${appleid}" dist # create-dmg doesn't create the prepending "v" to the version node_comp_version=$(python3 -c "print('$version'[1:])") mv "dist/${specterimg_filename} ${node_comp_version}.dmg" release/${specterimg_filename}-${version}.dmg @@ -211,12 +236,12 @@ if [ "$app_name" == "specter" ]; then echo "sha256sum * > SHA256SUMS-macos" echo "python3 ../../utils/github.py upload SHA256SUMS-macos" echo "gpg --detach-sign --armor SHA256SUMS-macos" - echo "python3 ../../utils/github.py upload SHA256SUMS-macos.asc" + echo "python3 ../utils/github.py upload SHA256SUMS-macos.asc" if [[ "$upload" = "True" ]]; then echo " --> This build got triggered for version $version" - . ../../specter_gh_upload.sh + . ../../specter_gh_upload.sh # A simple file looks like: export GH_BIN_UPLOAD_PW=...(GH token) export CI_COMMIT_TAG=$version if [[ -z "$CI_PROJECT_ROOT_NAMESPACE" ]]; then export CI_PROJECT_ROOT_NAMESPACE=cryptoadvance