Files
huya-go/internal/gui/logfile.go
T
yml2213 41565566c5 GUI 日志落盘,修复同一奖品重复兑换与监控结束竞态
- 日志落盘:新增 internal/gui/logfile.go,写到 <用户配置目录>/huya-go/logs/huya-go.log,单文件 5MB 后滚动保留 5 个备份;「日志」页签顶部显示路径并支持打开目录/复制路径;写盘失败自动降级为仅界面输出
- 修复重复兑换:界面选品会同时提交 pid、名称与 targetText 里的同一 pid,normalizeTargets 现按 pid/名称去重;runState 再按「账号+奖品」记账,多个 target 命中同一奖品时只兑换一次(新增 skip_duplicate 结果)
- 修复 use of closed network connection:成功后的异步刷新改为 runState.refresh 等待,RunTargets 返回前不再提前关闭连接
- queryHttpDns 增加 single-flight 与失败冷却,多账号并发建连只解析一次
- 新增 internal/gui/logfile_test.go 与 sniper/huya 回归测试;README 补日志说明
2026-10-13 20:34:43 +08:00

146 lines
3.1 KiB
Go

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 在系统用户配置目录下创建 <config>/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()
}