Fix Electron stuck on splash in hwibridge mode (#2600)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
k9ert 2026-04-12 14:42:52 +02:00 committed by GitHub
parent 496f3f5d79
commit 50d81434a8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 158 additions and 17 deletions

View file

@ -0,0 +1,91 @@
name: Electron smoketest
on:
pull_request:
paths:
- 'pyinstaller/electron/**'
- '.github/workflows/electron-smoketest.yml'
push:
branches: [master]
paths:
- 'pyinstaller/electron/**'
- '.github/workflows/electron-smoketest.yml'
jobs:
smoketest:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: pyinstaller/electron/package-lock.json
- name: Install xvfb and electron runtime libs
run: |
sudo apt-get update
sudo apt-get install -y \
xvfb \
libgtk-3-0 \
libnss3 \
libasound2t64 \
libgbm1 \
libxss1 \
libxtst6
- name: Install electron deps
working-directory: pyinstaller/electron
run: npm ci
- name: Syntax check all electron JS
working-directory: pyinstaller/electron
run: |
for f in main.js src/*.js; do
echo "checking $f"
node --check "$f"
done
- name: Seed dev data folder
run: |
mkdir -p "$RUNNER_TEMP/specter_dev"
echo '{"version":"","sha256":{"x64":"","arm64":""}}' \
> "$RUNNER_TEMP/specter_dev/version-data.json"
- name: Boot electron, assert splash reaches no-specterd branch
working-directory: pyinstaller/electron
env:
NODE_ENV: development
SPECTER_DATA_FOLDER: ${{ runner.temp }}/specter_dev
run: |
set -u
xvfb-run --auto-servernum --server-args='-screen 0 1280x960x24' \
./node_modules/.bin/electron . --no-sandbox > electron.log 2>&1 &
PID=$!
trap 'kill $PID 2>/dev/null || true; wait $PID 2>/dev/null || true' EXIT
# Expected log line emitted by uiHelpers.js:updatingLoaderMsg when
# main.js reaches the "specterd missing + no version" branch.
# If main.js changes this message, update both sides together.
EXPECTED="Specterd was not found and no version is configured"
for i in $(seq 1 30); do
if grep -qF "$EXPECTED" electron.log; then
echo "smoketest passed (splash reached no-specterd branch)"
exit 0
fi
if ! kill -0 $PID 2>/dev/null; then
echo "electron exited before reaching expected state"
echo "--- electron.log ---"
cat electron.log
exit 1
fi
sleep 1
done
echo "expected log line not seen within 30s"
echo "--- electron.log ---"
cat electron.log
exit 1

View file

@ -160,7 +160,9 @@ app.whenReady().then(() => {
downloadSpecterd(specterdPath)
} else {
updatingLoaderMsg(
'Specterd file could not be validated and no version is configured in the settings<br>Please go to Preferences and set version to fetch or add an executable manually...'
'Specterd file could not be validated and no version is configured in the settings<br>Please go to Preferences and set version to fetch or add an executable manually...',
false,
{ isHtml: true }
)
updateSpecterdStatus('Failed to locate specterd...')
}
@ -169,8 +171,13 @@ app.whenReady().then(() => {
if (appSettings.specterdVersion) {
downloadSpecterd(specterdPath)
} else {
// NB: smoketest workflow (.github/workflows/electron-smoketest.yml) greps
// for the leading "Specterd was not found and no version is configured"
// substring to assert splash reached this branch. Keep both in sync.
updatingLoaderMsg(
'Specterd was not found and no version is configured in the settings<br>Please go to Preferences and set version to fetch or add an executable manually...'
'Specterd was not found and no version is configured in the settings<br>Please go to Preferences and set version to fetch or add an executable manually...',
false,
{ isHtml: true }
)
updateSpecterdStatus('Failed to locate specterd...')
}
@ -200,6 +207,15 @@ app.on('before-quit', (event) => {
}
})
ipcMain.on('open-settings', (event) => {
const senderUrl = event.senderFrame?.url || ''
if (!senderUrl.startsWith('file://') || !senderUrl.endsWith('/splash.html')) {
logger.warn(`Rejected open-settings IPC from untrusted sender: ${senderUrl}`)
return
}
openPreferences()
})
ipcMain.on('request-mainprocess-action', (event, arg) => {
switch (arg.message) {
case 'save-preferences':

View file

@ -8,8 +8,6 @@
// 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) {
@ -19,10 +17,23 @@ window.addEventListener('DOMContentLoaded', () => {
window.api.receive('update-loader-message', (data) => {
const launchTextElement = document.getElementById('launch-text');
if (launchTextElement) {
launchTextElement.textContent = data.msg;
if (data.isHtml) {
launchTextElement.innerHTML = data.msg;
} else {
launchTextElement.textContent = data.msg;
}
updateSpinner(data.showSpinner);
}
const settingsBtn = document.getElementById('open-settings-btn');
if (settingsBtn) {
settingsBtn.classList.toggle('hidden', !data.showSettingsButton);
}
});
})
const openSettingsBtn = document.getElementById('open-settings-btn');
if (openSettingsBtn) {
openSettingsBtn.addEventListener('click', () => {
window.api.send('open-settings');
});
}
})

View file

@ -29,11 +29,13 @@ function checkSpecterd(logs, specterdStarted) {
}
let specterIsRunning = false
let currentSpecterURL = 'http://localhost:25441'
function startSpecterd(specterdPath, automaticWalletImport = false) {
if (platformName == 'win64') {
specterdPath += '.exe'
}
let hwiBridgeMode = appSettings.mode == 'hwibridge'
currentSpecterURL = hwiBridgeMode ? appSettings.specterURL : 'http://localhost:25441'
updatingLoaderMsg('Launching Specter ...', (showSpinner = 'true'))
updateSpecterdStatus('Launching Specter ...')
let specterdArgs = ['server']
@ -85,7 +87,7 @@ function startSpecterd(specterdPath, automaticWalletImport = false) {
}, 3000)
} else {
logger.info('Normal startup of Specter.')
createWindow(appSettings.specterURL)
createWindow(currentSpecterURL)
}
} else if (serverdStatus === 'timeout') {
showError('Specter does not seem to start. Check the logs in the menu for more details.')
@ -113,7 +115,7 @@ function startSpecterd(specterdPath, automaticWalletImport = false) {
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)
if (BrowserWindow.getAllWindows().length === 0) createWindow(currentSpecterURL)
})
// since these are streams, you can pipe them elsewhere
specterdProcess.on('close', (code) => {
@ -167,7 +169,7 @@ app.on('open-url', (_, url) => {
// 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/')
loadUrl(currentSpecterURL + '/wallets/new_wallet/')
let code = `
const importFromWalletSoftwareBtn = document.getElementById('import-from-wallet-software-btn')
if (importFromWalletSoftwareBtn) {

View file

@ -2,9 +2,12 @@
<link rel="stylesheet" type="text/css" href="./typography.css">
<link rel="stylesheet" type="text/css" href="./output.css">
<body class="bg-dark-900">
<div width="100%" height="100%" class="flex space-x-2 items-center justify-center h-screen">
<div width="100%" height="100%" class="flex flex-col space-y-4 items-center justify-center h-screen">
<div class="flex space-x-2 items-center">
<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>
<p id="launch-text" class="text-lg">Launching Specter Desktop...</p>
</div>
<button id="open-settings-btn" class="hidden px-4 py-2 bg-dark-700 text-white rounded hover:bg-dark-600 cursor-pointer">Open Settings</button>
</div>
<script src="renderer.js"></script>
</body>

View file

@ -20,16 +20,27 @@ const executeJavaScript = (code) => {
mainWindow.webContents.executeJavaScript(code)
}
function escapeHtml(str) {
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
}
function showError(error) {
updatingLoaderMsg('Specter encountered an error:' + error.toString())
}
function updatingLoaderMsg(msg, showSpinner = false) {
function updatingLoaderMsg(msg, showSpinner = false, { isHtml = false, showSettingsButton = false } = {}) {
if (mainWindow) {
// see preload.js where this is setup
mainWindow.webContents.send('update-loader-message', {
msg,
showSpinner,
isHtml,
showSettingsButton,
})
} else {
logger.error('mainWindow not initialized in updatingLoaderMsg')
@ -78,11 +89,18 @@ function initMainWindow(dimensions) {
return { action: 'deny' }
})
mainWindow.webContents.on('did-fail-load', function () {
mainWindow.webContents.on('did-fail-load', function (event, errorCode, errorDescription, validatedURL) {
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>`
)
const failedUrl = escapeHtml(validatedURL || appSettings.specterURL || '')
let msg
if (appSettings.mode === 'hwibridge') {
msg = `Failed to load remote Specter at: ${failedUrl}<br><br>` +
`You are running in <b>HWI Bridge mode</b>, which requires a remote Specter server.<br>` +
`If you want to run Specter locally instead, open <b>Settings</b> (${isMac ? 'Cmd' : 'Ctrl'}+, or via the tray icon) and switch to "Run Specter locally".`
} else {
msg = `Failed to load: ${failedUrl}<br>Please make sure the URL is entered correctly in the settings and try again...`
}
updatingLoaderMsg(msg, false, { isHtml: true, showSettingsButton: true })
})
return mainWindow
}