265 lines
8.1 KiB
TypeScript
265 lines
8.1 KiB
TypeScript
import * as plugins from '../plugins.js';
|
|
import * as paths from '../paths.js';
|
|
import type { IProcessLog } from '../shared/protocol/ipc.types.js';
|
|
import type { ProcessId } from '../shared/protocol/id.js';
|
|
|
|
/**
|
|
* Manages crash log storage for failed processes
|
|
*/
|
|
export class CrashLogManager {
|
|
private crashLogsDir: string;
|
|
private readonly MAX_CRASH_LOGS = 100;
|
|
private readonly MAX_LOG_SIZE_BYTES = 1024 * 1024; // 1MB
|
|
|
|
constructor() {
|
|
this.crashLogsDir = plugins.path.join(paths.tspmDir, 'crashlogs');
|
|
}
|
|
|
|
/**
|
|
* Save a crash log for a failed process
|
|
*/
|
|
public async saveCrashLog(
|
|
processId: ProcessId,
|
|
processName: string,
|
|
logs: IProcessLog[],
|
|
exitCode: number | null,
|
|
signal: string | null,
|
|
restartCount: number,
|
|
memoryUsage?: number
|
|
): Promise<void> {
|
|
try {
|
|
// Ensure directory exists
|
|
await this.ensureCrashLogsDir();
|
|
|
|
// Generate filename with timestamp
|
|
const timestamp = new Date();
|
|
const dateStr = this.formatDate(timestamp);
|
|
const sanitizedName = this.sanitizeFilename(processName);
|
|
const filename = `${dateStr}_${processId}_${sanitizedName}.log`;
|
|
const filepath = plugins.path.join(this.crashLogsDir, filename);
|
|
|
|
// Get recent logs that fit within size limit
|
|
const recentLogs = this.getRecentLogs(logs, this.MAX_LOG_SIZE_BYTES);
|
|
|
|
// Create crash report
|
|
const crashReport = this.formatCrashReport({
|
|
processId,
|
|
processName,
|
|
timestamp,
|
|
exitCode,
|
|
signal,
|
|
restartCount,
|
|
memoryUsage,
|
|
logs: recentLogs
|
|
});
|
|
|
|
// Write crash log
|
|
await plugins.smartfile.memory.toFs(crashReport, filepath);
|
|
|
|
// Rotate old logs if needed
|
|
await this.rotateOldLogs();
|
|
|
|
console.log(`Crash log saved: ${filename}`);
|
|
} catch (error) {
|
|
console.error(`Failed to save crash log for process ${processId}:`, error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Format date for filename: YYYY-MM-DD_HH-mm-ss
|
|
*/
|
|
private formatDate(date: Date): string {
|
|
const year = date.getFullYear();
|
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
const day = String(date.getDate()).padStart(2, '0');
|
|
const hours = String(date.getHours()).padStart(2, '0');
|
|
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
const seconds = String(date.getSeconds()).padStart(2, '0');
|
|
return `${year}-${month}-${day}_${hours}-${minutes}-${seconds}`;
|
|
}
|
|
|
|
/**
|
|
* Sanitize process name for use in filename
|
|
*/
|
|
private sanitizeFilename(name: string): string {
|
|
// Replace problematic characters with underscore
|
|
return name
|
|
.replace(/[^a-zA-Z0-9-_]/g, '_')
|
|
.replace(/_+/g, '_')
|
|
.substring(0, 50); // Limit length
|
|
}
|
|
|
|
/**
|
|
* Get recent logs that fit within the size limit
|
|
*/
|
|
private getRecentLogs(logs: IProcessLog[], maxBytes: number): IProcessLog[] {
|
|
if (logs.length === 0) return [];
|
|
|
|
// Start from the end and work backwards
|
|
const recentLogs: IProcessLog[] = [];
|
|
let currentSize = 0;
|
|
|
|
for (let i = logs.length - 1; i >= 0; i--) {
|
|
const log = logs[i];
|
|
const logSize = this.estimateLogSize(log);
|
|
|
|
if (currentSize + logSize > maxBytes && recentLogs.length > 0) {
|
|
// Would exceed limit, stop adding
|
|
break;
|
|
}
|
|
|
|
recentLogs.unshift(log);
|
|
currentSize += logSize;
|
|
}
|
|
|
|
return recentLogs;
|
|
}
|
|
|
|
/**
|
|
* Estimate size of a log entry in bytes
|
|
*/
|
|
private estimateLogSize(log: IProcessLog): number {
|
|
// Format: [timestamp] [type] message\n
|
|
const formatted = `[${new Date(log.timestamp).toISOString()}] [${log.type}] ${log.message}\n`;
|
|
return Buffer.byteLength(formatted, 'utf8');
|
|
}
|
|
|
|
/**
|
|
* Format a crash report with metadata and logs
|
|
*/
|
|
private formatCrashReport(data: {
|
|
processId: ProcessId;
|
|
processName: string;
|
|
timestamp: Date;
|
|
exitCode: number | null;
|
|
signal: string | null;
|
|
restartCount: number;
|
|
memoryUsage?: number;
|
|
logs: IProcessLog[];
|
|
}): string {
|
|
const lines: string[] = [
|
|
'================================================================================',
|
|
'TSPM CRASH REPORT',
|
|
'================================================================================',
|
|
`Process: ${data.processName} (ID: ${data.processId})`,
|
|
`Date: ${data.timestamp.toISOString()}`,
|
|
`Exit Code: ${data.exitCode ?? 'N/A'}`,
|
|
`Signal: ${data.signal ?? 'N/A'}`,
|
|
`Restart Attempt: ${data.restartCount}/10`,
|
|
];
|
|
|
|
if (data.memoryUsage !== undefined && data.memoryUsage > 0) {
|
|
lines.push(`Memory Usage: ${this.humanReadableBytes(data.memoryUsage)}`);
|
|
}
|
|
|
|
lines.push(
|
|
'================================================================================',
|
|
'',
|
|
`LAST ${data.logs.length} LOG ENTRIES:`,
|
|
'--------------------------------------------------------------------------------',
|
|
''
|
|
);
|
|
|
|
// Add log entries
|
|
for (const log of data.logs) {
|
|
const timestamp = new Date(log.timestamp).toISOString();
|
|
const type = log.type.toUpperCase().padEnd(6);
|
|
lines.push(`[${timestamp}] [${type}] ${log.message}`);
|
|
}
|
|
|
|
lines.push(
|
|
'',
|
|
'================================================================================',
|
|
'END OF CRASH REPORT',
|
|
'================================================================================',
|
|
''
|
|
);
|
|
|
|
return lines.join('\n');
|
|
}
|
|
|
|
/**
|
|
* Convert bytes to human-readable format
|
|
*/
|
|
private humanReadableBytes(bytes: number): string {
|
|
if (bytes === 0) return '0 Bytes';
|
|
const k = 1024;
|
|
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
}
|
|
|
|
/**
|
|
* Ensure crash logs directory exists
|
|
*/
|
|
private async ensureCrashLogsDir(): Promise<void> {
|
|
await plugins.smartfile.fs.ensureDir(this.crashLogsDir);
|
|
}
|
|
|
|
/**
|
|
* Rotate old crash logs when exceeding max count
|
|
*/
|
|
private async rotateOldLogs(): Promise<void> {
|
|
try {
|
|
// Get all crash log files
|
|
const files = await plugins.smartfile.fs.listFileTree(this.crashLogsDir, '*.log');
|
|
|
|
if (files.length <= this.MAX_CRASH_LOGS) {
|
|
return; // No rotation needed
|
|
}
|
|
|
|
// Get file stats and sort by modification time (oldest first)
|
|
const fileStats = await Promise.all(
|
|
files.map(async (file) => {
|
|
const filepath = plugins.path.join(this.crashLogsDir, file);
|
|
const stats = await plugins.smartfile.fs.stat(filepath);
|
|
return { filepath, mtime: stats.mtime.getTime() };
|
|
})
|
|
);
|
|
|
|
fileStats.sort((a, b) => a.mtime - b.mtime);
|
|
|
|
// Delete oldest files to stay under limit
|
|
const filesToDelete = fileStats.length - this.MAX_CRASH_LOGS;
|
|
for (let i = 0; i < filesToDelete; i++) {
|
|
await plugins.smartfile.fs.remove(fileStats[i].filepath);
|
|
console.log(`Rotated old crash log: ${plugins.path.basename(fileStats[i].filepath)}`);
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to rotate crash logs:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get list of crash logs for a specific process
|
|
*/
|
|
public async getCrashLogsForProcess(processId: ProcessId): Promise<string[]> {
|
|
try {
|
|
await this.ensureCrashLogsDir();
|
|
const files = await plugins.smartfile.fs.listFileTree(this.crashLogsDir, `*_${processId}_*.log`);
|
|
return files.map(file => plugins.path.join(this.crashLogsDir, file));
|
|
} catch (error) {
|
|
console.error(`Failed to get crash logs for process ${processId}:`, error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clean up all crash logs (for maintenance)
|
|
*/
|
|
public async cleanupAllCrashLogs(): Promise<void> {
|
|
try {
|
|
await this.ensureCrashLogsDir();
|
|
const files = await plugins.smartfile.fs.listFileTree(this.crashLogsDir, '*.log');
|
|
|
|
for (const file of files) {
|
|
const filepath = plugins.path.join(this.crashLogsDir, file);
|
|
await plugins.smartfile.fs.remove(filepath);
|
|
}
|
|
|
|
console.log(`Cleaned up ${files.length} crash logs`);
|
|
} catch (error) {
|
|
console.error('Failed to cleanup crash logs:', error);
|
|
}
|
|
}
|
|
} |