feat(cafegame): 自 go-hua 迁入网咖游戏识别与运行记录框架
- 游戏来源常量/按目录特征识别(云更新/网维大师/易乐游)/RunAppRecord 上报框架 - 网吧业务专用包, 附测试、examples/cafegame 例程与包 README
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
# cafegame
|
||||
|
||||
网咖游戏业务包:游戏来源常量(云更新/网维大师/易乐游)、按目录特征识别游戏
|
||||
(ID/名称/来源)、前台进程监控与运行记录上报回调框架。
|
||||
|
||||
> 迁移自 go-hua/cafegame,代码保持原样。**网吧业务专用包**(目录特征与各游戏
|
||||
> 平台的落盘规则强耦合)。
|
||||
|
||||
## 用法
|
||||
|
||||
```go
|
||||
import "git.zeroonesoft.cn/golib/zogo/cafegame"
|
||||
|
||||
// 游戏来源常量
|
||||
cafegame.SourceYGX // 1 云更新
|
||||
cafegame.SourceWWDS // 2 网维大师
|
||||
cafegame.SourceYLY // 3 易乐游
|
||||
cafegame.SourceToName(cafegame.SourceYGX)
|
||||
|
||||
// 按目录特征识别游戏(各平台一个判定函数)
|
||||
ok, info, err := cafegame.IsGameDirByYGX(`D:\40001示例游戏`)
|
||||
// info: AppData{AppId, AppName, AppSource, AppGuid...}
|
||||
|
||||
// 运行记录上报框架:启动后台监控,游戏开始/停止触发 recordCallback,
|
||||
// 周期性触发 countCallback(在线游戏数+前台进程),事件触发 eventCallback
|
||||
cafegame.RunAppRecord(recordCallback, countCallback, eventCallback,
|
||||
[]int{cafegame.SourceYGX}, 60 /* 上报间隔秒 */)
|
||||
```
|
||||
|
||||
完整可运行例程:[examples/cafegame/main.go](../examples/cafegame/main.go)
|
||||
|
||||
## 注意
|
||||
|
||||
- 目录识别规则与对应游戏平台版本耦合,平台更新落盘规则后需同步维护本包。
|
||||
- `RunAppRecord` 内部启动 goroutine,服务进程常驻场景使用;回调里不要阻塞。
|
||||
@@ -0,0 +1,63 @@
|
||||
package cafegame
|
||||
|
||||
// 游戏运行状态
|
||||
const GameStatusStart = 1 // 开始运行
|
||||
const GameStatusStop = 0 // 停止运行
|
||||
|
||||
const SourceYGX = 1 // 云更新
|
||||
const SourceWWDS = 2 // 网维大师
|
||||
const SourceYLY = 3 // 易乐游
|
||||
const SourceMTGX = 4 // 马蹄更新
|
||||
const SourceStory = 11 // 蘑菇
|
||||
const SourceGoodGame = 12 // 盖伦
|
||||
const SourceHaoSiJi = 13 // 好司机
|
||||
const SourceFast = 14 // 火箭
|
||||
const SourceWfGame = 15 // WF游戏
|
||||
// GetSourceAll 返回所有已支持的游戏来源 ID 列表
|
||||
func GetSourceAll() []int {
|
||||
return []int{SourceYGX, SourceWWDS, SourceYLY, SourceMTGX, SourceStory, SourceGoodGame, SourceHaoSiJi, SourceFast, SourceWfGame}
|
||||
}
|
||||
|
||||
// SourceToName 返回来源 ID 对应的名称,未知来源返回空字符串
|
||||
func SourceToName(id int) string {
|
||||
switch id {
|
||||
case SourceYGX:
|
||||
return "云更新"
|
||||
case SourceWWDS:
|
||||
return "网维大师"
|
||||
case SourceYLY:
|
||||
return "易乐游"
|
||||
case SourceMTGX:
|
||||
return "马蹄更新"
|
||||
case SourceStory:
|
||||
return "蘑菇"
|
||||
case SourceGoodGame:
|
||||
return "盖伦"
|
||||
case SourceHaoSiJi:
|
||||
return "好司机"
|
||||
case SourceFast:
|
||||
return "火箭"
|
||||
case SourceWfGame:
|
||||
return "WF游戏"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// 游戏监控事件类型
|
||||
const (
|
||||
Error = iota
|
||||
GameStart
|
||||
GameStop
|
||||
GameAdd
|
||||
GameDelete
|
||||
GameUpdate
|
||||
GameExist
|
||||
GameStartIntervalSmall
|
||||
ProcessExit
|
||||
ProcessIgnore
|
||||
ProcessSourceNotMatch
|
||||
ProcessExist
|
||||
ProcessChange
|
||||
ProcessPathDifferent
|
||||
ForegroundWindow
|
||||
)
|
||||
@@ -0,0 +1,53 @@
|
||||
package cafegame
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSourceToName(t *testing.T) {
|
||||
cases := map[int]string{
|
||||
SourceYGX: "云更新",
|
||||
SourceWWDS: "网维大师",
|
||||
SourceYLY: "易乐游",
|
||||
SourceMTGX: "马蹄更新",
|
||||
SourceStory: "蘑菇",
|
||||
SourceGoodGame: "盖伦",
|
||||
SourceHaoSiJi: "好司机",
|
||||
SourceFast: "火箭",
|
||||
SourceWfGame: "WF游戏",
|
||||
999: "", // 未知来源
|
||||
}
|
||||
for id, want := range cases {
|
||||
if got := SourceToName(id); got != want {
|
||||
t.Errorf("SourceToName(%d) = %q, want %q", id, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSourceAll(t *testing.T) {
|
||||
all := GetSourceAll()
|
||||
if len(all) == 0 {
|
||||
t.Fatal("GetSourceAll 不应为空")
|
||||
}
|
||||
for _, id := range all {
|
||||
if SourceToName(id) == "" {
|
||||
t.Errorf("来源 %d 无对应名称", id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGameStatusConstants(t *testing.T) {
|
||||
if GameStatusStart != 1 || GameStatusStop != 0 {
|
||||
t.Errorf("游戏状态常量错误: start=%d stop=%d", GameStatusStart, GameStatusStop)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsAllNumberHandle(t *testing.T) {
|
||||
if !IsAllNumberHandle("12345") {
|
||||
t.Error("纯数字应通过")
|
||||
}
|
||||
if IsAllNumberHandle("12a45") {
|
||||
t.Error("含字母应不通过")
|
||||
}
|
||||
if IsAllNumberHandle("") {
|
||||
t.Error("空串应不通过")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,678 @@
|
||||
//go:build windows
|
||||
|
||||
package cafegame
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// IsGameDirByYGX 云更新 id从40000开始
|
||||
func IsGameDirByYGX(dir string) (bool, AppData, error) {
|
||||
info := AppData{}
|
||||
gamePatch := filepath.Join(dir, "gamepatch")
|
||||
ok, err := pathExists(gamePatch)
|
||||
if err != nil {
|
||||
return false, info, err
|
||||
}
|
||||
if !ok {
|
||||
return false, info, nil
|
||||
}
|
||||
|
||||
gconfig := filepath.Join(gamePatch, "gconfig.ini")
|
||||
ok, err = pathExists(gconfig)
|
||||
if err != nil {
|
||||
return false, info, err
|
||||
}
|
||||
if !ok {
|
||||
return false, info, nil
|
||||
}
|
||||
|
||||
id, _ := getIdByNumberIco(dir)
|
||||
if id < 40000 {
|
||||
return false, info, nil
|
||||
}
|
||||
info.AppId = id
|
||||
info.AppName = filepath.Base(dir)
|
||||
info.AppDir = dir
|
||||
info.AppSource = SourceYGX
|
||||
|
||||
return true, info, nil
|
||||
}
|
||||
|
||||
// IsGameDirByStory 蘑菇 id从8000开始
|
||||
func IsGameDirByStory(dir string) (bool, AppData, error) {
|
||||
info := AppData{}
|
||||
gamePatch := filepath.Join(dir, "gamepatch")
|
||||
ok, err := pathExists(gamePatch)
|
||||
if err != nil {
|
||||
return false, info, err
|
||||
}
|
||||
if !ok {
|
||||
return false, info, nil
|
||||
}
|
||||
gconfig := filepath.Join(gamePatch, "gconfig.ini")
|
||||
ok, err = pathExists(gconfig)
|
||||
if err != nil {
|
||||
return false, info, err
|
||||
}
|
||||
if !ok {
|
||||
return false, info, nil
|
||||
}
|
||||
|
||||
id, _ := getIdByNumberIco(dir)
|
||||
if id < 10000 {
|
||||
return false, info, nil
|
||||
}
|
||||
info.AppId = id
|
||||
info.AppName = filepath.Base(dir)
|
||||
info.AppDir = dir
|
||||
info.AppSource = SourceStory
|
||||
|
||||
return true, info, nil
|
||||
}
|
||||
|
||||
// IsGameDirByWWDS 网维大师 ID 从 10001 开始
|
||||
func IsGameDirByWWDS(dir string) (bool, AppData, error) {
|
||||
info := AppData{}
|
||||
SunwardPkg := filepath.Join(dir, "SunwardPkg.idx")
|
||||
ok, err := pathExists(SunwardPkg)
|
||||
if err != nil {
|
||||
return false, info, err
|
||||
}
|
||||
if !ok {
|
||||
return false, info, nil
|
||||
}
|
||||
|
||||
SunwardPkgCks := filepath.Join(dir, "SunwardPkg.cks")
|
||||
ok, err = pathExists(SunwardPkgCks)
|
||||
if err != nil {
|
||||
return false, info, err
|
||||
}
|
||||
if !ok {
|
||||
fmt.Printf("没有cks文件 dir:%s\n", dir)
|
||||
return false, info, nil
|
||||
}
|
||||
|
||||
// 不能有这文件 这是盖伦文件
|
||||
GameIndex := filepath.Join(dir, "GameIndex.txt")
|
||||
ok, err = pathExists(GameIndex)
|
||||
if err != nil {
|
||||
return false, info, err
|
||||
}
|
||||
if ok {
|
||||
return false, info, nil
|
||||
}
|
||||
|
||||
// 不能有这文件,这是易乐游文件
|
||||
GameInfodb := filepath.Join(dir, "GameInfodb.gz")
|
||||
ok, err = pathExists(GameInfodb)
|
||||
if err != nil {
|
||||
return false, info, err
|
||||
}
|
||||
if ok {
|
||||
return false, info, nil
|
||||
}
|
||||
|
||||
// 不能有这文件 这是好司机文件
|
||||
hsjGame := filepath.Join(dir, "hsjGame.exe")
|
||||
ok, err = pathExists(hsjGame)
|
||||
if err != nil {
|
||||
return false, info, err
|
||||
}
|
||||
if ok {
|
||||
return false, info, nil
|
||||
}
|
||||
|
||||
id, _ := getIdBySunwardPkg(dir)
|
||||
if id < 10000 {
|
||||
return false, info, nil
|
||||
}
|
||||
info.AppId = id
|
||||
info.AppName = filepath.Base(dir)
|
||||
info.AppDir = dir
|
||||
info.AppSource = SourceWWDS
|
||||
|
||||
return true, info, nil
|
||||
}
|
||||
|
||||
// IsGameDirByGoodGame 盖伦 id应该是从 1001开始 steam是第一个游戏 1011
|
||||
func IsGameDirByGoodGame(dir string) (bool, AppData, error) {
|
||||
info := AppData{}
|
||||
GameIndex := filepath.Join(dir, "GameIndex.txt")
|
||||
ok, err := pathExists(GameIndex)
|
||||
if err != nil {
|
||||
return false, info, err
|
||||
}
|
||||
if !ok {
|
||||
return false, info, nil
|
||||
}
|
||||
GameCksum := filepath.Join(dir, "GameCksum.txt")
|
||||
ok, err = pathExists(GameCksum)
|
||||
if err != nil {
|
||||
return false, info, err
|
||||
}
|
||||
if !ok {
|
||||
return false, info, nil
|
||||
}
|
||||
|
||||
id, _ := getIdBySunwardPkg(dir)
|
||||
if id < 1000 {
|
||||
return false, info, nil
|
||||
}
|
||||
info.AppId = id
|
||||
info.AppName = filepath.Base(dir)
|
||||
info.AppDir = dir
|
||||
info.AppSource = SourceGoodGame
|
||||
|
||||
return true, info, nil
|
||||
}
|
||||
|
||||
// IsGameDirByYLY 易乐游
|
||||
func IsGameDirByYLY(dir string) (bool, AppData, error) {
|
||||
info := AppData{}
|
||||
GameInfodb := filepath.Join(dir, "GameInfodb.gz")
|
||||
ok, err := pathExists(GameInfodb)
|
||||
if err != nil {
|
||||
return false, info, err
|
||||
}
|
||||
if !ok {
|
||||
return false, info, nil
|
||||
}
|
||||
Gamever := filepath.Join(dir, "Gamever.ver")
|
||||
ok, err = pathExists(Gamever)
|
||||
if err != nil {
|
||||
return false, info, err
|
||||
}
|
||||
if !ok {
|
||||
return false, info, nil
|
||||
}
|
||||
|
||||
// TODO: 没有游戏ID,只有GUID
|
||||
guid, _ := GetGuidByYLY(dir)
|
||||
if guid == "{00000000-0000-0000-0000-000000000000}" {
|
||||
return false, info, nil
|
||||
}
|
||||
info.AppId = 0
|
||||
info.AppName = filepath.Base(dir)
|
||||
info.AppDir = dir
|
||||
info.AppSource = SourceYLY
|
||||
info.AppGuid = guid
|
||||
|
||||
return true, info, nil
|
||||
}
|
||||
|
||||
// IsGameDirByHaoSiJi 好司机
|
||||
func IsGameDirByHaoSiJi(dir string) (bool, AppData, error) {
|
||||
info := AppData{}
|
||||
hsjGame := filepath.Join(dir, "hsjGame.exe")
|
||||
ok, err := pathExists(hsjGame)
|
||||
if err != nil {
|
||||
return false, info, err
|
||||
}
|
||||
if !ok {
|
||||
return false, info, nil
|
||||
}
|
||||
|
||||
hsjLoader := filepath.Join(dir, "hsjLoader.exe")
|
||||
ok, err = pathExists(hsjLoader)
|
||||
if !ok {
|
||||
return false, info, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, info, err
|
||||
}
|
||||
|
||||
SunwardPkg := filepath.Join(dir, "SunwardPkg.idx")
|
||||
ok, err = pathExists(SunwardPkg)
|
||||
if !ok {
|
||||
return false, info, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, info, err
|
||||
}
|
||||
|
||||
id, _ := getIdBySunwardPkg(dir)
|
||||
if id == 0 {
|
||||
return false, info, nil
|
||||
}
|
||||
|
||||
guid, _ := GetGuidByYLY(dir)
|
||||
if guid == "{00000000-0000-0000-0000-000000000000}" {
|
||||
return false, info, nil
|
||||
}
|
||||
|
||||
info.AppId = id
|
||||
info.AppName = filepath.Base(dir)
|
||||
info.AppDir = dir
|
||||
info.AppSource = SourceHaoSiJi
|
||||
info.AppGuid = guid
|
||||
return true, info, nil
|
||||
}
|
||||
|
||||
// IsGameDirByMTGX 马蹄更新 id从10000开始
|
||||
func IsGameDirByMTGX(dir string) (bool, AppData, error) {
|
||||
info := AppData{}
|
||||
patchdata := filepath.Join(dir, "patchdata")
|
||||
ok, err := pathExists(patchdata)
|
||||
if err != nil {
|
||||
return false, info, err
|
||||
}
|
||||
if !ok {
|
||||
return false, info, nil
|
||||
}
|
||||
config := filepath.Join(patchdata, "config.ini")
|
||||
ok, err = pathExists(config)
|
||||
if err != nil {
|
||||
return false, info, err
|
||||
}
|
||||
if !ok {
|
||||
return false, info, nil
|
||||
}
|
||||
|
||||
ndstorrent := filepath.Join(dir, "ndstorrent.idx")
|
||||
ok, err = pathExists(ndstorrent)
|
||||
if err != nil {
|
||||
return false, info, err
|
||||
}
|
||||
if !ok {
|
||||
return false, info, nil
|
||||
}
|
||||
|
||||
id, _ := getIdByNumberIco(dir)
|
||||
if id < 10000 {
|
||||
return false, info, nil
|
||||
}
|
||||
info.AppId = id
|
||||
info.AppName = filepath.Base(dir)
|
||||
info.AppDir = dir
|
||||
info.AppSource = SourceMTGX
|
||||
|
||||
return true, info, nil
|
||||
}
|
||||
|
||||
// IsGameDirByFast 火箭 id从8000开始
|
||||
func IsGameDirByFast(dir string) (bool, AppData, error) {
|
||||
info := AppData{}
|
||||
patchdata := filepath.Join(dir, "patchdata")
|
||||
ok, err := pathExists(patchdata)
|
||||
if err != nil {
|
||||
return false, info, err
|
||||
}
|
||||
if !ok {
|
||||
return false, info, nil
|
||||
}
|
||||
|
||||
rkttorrent := filepath.Join(dir, "rkttorrent.idx")
|
||||
ok, err = pathExists(rkttorrent)
|
||||
if err != nil {
|
||||
return false, info, err
|
||||
}
|
||||
if !ok {
|
||||
return false, info, nil
|
||||
}
|
||||
|
||||
gconfig := filepath.Join(patchdata, "config.ini")
|
||||
ok, err = pathExists(gconfig)
|
||||
if err != nil {
|
||||
return false, info, err
|
||||
}
|
||||
if !ok {
|
||||
return false, info, nil
|
||||
}
|
||||
|
||||
id, _ := getIdByNumberIco(dir)
|
||||
if id < 8000 {
|
||||
return false, info, nil
|
||||
}
|
||||
info.AppId = id
|
||||
info.AppName = filepath.Base(dir)
|
||||
info.AppDir = dir
|
||||
info.AppSource = SourceFast
|
||||
|
||||
return true, info, nil
|
||||
}
|
||||
|
||||
// IsGameDirByWfGame WF游戏
|
||||
func IsGameDirByWfGame(dir string) (bool, AppData, error) {
|
||||
info := AppData{}
|
||||
ndsgameres := filepath.Join(dir, "ndsgameres.index")
|
||||
ok, err := pathExists(ndsgameres)
|
||||
if err != nil {
|
||||
return false, info, err
|
||||
}
|
||||
if !ok {
|
||||
return false, info, nil
|
||||
}
|
||||
|
||||
dd_launcher := filepath.Join(dir, "dd_launcher.json")
|
||||
ok, err = pathExists(dd_launcher)
|
||||
if err != nil {
|
||||
return false, info, err
|
||||
}
|
||||
if !ok {
|
||||
return false, info, nil
|
||||
}
|
||||
|
||||
id, _ := getIdByDdLauncher(dir)
|
||||
if id == 0 {
|
||||
return false, info, nil
|
||||
}
|
||||
info.AppId = id
|
||||
info.AppName = filepath.Base(dir)
|
||||
info.AppDir = dir
|
||||
info.AppSource = SourceWfGame
|
||||
|
||||
return true, info, nil
|
||||
}
|
||||
|
||||
// 网维大师 盖伦 好司机
|
||||
func getIdBySunwardPkg(dir string) (int, error) {
|
||||
path := filepath.Join(dir, "SunwardPkg.idx")
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
fmt.Println("Error opening file:", err)
|
||||
return 0, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Read a 4-byte integer
|
||||
var num int32
|
||||
err = binary.Read(file, binary.BigEndian, &num)
|
||||
if err != nil {
|
||||
fmt.Println("Error reading integer:", err)
|
||||
return 0, err
|
||||
}
|
||||
// fmt.Println("Read integer:", num)
|
||||
return int(num), nil
|
||||
}
|
||||
|
||||
// 云更新 蘑菇 火箭使用
|
||||
func getIdByNumberIco(dir string) (int, error) {
|
||||
// 获该目录下所有文件
|
||||
fileList, _ := getFileList(dir)
|
||||
for _, s := range fileList {
|
||||
ext := filepath.Ext(s)
|
||||
// 判断是否ico图标
|
||||
if strings.ToLower(ext) == ".ico" {
|
||||
// fmt.Printf("ico: %s\n", s)
|
||||
// 通过ico图标的数字确定游戏ID
|
||||
fileName := strings.TrimSuffix(filepath.Base(s), filepath.Ext(s))
|
||||
if IsAllNumberHandle(fileName) {
|
||||
gameId, _ := strconv.Atoi(fileName)
|
||||
if gameId >= 8000 {
|
||||
return gameId, nil
|
||||
}
|
||||
}
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// DdLauncherJson DDL Launcher 平台游戏信息(JSON 配置)
|
||||
type DdLauncherJson struct {
|
||||
Gameid int `json:"gameid"`
|
||||
}
|
||||
|
||||
// getIdByDdLauncher 从 DDL Launcher 配置文件中读取游戏 ID
|
||||
func getIdByDdLauncher(dir string) (int, error) {
|
||||
path := filepath.Join(dir, "dd_launcher.json")
|
||||
|
||||
ok, err := pathExists(path)
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if !ok {
|
||||
return 0, errors.New("文件不存在")
|
||||
}
|
||||
|
||||
buf, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return 0, err
|
||||
}
|
||||
|
||||
data := DdLauncherJson{}
|
||||
err = json.Unmarshal(buf, &data)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if data.Gameid == 0 {
|
||||
return 0, errors.New("游戏ID为空")
|
||||
}
|
||||
|
||||
return data.Gameid, nil
|
||||
}
|
||||
|
||||
// GetGuidByYLY 从易乐游目录配置中读取游戏 GUID
|
||||
func GetGuidByYLY(dir string) (string, error) {
|
||||
path := filepath.Join(dir, "$ResMetadata$", "ResAppData.dat")
|
||||
|
||||
ok, err := pathExists(path)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !ok {
|
||||
return "", errors.New("文件不存在")
|
||||
}
|
||||
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return "", err
|
||||
}
|
||||
defer file.Close()
|
||||
// 设置读取起始位置为第25个字节
|
||||
_, err = file.Seek(24, 0)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return "", err
|
||||
}
|
||||
// 创建一个字节切片来存储GUID
|
||||
buf := make([]byte, 16)
|
||||
// 读取GUID到字节切片中
|
||||
_, err = file.Read(buf)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return "", err
|
||||
}
|
||||
guidInt32 := binary.LittleEndian.Uint32(buf[:4])
|
||||
guidInt16_1 := binary.LittleEndian.Uint16(buf[4:6])
|
||||
guidInt16_2 := binary.LittleEndian.Uint16(buf[6:8])
|
||||
guidBytes_1 := buf[8:10]
|
||||
guidBytes_2 := buf[10:]
|
||||
guid := fmt.Sprintf("{%08x-%04x-%04x-%s-%s}", guidInt32, guidInt16_1, guidInt16_2, hex.EncodeToString(guidBytes_1), hex.EncodeToString(guidBytes_2))
|
||||
guid = strings.ToTitle(guid)
|
||||
return guid, err
|
||||
}
|
||||
|
||||
// GetGameInfoByDir 获取游戏信息 从目录获取
|
||||
func GetGameInfoByDir(root string) (AppData, error) {
|
||||
// fmt.Println(root)
|
||||
data := AppData{}
|
||||
|
||||
// 云更新
|
||||
ok, data, err := IsGameDirByYGX(root)
|
||||
if ok {
|
||||
return data, err
|
||||
}
|
||||
|
||||
// 网维大师
|
||||
ok, data, err = IsGameDirByWWDS(root)
|
||||
if ok {
|
||||
return data, err
|
||||
}
|
||||
|
||||
// 易乐游
|
||||
ok, data, err = IsGameDirByYLY(root)
|
||||
if ok {
|
||||
return data, err
|
||||
}
|
||||
|
||||
// 马蹄更新
|
||||
ok, data, err = IsGameDirByMTGX(root)
|
||||
if ok {
|
||||
return data, err
|
||||
}
|
||||
|
||||
// 蘑菇
|
||||
ok, data, err = IsGameDirByStory(root)
|
||||
if ok {
|
||||
return data, err
|
||||
}
|
||||
|
||||
// 盖伦
|
||||
ok, data, err = IsGameDirByGoodGame(root)
|
||||
if ok {
|
||||
return data, err
|
||||
}
|
||||
|
||||
// 好司机
|
||||
ok, data, err = IsGameDirByHaoSiJi(root)
|
||||
if ok {
|
||||
return data, err
|
||||
}
|
||||
|
||||
// 火箭
|
||||
ok, data, err = IsGameDirByFast(root)
|
||||
if ok {
|
||||
return data, err
|
||||
}
|
||||
|
||||
// WF游戏
|
||||
ok, data, err = IsGameDirByWfGame(root)
|
||||
if ok {
|
||||
return data, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
|
||||
}
|
||||
|
||||
// GetGameInfoByPath 获取游戏信息 从路径获取
|
||||
func GetGameInfoByPath(exePath string) (AppData, error) {
|
||||
|
||||
path := filepath.ToSlash(exePath)
|
||||
pathArr := strings.Split(path, "/")
|
||||
// 3个/ 比如 D:/网络游戏/英雄联盟/xxx.exe
|
||||
|
||||
// 长度3 是 D:/英雄联盟/xxx.exe
|
||||
if len(pathArr) < 3 {
|
||||
return AppData{}, errors.New("不支持1级目录")
|
||||
}
|
||||
|
||||
dir := ""
|
||||
// 连接二级目录
|
||||
for i, s := range pathArr {
|
||||
|
||||
// 最后是文件名,不检测
|
||||
if i >= len(pathArr)-1 {
|
||||
break
|
||||
}
|
||||
// 最多支持3级目录 如 D:/网络游戏/腾讯游戏/英雄联盟/xxx.exe
|
||||
if i > 4 {
|
||||
break
|
||||
}
|
||||
if i == 0 {
|
||||
dir = s
|
||||
} else {
|
||||
dir = filepath.Join(dir, string(filepath.Separator), s)
|
||||
}
|
||||
|
||||
// 获取游戏信息
|
||||
info, err := GetGameInfoByDir(dir)
|
||||
if err != nil {
|
||||
return info, err
|
||||
}
|
||||
|
||||
if info.AppId > 0 || info.AppGuid != "" {
|
||||
return info, nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return AppData{}, nil
|
||||
|
||||
}
|
||||
|
||||
// 获取文件列表
|
||||
func getFileList(dir string) ([]string, error) {
|
||||
list := make([]string, 0)
|
||||
//fmt.Println(dir)
|
||||
f, err := os.Open(dir)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
files, err := f.Readdir(-1)
|
||||
f.Close()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
if !file.IsDir() && file.Name() != "$RECYCLE.BIN" && file.Name() != "System Volume Information" {
|
||||
list = append(list, filepath.Join(dir, file.Name()))
|
||||
}
|
||||
}
|
||||
|
||||
return list, nil
|
||||
}
|
||||
|
||||
// IsAllNumberHandle /** 是否全部是数字 */
|
||||
func IsAllNumberHandle(str string) bool {
|
||||
// 空串不认为是数字
|
||||
if str == "" {
|
||||
return false
|
||||
}
|
||||
anyReplaceList := []int32{'.', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0'}
|
||||
for strIndex, strInt := range str {
|
||||
if strIndex == 0 {
|
||||
if strInt == '-' {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if IsHaveReplaceList(strInt, anyReplaceList) {
|
||||
continue
|
||||
}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// IsHaveReplaceList 是否在过滤列表中
|
||||
func IsHaveReplaceList(strInt int32, replaceList []int32) bool {
|
||||
for _, chStrInt := range replaceList {
|
||||
if chStrInt == strInt {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// pathExists 判断一个文件或文件夹是否存在
|
||||
// 输入文件路径,根据返回的bool值来判断文件或文件夹是否存在
|
||||
func pathExists(path string) (bool, error) {
|
||||
_, err := os.Stat(path)
|
||||
if err == nil {
|
||||
return true, nil
|
||||
}
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
//go:build windows
|
||||
|
||||
package cafegame
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ProcessMonitor 单个游戏进程的监视器:等待进程退出后向 waitChan 发送结果
|
||||
type ProcessMonitor struct {
|
||||
pid int
|
||||
exeName string
|
||||
exePath string
|
||||
AppData
|
||||
status int
|
||||
waitChan chan AppWaitData
|
||||
}
|
||||
|
||||
// NewProcessMonitor 创建进程监视器
|
||||
func NewProcessMonitor(pid int, exePath string, app AppData, waitChan chan AppWaitData) *ProcessMonitor {
|
||||
process := new(ProcessMonitor)
|
||||
process.pid = pid
|
||||
process.exeName = filepath.Base(exePath)
|
||||
process.exePath = exePath
|
||||
process.waitChan = waitChan
|
||||
process.AppData = app
|
||||
return process
|
||||
}
|
||||
|
||||
// monitor 阻塞等待进程退出
|
||||
func (receiver *ProcessMonitor) monitor() error {
|
||||
defer func() {
|
||||
receiver.waitChan <- AppWaitData{Pid: receiver.pid, AppData: receiver.AppData}
|
||||
}()
|
||||
process, err := os.FindProcess(receiver.pid)
|
||||
if err != nil {
|
||||
// 查找进程失败
|
||||
return err
|
||||
}
|
||||
_, err = process.Wait()
|
||||
if err != nil {
|
||||
//"进程运行错误
|
||||
return err
|
||||
}
|
||||
// 进程已经结束
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start 在后台协程中开始监视进程
|
||||
func (receiver *ProcessMonitor) Start() {
|
||||
go receiver.monitor()
|
||||
}
|
||||
|
||||
// AppMonitor 单个游戏的监控器:聚合该游戏的所有进程
|
||||
type AppMonitor struct {
|
||||
AppData
|
||||
processList []*ProcessMonitor
|
||||
mux sync.Mutex
|
||||
waitChan chan AppWaitData
|
||||
recordId int
|
||||
startTime int64
|
||||
}
|
||||
|
||||
// NewAppMonitor 创建游戏监控器
|
||||
func NewAppMonitor(app AppData, path string, waitChan chan AppWaitData) *AppMonitor {
|
||||
monitor := new(AppMonitor)
|
||||
monitor.AppData = app
|
||||
monitor.waitChan = waitChan
|
||||
monitor.startTime = time.Now().Unix()
|
||||
return monitor
|
||||
}
|
||||
|
||||
// AddProcess 添加该游戏的一个进程(之前无进程时重置开始时间)
|
||||
func (receiver *AppMonitor) AddProcess(pid int, exePath string) {
|
||||
receiver.mux.Lock()
|
||||
defer receiver.mux.Unlock()
|
||||
// 没有进程,重新设置开始时间为当前时间
|
||||
if len(receiver.processList) == 0 {
|
||||
receiver.startTime = time.Now().Unix()
|
||||
}
|
||||
process := NewProcessMonitor(pid, exePath, receiver.AppData, receiver.waitChan)
|
||||
process.Start()
|
||||
receiver.processList = append(receiver.processList, process)
|
||||
}
|
||||
|
||||
// RemoveProcess 移除指定 PID 的进程
|
||||
func (receiver *AppMonitor) RemoveProcess(pid int) {
|
||||
receiver.mux.Lock()
|
||||
defer receiver.mux.Unlock()
|
||||
|
||||
for i := 0; i < len(receiver.processList); i++ {
|
||||
if pid == receiver.processList[i].pid {
|
||||
receiver.processList = append(receiver.processList[0:i], receiver.processList[i+1:]...)
|
||||
i--
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AppIsRun 该游戏是否仍有进程在运行
|
||||
func (receiver *AppMonitor) AppIsRun() bool {
|
||||
receiver.mux.Lock()
|
||||
defer receiver.mux.Unlock()
|
||||
|
||||
return len(receiver.processList) > 0
|
||||
}
|
||||
|
||||
// GetStartInterval 返回游戏开始运行至今的秒数
|
||||
func (receiver *AppMonitor) GetStartInterval() int64 {
|
||||
receiver.mux.Lock()
|
||||
defer receiver.mux.Unlock()
|
||||
now := time.Now().Unix()
|
||||
interval := now - receiver.startTime
|
||||
return interval
|
||||
}
|
||||
@@ -0,0 +1,597 @@
|
||||
//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
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
//go:build windows
|
||||
|
||||
package cafegame
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// newYGXGameDir 构造一个云更新游戏目录(gamepatch/gconfig.ini + 数字 ico)
|
||||
func newYGXGameDir(t *testing.T, id int) string {
|
||||
dir := filepath.Join(t.TempDir(), "测试游戏")
|
||||
if err := os.MkdirAll(filepath.Join(dir, "gamepatch"), 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "gamepatch", "gconfig.ini"), nil, 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, strconv.Itoa(id)+".ico"), nil, 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
func TestIsGameDirByYGX(t *testing.T) {
|
||||
dir := newYGXGameDir(t, 40001)
|
||||
|
||||
ok, info, err := IsGameDirByYGX(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("IsGameDirByYGX: %v", err)
|
||||
}
|
||||
if !ok {
|
||||
t.Fatal("云更新目录应识别成功")
|
||||
}
|
||||
if info.AppId != 40001 {
|
||||
t.Errorf("AppId = %d, want 40001", info.AppId)
|
||||
}
|
||||
if info.AppSource != SourceYGX {
|
||||
t.Errorf("AppSource = %d, want SourceYGX(%d)", info.AppSource, SourceYGX)
|
||||
}
|
||||
if info.AppName != "测试游戏" {
|
||||
t.Errorf("AppName = %q", info.AppName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsGameDirByYGXNegative(t *testing.T) {
|
||||
// 空目录(无 gamepatch)
|
||||
if ok, _, _ := IsGameDirByYGX(t.TempDir()); ok {
|
||||
t.Error("空目录不应识别为云更新")
|
||||
}
|
||||
|
||||
// 有 gamepatch/gconfig.ini 但 ico 编号过小
|
||||
dir := filepath.Join(t.TempDir(), "small")
|
||||
_ = os.MkdirAll(filepath.Join(dir, "gamepatch"), 0755)
|
||||
_ = os.WriteFile(filepath.Join(dir, "gamepatch", "gconfig.ini"), nil, 0644)
|
||||
_ = os.WriteFile(filepath.Join(dir, "12345.ico"), nil, 0644)
|
||||
if ok, _, _ := IsGameDirByYGX(dir); ok {
|
||||
t.Error("ico 编号小于 40000 不应识别为云更新")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsGameDirByStory(t *testing.T) {
|
||||
dir := newYGXGameDir(t, 10001)
|
||||
ok, info, err := IsGameDirByStory(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("IsGameDirByStory: %v", err)
|
||||
}
|
||||
if !ok {
|
||||
t.Fatal("蘑菇目录应识别成功")
|
||||
}
|
||||
if info.AppSource != SourceStory {
|
||||
t.Errorf("AppSource = %d, want SourceStory(%d)", info.AppSource, SourceStory)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetGuidByYLY(t *testing.T) {
|
||||
// 构造易乐游 $ResMetadata$/ResAppData.dat:24 字节头 + GUID 结构
|
||||
dir := t.TempDir()
|
||||
data := make([]byte, 40)
|
||||
binary.LittleEndian.PutUint32(data[24:28], 0x12345678)
|
||||
binary.LittleEndian.PutUint16(data[28:30], 0xABCD)
|
||||
binary.LittleEndian.PutUint16(data[30:32], 0xEF01)
|
||||
copy(data[32:36], []byte{1, 2, 3, 4})
|
||||
copy(data[36:40], []byte{5, 6, 7, 8})
|
||||
|
||||
if err := os.MkdirAll(filepath.Join(dir, "$ResMetadata$"), 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "$ResMetadata$", "ResAppData.dat"), data, 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
guid, err := GetGuidByYLY(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("GetGuidByYLY: %v", err)
|
||||
}
|
||||
want := "{12345678-ABCD-EF01-0102-030405060708}"
|
||||
if guid != want {
|
||||
t.Errorf("GUID = %q, want %q", guid, want)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
//go:build windows
|
||||
|
||||
package cafegame
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var mux sync.Mutex
|
||||
var appIgnoreProcess []AppIgnoreProcess
|
||||
|
||||
// AppIgnoreProcess 需要忽略的游戏进程配置(不参与监控统计)
|
||||
type AppIgnoreProcess struct {
|
||||
AppId int `json:"appId" gorm:"column:app_id"`
|
||||
AppName string `json:"appName" gorm:"column:app_name"`
|
||||
AppSource int `json:"appSource" gorm:"column:app_source"`
|
||||
AppGuid string `json:"appGuid" gorm:"column:app_guid"`
|
||||
ProcessName []string `json:"processName" gorm:"column:process_name"`
|
||||
Status int `json:"status" gorm:"column:status"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
mux.Lock()
|
||||
appIgnoreProcess = make([]AppIgnoreProcess, 0)
|
||||
mux.Unlock()
|
||||
}
|
||||
|
||||
// AddIgnoreProcessItem 添加单条忽略配置
|
||||
func AddIgnoreProcessItem(item AppIgnoreProcess) {
|
||||
mux.Lock()
|
||||
defer mux.Unlock()
|
||||
appIgnoreProcess = append(appIgnoreProcess, item)
|
||||
}
|
||||
|
||||
// AddIgnoreProcessList 批量添加忽略配置
|
||||
func AddIgnoreProcessList(list []AppIgnoreProcess) {
|
||||
mux.Lock()
|
||||
defer mux.Unlock()
|
||||
appIgnoreProcess = append(appIgnoreProcess, list...)
|
||||
}
|
||||
|
||||
// checkGameProcessIsIgnore 判断指定游戏的进程名是否在忽略列表中(大小写不敏感)
|
||||
func checkGameProcessIsIgnore(appId int, processName string) bool {
|
||||
if appId == 0 || processName == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
mux.Lock()
|
||||
defer mux.Unlock()
|
||||
|
||||
for _, game := range appIgnoreProcess {
|
||||
if appId == game.AppId && appId != 0 {
|
||||
for _, s := range game.ProcessName {
|
||||
if strings.ToLower(s) == strings.ToLower(processName) && processName != "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
// Package cafegame 提供网咖游戏目录识别与游戏运行监控/统计(Windows)。
|
||||
package cafegame
|
||||
|
||||
// AppData 单个游戏信息(目录识别结果)
|
||||
type AppData struct {
|
||||
AppSource int `json:"appSource"`
|
||||
AppId int `json:"appId"`
|
||||
AppName string `json:"appName"`
|
||||
AppAlias string `json:"appAlias"`
|
||||
AppDir string `json:"appDir"`
|
||||
AppGuid string `json:"appGuid"`
|
||||
}
|
||||
|
||||
// CountItem 单条游戏统计记录
|
||||
type CountItem struct {
|
||||
Runs int64 `json:"runs"` // 运行次数
|
||||
RunTime int64 `json:"runTime"` // 运行时间
|
||||
Duration int64 `json:"duration"` // 运行时长
|
||||
Foreground int64 `json:"foreground"` // 前台时长
|
||||
}
|
||||
|
||||
// AppCountInfo 单个游戏的统计汇总
|
||||
type AppCountInfo struct {
|
||||
Source int `json:"source"`
|
||||
Id int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// AppCountItem 上报用的游戏统计项(游戏信息 + 统计)
|
||||
type AppCountItem struct {
|
||||
AppCountInfo
|
||||
CountItem
|
||||
}
|
||||
|
||||
// AppCountReqData 游戏统计请求数据
|
||||
type AppCountReqData struct {
|
||||
HostName string `json:"hostName"`
|
||||
HostIp string `json:"hostIp"`
|
||||
GatewayMac string `json:"gatewayMac"`
|
||||
Apps []AppData `json:"game"`
|
||||
BootTime uint64 `json:"bootTime"`
|
||||
Duration int `json:"duration"`
|
||||
Foreground []ForegroundApp `json:"foreground"`
|
||||
Time int64 `json:"time"`
|
||||
}
|
||||
|
||||
// ForegroundApp 前台窗口关联的游戏及持续时间(秒)
|
||||
type ForegroundApp struct {
|
||||
AppData
|
||||
Duration int
|
||||
}
|
||||
|
||||
// AppDownloadReqData 游戏下载上报数据
|
||||
type AppDownloadReqData struct {
|
||||
AppId int `json:"appId"`
|
||||
AppName string `json:"appName"`
|
||||
AppSource int `json:"appSource"`
|
||||
Status int `json:"status"`
|
||||
Progress float32 `json:"progress"`
|
||||
UpdateQuantity float64 `json:"updateQuantity"`
|
||||
Residue float64 `json:"residue"`
|
||||
Speed float64 `json:"speed"`
|
||||
ConnNumber int `json:"connNumber"`
|
||||
Remainder int `json:"remainder"`
|
||||
}
|
||||
|
||||
// AppRunReqData 游戏运行上报数据
|
||||
type AppRunReqData struct {
|
||||
HostName string `json:"hostName"`
|
||||
HostIp string `json:"hostIp"`
|
||||
GatewayMac string `json:"gatewayMac"`
|
||||
App AppData `json:"app"`
|
||||
BootTime uint64 `json:"bootTime"`
|
||||
Status int `json:"status"`
|
||||
Time int64 `json:"time"`
|
||||
RecordId int `json:"recordId"`
|
||||
}
|
||||
|
||||
// AppEventData 游戏事件上报数据
|
||||
type AppEventData struct {
|
||||
AppId int `json:"appId"` // 游戏ID
|
||||
AppName string `json:"appName"` // 游戏名称
|
||||
AppSource int `json:"appSource"` // 游戏来源
|
||||
ProcessPid int `json:"processPid"` // 进程ID
|
||||
ProcessPath string `json:"processPath"` // 进程路径
|
||||
ProcessOldPath string `json:"processOldPath"` // 进程原路径
|
||||
EventType int `json:"eventType"` // 事件类型
|
||||
EventSource string `json:"eventSource"` // 事件来源
|
||||
Message string `json:"message"` // 事件信息
|
||||
}
|
||||
Reference in New Issue
Block a user