package logger import ( "fmt" "io" "os" "path/filepath" "strings" "sync" "time" ) type LogLevel int const ( DEBUG LogLevel = iota INFO WARN ERROR ) var levelNames = map[LogLevel]string{ DEBUG: "DEBUG", INFO: "INFO", WARN: "WARN", ERROR: "ERROR", } type Logger struct { mu sync.Mutex logFile *os.File logPath string logDir string } var ( defaultLogger *Logger once sync.Once ) // Init initializes the centralized logger with file & console outputs. func Init() (*Logger, error) { var initErr error once.Do(func() { configDir, err := os.UserConfigDir() if err != nil { configDir = "." } logDir := filepath.Join(configDir, "g-launcher", "logs") if err := os.MkdirAll(logDir, 0755); err != nil { initErr = fmt.Errorf("create log dir: %w", err) return } logPath := filepath.Join(logDir, "launcher.log") file, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) if err != nil { initErr = fmt.Errorf("open log file: %w", err) return } defaultLogger = &Logger{ logFile: file, logPath: logPath, logDir: logDir, } defaultLogger.write(INFO, "=== G-Launcher Log Session Started ===") defaultLogger.write(INFO, fmt.Sprintf("Log Path: %s", logPath)) }) return defaultLogger, initErr } // GetInstance returns the default logger, initializing if necessary. func GetInstance() *Logger { if defaultLogger == nil { _, _ = Init() } return defaultLogger } func (l *Logger) write(level LogLevel, message string) { if l == nil { return } l.mu.Lock() defer l.mu.Unlock() timestamp := time.Now().Format("2006-01-02 15:04:05.000") lvl := levelNames[level] line := fmt.Sprintf("[%s] [%-5s] %s\n", timestamp, lvl, message) // Write to console fmt.Print(line) // Write to log file if l.logFile != nil { _, _ = l.logFile.WriteString(line) _ = l.logFile.Sync() } } func (l *Logger) Log(levelStr string, message string) { lvl := INFO switch strings.ToUpper(strings.TrimSpace(levelStr)) { case "DEBUG": lvl = DEBUG case "WARN", "WARNING": lvl = WARN case "ERROR": lvl = ERROR } l.write(lvl, message) } func (l *Logger) Debug(format string, args ...interface{}) { l.write(DEBUG, fmt.Sprintf(format, args...)) } func (l *Logger) Info(format string, args ...interface{}) { l.write(INFO, fmt.Sprintf(format, args...)) } func (l *Logger) Warn(format string, args ...interface{}) { l.write(WARN, fmt.Sprintf(format, args...)) } func (l *Logger) Error(format string, args ...interface{}) { l.write(ERROR, fmt.Sprintf(format, args...)) } func (l *Logger) GetLogPath() string { if l == nil { return "" } return l.logPath } func (l *Logger) GetLogDir() string { if l == nil { return "" } return l.logDir } func (l *Logger) ReadRecentLogs(maxLines int) (string, error) { if l == nil || l.logPath == "" { return "", fmt.Errorf("logger not initialized") } l.mu.Lock() defer l.mu.Unlock() data, err := os.ReadFile(l.logPath) if err != nil { return "", err } lines := strings.Split(string(data), "\n") if len(lines) <= maxLines { return string(data), nil } recent := lines[len(lines)-maxLines:] return strings.Join(recent, "\n"), nil } func (l *Logger) Close() { l.mu.Lock() defer l.mu.Unlock() if l.logFile != nil { _ = l.logFile.Close() l.logFile = nil } } // Global package-level wrappers func Debug(format string, args ...interface{}) { GetInstance().Debug(format, args...) } func Info(format string, args ...interface{}) { GetInstance().Info(format, args...) } func Warn(format string, args ...interface{}) { GetInstance().Warn(format, args...) } func Error(format string, args ...interface{}) { GetInstance().Error(format, args...) } func GetLogPath() string { return GetInstance().GetLogPath() } func GetLogDir() string { return GetInstance().GetLogDir() } func ReadRecentLogs(maxLines int) (string, error) { return GetInstance().ReadRecentLogs(maxLines) } // CaptureProcessOutput attaches pipes to a command and logs its output in real-time. func (l *Logger) CaptureProcessOutput(prefix string, stdout, stderr io.ReadCloser) { go func() { buf := make([]byte, 1024) for { n, err := stdout.Read(buf) if n > 0 { msg := strings.TrimSpace(string(buf[:n])) if msg != "" { l.Info("[%s:STDOUT] %s", prefix, msg) } } if err != nil { break } } }() go func() { buf := make([]byte, 1024) for { n, err := stderr.Read(buf) if n > 0 { msg := strings.TrimSpace(string(buf[:n])) if msg != "" { l.Warn("[%s:STDERR] %s", prefix, msg) } } if err != nil { break } } }() }