//go:build windows package cafegame import ( "fmt" "github.com/lxn/win" "github.com/shirou/gopsutil/v3/process" "path/filepath" "strings" "sync" "time" "unicode" ) // SimpleProcessData 简化版进程数据(用于已存在进程列表) type SimpleProcessData struct { pid int name string exeFile string status int game bool } // RecordCallback 游戏记录回调:游戏开始/停止时提交,返回记录 ID type RecordCallback func(app AppData, status int) (int, error) // CountCallback 游戏统计回调:周期性提交统计信息 type CountCallback func(apps []AppData, interval int, foreground []ForegroundApp) error // EventCallback 游戏事件回调 type EventCallback func(eventData AppEventData) // AppRecord 游戏运行记录/监控主对象 type AppRecord struct { existProcessList []SimpleProcessData appMonitorList []*AppMonitor mux sync.Mutex waitChan chan AppWaitData postRecord RecordCallback // 提交记录 回调方法 postCount CountCallback // 提交统计 回调方法 eventCallback EventCallback // 事件回调方法 sourceList []int // 来源列表 表示需要监控哪个来源的游戏 reportInterval int } // AppWaitData 进程退出等待结果 type AppWaitData struct { AppData Pid int Status int } var appRecord *AppRecord var foregroundGame []ForegroundApp // RunAppRecord 运行游戏记录/监控 入口函数 func RunAppRecord(recordCallback RecordCallback, countCallback CountCallback, eventCallback EventCallback, sourceList []int, reportInterval int) { time.Sleep(time.Second * 10) foregroundGame = make([]ForegroundApp, 0) appRecord = new(AppRecord) appRecord.waitChan = make(chan AppWaitData, 100) appRecord.postRecord = recordCallback appRecord.postCount = countCallback appRecord.eventCallback = eventCallback appRecord.sourceList = sourceList appRecord.reportInterval = reportInterval // 检查上报间隔范围 if appRecord.reportInterval < 60 { appRecord.reportInterval = 60 } else if appRecord.reportInterval > 600 { appRecord.reportInterval = 600 } appRecord.Start() } // 应用事件 func (receiver *AppRecord) appEvent(eventData AppEventData) { if appRecord.eventCallback != nil { appRecord.eventCallback(eventData) } } // AppMonitorIsRun 指定游戏监控否运行 func (receiver *AppRecord) AppMonitorIsRun(gameId int, name string) bool { receiver.mux.Lock() defer receiver.mux.Unlock() for _, game := range receiver.appMonitorList { if game.AppId == gameId { return true } } return false } // ProcessIsExist 进程是否存在 func (receiver *AppRecord) ProcessIsExist(pid int, path string) bool { receiver.mux.Lock() defer receiver.mux.Unlock() for _, data := range receiver.existProcessList { if pid == data.pid { if strings.ToLower(path) == strings.ToLower(data.exeFile) { return true } // 进程路径不一致 receiver.appEvent(AppEventData{ ProcessPid: data.pid, ProcessPath: path, ProcessOldPath: data.exeFile, EventType: ProcessPathDifferent, EventSource: "AddProcess", }) return true } } return false } // RemoveProcess 移除进程 func (receiver *AppRecord) RemoveProcess(pid int) { receiver.mux.Lock() defer receiver.mux.Unlock() for i := 0; i < len(receiver.existProcessList); i++ { if pid == receiver.existProcessList[i].pid { receiver.existProcessList = append(receiver.existProcessList[:i], receiver.existProcessList[i+1:]...) i-- } } } // AddProcess 添加进程到已经存在列表 func (receiver *AppRecord) AddProcess(pid int, path string) { receiver.mux.Lock() defer receiver.mux.Unlock() processData := SimpleProcessData{ pid: pid, name: filepath.Base(path), exeFile: path, status: 1, } for i, data := range receiver.existProcessList { if pid == data.pid { if strings.ToLower(path) == strings.ToLower(data.exeFile) { // 进程已存在 receiver.appEvent(AppEventData{ ProcessPid: data.pid, ProcessPath: data.exeFile, EventType: ProcessExist, EventSource: "AddProcess", }) return } else { // 进程信息变更 receiver.appEvent(AppEventData{ ProcessPid: data.pid, ProcessPath: path, ProcessOldPath: data.exeFile, EventType: ProcessChange, EventSource: "AddProcess", }) receiver.existProcessList[i] = processData } return } } receiver.existProcessList = append(receiver.existProcessList, processData) } // AddAppMonitor 添加监控 func (receiver *AppRecord) AddAppMonitor(info AppData, pid int, path string) { receiver.mux.Lock() defer receiver.mux.Unlock() // 检查游戏是否已监控 for _, game := range receiver.appMonitorList { if game.AppId == info.AppId { // 已经监控,添加进程 if !game.AppIsRun() { // 两次启动间隔 要大于60秒,防止多次运行 重复统计。如:游戏进程不断重新启动,崩溃重启等。 if game.GetStartInterval() > 60 { if receiver.postRecord != nil { id, _ := receiver.postRecord(game.AppData, GameStatusStart) game.recordId = id } } else { // 游戏已存在 但启动间隔小于60秒,可能是游戏进程崩溃重启,忽略统计 receiver.appEvent(AppEventData{ AppId: info.AppId, AppName: info.AppName, ProcessPid: pid, ProcessPath: path, EventType: GameStartIntervalSmall, EventSource: "AddAppMonitor", }) return } } game.AddProcess(pid, path) // 游戏已存在 receiver.appEvent(AppEventData{ AppId: info.AppId, AppName: info.AppName, ProcessPid: pid, ProcessPath: path, EventType: GameExist, EventSource: "AddAppMonitor", }) return } } // 没有监控,添加游戏 game := NewAppMonitor(info, path, receiver.waitChan) game.AddProcess(pid, path) receiver.appMonitorList = append(receiver.appMonitorList, game) // 新增游戏 receiver.appEvent(AppEventData{ AppId: info.AppId, AppName: info.AppName, ProcessPid: pid, ProcessPath: path, EventType: GameAdd, EventSource: "AddAppMonitor", }) if receiver.postRecord != nil { id, _ := receiver.postRecord(game.AppData, GameStatusStart) game.recordId = id } } // Start 启动定时检查与进程退出监听协程 func (receiver *AppRecord) Start() { go receiver.timer() go receiver.readWaitChan() } // 定时器 每隔10秒检查一次所有系统中所有进程 func (receiver *AppRecord) timer() { i := 0 interval := 10 for { err := receiver.checkProcess() if err != nil { receiver.appEvent(AppEventData{ EventType: Error, EventSource: "checkProcess", Message: fmt.Sprintf("检查进程错误:%s", err.Error()), }) } receiver.checkForegroundApp(interval) time.Sleep(time.Second * time.Duration(interval)) i += interval // 60秒提交一次游戏统计信息 if i >= appRecord.reportInterval { i = 0 err = receiver.count() if err != nil { receiver.appEvent(AppEventData{ EventType: Error, EventSource: "checkProcess", Message: fmt.Sprintf("提交游戏统计错误:%s", err.Error()), }) } } } } // checkForegroundApp 检测前端窗口关联游戏 func (receiver *AppRecord) checkForegroundApp(interval int) { hWnd := win.GetForegroundWindow() if hWnd == 0 { receiver.appEvent(AppEventData{ EventType: ForegroundWindow, EventSource: "checkForegroundApp", Message: "无前端窗口", }) return } receiver.appEvent(AppEventData{ EventType: ForegroundWindow, EventSource: "checkForegroundApp", Message: fmt.Sprintf("前端窗口句柄:%d", hWnd), }) pid := uint32(0) ret := win.GetWindowThreadProcessId(hWnd, &pid) if ret == 0 { receiver.appEvent(AppEventData{ EventType: ForegroundWindow, EventSource: "checkForegroundApp", Message: "获取前端窗口关联进程失败", }) return } receiver.appEvent(AppEventData{ EventType: ForegroundWindow, EventSource: "checkForegroundApp", Message: fmt.Sprintf("前端窗口关联进程ID:%d", pid), }) game, ok := receiver.getGameByPid(int(pid)) if !ok { receiver.appEvent(AppEventData{ EventType: ForegroundWindow, EventSource: "checkForegroundApp", Message: "未能获取前端窗口关联游戏", }) return } receiver.appEvent(AppEventData{ EventType: ForegroundWindow, EventSource: "checkForegroundApp", Message: fmt.Sprintf("前端窗口关联游戏 id:%d name:%s", game.AppId, game.AppName), }) receiver.mux.Lock() defer receiver.mux.Unlock() for i, info := range foregroundGame { if info.AppId == game.AppId && info.AppSource == game.AppSource { foregroundGame[i].Duration += interval receiver.appEvent(AppEventData{ EventType: ForegroundWindow, EventSource: "checkForegroundApp", Message: fmt.Sprintf("找到游戏 id:%d name:%s duration:%d", game.AppId, game.AppName, foregroundGame[i].Duration), }) return } } receiver.appEvent(AppEventData{ EventType: ForegroundWindow, EventSource: "checkForegroundApp", Message: fmt.Sprintf("新增游戏 id:%d name:%s duration:%d", game.AppId, game.AppName, interval), }) foregroundGame = append(foregroundGame, ForegroundApp{ AppData: game, Duration: interval, }) } // getGameByPid 根据pid获取游戏信息 func (receiver *AppRecord) getGameByPid(pid int) (AppData, bool) { receiver.mux.Lock() defer receiver.mux.Unlock() for _, game := range receiver.appMonitorList { for _, processMonitor := range game.processList { if processMonitor.pid == pid { return game.AppData, true } } } return AppData{}, false } // count 提交游戏统计信息 func (receiver *AppRecord) count() error { receiver.mux.Lock() list := make([]AppData, 0) for _, game := range receiver.appMonitorList { if game.AppIsRun() { list = append(list, game.AppData) } } foreground := make([]ForegroundApp, 0) for i, info := range foregroundGame { if info.Duration > 0 { foreground = append(foreground, info) foregroundGame[i].Duration = 0 } } receiver.mux.Unlock() if receiver.postCount != nil { err := receiver.postCount(list, 60, foreground) if err != nil { return err } } return nil } // 检查系统中所有进程 func (receiver *AppRecord) checkProcess() error { processList, err := getProcessInfo() if err != nil { return err } for _, info := range processList { driveLetter := int32(0) for _, char := range info.ExE { driveLetter = char break } // 盘符是否为字母 if !unicode.IsLetter(driveLetter) { continue } // 跳过B和C盘 caps := strings.ToTitle(info.ExE[0:1]) if caps == "B" || caps == "C" { continue } // 进程是否已经存在 已经在监控中 if appRecord.ProcessIsExist(int(info.Pid), info.ExE) { continue } // 检测是否游戏进程 appRecord.GameCheck(int(info.Pid), info.ExE) // 添加到已存在进程,防止重复检测浪费CPU资源 appRecord.AddProcess(int(info.Pid), info.ExE) } return nil } // GameCheck 检测进程是否游戏进程 func (receiver *AppRecord) GameCheck(pid int, exePath string) error { info, err := GetGameInfoByPath(exePath) if err != nil { return err } if info.AppId > 0 { if receiver.inSource(info.AppSource) { // 检查进程是否被忽略 if !checkGameProcessIsIgnore(info.AppId, filepath.Base(exePath)) { // 没有被忽略 开始监控 appRecord.AddAppMonitor(info, pid, exePath) } else { // 进程被忽略 receiver.appEvent(AppEventData{ AppId: info.AppId, AppName: info.AppName, ProcessPid: pid, ProcessPath: exePath, EventType: ProcessIgnore, EventSource: "GameCheck", }) } } else { // 进程来源不在列表中 receiver.appEvent(AppEventData{ AppId: info.AppId, AppName: info.AppName, ProcessPid: pid, ProcessPath: exePath, EventType: ProcessSourceNotMatch, EventSource: "GameCheck", }) } } return nil } // inSource 判断来源是否在监控列表中 func (receiver *AppRecord) inSource(source int) bool { for _, id := range receiver.sourceList { if source == id { return true } } return false } // readWaitChan 消费进程退出结果:移除进程并延迟检查游戏是否整体退出 func (receiver *AppRecord) readWaitChan() { // 进程结束通道 for waitData := range receiver.waitChan { // 进程结束事件 receiver.appEvent(AppEventData{ AppId: waitData.AppId, AppName: waitData.AppName, ProcessPid: waitData.Pid, EventType: ProcessExit, EventSource: "readWaitChan", }) // 从进程列表中移除 receiver.RemoveProcess(waitData.Pid) // 检查游戏是否还有进程在运行 go func(data AppWaitData) { time.Sleep(time.Second * 15) receiver.checkGameIsRun(data) }(waitData) } } // checkGameIsRun 移除已退出的进程,游戏全部进程退出时结束统计 func (receiver *AppRecord) checkGameIsRun(data AppWaitData) { receiver.mux.Lock() defer receiver.mux.Unlock() for _, game := range receiver.appMonitorList { if game.AppId == data.AppId { game.RemoveProcess(data.Pid) if !game.AppIsRun() { // 游戏停止运行 receiver.appEvent(AppEventData{ AppId: game.AppId, AppName: game.AppName, EventType: GameStop, EventSource: "checkGameIsRun", }) if receiver.postRecord != nil { _, _ = receiver.postRecord(game.AppData, GameStatusStop) } } } } } // MemoryInfoStat 进程内存信息(RSS/虚拟内存) type MemoryInfoStat struct { Rss uint64 `json:"res"` Vms uint64 `json:"vms"` } // ProcessInfo 进程信息(PID/名称/路径/CPU/内存/启动时间) type ProcessInfo struct { Pid int32 `json:"pid"` Name string `json:"name"` ExE string `json:"exe"` Cmd string `json:"cmd"` Cpu float64 `json:"cpu"` Mem MemoryInfoStat `json:"mem"` CreateTime int64 `json:"createTime"` } // 获取系统中所有进程信息 func getProcessInfo() ([]ProcessInfo, error) { v, err := process.Processes() if err != nil { return nil, err } info := make([]ProcessInfo, len(v)) for i, p := range v { name := "" exe, _ := p.Exe() cmd, _ := p.Cmdline() // 先从执行文件中获取进程名 // 如果执行文件为空,再调用 Name() 方式,Name()方式很占用CPU if exe == "" { name, _ = p.Name() } else { name = filepath.Base(exe) } mem, _ := p.MemoryInfo() cpu, _ := p.CPUPercent() createTime, _ := p.CreateTime() p.Times() // 不获取线程数 太占用用CPU // p.NumThreads() info[i].Pid = p.Pid info[i].Name = name info[i].ExE = exe info[i].Cmd = cmd info[i].Cpu = cpu if mem != nil { info[i].Mem = MemoryInfoStat{ Rss: mem.RSS, Vms: mem.VMS, } } info[i].CreateTime = createTime if name == "QQ.exe" { //fmt.Printf("Pid:%d name:%s exe:%s\n", p.Pid, name, exe) //fmt.Printf("工作集:%d 提交大小:%d\n", mem.RSS, mem.VMS) //fmt.Printf("CPU:%.2f%% \n", cpu) } } if err != nil { return nil, err } return info, nil }