Kn/macos signing (#2432)

* chore: migrate from altool to notarytool

* fix litte things in build-common

* add pyenv install in build-osx

* add pyinstaller/electron/signing_logs to gitignore

* chore: migrate from altool to notarytool

* fix little things in build.common

* add pyenv install in build osx

* add pyinatsller electron signing logs to gitignore

* updated build-osx.sh

* fix entitlement

* heavily refactoring the electron app

* polish and improve

* further bugfixing and polishing

* tiny change to improve support of MacOS

* Fix dependency issues

---------

Co-authored-by: Manolis <moneymanolis@protonmail.com>
This commit is contained in:
k9ert 2024-05-01 12:31:32 +02:00 committed by GitHub
parent 7970c3d8ab
commit e9dc494d22
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 932 additions and 803 deletions

1
.gitignore vendored
View file

@ -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

View file

@ -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"
},
}

View file

@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.device.camera</key>

View file

@ -1,23 +1,20 @@
<html>
<link rel="stylesheet" type="text/css" href="./styles.css">
<body style="overflow: auto; height: 100%;">
<div class="card" style="width:90%">
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.
<pre><code id="specterapp-logs"></code></pre>
<br>
</div>
<link rel="stylesheet" type="text/css" href="./output.css" />
<body style="overflow: auto; height: 100%">
<div class="card" style="width: 90%">
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.
<pre><code id="specterapp-logs"></code></pre>
<br />
</div>
<script>
const fs = require('fs')
const path = require('path')
const helpers = require('./helpers')
helpers.getSpecterAppLogs( (lines) => {
document.getElementById('specterapp-logs').innerText = lines
})
const fs = require('fs')
const path = require('path')
const helpers = require('./src/helpers')
helpers.getSpecterAppLogs((lines) => {
document.getElementById('specterapp-logs').innerText = lines
})
</script>
</body>
</html>
</body>
</html>

View file

@ -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
}

View file

@ -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}<br>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:<br>' + 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)
})

View file

@ -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"
}

View file

@ -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"
}
}
}

View file

@ -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])
}

View file

@ -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);
}
});
})

View file

@ -6,4 +6,5 @@
<svg id="spinner" class="text-white rounded-full bg-dark-700 w-4 h-4 animate-spin hidden" version="1.1" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><!--Generated by IJSVG (https://github.com/iconjar/IJSVG)--><path d="M52.8359,43.6067l1.30463e-08,-1.88843e-08c-0.312506,0.452349 -0.932543,0.565714 -1.38489,0.253208c-0.0990138,-0.0684038 -0.184804,-0.154194 -0.253208,-0.253208l-6.9888,-9.9742l-5.91546e-08,-8.31834e-08c-0.328807,-0.46237 -0.220533,-1.10375 0.241837,-1.43255c0.168889,-0.120103 0.370155,-0.186318 0.577363,-0.189948h4.9835l3.54852e-07,0.000503677c-0.00686545,-9.94223 -8.0722,-17.9964 -18.0144,-17.9896c-4.13508,0.00285541 -8.14312,1.42919 -11.3504,4.03924l4.60883e-08,-3.91242e-08c-0.842285,0.715013 -2.10472,0.611839 -2.81974,-0.230446c-0.715013,-0.842285 -0.611839,-2.10472 0.230446,-2.81974l3.46098e-07,-2.83814e-07c9.39634,-7.70537 23.26,-6.33457 30.9654,3.06177c3.22304,3.93035 4.98589,8.85536 4.98903,13.9382h4.9935l2.44822e-09,4.33431e-11c0.567328,0.0100438 1.0191,0.478096 1.00905,1.04542c-0.0036664,0.207098 -0.0698445,0.408252 -0.189853,0.577075Zm-40.0129,-23.1717l6.9888,9.9742l-2.71216e-09,-3.81325e-09c0.32881,0.4623 0.220594,1.10362 -0.241706,1.43243c-0.168809,0.120065 -0.369975,0.186285 -0.577094,0.189969h-4.983l2.60913e-07,0.000383721c0.0068661,9.94206 8.07207,17.9961 18.0141,17.9893c4.1354,-0.00285595 8.14372,-1.42943 11.3511,-4.03989l-7.47965e-08,6.34738e-08c0.8424,-0.714877 2.10482,-0.6115 2.8197,0.2309c0.714877,0.8424 0.6115,2.10482 -0.2309,2.8197l4.04125e-07,-3.31403e-07c-9.39617,7.70531 -23.2597,6.33461 -30.965,-3.06156c-3.22311,-3.93039 -4.98597,-8.85549 -4.98903,-13.9384h-4.9947l5.45126e-08,9.68797e-10c-0.567328,-0.0100825 -1.01906,-0.478166 -1.00898,-1.04549c0.00367938,-0.207033 0.0698308,-0.408123 0.189782,-0.576906l6.9888,-9.9742l-3.21808e-08,4.66031e-08c0.312407,-0.452417 0.93242,-0.565917 1.38484,-0.25351c0.0990287,0.0683822 0.184838,0.154154 0.253263,0.253153Z" fill="currentColor" fill-rule="evenodd"></path></svg>
<p id="launch-text" class="text-lg">Launching Specter Desktop...</p>
</div>
<script src="renderer.js"></script>
</body>

View file

@ -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,
}

View file

@ -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,
}

View file

@ -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,
}

View file

@ -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,
}

View file

@ -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,
}

View file

@ -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}<br>Please make sure the URL is entered correctly in the settings and try again...</b>`
)
})
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,
}

View file

@ -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",

View file

@ -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

View file

@ -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

View file

@ -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 "<yourAppleID>"
# * create profile via xcrun notarytool store-credentials --apple-id "<YourAppleID>" --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 ../..
}

View file

@ -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