/** * Galaxy Strike Online - Logging System * Provides file-based logging with rotation and formatting * Renderer process version that uses IPC to communicate with main process */ class Logger { constructor() { this.logLevel = 'INFO'; this.isRenderer = typeof window !== 'undefined' && typeof window.electronAPI !== 'undefined'; this.timers = new Map(); this.levels = { ERROR: 0, WARN: 1, INFO: 2, DEBUG: 3 }; } async initialize(appDataPath) { if (this.isRenderer) { // In renderer process, just log that we're ready console.log('Logger initialized in renderer process'); return; } // Main process initialization (original code) const fs = require('fs').promises; const path = require('path'); try { // Set up log directory in app storage location this.logDir = path.join(appDataPath, 'logs'); // Create logs directory if it doesn't exist await fs.mkdir(this.logDir, { recursive: true }); // Set current log file with full timestamp (YYYY-MM-DD-HH-MM-SS) const now = new Date(); const timestamp = now.toISOString() .replace(/T/, '-') .replace(/\..+/, '') .replace(/:/g, '-'); this.currentLogFile = path.join(this.logDir, `galaxy-strike-${timestamp}.log`); // Test write to ensure permissions await this.writeToFile('=== Galaxy Strike Online Log Session Started ===\n'); this.isInitialized = true; console.log(`Logger initialized: ${this.logDir}`); } catch (error) { console.error('Failed to initialize logger:', error); this.isInitialized = false; } } async writeToFile(message) { if (this.isRenderer) { // In renderer process, send to main process via IPC return; } if (!this.isInitialized || !this.currentLogFile) return; try { const fs = require('fs').promises; // Check if file needs rotation await this.rotateLogIfNeeded(); // Append message to current log file await fs.appendFile(this.currentLogFile, message, 'utf8'); } catch (error) { console.error('Failed to write to log file:', error); } } async rotateLogIfNeeded() { if (this.isRenderer) return; const fs = require('fs').promises; const path = require('path'); try { const stats = await fs.stat(this.currentLogFile); if (stats.size >= this.maxFileSize) { // Rotate log file with full timestamp const now = new Date(); const timestamp = now.toISOString() .replace(/T/, '-') .replace(/\..+/, '') .replace(/:/g, '-'); const rotatedFile = path.join(this.logDir, `galaxy-strike-${timestamp}.log`); await fs.rename(this.currentLogFile, rotatedFile); // Clean up old log files await this.cleanupOldLogs(); // Create new current log file with new timestamp const newTimestamp = new Date().toISOString() .replace(/T/, '-') .replace(/\..+/, '') .replace(/:/g, '-'); this.currentLogFile = path.join(this.logDir, `galaxy-strike-${newTimestamp}.log`); await this.writeToFile('=== Log Rotated ===\n'); } } catch (error) { // File might not exist yet, which is fine if (error.code !== 'ENOENT') { console.error('Failed to rotate log:', error); } } } async cleanupOldLogs() { if (this.isRenderer) return; const fs = require('fs').promises; const path = require('path'); try { const files = await fs.readdir(this.logDir); const logFiles = files .filter(file => file.startsWith('galaxy-strike-') && file.endsWith('.log')) .map(file => ({ name: file, path: path.join(this.logDir, file) })); if (logFiles.length > this.maxLogFiles) { // Get file stats and sort by modification time const filesWithStats = await Promise.all( logFiles.map(async file => { const stats = await fs.stat(file.path); return { ...file, mtime: stats.mtime }; }) ); filesWithStats.sort((a, b) => b.mtime - a.mtime); // Delete oldest files const filesToDelete = filesWithStats.slice(this.maxLogFiles); for (const file of filesToDelete) { await fs.unlink(file.path); } } } catch (error) { console.error('Failed to cleanup old logs:', error); } } formatMessage(level, message, data = null) { const timestamp = new Date().toISOString(); const dataStr = data ? ` | Data: ${JSON.stringify(data)}` : ''; return `[${timestamp}] [${level}] ${message}${dataStr}\n`; } shouldLog(level) { const currentLevel = this.levels[this.logLevel] || 2; const messageLevel = this.levels[level] || 2; return messageLevel <= currentLevel; } async log(level, message, data = null) { if (!this.shouldLog(level)) return; if (this.isRenderer && window.electronAPI) { // Send to main process via IPC - ensure data is serializable try { const serializableData = data ? this.makeSerializable(data) : null; window.electronAPI.log(level, message, serializableData); } catch (error) { console.error('Failed to send log to main process:', error); // Fallback to console console.log(`[${level}] ${message}`, data || ''); } } else { // Main process logging const formattedMessage = this.formatMessage(level, message, data); await this.writeToFile(formattedMessage); console.log(`[${level}] ${message}`, data || ''); } } makeSerializable(obj) { try { // Convert to JSON and back to ensure it's serializable return JSON.parse(JSON.stringify(obj)); } catch (error) { // If not serializable, convert to string representation return { type: 'non-serializable', toString: obj.toString ? obj.toString() : String(obj), constructor: obj.constructor ? obj.constructor.name : 'Unknown' }; } } async error(message, data = null) { await this.log('error', message, data); console.error(`[ERROR] ${message}`, data || ''); } async warn(message, data = null) { await this.log('warn', message, data); console.warn(`[WARN] ${message}`, data || ''); } async info(message, data = null) { await this.log('info', message, data); } async debug(message, data = null) { await this.log('debug', message, data); } async gameEvent(eventType, details) { const message = `Game Event: ${eventType}`; await this.info(message, details); } // Timing methods startTimer(name) { this.timers.set(name, performance.now()); } endTimer(name) { const startTime = this.timers.get(name); if (startTime) { const duration = performance.now() - startTime; this.timers.delete(name); return duration; } return 0; } async timeAsync(name, asyncFunction) { this.startTimer(name); try { const result = await asyncFunction(); const duration = this.endTimer(name); await this.info(`${name} completed`, { duration: `${duration.toFixed(2)}ms` }); return result; } catch (error) { const duration = this.endTimer(name); await this.error(`${name} failed`, { duration: `${duration.toFixed(2)}ms`, error: error.message }); throw error; } } async playerAction(action, details) { const message = `Player Action: ${action}`; await this.info(message, details); } async systemEvent(system, event, details) { const message = `System Event: ${system} - ${event}`; await this.info(message, details); } async errorEvent(error, context = null) { const message = `Error Event: ${error.message || error}`; const data = { stack: error.stack, context: context }; await this.error(message, data); } setLogLevel(level) { if (this.levels.hasOwnProperty(level)) { this.logLevel = level; this.info(`Log level changed to: ${level}`); } } getLogInfo() { return { isInitialized: this.isInitialized, isRenderer: this.isRenderer, logDirectory: this.logDir, currentLogFile: this.currentLogFile, logLevel: this.logLevel }; } } // Export singleton instance const logger = new Logger(); // For Node.js environments if (typeof module !== 'undefined' && module.exports) { module.exports = logger; } // For browser environments if (typeof window !== 'undefined') { window.logger = logger; }