#!/usr/bin/env node /** * Runtime Dependency Checker for Packaged 4DSTAR App * * This script can be run inside a packaged app to verify all dependencies * are available at runtime. Useful for testing the .dmg on different user accounts. */ const fs = require('fs'); const path = require('path'); const { spawn } = require('child_process'); class RuntimeDependencyChecker { constructor() { this.isPackaged = process.env.NODE_ENV === 'production' || process.resourcesPath; this.appPath = this.isPackaged ? process.resourcesPath : __dirname; this.results = { environment: {}, backend: {}, nodeModules: {}, permissions: {}, errors: [], warnings: [] }; } log(message, type = 'info') { const prefix = { 'info': 'šŸ“‹', 'success': 'āœ…', 'warning': 'āš ļø', 'error': 'āŒ' }[type] || 'ā„¹ļø'; console.log(`${prefix} ${message}`); } checkEnvironment() { this.log('Checking runtime environment...', 'info'); this.results.environment = { platform: process.platform, arch: process.arch, nodeVersion: process.version, electronVersion: process.versions.electron, isPackaged: this.isPackaged, appPath: this.appPath, resourcesPath: process.resourcesPath || 'N/A', execPath: process.execPath, cwd: process.cwd(), user: process.env.USER || process.env.USERNAME || 'unknown', home: process.env.HOME || process.env.USERPROFILE || 'unknown' }; this.log(`Platform: ${this.results.environment.platform}`, 'info'); this.log(`Architecture: ${this.results.environment.arch}`, 'info'); this.log(`Packaged: ${this.results.environment.isPackaged}`, 'info'); this.log(`User: ${this.results.environment.user}`, 'info'); this.log(`App Path: ${this.results.environment.appPath}`, 'info'); } checkBackendExecutable() { this.log('Checking Python backend executable...', 'info'); const executableName = process.platform === 'win32' ? 'fourdst-backend.exe' : 'fourdst-backend'; let backendPath; if (this.isPackaged) { backendPath = path.join(this.appPath, 'backend', executableName); } else { backendPath = path.join(__dirname, '..', 'build', 'electron', 'dist', 'fourdst-backend', executableName); } this.results.backend.expectedPath = backendPath; this.results.backend.exists = fs.existsSync(backendPath); if (!this.results.backend.exists) { this.results.errors.push(`Backend executable not found: ${backendPath}`); this.log(`Backend executable not found: ${backendPath}`, 'error'); return false; } this.log(`Backend executable found: ${backendPath}`, 'success'); // Check permissions try { const stats = fs.statSync(backendPath); this.results.backend.size = stats.size; this.results.backend.mode = stats.mode.toString(8); this.results.backend.isExecutable = !!(stats.mode & parseInt('111', 8)); if (!this.results.backend.isExecutable) { this.results.errors.push('Backend executable lacks execute permissions'); this.log('Backend executable lacks execute permissions', 'error'); return false; } this.log(`Backend size: ${this.results.backend.size} bytes`, 'info'); this.log(`Backend permissions: ${this.results.backend.mode}`, 'info'); } catch (e) { this.results.errors.push(`Failed to check backend stats: ${e.message}`); this.log(`Failed to check backend stats: ${e.message}`, 'error'); return false; } return true; } async testBackendExecution() { if (!this.results.backend.exists) { return false; } this.log('Testing backend execution...', 'info'); return new Promise((resolve) => { const testArgs = ['inspect_bundle', JSON.stringify({ bundle_path: '/nonexistent/test.fbundle' })]; const backendProcess = spawn(this.results.backend.expectedPath, testArgs, { timeout: 15000, env: { ...process.env, PYTHONPATH: '' } // Clear PYTHONPATH to test self-containment }); let stdout = ''; let stderr = ''; backendProcess.stdout.on('data', (data) => { stdout += data.toString(); }); backendProcess.stderr.on('data', (data) => { stderr += data.toString(); }); backendProcess.on('close', (code) => { this.results.backend.testExecution = { exitCode: code, stdoutLength: stdout.length, stderrLength: stderr.length, stdout: stdout.substring(0, 500), // First 500 chars stderr: stderr.substring(0, 500) }; if (stdout.length > 0) { try { const result = JSON.parse(stdout.trim()); this.results.backend.producesValidJSON = true; this.results.backend.jsonResponse = result; if (result.success === false && result.error) { this.log('Backend produces valid JSON error responses', 'success'); resolve(true); } else { this.log('Backend JSON response format unexpected', 'warning'); this.results.warnings.push('Backend JSON response format unexpected'); resolve(true); } } catch (e) { this.results.backend.producesValidJSON = false; this.results.errors.push(`Backend output is not valid JSON: ${e.message}`); this.log(`Backend output is not valid JSON: ${e.message}`, 'error'); this.log(`Raw stdout (first 200 chars): "${stdout.substring(0, 200)}"`, 'error'); resolve(false); } } else { this.results.backend.producesValidJSON = false; this.results.errors.push('Backend produced no stdout output'); this.log('Backend produced no stdout output', 'error'); if (stderr.length > 0) { this.log(`Stderr: ${stderr.substring(0, 200)}`, 'error'); } resolve(false); } }); backendProcess.on('error', (err) => { this.results.backend.executionError = err.message; this.results.errors.push(`Failed to execute backend: ${err.message}`); this.log(`Failed to execute backend: ${err.message}`, 'error'); resolve(false); }); }); } checkNodeModules() { this.log('Checking Node.js modules...', 'info'); const requiredModules = [ 'fs-extra', 'js-yaml', 'adm-zip', '@electron/remote', 'python-shell', 'plotly.js-dist', 'electron-squirrel-startup' ]; this.results.nodeModules.checked = {}; for (const moduleName of requiredModules) { try { const modulePath = require.resolve(moduleName); this.results.nodeModules.checked[moduleName] = { available: true, path: modulePath }; this.log(`āœ“ ${moduleName}`, 'success'); } catch (e) { this.results.nodeModules.checked[moduleName] = { available: false, error: e.message }; this.results.errors.push(`Module ${moduleName} not available: ${e.message}`); this.log(`āœ— ${moduleName}: ${e.message}`, 'error'); } } return Object.values(this.results.nodeModules.checked).every(mod => mod.available); } checkFilePermissions() { this.log('Checking file permissions...', 'info'); const testPaths = [ this.appPath, path.join(this.appPath, 'backend'), this.results.backend.expectedPath ]; this.results.permissions.paths = {}; for (const testPath of testPaths) { try { if (fs.existsSync(testPath)) { const stats = fs.statSync(testPath); this.results.permissions.paths[testPath] = { readable: true, mode: stats.mode.toString(8), isDirectory: stats.isDirectory(), isFile: stats.isFile() }; this.log(`āœ“ ${testPath} (${stats.mode.toString(8)})`, 'success'); } else { this.results.permissions.paths[testPath] = { readable: false, exists: false }; this.log(`āœ— ${testPath} does not exist`, 'warning'); } } catch (e) { this.results.permissions.paths[testPath] = { readable: false, error: e.message }; this.results.errors.push(`Cannot access ${testPath}: ${e.message}`); this.log(`āœ— ${testPath}: ${e.message}`, 'error'); } } } async runFullCheck() { this.log('Starting runtime dependency check...', 'info'); this.checkEnvironment(); const backendExists = this.checkBackendExecutable(); const backendWorks = backendExists ? await this.testBackendExecution() : false; const nodeModulesOk = this.checkNodeModules(); this.checkFilePermissions(); // Generate summary this.log('\n=== RUNTIME DEPENDENCY CHECK SUMMARY ===', 'info'); const checks = { 'Environment': true, // Always passes 'Backend Executable': backendExists, 'Backend Execution': backendWorks, 'Node Modules': nodeModulesOk, 'File Permissions': this.results.errors.filter(e => e.includes('Cannot access')).length === 0 }; let allPassed = true; for (const [check, passed] of Object.entries(checks)) { const status = passed ? 'āœ… PASS' : 'āŒ FAIL'; this.log(`${check}: ${status}`, passed ? 'success' : 'error'); if (!passed) allPassed = false; } if (this.results.warnings.length > 0) { this.log(`\nāš ļø ${this.results.warnings.length} warnings:`, 'warning'); this.results.warnings.forEach(warning => this.log(` - ${warning}`, 'warning')); } if (this.results.errors.length > 0) { this.log(`\nāŒ ${this.results.errors.length} errors:`, 'error'); this.results.errors.forEach(error => this.log(` - ${error}`, 'error')); } if (allPassed && this.results.errors.length === 0) { this.log('\nšŸŽ‰ All runtime dependencies are available! App should work correctly.', 'success'); } else { this.log('\nšŸ’„ Runtime dependency issues found. App may not work correctly.', 'error'); } // Save results to file for debugging const resultsPath = path.join(process.cwd(), 'runtime-check-results.json'); try { fs.writeFileSync(resultsPath, JSON.stringify(this.results, null, 2)); this.log(`\nšŸ“„ Detailed results saved to: ${resultsPath}`, 'info'); } catch (e) { this.log(`Failed to save results: ${e.message}`, 'warning'); } return allPassed && this.results.errors.length === 0; } } // Run check if called directly if (require.main === module) { const checker = new RuntimeDependencyChecker(); checker.runFullCheck().then(success => { process.exit(success ? 0 : 1); }).catch(error => { console.error('Runtime check failed with error:', error); process.exit(1); }); } module.exports = { RuntimeDependencyChecker };