UNPKG

revit-journal-assist

Version:

A web-based interface for viewing Autodesk Revit journal logs

415 lines (361 loc) 13.9 kB
const express = require('express'); const http = require('http'); const path = require('path'); const fs = require('fs-extra'); const os = require('os'); const systray = require('systray'); // Import open correctly for version 9.x const { default: openBrowser } = require('open'); const socketIo = require('socket.io'); const { exec } = require('child_process'); const isElevated = require('is-elevated'); const util = require('util'); const execPromise = util.promisify(exec); // Check for administrative privileges and relaunch if needed async function checkAndElevate() { try { // Fix: isElevated is a promise that resolves to a boolean const elevated = await isElevated; if (!elevated) { console.log('Application requires administrative privileges. Attempting to restart with elevation...'); // Get the path to the current executable const exePath = process.execPath; // Use the built-in Windows UAC prompt by executing with 'runas' verb const { spawn } = require('child_process'); const shell = require('node:os').platform() === 'win32'; // Use PowerShell to start the process with admin rights spawn('powershell.exe', [ '-Command', `Start-Process -FilePath "${exePath}" -Verb RunAs` ], { shell, detached: true, stdio: 'ignore' }).unref(); // Exit the current non-elevated process process.exit(0); } else { console.log('Running with administrative privileges'); startApplication(); } } catch (error) { console.error('Error checking elevation:', error); // Continue anyway in case of error startApplication(); } } // Function to check if firewall rule exists async function checkFirewallRule(ruleName, port) { try { // Fix: Use -NoProfile to bypass the user profile which is causing errors const { stdout } = await execPromise(`powershell -NoProfile -Command "Get-NetFirewallRule -DisplayName '${ruleName}' -ErrorAction SilentlyContinue"`); return stdout.trim() !== ''; } catch (error) { console.error('Error checking firewall rule:', error); return false; } } // Function to add firewall rule for remote access async function addFirewallRule(ruleName, port) { try { // Fix: Use -NoProfile and add -ExecutionPolicy Bypass for better compatibility await execPromise( `powershell -NoProfile -ExecutionPolicy Bypass -Command "New-NetFirewallRule -DisplayName '${ruleName}' -Direction Inbound -Protocol TCP -LocalPort ${port} -Action Allow -Profile Private,Domain"` ); console.log(`Firewall rule '${ruleName}' added for port ${port}`); return true; } catch (error) { console.error('Error adding firewall rule:', error); // If firewall commands fail, we'll just assume remote access works and continue console.log('Continuing without firewall rule - may require manual firewall configuration'); return true; } } // Function to remove firewall rule async function removeFirewallRule(ruleName) { try { // Fix: Use -NoProfile and add -ExecutionPolicy Bypass for better compatibility await execPromise(`powershell -NoProfile -ExecutionPolicy Bypass -Command "Remove-NetFirewallRule -DisplayName '${ruleName}' -ErrorAction SilentlyContinue"`); console.log(`Firewall rule '${ruleName}' removed`); return true; } catch (error) { console.error('Error removing firewall rule:', error); return false; } } // Function to get the best local IP address for remote access function getLocalIP() { const networkInterfaces = os.networkInterfaces(); let localIP = 'localhost'; // Find the first non-internal IPv4 address Object.keys(networkInterfaces).forEach((ifname) => { networkInterfaces[ifname].forEach((iface) => { if ('IPv4' === iface.family && !iface.internal) { localIP = iface.address; } }); }); return localIP; } // Application configuration and main code async function startApplication() { const app = express(); const server = http.createServer(app); const io = socketIo(server, { cors: { origin: "*", methods: ["GET", "POST"] } }); const PORT = 3000; const RULE_NAME = 'Revit Journal Assist'; // Check if remote access is enabled (firewall rule exists) let remoteAccessEnabled = await checkFirewallRule(RULE_NAME, PORT); // Enable remote access by default if not already enabled if (!remoteAccessEnabled) { console.log('Remote access not enabled. Enabling by default...'); remoteAccessEnabled = await addFirewallRule(RULE_NAME, PORT); } console.log(`Remote access is ${remoteAccessEnabled ? 'enabled' : 'disabled'}`); // Serve static files app.use(express.static(path.join(__dirname, '../public'))); // Also serve assets app.use('/assets', express.static(path.join(__dirname, '../assets'))); // Routes app.get('/', (req, res) => { res.sendFile(path.join(__dirname, '../public', 'index.html')); }); // API endpoint to get user directories app.get('/api/users', (req, res) => { try { const usersDir = path.join(os.homedir(), '../'); const users = fs.readdirSync(usersDir) .filter(user => { const userPath = path.join(usersDir, user); return fs.statSync(userPath).isDirectory() && /^[a-zA-Z0-9]/.test(user) && // Start with alphanumeric !['Public', 'Default', 'Default User', 'All Users', 'SYSTEM'].includes(user) && !user.startsWith('.') && // Not hidden !user.startsWith('$'); // Not system }); res.json(users); } catch (error) { console.error('Error getting users:', error); res.status(500).send('Error retrieving user directories'); } }); // API endpoint to get Revit versions for a user app.get('/api/user/:user/revit', (req, res) => { try { const user = req.params.user; const userPath = path.join(os.homedir(), '../', user); const revitBase = path.join(userPath, 'AppData/Local/Autodesk/Revit'); if (!fs.existsSync(revitBase)) { return res.json([]); } // Get all Revit version folders using pattern matching const versions = fs.readdirSync(revitBase) .filter(dir => dir.startsWith('Autodesk Revit 20') && fs.existsSync(path.join(revitBase, dir, 'Journals'))) .map(dir => ({ version: dir.replace('Autodesk Revit ', ''), path: path.join(revitBase, dir, 'Journals') })); res.json(versions); } catch (error) { console.error('Error getting Revit versions:', error); res.status(500).send('Error retrieving Revit versions'); } }); // API endpoint to get journal logs for a specific Revit version app.get('/api/user/:user/revit/:version', (req, res) => { try { const { user, version } = req.params; const userPath = path.join(os.homedir(), '../', user); // Map version parameter to actual directory path const journalDir = path.join(userPath, 'AppData/Local/Autodesk/Revit/Autodesk Revit ' + version + '/Journals'); if (!fs.existsSync(journalDir)) { return res.status(404).send('Journal directory not found'); } const files = fs.readdirSync(journalDir) .filter(file => file.endsWith('.txt')) .map(file => { const filePath = path.join(journalDir, file); const stats = fs.statSync(filePath); return { name: file, path: filePath, size: stats.size, created: stats.birthtime, modified: stats.mtime }; }) .sort((a, b) => b.modified - a.modified); // Sort by most recent first res.json(files); } catch (error) { console.error('Error getting journal logs:', error); res.status(500).send('Error retrieving journal logs'); } }); // API endpoint to get journal file content app.get('/api/journal', (req, res) => { try { const filePath = req.query.path; if (!filePath) { return res.status(400).send('File path is required'); } if (!fs.existsSync(filePath)) { return res.status(404).send('File not found'); } const content = fs.readFileSync(filePath, 'utf8'); res.send(content); } catch (error) { console.error('Error reading journal file:', error); res.status(500).send('Error reading journal file'); } }); // API endpoint to download a journal file app.get('/api/download', (req, res) => { try { const filePath = req.query.path; if (!filePath) { return res.status(400).send('File path is required'); } if (!fs.existsSync(filePath)) { return res.status(404).send('File not found'); } res.download(filePath); } catch (error) { console.error('Error downloading file:', error); res.status(500).send('Error downloading file'); } }); // Get local IP address const localIP = getLocalIP(); // Get hostname for display const hostname = os.hostname(); // System Tray configuration // Path to the icon file - specifically using systray.ico const iconPath = path.join(__dirname, '../assets/systray.ico'); // Create system tray menu items const sysItems = [ { title: "Open Web UI", tooltip: "Open the web interface in your default browser", checked: false, enabled: true }, { title: "Enable Remote Access", tooltip: "Toggle remote access via firewall rule", checked: remoteAccessEnabled, enabled: true }, { title: "Exit", tooltip: "Exit Revit Journal Assist", checked: false, enabled: true } ]; // Initialize the systray instance const sysTray = new systray.default({ menu: { icon: fs.existsSync(iconPath) ? fs.readFileSync(iconPath).toString('base64') : 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAArlBMVEUAAAAAtAIAtAIAtAIAtAIAtAIAtAIAtAIAtAIAtAIAtAIAtAIAtAIAtAIAtAIAtAIAtAIAtAIAtAIAtAIAtAIAtAIAtAIAtAIAtAIAtAIAtAIAtAIAtAIAtAIAtAIAtAIAtAIAtAIAtAIAtAIAtAIAtAIAtAIAtAIAtAIAtAIAtAIAtAIAtAIAtAIAtAIAtAIAtAIAtAIAtAIAtAIAtAIAtAIAtAIAtAIAtAL///9EMR69AAAAOHRSTlMAAAQpXa3N0M+waC4HDU6i5ffyncM7QnwtdtL6BJjz6UIJ2VkgxUkL+isiYIQE2p5e8n4ZFIYDYcraiEgAAAABYktHRDnXABfwAAAAB3RJTUUH6AQfCQwkvUZ1JgAAAJRJREFUGNNjYGBkYGBiZmFlYWXjAAPGfA5OLm4eXj4QjV9AUEhYRFRMXEJSSlpGVk5egUFRSVlFVU1dQ1NLW0dXT9+AgdHQyNjE1MzcwtLK2gbJ5bZ29hCBfAesAvZAJwnYO8olJDo5O7u4ujm6e3h6efswMPj6+QcEBgWHhIaFR0RGRTMwxMTGxSckJiUnJaekQuwBACU1ES2SDtu8AAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDI0LTA0LTMxVDA5OjEyOjM2KzAwOjAw/pCA7wAAACV0RVh0ZGF0ZTptb2RpZnkAMjAyNC0wNC0zMVQwOToxMjozNiswMDowMI/NeFMAAAAodEVYdGRhdGU6dGltZXN0YW1wADIwMjQtMDQtMzFUMDk6MTI6MzYrMDA6MDD+mFmwAAAAAElFTkSuQmCC', // Fallback to embedded icon if file doesn't exist title: "Revit Journal Assist", tooltip: "Revit Journal Assist", items: sysItems }, debug: false, copyDir: true }); // Handle system tray item clicks sysTray.onClick(async action => { if (action.seq_id === 0) { // Open Web UI openBrowser(`http://localhost:${PORT}`); } else if (action.seq_id === 1) { // Toggle remote access if (remoteAccessEnabled) { // Disable remote access const success = await removeFirewallRule(RULE_NAME); if (success) { remoteAccessEnabled = false; } } else { // Enable remote access const success = await addFirewallRule(RULE_NAME, PORT); if (success) { remoteAccessEnabled = true; } } // Update menu item sysTray.sendAction({ type: 'update-item', item: { ...sysItems[1], checked: remoteAccessEnabled }, seq_id: 1 }); } else if (action.seq_id === 2) { // Exit try { sysTray.kill(); } catch (e) { console.error('Error killing systray:', e); } process.exit(0); } }); // Socket.io connection handling with connection tracking let connectedClients = new Set(); io.on('connection', (socket) => { if (!connectedClients.has(socket.id)) { connectedClients.add(socket.id); console.log(`Client connected (${connectedClients.size} active connections)`); // Handle getLocalIP event for frontend socket.on('getLocalIP', () => { socket.emit('localIP', hostname); }); socket.on('disconnect', () => { connectedClients.delete(socket.id); console.log(`Client disconnected (${connectedClients.size} active connections)`); }); } }); // Start server and system tray server.listen(PORT, '0.0.0.0', async () => { console.log(`Revit Journal Assist server running at http://0.0.0.0:${PORT}`); if (remoteAccessEnabled) { const localIP = getLocalIP(); console.log(`Remote access enabled at http://${localIP}:${PORT}`); } try { // Start the system tray console.log('System tray initialized'); console.log(`Icon path: ${iconPath} (${fs.existsSync(iconPath) ? 'exists' : 'not found'})`); } catch (err) { console.error('Error initializing system tray:', err); } }); // Handle process termination process.on('SIGINT', () => { try { sysTray.kill(); } catch (e) { console.error('Error killing systray:', e); } process.exit(0); }); process.on('SIGTERM', () => { try { sysTray.kill(); } catch (e) { console.error('Error killing systray:', e); } process.exit(0); }); } // Start the application with elevation check checkAndElevate();