revit-journal-assist
Version:
A web-based interface for viewing Autodesk Revit journal logs
415 lines (361 loc) • 13.9 kB
JavaScript
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();