- 日志落盘:新增 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 补日志说明
146 lines
3.1 KiB
Go
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()
|
|
}
|