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"` // 事件信息
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
// cafegame 包示例:网咖游戏识别(按各游戏平台目录特征判断游戏来源与 ID)
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"git.zeroonesoft.cn/golib/zogo/cafegame"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// 游戏来源常量与名称
|
||||||
|
for _, id := range cafegame.GetSourceAll() {
|
||||||
|
fmt.Printf("来源 %2d: %s\n", id, cafegame.SourceToName(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构造一个模拟的"云更新"游戏目录,演示目录识别
|
||||||
|
// 云更新特征:目录下有 gamepatch/gconfig.ini,且有数字命名的 .ico 图标(ID≥40000)
|
||||||
|
root := filepath.Join(os.TempDir(), "go-hua-cafegame-demo", "40001示例游戏")
|
||||||
|
_ = os.MkdirAll(filepath.Join(root, "gamepatch"), 0755)
|
||||||
|
_ = os.WriteFile(filepath.Join(root, "gamepatch", "gconfig.ini"), nil, 0644)
|
||||||
|
_ = os.WriteFile(filepath.Join(root, "40001.ico"), nil, 0644)
|
||||||
|
defer os.RemoveAll(filepath.Dir(root))
|
||||||
|
|
||||||
|
ok, info, err := cafegame.IsGameDirByYGX(root)
|
||||||
|
fmt.Println("是否云更新游戏目录:", ok, "错误:", err)
|
||||||
|
if ok {
|
||||||
|
fmt.Printf("识别结果: Id=%d Name=%s Source=%d(%s)\n",
|
||||||
|
info.AppId, info.AppName, info.AppSource, cafegame.SourceToName(info.AppSource))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 实际使用中扫描游戏盘根目录,自动识别所有游戏:
|
||||||
|
// entries, _ := os.ReadDir("D:\\")
|
||||||
|
// for _, e := range entries {
|
||||||
|
// if info, err := cafegame.GetGameInfoByDir(filepath.Join("D:\\", e.Name())); err == nil && (info.AppId > 0 || info.AppGuid != "") {
|
||||||
|
// fmt.Printf("%+v\n", info)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// 游戏运行记录/监控入口(Windows):
|
||||||
|
// cafegame.RunAppRecord(recordCallback, countCallback, eventCallback, sourceList, reportInterval)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user