-
-
Notifications
You must be signed in to change notification settings - Fork 174
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add checks to ensure desktop app runs
nklayman/vue-cli-plugin-electron-builder/issues/1622 electron/electron/issues/21457 electron/asar/issues/249 Fix desktop applications failing with following error: ``` A JavaScript error occurred in the main process Uncaught Exception: Error [ERR_REQUIRE_ESM]: require() of ES Module /tmp/.mount_privacSXvQfc/resources/app.asar/index.js not supported. index.js is treated as an ES module file as it is a .js file whose nearest parent package.json contains "type": "module" which declares all .js files in that package scope as ES modules. Instead rename index.js to end in .cjs, change the requiring code to use dynamic import() which is available in all CommonJS modules, or change "type": "module" to "type": "commonjs" in /tmp/.mount_privacSXvQfc/resources/app.asar/package.json to treat all .js files as CommonJS (using .mjs for all ES modules instead). at f._load (node:electron/js2c/asar_bundle:2:13330) at node:electron/js2c/browser_init:2:123492 at node:electron/js2c/browser_init:2:123695 at node:electron/js2c/browser_init:2:123699 at f._load (node:electron/js2c/asar_bundle:2:13330) ```
- Loading branch information
1 parent
0d15992
commit ea25920
Showing
3 changed files
with
322 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
name: checks.desktop-runtime-errors | ||
# Verifies desktop builds for Electron applications across multiple OS platforms (macOS ,Ubuntu, and Windows). | ||
|
||
on: | ||
push: | ||
pull_request: | ||
|
||
jobs: | ||
build-desktop: | ||
strategy: | ||
matrix: | ||
os: [ macos, ubuntu, windows ] | ||
fail-fast: false # Allows to see results from other combinations | ||
runs-on: ${{ matrix.os }}-latest | ||
steps: | ||
- | ||
name: Checkout | ||
uses: actions/checkout@v2 | ||
- | ||
name: Setup node | ||
uses: ./.github/actions/setup-node | ||
- | ||
name: Configure Ubuntu | ||
if: matrix.os == 'ubuntu' | ||
run: |- | ||
sudo apt update | ||
# Configure AppImage dependencies | ||
sudo apt install -y libfuse2 | ||
# Configure fake (virtual) display | ||
sudo apt install -y xvfb | ||
sudo Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & | ||
echo "DISPLAY=:99" >> $GITHUB_ENV | ||
- | ||
name: Configure macOS | ||
if: matrix.os == 'macos' | ||
# Disable Gatekeeper as Electron app isn't signed and notarized | ||
run: sudo spctl --master-disable | ||
- name: Take screenshot | ||
if: matrix.os == 'macos' | ||
run: | | ||
screencapture screenshot.jpg | ||
- name: Upload screenshot | ||
if: matrix.os == 'macos' | ||
uses: actions/upload-artifact@v2 | ||
with: | ||
name: screenshot | ||
path: screenshot.jpg | ||
- | ||
name: Test | ||
run: |- | ||
node scripts/check-desktop-runtime-errors.js |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,265 @@ | ||
/** | ||
* A script for automating the build, execution, and verification of an Electron distributions. | ||
* It builds and executes the packages application for a specified duration to check for runtime errors. | ||
* | ||
* Usage: | ||
* - --build: Clears the electron distribution directory and forces a rebuild of the Electron app. | ||
*/ | ||
|
||
import { execSync, spawn } from 'node:child_process'; | ||
import { platform } from 'node:os'; | ||
import fs, { access, readdir, rmdir } from 'node:fs/promises'; | ||
|
||
const DESKTOP_BUILD_COMMAND = 'npm run electron:build -- -p never'; | ||
const DESKTOP_DIST_PATH = 'dist_electron'; | ||
const NPM_MODULES_PATH = './node_modules'; | ||
const APP_EXECUTION_DURATION_IN_SECONDS = 15; | ||
const FORCE_REBUILD = process.argv.includes('--build'); | ||
|
||
async function main() { | ||
try { | ||
await npmInstall(); | ||
await build(); | ||
const currentPlatform = platform(); | ||
const executor = PLATFORM_EXECUTORS[currentPlatform]; | ||
if (!executor) { | ||
throw new Error(`Unsupported OS: ${currentPlatform}`); | ||
} | ||
const { stderr, stdout, isCrashed } = await executor(); | ||
if(stdout) { | ||
log(`Stdout:\n ${stdout}`); | ||
} | ||
if(isCrashed) { | ||
die(`Application crashed during execution.`); | ||
} | ||
ensureNoErrors(stderr); | ||
log('Application ran without errors.'); | ||
process.exit(0); | ||
} catch (error) { | ||
console.error(error); | ||
die('Unexpected error'); | ||
} | ||
} | ||
|
||
const SUPPORTED_PLATFORMS = { | ||
MAC: 'darwin', | ||
LINUX: 'linux', | ||
WINDOWS: 'win32' | ||
}; | ||
|
||
const PLATFORM_EXECUTORS = { | ||
[SUPPORTED_PLATFORMS.MAC]: executeDmg, | ||
[SUPPORTED_PLATFORMS.LINUX]: executeAppImage, | ||
[SUPPORTED_PLATFORMS.WINDOWS]: executeMsi, | ||
}; | ||
|
||
function executeMsi() { | ||
throw new Error('not yet supported'); | ||
} | ||
|
||
async function isDirMissingOrEmpty(dir) { | ||
if(!dir) { throw new Error('Missing directory'); } | ||
if(!await exists(dir)) { | ||
return true; | ||
} | ||
const contents = await readdir(dir); | ||
|
||
return contents.length === 0; | ||
} | ||
|
||
async function npmInstall() { | ||
if (!await isDirMissingOrEmpty(NPM_MODULES_PATH)) { | ||
log(`"${NPM_MODULES_PATH}" exists and is not empty, skipping desktop build npm install.`); | ||
return; | ||
} | ||
log(`Installing dependencies...`); | ||
execSync('npm install', { stdio: 'inherit' }); | ||
} | ||
|
||
async function build() { | ||
if (!await isDirMissingOrEmpty(DESKTOP_DIST_PATH)) { | ||
if(FORCE_REBUILD) { | ||
log(`Clearing "${DESKTOP_DIST_PATH}" for a fresh build due to --build flag.`); | ||
await rmdir(DESKTOP_DIST_PATH, { recursive: true }); | ||
} else { | ||
log(`"${DESKTOP_DIST_PATH}" exists and is not empty, skipping desktop build (${DESKTOP_BUILD_COMMAND}).`); | ||
return; | ||
} | ||
} | ||
log('Building the project...'); | ||
execSync(DESKTOP_BUILD_COMMAND, { stdio: 'inherit' }); | ||
} | ||
|
||
function findFileByExtension(extension) { | ||
const files = execSync(`find ./${DESKTOP_DIST_PATH} -type f -name '*.${extension}'`) | ||
.toString() | ||
.trim() | ||
.split('\n'); | ||
|
||
if (!files.length) { | ||
die(`No ${extension} found in ${DESKTOP_DIST_PATH} directory.`); | ||
} | ||
if (files.length > 1) { | ||
die(`Found multiple ${extension} files: ${files.join(', ')}`); | ||
} | ||
return files[0]; | ||
} | ||
|
||
function executeAppImage() { | ||
const appFile = findFileByExtension('AppImage'); | ||
makeExecutable(appFile); | ||
return execute(appFile); | ||
} | ||
|
||
function makeExecutable(appFile) { | ||
if(!appFile) { throw new Error('missing file'); } | ||
if (isExecutable(appFile)) { | ||
log('AppImage is already executable.'); | ||
return; | ||
} | ||
log('Making it executable...'); | ||
execSync(`chmod +x ${appFile}`); | ||
|
||
function isExecutable(file) { | ||
try { | ||
execSync(`test -x ${file}`); | ||
return true; | ||
} catch { | ||
return false; | ||
} | ||
} | ||
} | ||
|
||
async function executeDmg() { | ||
const filePath = findFileByExtension('dmg'); | ||
const { mountPath } = mountDmg(filePath); | ||
const appPath = await findMacAppExecutablePath(mountPath); | ||
|
||
try { | ||
return await execute(appPath); | ||
} finally { | ||
tryDetachMount(mountPath); | ||
} | ||
} | ||
|
||
async function findMacAppExecutablePath(mountPath) { | ||
const appFolder = execSync(`find '${mountPath}' -maxdepth 1 -type d -name "*.app"`) | ||
.toString() | ||
.trim(); | ||
const appName = appFolder.split('/').pop().replace('.app', ''); | ||
const appPath = `${appFolder}/Contents/MacOS/${appName}`; | ||
if(await exists(appPath)) { | ||
log(`Application is located at ${appPath}`); | ||
} else { | ||
die(`Application does not exist at ${appPath}`); | ||
} | ||
return appPath; | ||
} | ||
|
||
function mountDmg(dmgFile) { | ||
const hdiutilOutput = execSync(`hdiutil attach '${dmgFile}'`).toString(); | ||
const mountPathMatch = hdiutilOutput.match(/\/Volumes\/[^\n]+/); | ||
const mountPath = mountPathMatch ? mountPathMatch[0] : null; | ||
return { | ||
mountPath, | ||
}; | ||
} | ||
|
||
function tryDetachMount(mountPath, retries = 3) { | ||
while (retries-- > 0) { | ||
try { | ||
execSync(`hdiutil detach '${mountPath}'`); | ||
break; | ||
} catch (error) { | ||
if (retries <= 0) { | ||
console.error(`Failed to detach mount after multiple attempts: ${mountPath}`); | ||
} else { | ||
sleep(500); | ||
} | ||
} | ||
} | ||
} | ||
|
||
function execute(appFile) { | ||
if(!appFile) { throw new Error('missing file'); }; | ||
log(`Executing the AppImage for ${APP_EXECUTION_DURATION_IN_SECONDS} seconds to check for errors...`); | ||
let explicitlyKilled = false; | ||
return new Promise((resolve, reject) => { | ||
let stderrData = ''; | ||
let stdoutData = ''; | ||
|
||
const child = spawn(appFile); | ||
|
||
child.stderr.on('data', (data) => { | ||
stderrData += data.toString(); | ||
}); | ||
|
||
child.stdout.on('data', (data) => { | ||
stdoutData += data.toString(); | ||
}); | ||
|
||
child.on('exit', (code, signal) => { | ||
log(`Application exited with code ${code}`); | ||
if(explicitlyKilled) { | ||
return; | ||
} | ||
resolve({ | ||
stderr: stderrData, | ||
stdout: stdoutData, | ||
isCrashed: true, | ||
}); | ||
}); | ||
|
||
child.on('error', (error) => { | ||
reject(error); | ||
}); | ||
|
||
setTimeout(() => { | ||
explicitlyKilled = true; | ||
child.kill(); | ||
resolve({ | ||
stderr: stderrData, | ||
stdout: stdoutData, | ||
isCrashed: false, | ||
}); | ||
}, APP_EXECUTION_DURATION_IN_SECONDS * 1000); | ||
}); | ||
} | ||
|
||
function sleep(milliseconds) { | ||
const date = Date.now(); | ||
let currentDate = null; | ||
do { | ||
currentDate = Date.now(); | ||
} while (currentDate - date < milliseconds); | ||
} | ||
|
||
function ensureNoErrors(stderr) { | ||
if (stderr && stderr.length > 0) { | ||
die(`Errors detected while running the AppImage:\n${stderr}`); | ||
} | ||
} | ||
|
||
async function exists(path) { | ||
try { | ||
await access(path, fs.constants.F_OK); | ||
return true; | ||
} catch { | ||
return false; | ||
} | ||
} | ||
|
||
function log(message) { | ||
const separator = '======================================'; | ||
const ansiiBold = '\x1b[1m'; | ||
const ansiiReset = '\x1b[0m'; | ||
console.log(`${separator}\n${ansiiBold}${message}${ansiiReset}`); | ||
} | ||
|
||
function die(message) { | ||
const separator = '======================================'; | ||
console.error(`${separator}\n${message}\n${separator}`); | ||
process.exit(1); | ||
} | ||
|
||
await main(); |