package gui import ( "fmt" "os" "os/exec" "path/filepath" "runtime" "time" ) const ( // appDirName 是用户配置目录下的应用目录名。 appDirName = "huya-go" logSubDir = "logs" logName = "huya-go.log" defaultMaxLogFileBytes = 5 << 20 // 单个日志文件上限 5MB defaultMaxLogBackups = 5 // 最多保留 5 个滚动文件 ) // logSink 把日志追加写到磁盘,超过大小上限时按 .1/.2/... 滚动,避免长期运行写满磁盘。 type logSink struct { dir string path string file *os.File size int64 maxBytes int64 backups int } // newLogSink 在系统用户配置目录下创建 /huya-go/logs/huya-go.log。 func newLogSink() (*logSink, error) { base, err := os.UserConfigDir() if err != nil { return nil, err } return newLogSinkAt(filepath.Join(base, appDirName, logSubDir)) } func newLogSinkAt(dir string) (*logSink, error) { if err := os.MkdirAll(dir, 0o755); err != nil { return nil, err } sink := &logSink{ dir: dir, path: filepath.Join(dir, logName), maxBytes: defaultMaxLogFileBytes, backups: defaultMaxLogBackups, } if err := sink.open(); err != nil { return nil, err } return sink, nil } // Dir 返回日志目录。 func (s *logSink) Dir() string { return s.dir } // Path 返回当前日志文件路径。 func (s *logSink) Path() string { return s.path } func (s *logSink) open() error { file, err := os.OpenFile(s.path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644) if err != nil { return err } info, err := file.Stat() if err != nil { _ = file.Close() return err } s.file = file s.size = info.Size() return nil } // Write 追加一条日志,必要时先滚动。 func (s *logSink) Write(entry LogEntry) error { if s.file == nil { if err := s.open(); err != nil { return err } } line := formatLogLine(entry) if s.maxBytes > 0 && s.size+int64(len(line)) > s.maxBytes { if err := s.rotate(); err != nil { return err } } n, err := s.file.WriteString(line) s.size += int64(n) return err } func (s *logSink) rotate() error { if s.file != nil { _ = s.file.Close() s.file = nil } for i := s.backups - 1; i >= 1; i-- { from := fmt.Sprintf("%s.%d", s.path, i) if _, err := os.Stat(from); err != nil { continue } _ = os.Rename(from, fmt.Sprintf("%s.%d", s.path, i+1)) } if _, err := os.Stat(s.path); err == nil { _ = os.Rename(s.path, s.path+".1") } s.size = 0 return s.open() } // Close 关闭当前文件。 func (s *logSink) Close() error { if s.file == nil { return nil } err := s.file.Close() s.file = nil return err } func formatLogLine(entry LogEntry) string { at := entry.at if at.IsZero() { at = time.Now() } return fmt.Sprintf("%s [%s] %s\n", at.Format("2006-01-02 15:04:05.000"), entry.Level, entry.Message) } // openDirectory 调用系统文件管理器打开目录。 func openDirectory(dir string) error { var cmd *exec.Cmd switch runtime.GOOS { case "darwin": cmd = exec.Command("open", dir) case "windows": cmd = exec.Command("explorer", dir) default: cmd = exec.Command("xdg-open", dir) } return cmd.Start() }