Initial qiwei secondary development handoff
This commit is contained in:
337
helper/version_diagnostics.go
Normal file
337
helper/version_diagnostics.go
Normal file
@@ -0,0 +1,337 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
const fallbackDLLVersion = "4.1.33.6009"
|
||||
const legacyLoaderSHA256 = "ce4557bf449fd53078f2eaa9becbf43b6f2bf40f16199e1a4bd6088ab233a65a"
|
||||
|
||||
type DLLBundle struct {
|
||||
HelperPath string
|
||||
LoaderPath string
|
||||
HelperVersion string
|
||||
LoaderVersion string
|
||||
WxWorkPath string
|
||||
WxWorkVersion string
|
||||
Compatible bool
|
||||
Message string
|
||||
}
|
||||
|
||||
type vsFixedFileInfo struct {
|
||||
Signature uint32
|
||||
StrucVersion uint32
|
||||
FileVersionMS uint32
|
||||
FileVersionLS uint32
|
||||
ProductVersionMS uint32
|
||||
ProductVersionLS uint32
|
||||
FileFlagsMask uint32
|
||||
FileFlags uint32
|
||||
FileOS uint32
|
||||
FileType uint32
|
||||
FileSubtype uint32
|
||||
FileDateMS uint32
|
||||
FileDateLS uint32
|
||||
}
|
||||
|
||||
func resolveDLLBundle() DLLBundle {
|
||||
exeDir := "."
|
||||
if exePath, err := os.Executable(); err == nil {
|
||||
exeDir = filepath.Dir(exePath)
|
||||
}
|
||||
|
||||
wxWorkPath := detectWxWorkPath()
|
||||
if wxWorkPath == "" {
|
||||
wxWorkPath = `C:\企业微信\WXWork\WXWork.exe`
|
||||
}
|
||||
wxWorkVersion := ""
|
||||
if _, err := os.Stat(wxWorkPath); err == nil {
|
||||
wxWorkVersion, _ = getWindowsFileVersion(wxWorkPath)
|
||||
}
|
||||
|
||||
versions := make([]string, 0, 4)
|
||||
if wxWorkVersion != "" {
|
||||
versions = append(versions, wxWorkVersion)
|
||||
}
|
||||
versions = append(versions, fallbackDLLVersion)
|
||||
versions = append(versions, scanDLLVersions(exeDir)...)
|
||||
|
||||
seen := make(map[string]bool)
|
||||
for _, version := range versions {
|
||||
version = strings.TrimSpace(version)
|
||||
if version == "" || seen[version] {
|
||||
continue
|
||||
}
|
||||
seen[version] = true
|
||||
helperPath := filepath.Join(exeDir, "Helper_"+version+".dll")
|
||||
loaderPath := filepath.Join(exeDir, "Loader_"+version+".dll")
|
||||
if fileExists(helperPath) && fileExists(loaderPath) {
|
||||
compatible := wxWorkVersion != "" && sameVersionFamily(wxWorkVersion, version)
|
||||
message := ""
|
||||
if !compatible && wxWorkVersion != "" {
|
||||
message = fmt.Sprintf("WXWork %s is not compatible with Helper/Loader %s. Put Helper_%s.dll and Loader_%s.dll in build/bin to enable account/message callbacks.", wxWorkVersion, version, wxWorkVersion, wxWorkVersion)
|
||||
}
|
||||
if compatible && version != fallbackDLLVersion {
|
||||
fallbackLoaderPath := filepath.Join(exeDir, "Loader_"+fallbackDLLVersion+".dll")
|
||||
if sameFileContent(loaderPath, fallbackLoaderPath) || fileMatchesSHA256(loaderPath, legacyLoaderSHA256) {
|
||||
compatible = false
|
||||
message = fmt.Sprintf("Loader_%s.dll has the same content as Loader_%s.dll. Replace it with the real Loader_%s.dll before starting WXWork.", version, fallbackDLLVersion, version)
|
||||
}
|
||||
}
|
||||
return DLLBundle{
|
||||
HelperPath: helperPath,
|
||||
LoaderPath: loaderPath,
|
||||
HelperVersion: version,
|
||||
LoaderVersion: version,
|
||||
WxWorkPath: wxWorkPath,
|
||||
WxWorkVersion: wxWorkVersion,
|
||||
Compatible: compatible,
|
||||
Message: message,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
message := "No Helper/Loader DLL pair found."
|
||||
if wxWorkVersion != "" {
|
||||
message = fmt.Sprintf("No Helper_%s.dll and Loader_%s.dll found in %s.", wxWorkVersion, wxWorkVersion, exeDir)
|
||||
}
|
||||
return DLLBundle{
|
||||
WxWorkPath: wxWorkPath,
|
||||
WxWorkVersion: wxWorkVersion,
|
||||
Message: message,
|
||||
}
|
||||
}
|
||||
|
||||
func detectWxWorkPath() string {
|
||||
if path := getRunningProcessImagePath("WXWork.exe"); path != "" {
|
||||
return path
|
||||
}
|
||||
if path, err := getWxWorkInstallPath(); err == nil && fileExists(path) {
|
||||
return path
|
||||
}
|
||||
candidates := []string{
|
||||
`C:\Program Files (x86)\WXWork\WXWork.exe`,
|
||||
`C:\Program Files\WXWork\WXWork.exe`,
|
||||
`C:\企业微信\WXWork\WXWork.exe`,
|
||||
}
|
||||
for _, path := range candidates {
|
||||
if fileExists(path) {
|
||||
return path
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func getRunningProcessImagePath(processName string) string {
|
||||
hSnapshot, err := syscall.CreateToolhelp32Snapshot(syscall.TH32CS_SNAPPROCESS, 0)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
defer syscall.CloseHandle(hSnapshot)
|
||||
|
||||
var pe32 syscall.ProcessEntry32
|
||||
pe32.Size = uint32(unsafe.Sizeof(pe32))
|
||||
if err := syscall.Process32First(hSnapshot, &pe32); err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
for {
|
||||
name := syscall.UTF16ToString(pe32.ExeFile[:])
|
||||
if strings.EqualFold(name, processName) {
|
||||
if path := queryProcessImagePath(pe32.ProcessID); path != "" {
|
||||
return path
|
||||
}
|
||||
}
|
||||
if err := syscall.Process32Next(hSnapshot, &pe32); err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func queryProcessImagePath(pid uint32) string {
|
||||
const processQueryLimitedInformation = 0x1000
|
||||
handle, err := syscall.OpenProcess(processQueryLimitedInformation, false, pid)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
defer syscall.CloseHandle(handle)
|
||||
|
||||
kernel32 := syscall.NewLazyDLL("kernel32.dll")
|
||||
queryFullProcessImageName := kernel32.NewProc("QueryFullProcessImageNameW")
|
||||
buf := make([]uint16, 32768)
|
||||
size := uint32(len(buf))
|
||||
ret, _, _ := queryFullProcessImageName.Call(
|
||||
uintptr(handle),
|
||||
0,
|
||||
uintptr(unsafe.Pointer(&buf[0])),
|
||||
uintptr(unsafe.Pointer(&size)),
|
||||
)
|
||||
if ret == 0 || size == 0 {
|
||||
return ""
|
||||
}
|
||||
return syscall.UTF16ToString(buf[:size])
|
||||
}
|
||||
|
||||
func sameFileContent(pathA, pathB string) bool {
|
||||
infoA, errA := os.Stat(pathA)
|
||||
infoB, errB := os.Stat(pathB)
|
||||
if errA != nil || errB != nil || infoA.Size() != infoB.Size() {
|
||||
return false
|
||||
}
|
||||
hashA, errA := fileSHA256(pathA)
|
||||
hashB, errB := fileSHA256(pathB)
|
||||
return errA == nil && errB == nil && hashA == hashB
|
||||
}
|
||||
|
||||
func fileMatchesSHA256(path string, expected string) bool {
|
||||
hash, err := fileSHA256(path)
|
||||
return err == nil && strings.EqualFold(hex.EncodeToString(hash[:]), expected)
|
||||
}
|
||||
|
||||
func fileSHA256(path string) ([32]byte, error) {
|
||||
var zero [32]byte
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return zero, err
|
||||
}
|
||||
defer f.Close()
|
||||
h := sha256.New()
|
||||
if _, err := io.Copy(h, f); err != nil {
|
||||
return zero, err
|
||||
}
|
||||
var sum [32]byte
|
||||
copy(sum[:], h.Sum(nil))
|
||||
return sum, nil
|
||||
}
|
||||
|
||||
func scanDLLVersions(dir string) []string {
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
hasHelper := make(map[string]bool)
|
||||
hasLoader := make(map[string]bool)
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
name := entry.Name()
|
||||
lower := strings.ToLower(name)
|
||||
if strings.HasPrefix(lower, "helper_") && strings.HasSuffix(lower, ".dll") {
|
||||
hasHelper[versionFromDLLName(name)] = true
|
||||
}
|
||||
if strings.HasPrefix(lower, "loader_") && strings.HasSuffix(lower, ".dll") {
|
||||
hasLoader[versionFromDLLName(name)] = true
|
||||
}
|
||||
}
|
||||
versions := make([]string, 0)
|
||||
for version := range hasHelper {
|
||||
if version != "" && hasLoader[version] {
|
||||
versions = append(versions, version)
|
||||
}
|
||||
}
|
||||
return versions
|
||||
}
|
||||
|
||||
func fileExists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func getWxWorkVersionDiagnostics() map[string]interface{} {
|
||||
bundle := resolveDLLBundle()
|
||||
return map[string]interface{}{
|
||||
"wxWorkPath": bundle.WxWorkPath,
|
||||
"wxWorkVersion": bundle.WxWorkVersion,
|
||||
"helperPath": bundle.HelperPath,
|
||||
"loaderPath": bundle.LoaderPath,
|
||||
"helperVersion": bundle.HelperVersion,
|
||||
"loaderVersion": bundle.LoaderVersion,
|
||||
"compatible": bundle.Compatible,
|
||||
"message": bundle.Message,
|
||||
}
|
||||
}
|
||||
|
||||
func versionFromDLLName(path string) string {
|
||||
name := filepath.Base(path)
|
||||
name = strings.TrimSuffix(name, filepath.Ext(name))
|
||||
idx := strings.LastIndex(name, "_")
|
||||
if idx < 0 || idx+1 >= len(name) {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(name[idx+1:])
|
||||
}
|
||||
|
||||
func sameVersionFamily(actual string, expected string) bool {
|
||||
actualParts := strings.Split(actual, ".")
|
||||
expectedParts := strings.Split(expected, ".")
|
||||
if len(actualParts) < 3 || len(expectedParts) < 3 {
|
||||
return actual == expected
|
||||
}
|
||||
return actualParts[0] == expectedParts[0] &&
|
||||
actualParts[1] == expectedParts[1] &&
|
||||
actualParts[2] == expectedParts[2]
|
||||
}
|
||||
|
||||
func getWindowsFileVersion(path string) (string, error) {
|
||||
versionDLL := syscall.NewLazyDLL("version.dll")
|
||||
getFileVersionInfoSize := versionDLL.NewProc("GetFileVersionInfoSizeW")
|
||||
getFileVersionInfo := versionDLL.NewProc("GetFileVersionInfoW")
|
||||
verQueryValue := versionDLL.NewProc("VerQueryValueW")
|
||||
|
||||
pathPtr, err := syscall.UTF16PtrFromString(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var handle uint32
|
||||
size, _, _ := getFileVersionInfoSize.Call(
|
||||
uintptr(unsafe.Pointer(pathPtr)),
|
||||
uintptr(unsafe.Pointer(&handle)),
|
||||
)
|
||||
if size == 0 {
|
||||
return "", fmt.Errorf("GetFileVersionInfoSizeW returned 0")
|
||||
}
|
||||
|
||||
buf := make([]byte, size)
|
||||
ret, _, err := getFileVersionInfo.Call(
|
||||
uintptr(unsafe.Pointer(pathPtr)),
|
||||
0,
|
||||
size,
|
||||
uintptr(unsafe.Pointer(&buf[0])),
|
||||
)
|
||||
if ret == 0 {
|
||||
return "", fmt.Errorf("GetFileVersionInfoW failed: %v", err)
|
||||
}
|
||||
|
||||
root, err := syscall.UTF16PtrFromString(`\`)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var block uintptr
|
||||
var blockLen uint32
|
||||
ret, _, err = verQueryValue.Call(
|
||||
uintptr(unsafe.Pointer(&buf[0])),
|
||||
uintptr(unsafe.Pointer(root)),
|
||||
uintptr(unsafe.Pointer(&block)),
|
||||
uintptr(unsafe.Pointer(&blockLen)),
|
||||
)
|
||||
if ret == 0 || block == 0 {
|
||||
return "", fmt.Errorf("VerQueryValueW failed: %v", err)
|
||||
}
|
||||
|
||||
info := (*vsFixedFileInfo)(unsafe.Pointer(block))
|
||||
major := info.FileVersionMS >> 16
|
||||
minor := info.FileVersionMS & 0xffff
|
||||
build := info.FileVersionLS >> 16
|
||||
patch := info.FileVersionLS & 0xffff
|
||||
return fmt.Sprintf("%d.%d.%d.%d", major, minor, build, patch), nil
|
||||
}
|
||||
Reference in New Issue
Block a user