zwave-js-ui
Version:
Z-Wave Control Panel and MQTT Gateway
221 lines (220 loc) • 8.63 kB
JavaScript
import { transports } from 'winston';
import { customFormat, logContainer } from "./logger.js";
import archiver from 'archiver';
import { joinPath, pathExists } from "./utils.js";
import { storeDir } from "../config/app.js";
import { rm, mkdir } from 'node:fs/promises';
import { createWriteStream } from 'node:fs';
import { setTimeout } from 'node:timers/promises';
import { createDefaultTransportFormat } from '@zwave-js/core/bindings/log/node';
import { JSONTransport } from '@zwave-js/log-transport-json';
const debugTempDir = joinPath(storeDir, '.debug-temp');
class DebugManager {
session = null;
/**
* Initialize the debug manager by cleaning up any old temp files
*/
async init() {
// Clean up old debug temp directory on startup
if (await pathExists(debugTempDir)) {
await rm(debugTempDir, { recursive: true, force: true });
}
}
/**
* Check if a debug session is active
*/
isSessionActive() {
return this.session !== null;
}
/**
* Start a debug capture session
*/
async startSession(zwaveClient, originalLogLevel, restartDriver = false) {
if (this.session) {
throw new Error('A debug session is already active');
}
// Ensure debug temp directory exists
await mkdir(debugTempDir, { recursive: true });
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const logFilePath = joinPath(debugTempDir, `ui-logs-${timestamp}.log`);
const driverLogFilePath = joinPath(debugTempDir, `driver-logs-${timestamp}.log`);
// Create a file transport to capture UI logs
const transport = new transports.File({
filename: logFilePath,
format: customFormat(true),
level: 'debug',
});
// Add transport to all existing loggers
logContainer.loggers.forEach((logger) => {
logger.add(transport);
// Also set logger level to debug
logger.level = 'debug';
});
// Set up driver debug transport that persists across restarts
let driverDebugTransport = undefined;
let driverLogStream = undefined;
const debugTransport = new JSONTransport();
debugTransport.format = createDefaultTransportFormat(false, true);
// Write driver logs to file
driverLogStream = createWriteStream(driverLogFilePath);
debugTransport.stream.on('data', (data) => {
driverLogStream.write(data.message.toString() + '\n');
});
driverDebugTransport = debugTransport;
// Register transport so it persists across driver restarts
zwaveClient.addExtraLogTransport(debugTransport, 'debug');
this.session = {
startTime: new Date(),
logFilePath,
driverLogFilePath,
transport,
originalLogLevel,
driverDebugTransport,
driverLogStream,
zwaveClient,
};
// Restart driver if requested to capture startup logs
if (restartDriver && zwaveClient.driverReady) {
await zwaveClient.restart();
}
}
/**
* Stop the debug session and generate a zip file with logs and node dumps
*/
async stopSession(nodeIds) {
const session = this.session;
await this.restoreSession(session);
// Wait a bit to ensure all logs are flushed to disk
await setTimeout(200);
// Create archive
const archive = archiver('zip', {
zlib: { level: 9 }, // Maximum compression
});
// Add UI logs to archive
if (await pathExists(session.logFilePath)) {
archive.file(session.logFilePath, {
name: `ui-logs-${session.startTime.toISOString()}.log`,
});
}
// Add driver logs to archive
if (await pathExists(session.driverLogFilePath)) {
archive.file(session.driverLogFilePath, {
name: `driver-logs-${session.startTime.toISOString()}.log`,
});
}
// Add node dumps to archive
for (const nodeId of nodeIds) {
try {
const driverDump = session.zwaveClient.dumpNode(nodeId);
archive.append(JSON.stringify(driverDump, null, 2), {
name: `node-${nodeId}-driver-dump.json`,
});
// Get node from client for UI dump
const node = session.zwaveClient.getNode(nodeId);
if (node) {
const uiDump = session.zwaveClient.nodes.get(nodeId);
if (uiDump) {
archive.append(JSON.stringify(uiDump, null, 2), {
name: `node-${nodeId}-ui-dump.json`,
});
}
}
}
catch (error) {
// Log error but continue with other nodes
archive.append(`Error dumping node ${nodeId}: ${error.message}`, {
name: `node-${nodeId}-error.txt`,
});
}
}
// Add session metadata
const metadata = {
startTime: session.startTime.toISOString(),
endTime: new Date().toISOString(),
duration: new Date().getTime() - session.startTime.getTime() + 'ms',
nodesIncluded: nodeIds,
};
archive.append(JSON.stringify(metadata, null, 2), {
name: 'session-metadata.json',
});
// Finalize the archive
await archive.finalize();
// Prepare cleanup function to delete temp files after download
const cleanup = async () => {
await this.cleanupTempFiles(session.logFilePath, session.driverLogFilePath);
};
return { archive, cleanup };
}
/**
* Cancel the current debug session without generating a package
*/
async cancelSession() {
const session = this.session;
await this.restoreSession(session);
// Clean up temp files
await this.cleanupTempFiles(session.logFilePath, session.driverLogFilePath);
}
async restoreSession(session) {
if (!this.session) {
throw new Error('No active debug session');
}
// Remove the debug transport from all loggers and restore log level
logContainer.loggers.forEach((logger) => {
logger.remove(session.transport);
logger.level = session.originalLogLevel;
});
// wait for transport to close properly
await new Promise((resolve) => {
session.transport.on('finish', () => resolve());
session.transport.end();
});
// Restore original driver log level
await this.restoreDriverLogLevel(session);
// Clear session
this.session = null;
}
/**
* Restore the driver log level after a debug session
*/
async restoreDriverLogLevel(session) {
if (session.driverDebugTransport) {
// Remove extra transport (works even if driver was restarted)
session.zwaveClient.removeExtraLogTransport(session.driverDebugTransport);
// Restore original log level if driver is still running
if (session.zwaveClient.driverReady) {
session.zwaveClient.driver.updateLogConfig({
level: session.originalLogLevel,
});
}
// Clean up debug transport
if (session.driverDebugTransport.stream) {
session.driverDebugTransport.stream.destroy();
}
// Close driver log stream properly
if (session.driverLogStream) {
await new Promise((resolve, reject) => {
session.driverLogStream.end(() => resolve());
session.driverLogStream.on('error', reject);
});
}
}
}
/**
* Clean up temporary files
*/
async cleanupTempFiles(logFilePath, driverLogFilePath) {
try {
if (await pathExists(logFilePath)) {
await rm(logFilePath, { force: true });
}
if (await pathExists(driverLogFilePath)) {
await rm(driverLogFilePath, { force: true });
}
}
catch (error) {
// Log but don't throw - cleanup is best effort
console.error('Error cleaning up debug temp files:', error);
}
}
}
export default new DebugManager();