package main import ( "context" "fmt" "io" "os" "path/filepath" "sync" "time" log "github.com/sirupsen/logrus" prefixed "github.com/x-cray/logrus-prefixed-formatter" ) var Log *log.Logger var logFile *os.File var logWriter *syncWriter var logCtx context.Context var logCancel context.CancelFunc // syncWriter écrit de manière synchrone (pas de buffer) type syncWriter struct { file *os.File mutex sync.Mutex } func (w *syncWriter) Write(p []byte) (n int, err error) { w.mutex.Lock() defer w.mutex.Unlock() n, err = w.file.Write(p) if err == nil { w.file.Sync() // Force l'écriture immédiate sur disque } return n, err } func NewLog() *log.Logger { // ✅ Vérifier que Cfg existe if Cfg == nil { panic("Config not initialized! Call NewConfig() before NewLog()") } // ✅ Chemin du log à côté de l'exe exe, _ := os.Executable() exePath := filepath.Dir(exe) logPath := filepath.Join(exePath, "flexradio.log") if Cfg.General.DeleteLogFileAtStart { if _, err := os.Stat(logPath); err == nil { os.Remove(logPath) } } logCtx, logCancel = context.WithCancel(context.Background()) var w io.Writer if Cfg.General.LogToFile { f, err := os.OpenFile(logPath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) if err != nil { panic(fmt.Sprintf("Cannot open log file %s: %v", logPath, err)) } logFile = f logWriter = &syncWriter{file: f} // ✅ IMPORTANT: Vérifier si Stdout est disponible (mode console vs GUI) if isConsoleAvailable() { // Mode console : log vers fichier ET console w = io.MultiWriter(os.Stdout, logWriter) } else { // Mode GUI (windowsgui) : log SEULEMENT vers fichier w = logWriter } } else { // Log uniquement vers console (si disponible) if isConsoleAvailable() { w = os.Stdout } else { // Pas de console, pas de log fichier -> log vers null w = io.Discard } } Log = &log.Logger{ Out: w, Formatter: &prefixed.TextFormatter{ DisableColors: !isConsoleAvailable(), TimestampFormat: "02-01-2006 15:04:05", FullTimestamp: true, ForceFormatting: true, DisableSorting: true, // ✅ Ajoute QuoteEmptyFields: true, // ✅ Ajoute SpacePadding: 0, // ✅ Ajoute (pas d'espace) }, Hooks: make(log.LevelHooks), } if Cfg.General.LogLevel == "DEBUG" { Log.Level = log.DebugLevel } else if Cfg.General.LogLevel == "INFO" { Log.Level = log.InfoLevel } else if Cfg.General.LogLevel == "WARN" { Log.Level = log.WarnLevel } else { Log.Level = log.InfoLevel } logBuffer = NewLogBuffer(500) // Garde les 500 derniers logs // Log.AddHook(&LogHook{buffer: logBuffer}) // ✅ Premier vrai log Log.Infof("Logger initialized - Level: %s, ToFile: %v, LogPath: %s", Cfg.General.LogLevel, Cfg.General.LogToFile, logPath) return Log } func InitLogHook() { if logBuffer == nil { logBuffer = NewLogBuffer(500) } Log.AddHook(&LogHook{buffer: logBuffer}) Log.Info("Log hook initialized and broadcasting enabled") } // ✅ Détecter si on a une console (fonctionne sur Windows) func isConsoleAvailable() bool { // Si Stdout est nil ou invalide, on n'a pas de console stat, err := os.Stdout.Stat() if err != nil { return false } // Si c'est un char device, on a une console return (stat.Mode() & os.ModeCharDevice) != 0 } // ✅ Fonction pour fermer proprement le log func CloseLog() { if Log != nil { Log.Info("Closing log file...") } if logCancel != nil { logCancel() } time.Sleep(200 * time.Millisecond) // Donne le temps d'écrire if logWriter != nil { logWriter.mutex.Lock() if logFile != nil { logFile.Sync() logFile.Close() logFile = nil } logWriter.mutex.Unlock() } }