- 游戏来源常量/按目录特征识别(云更新/网维大师/易乐游)/RunAppRecord 上报框架 - 网吧业务专用包, 附测试、examples/cafegame 例程与包 README
598 lines
15 KiB
Go
598 lines
15 KiB
Go
//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
|
|
}
|