fix build macos
This commit is contained in:
BIN
client/.DS_Store
vendored
Normal file
BIN
client/.DS_Store
vendored
Normal file
Binary file not shown.
@@ -16,4 +16,18 @@ to this in your browser, and you can call your Go code from devtools.
|
||||
|
||||
## Building
|
||||
|
||||
To build a redistributable, production mode package, use `wails build`.
|
||||
### Windows
|
||||
To build for Windows:
|
||||
- Run `wails build` (or `wails build -platform windows/amd64`)
|
||||
- Or use the PowerShell build script: `./build.ps1`
|
||||
|
||||
### macOS (ARM & Intel)
|
||||
To build for macOS:
|
||||
- To build Apple Silicon (ARM64) target: `wails build -platform darwin/arm64`
|
||||
- To build Intel (AMD64) target: `wails build -platform darwin/amd64`
|
||||
- To build a Universal macOS binary: `wails build -platform darwin/universal`
|
||||
- Alternatively, run the helper script to build both ARM64 and AMD64 targets:
|
||||
```bash
|
||||
chmod +x build.sh
|
||||
./build.sh
|
||||
```
|
||||
|
||||
30
client/build.sh
Executable file
30
client/build.sh
Executable file
@@ -0,0 +1,30 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Change directory to client folder if not already there
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
echo "============================================="
|
||||
echo " BUILDING WAILS CLIENT FOR MACOS "
|
||||
echo "============================================="
|
||||
|
||||
# Check if wails is installed
|
||||
WAILS_CMD="wails"
|
||||
if ! command -v wails &> /dev/null; then
|
||||
if [ -f "$HOME/go/bin/wails" ]; then
|
||||
WAILS_CMD="$HOME/go/bin/wails"
|
||||
echo "[*] Found Wails CLI at $WAILS_CMD"
|
||||
else
|
||||
echo "[-] Wails CLI not found. Please install Wails first."
|
||||
echo " Run: go install github.com/wailsapp/wails/v2/cmd/wails@latest"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "[*] Building macOS Apple Silicon (arm64)..."
|
||||
"$WAILS_CMD" build -platform darwin/arm64
|
||||
|
||||
echo "[*] Building macOS Intel (amd64)..."
|
||||
"$WAILS_CMD" build -platform darwin/amd64
|
||||
|
||||
echo "[+] macOS arm64 and amd64 builds completed successfully!"
|
||||
BIN
client/build/.DS_Store
vendored
Normal file
BIN
client/build/.DS_Store
vendored
Normal file
Binary file not shown.
0
client/frontend/wailsjs/go/main/App.d.ts
vendored
Normal file → Executable file
0
client/frontend/wailsjs/go/main/App.d.ts
vendored
Normal file → Executable file
0
client/frontend/wailsjs/go/main/App.js
Normal file → Executable file
0
client/frontend/wailsjs/go/main/App.js
Normal file → Executable file
@@ -2,74 +2,11 @@ package blocker
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var (
|
||||
user32 = syscall.NewLazyDLL("user32.dll")
|
||||
procEnumWindows = user32.NewProc("EnumWindows")
|
||||
procIsWindowVisible = user32.NewProc("IsWindowVisible")
|
||||
procGetWindowTextW = user32.NewProc("GetWindowTextW")
|
||||
procGetWindowTextLengthW = user32.NewProc("GetWindowTextLengthW")
|
||||
procGetWindowThreadProcessId = user32.NewProc("GetWindowThreadProcessId")
|
||||
procGetWindow = user32.NewProc("GetWindow")
|
||||
procGetWindowLongW = user32.NewProc("GetWindowLongW")
|
||||
procGetAncestor = user32.NewProc("GetAncestor")
|
||||
|
||||
dwmapi = syscall.NewLazyDLL("dwmapi.dll")
|
||||
procDwmGetWindowAttribute = dwmapi.NewProc("DwmGetWindowAttribute")
|
||||
|
||||
kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
||||
)
|
||||
|
||||
const (
|
||||
GW_OWNER = 4
|
||||
WS_EX_TOOLWINDOW = 0x00000080
|
||||
DWMWA_CLOAKED = 14
|
||||
)
|
||||
|
||||
func isRealGUIWindow(hwnd uintptr) bool {
|
||||
// 1. Phải đang hiển thị
|
||||
ret, _, _ := procIsWindowVisible.Call(hwnd)
|
||||
if ret == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// 2. Không được có chủ sở hữu (phải là cửa sổ chính - top-level)
|
||||
owner, _, _ := procGetWindow.Call(hwnd, GW_OWNER)
|
||||
if owner != 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// 3. Không phải là tool window (WS_EX_TOOLWINDOW)
|
||||
gwlExStyle := int32(-20) // GWL_EXSTYLE = -20
|
||||
style, _, _ := procGetWindowLongW.Call(hwnd, uintptr(gwlExStyle))
|
||||
if (style & WS_EX_TOOLWINDOW) != 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// 4. Không bị cloaked bởi DWM (ví dụ: app UWP bị treo/chạy ngầm, màn hình ảo)
|
||||
var cloaked uint32
|
||||
hr, _, _ := procDwmGetWindowAttribute.Call(hwnd, DWMWA_CLOAKED, uintptr(unsafe.Pointer(&cloaked)), 4)
|
||||
if hr == 0 && cloaked != 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
type WindowInfo struct {
|
||||
PID uint32
|
||||
Title string
|
||||
ProcessName string
|
||||
}
|
||||
|
||||
type Blocker struct {
|
||||
mu sync.Mutex
|
||||
allowedKeywords []string
|
||||
@@ -84,121 +21,6 @@ var Instance = &Blocker{
|
||||
stopChan: make(chan struct{}),
|
||||
}
|
||||
|
||||
func getProcessMap() (map[uint32]string, map[uint32]uint32, error) {
|
||||
snapshot, err := syscall.CreateToolhelp32Snapshot(syscall.TH32CS_SNAPPROCESS, 0)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer syscall.CloseHandle(snapshot)
|
||||
|
||||
var pe syscall.ProcessEntry32
|
||||
pe.Size = uint32(unsafe.Sizeof(pe))
|
||||
|
||||
err = syscall.Process32First(snapshot, &pe)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
pm := make(map[uint32]string)
|
||||
parents := make(map[uint32]uint32)
|
||||
for {
|
||||
name := syscall.UTF16ToString(pe.ExeFile[:])
|
||||
pm[pe.ProcessID] = name
|
||||
parents[pe.ProcessID] = pe.ParentProcessID
|
||||
|
||||
err = syscall.Process32Next(snapshot, &pe)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
return pm, parents, nil
|
||||
}
|
||||
|
||||
func getWindowText(hwnd uintptr) string {
|
||||
length, _, _ := procGetWindowTextLengthW.Call(hwnd)
|
||||
if length == 0 {
|
||||
return ""
|
||||
}
|
||||
buf := make([]uint16, length+1)
|
||||
procGetWindowTextW.Call(hwnd, uintptr(unsafe.Pointer(&buf[0])), length+1)
|
||||
return syscall.UTF16ToString(buf)
|
||||
}
|
||||
|
||||
var (
|
||||
enumWindowsMutex sync.Mutex
|
||||
enumWindowsList []WindowInfo
|
||||
enumProcessMap map[uint32]string
|
||||
enumParentMap map[uint32]uint32
|
||||
)
|
||||
|
||||
var enumWindowsCallback = syscall.NewCallback(func(hwnd uintptr, lParam uintptr) uintptr {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("[BLOCKER] Callback panic recovered: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
if !isRealGUIWindow(hwnd) {
|
||||
return 1
|
||||
}
|
||||
|
||||
// Kiểm tra xem chủ sở hữu gốc (root owner) của cửa sổ này có thuộc về app ta hay không
|
||||
rootHwnd, _, _ := procGetAncestor.Call(hwnd, 3) // GA_ROOTOWNER = 3
|
||||
if rootHwnd != 0 {
|
||||
var rootPid uint32
|
||||
procGetWindowThreadProcessId.Call(rootHwnd, uintptr(unsafe.Pointer(&rootPid)))
|
||||
if rootPid == uint32(os.Getpid()) {
|
||||
return 1 // Cửa sổ thuộc về WebView2 / app của ta, bỏ qua không quét
|
||||
}
|
||||
}
|
||||
|
||||
title := getWindowText(hwnd)
|
||||
if title == "" {
|
||||
return 1
|
||||
}
|
||||
|
||||
var pid uint32
|
||||
procGetWindowThreadProcessId.Call(hwnd, uintptr(unsafe.Pointer(&pid)))
|
||||
|
||||
procName := enumProcessMap[pid]
|
||||
if procName == "" {
|
||||
procName = "Unknown"
|
||||
}
|
||||
|
||||
enumWindowsList = append(enumWindowsList, WindowInfo{
|
||||
PID: pid,
|
||||
Title: title,
|
||||
ProcessName: procName,
|
||||
})
|
||||
|
||||
return 1
|
||||
})
|
||||
|
||||
func EnumerateGUIWindows() ([]WindowInfo, map[uint32]uint32, error) {
|
||||
pMap, parentMap, err := getProcessMap()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
enumWindowsMutex.Lock()
|
||||
defer enumWindowsMutex.Unlock()
|
||||
|
||||
enumWindowsList = make([]WindowInfo, 0, 100)
|
||||
enumProcessMap = pMap
|
||||
enumParentMap = parentMap
|
||||
|
||||
procEnumWindows.Call(enumWindowsCallback, 0)
|
||||
|
||||
// Clean up map reference so GC can reclaim it
|
||||
enumProcessMap = nil
|
||||
enumParentMap = nil
|
||||
|
||||
// Copy to a new slice to return safely
|
||||
res := make([]WindowInfo, len(enumWindowsList))
|
||||
copy(res, enumWindowsList)
|
||||
return res, parentMap, nil
|
||||
}
|
||||
|
||||
func parseKeywordList(keywords string) []string {
|
||||
keywords = strings.ReplaceAll(keywords, "\r\n", "\n")
|
||||
parts := strings.FieldsFunc(keywords, func(r rune) bool {
|
||||
@@ -253,113 +75,9 @@ func matchesAllowedKeyword(kw, pNameLower, wTitleLower string) bool {
|
||||
|
||||
func (b *Blocker) SetKeywords(keywords string) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
b.allowedKeywords = parseKeywordList(keywords)
|
||||
log.Printf("[BLOCKER] Keywords updated: %v", b.allowedKeywords)
|
||||
}
|
||||
|
||||
var systemAllowed = map[string]bool{
|
||||
"explorer.exe": true,
|
||||
"taskmgr.exe": true,
|
||||
"cmd.exe": true,
|
||||
"powershell.exe": true,
|
||||
"conhost.exe": true,
|
||||
"client.exe": true, // App Wails của ta
|
||||
"simple_care_v1.0.exe": true,
|
||||
"wails.exe": true,
|
||||
"msedgewebview2.exe": true, // WebView2 runtime của Wails
|
||||
"code.exe": true, // VSCode
|
||||
"cursor.exe": true, // Cursor
|
||||
"windsurf.exe": true, // Windsurf
|
||||
"goland.exe": true, // GoLand
|
||||
"goland64.exe": true, // GoLand
|
||||
"idea64.exe": true, // IntelliJ IDEA
|
||||
"clion64.exe": true, // CLion
|
||||
"webstorm64.exe": true, // WebStorm
|
||||
"pycharm64.exe": true, // PyCharm
|
||||
"rider64.exe": true, // Rider
|
||||
"studio64.exe": true, // Android Studio
|
||||
"eclipse.exe": true, // Eclipse
|
||||
"sublime_text.exe": true, // Sublime Text
|
||||
"notepad++.exe": true, // Notepad++
|
||||
"devenv.exe": true, // Visual Studio
|
||||
"git-bash.exe": true, // Git Bash
|
||||
"bash.exe": true, // Bash
|
||||
}
|
||||
|
||||
func isDescendant(pid, targetPid uint32, parentMap map[uint32]uint32) bool {
|
||||
curr := pid
|
||||
for i := 0; i < 16; i++ {
|
||||
parent, ok := parentMap[curr]
|
||||
if !ok || parent == 0 {
|
||||
return false
|
||||
}
|
||||
if parent == targetPid {
|
||||
return true
|
||||
}
|
||||
curr = parent
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (b *Blocker) checkAndKill() {
|
||||
b.mu.Lock()
|
||||
keywords := make([]string, len(b.allowedKeywords))
|
||||
copy(keywords, b.allowedKeywords)
|
||||
b.mu.Unlock()
|
||||
|
||||
// Nếu không cấu hình keyword thì không chặn gì cả
|
||||
if len(keywords) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
currentExec := ""
|
||||
if execPath, err := os.Executable(); err == nil {
|
||||
currentExec = strings.ToLower(filepath.Base(execPath))
|
||||
}
|
||||
|
||||
windows, parentMap, err := EnumerateGUIWindows()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
myPid := uint32(os.Getpid())
|
||||
for _, w := range windows {
|
||||
pNameLower := strings.ToLower(w.ProcessName)
|
||||
wTitleLower := strings.ToLower(w.Title)
|
||||
|
||||
// 1. Luôn cho phép hệ thống/app cốt lõi hoặc chính tiến trình này (bao gồm tiến trình con/cháu, và đổi tên)
|
||||
isOurApp := w.PID == myPid || isDescendant(w.PID, myPid, parentMap)
|
||||
if isOurApp || (currentExec != "" && pNameLower == currentExec) || systemAllowed[pNameLower] || strings.Contains(pNameLower, "antigravity") || strings.Contains(wTitleLower, "antigravity") {
|
||||
continue
|
||||
}
|
||||
|
||||
// 2. Kiểm tra xem có chứa bất kỳ từ khóa nào được cho phép không
|
||||
allowed := false
|
||||
for _, kw := range keywords {
|
||||
if matchesAllowedKeyword(kw, pNameLower, wTitleLower) {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Nếu không nằm trong whitelist, tắt ứng dụng
|
||||
if !allowed {
|
||||
if b.OnBlocked != nil {
|
||||
b.OnBlocked(w.ProcessName, w.Title)
|
||||
}
|
||||
log.Printf("[BLOCKER] KILLED unauthorized application: %s (PID: %d, Title: %s)", w.ProcessName, w.PID, w.Title)
|
||||
h, err := syscall.OpenProcess(syscall.PROCESS_TERMINATE, false, w.PID)
|
||||
if err == nil {
|
||||
errTerm := syscall.TerminateProcess(h, 0)
|
||||
_ = syscall.CloseHandle(h)
|
||||
if errTerm == nil && b.OnKill != nil {
|
||||
b.OnKill(w.ProcessName, w.Title)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Printf("[BLOCKER] Keywords updated: %v", b.allowedKeywords)
|
||||
}
|
||||
|
||||
func (b *Blocker) Start() {
|
||||
|
||||
133
client/internal/blocker/blocker_darwin.go
Normal file
133
client/internal/blocker/blocker_darwin.go
Normal file
@@ -0,0 +1,133 @@
|
||||
//go:build darwin
|
||||
|
||||
package blocker
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var systemAllowed = map[string]bool{
|
||||
"finder": true,
|
||||
"terminal": true,
|
||||
"iterm": true,
|
||||
"iterm2": true,
|
||||
"bash": true,
|
||||
"zsh": true,
|
||||
"sh": true,
|
||||
"wails": true,
|
||||
"code": true,
|
||||
"cursor": true,
|
||||
"windsurf": true,
|
||||
"goland": true,
|
||||
"idea": true,
|
||||
"clion": true,
|
||||
"webstorm": true,
|
||||
"pycharm": true,
|
||||
"rider": true,
|
||||
"studio": true,
|
||||
"eclipse": true,
|
||||
"sublime_text": true,
|
||||
"git": true,
|
||||
"docker": true,
|
||||
"system events": true,
|
||||
"osascript": true,
|
||||
"client": true,
|
||||
"simple_care_v1.0": true,
|
||||
}
|
||||
|
||||
func getVisibleProcesses() (map[uint32]string, error) {
|
||||
script := `tell application "System Events"
|
||||
set nameList to name of every process whose visible is true
|
||||
set pidList to unix id of every process whose visible is true
|
||||
set out to ""
|
||||
repeat with i from 1 to count of nameList
|
||||
set out to out & item i of nameList & ":" & item i of pidList & "\n"
|
||||
end repeat
|
||||
return out
|
||||
end tell`
|
||||
cmd := exec.Command("osascript", "-e", script)
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
procs := make(map[uint32]string)
|
||||
lines := strings.Split(string(out), "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
idx := strings.LastIndex(line, ":")
|
||||
if idx == -1 {
|
||||
continue
|
||||
}
|
||||
pName := line[:idx]
|
||||
pIdStr := line[idx+1:]
|
||||
var pid uint32
|
||||
if _, err := fmt.Sscanf(pIdStr, "%d", &pid); err == nil {
|
||||
procs[pid] = pName
|
||||
}
|
||||
}
|
||||
return procs, nil
|
||||
}
|
||||
|
||||
func (b *Blocker) checkAndKill() {
|
||||
b.mu.Lock()
|
||||
keywords := make([]string, len(b.allowedKeywords))
|
||||
copy(keywords, b.allowedKeywords)
|
||||
b.mu.Unlock()
|
||||
|
||||
if len(keywords) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
currentExec := ""
|
||||
if execPath, err := os.Executable(); err == nil {
|
||||
currentExec = strings.ToLower(filepath.Base(execPath))
|
||||
}
|
||||
|
||||
procs, err := getVisibleProcesses()
|
||||
if err != nil {
|
||||
log.Printf("[BLOCKER] Failed to get visible processes: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
myPid := uint32(os.Getpid())
|
||||
for pid, name := range procs {
|
||||
pNameLower := strings.ToLower(name)
|
||||
|
||||
// 1. Always allow our app, system/critical developer tools, or agent helpers
|
||||
if pid == myPid || (currentExec != "" && pNameLower == currentExec) || systemAllowed[pNameLower] || strings.Contains(pNameLower, "antigravity") {
|
||||
continue
|
||||
}
|
||||
|
||||
// 2. Check if the process name contains any allowed keywords
|
||||
allowed := false
|
||||
for _, kw := range keywords {
|
||||
if matchesAllowedKeyword(kw, pNameLower, pNameLower) {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 3. If not allowed, kill the application
|
||||
if !allowed {
|
||||
if b.OnBlocked != nil {
|
||||
b.OnBlocked(name, name)
|
||||
}
|
||||
log.Printf("[BLOCKER] KILLED unauthorized application: %s (PID: %d)", name, pid)
|
||||
proc, err := os.FindProcess(int(pid))
|
||||
if err == nil {
|
||||
errKill := proc.Kill()
|
||||
if errKill == nil && b.OnKill != nil {
|
||||
b.OnKill(name, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
5
client/internal/blocker/blocker_other.go
Normal file
5
client/internal/blocker/blocker_other.go
Normal file
@@ -0,0 +1,5 @@
|
||||
//go:build !windows && !darwin
|
||||
|
||||
package blocker
|
||||
|
||||
func (b *Blocker) checkAndKill() {}
|
||||
290
client/internal/blocker/blocker_windows.go
Normal file
290
client/internal/blocker/blocker_windows.go
Normal file
@@ -0,0 +1,290 @@
|
||||
//go:build windows
|
||||
|
||||
package blocker
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var (
|
||||
user32 = syscall.NewLazyDLL("user32.dll")
|
||||
procEnumWindows = user32.NewProc("EnumWindows")
|
||||
procIsWindowVisible = user32.NewProc("IsWindowVisible")
|
||||
procGetWindowTextW = user32.NewProc("GetWindowTextW")
|
||||
procGetWindowTextLengthW = user32.NewProc("GetWindowTextLengthW")
|
||||
procGetWindowThreadProcessId = user32.NewProc("GetWindowThreadProcessId")
|
||||
procGetWindow = user32.NewProc("GetWindow")
|
||||
procGetWindowLongW = user32.NewProc("GetWindowLongW")
|
||||
procGetAncestor = user32.NewProc("GetAncestor")
|
||||
|
||||
dwmapi = syscall.NewLazyDLL("dwmapi.dll")
|
||||
procDwmGetWindowAttribute = dwmapi.NewProc("DwmGetWindowAttribute")
|
||||
|
||||
kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
||||
)
|
||||
|
||||
const (
|
||||
GW_OWNER = 4
|
||||
WS_EX_TOOLWINDOW = 0x00000080
|
||||
DWMWA_CLOAKED = 14
|
||||
)
|
||||
|
||||
func isRealGUIWindow(hwnd uintptr) bool {
|
||||
// 1. Phải đang hiển thị
|
||||
ret, _, _ := procIsWindowVisible.Call(hwnd)
|
||||
if ret == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// 2. Không được có chủ sở hữu (phải là cửa sổ chính - top-level)
|
||||
owner, _, _ := procGetWindow.Call(hwnd, GW_OWNER)
|
||||
if owner != 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// 3. Không phải là tool window (WS_EX_TOOLWINDOW)
|
||||
gwlExStyle := int32(-20) // GWL_EXSTYLE = -20
|
||||
style, _, _ := procGetWindowLongW.Call(hwnd, uintptr(gwlExStyle))
|
||||
if (style & WS_EX_TOOLWINDOW) != 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// 4. Không bị cloaked bởi DWM (ví dụ: app UWP bị treo/chạy ngầm, màn hình ảo)
|
||||
var cloaked uint32
|
||||
hr, _, _ := procDwmGetWindowAttribute.Call(hwnd, DWMWA_CLOAKED, uintptr(unsafe.Pointer(&cloaked)), 4)
|
||||
if hr == 0 && cloaked != 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
type WindowInfo struct {
|
||||
PID uint32
|
||||
Title string
|
||||
ProcessName string
|
||||
}
|
||||
|
||||
func getProcessMap() (map[uint32]string, map[uint32]uint32, error) {
|
||||
snapshot, err := syscall.CreateToolhelp32Snapshot(syscall.TH32CS_SNAPPROCESS, 0)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer syscall.CloseHandle(snapshot)
|
||||
|
||||
var pe syscall.ProcessEntry32
|
||||
pe.Size = uint32(unsafe.Sizeof(pe))
|
||||
|
||||
err = syscall.Process32First(snapshot, &pe)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
pm := make(map[uint32]string)
|
||||
parents := make(map[uint32]uint32)
|
||||
for {
|
||||
name := syscall.UTF16ToString(pe.ExeFile[:])
|
||||
pm[pe.ProcessID] = name
|
||||
parents[pe.ProcessID] = pe.ParentProcessID
|
||||
|
||||
err = syscall.Process32Next(snapshot, &pe)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
return pm, parents, nil
|
||||
}
|
||||
|
||||
func getWindowText(hwnd uintptr) string {
|
||||
length, _, _ := procGetWindowTextLengthW.Call(hwnd)
|
||||
if length == 0 {
|
||||
return ""
|
||||
}
|
||||
buf := make([]uint16, length+1)
|
||||
procGetWindowTextW.Call(hwnd, uintptr(unsafe.Pointer(&buf[0])), length+1)
|
||||
return syscall.UTF16ToString(buf)
|
||||
}
|
||||
|
||||
var (
|
||||
enumWindowsMutex sync.Mutex
|
||||
enumWindowsList []WindowInfo
|
||||
enumProcessMap map[uint32]string
|
||||
enumParentMap map[uint32]uint32
|
||||
)
|
||||
|
||||
var enumWindowsCallback = syscall.NewCallback(func(hwnd uintptr, lParam uintptr) uintptr {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("[BLOCKER] Callback panic recovered: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
if !isRealGUIWindow(hwnd) {
|
||||
return 1
|
||||
}
|
||||
|
||||
// Kiểm tra xem chủ sở hữu gốc (root owner) của cửa sổ này có thuộc về app ta hay không
|
||||
rootHwnd, _, _ := procGetAncestor.Call(hwnd, 3) // GA_ROOTOWNER = 3
|
||||
if rootHwnd != 0 {
|
||||
var rootPid uint32
|
||||
procGetWindowThreadProcessId.Call(rootHwnd, uintptr(unsafe.Pointer(&rootPid)))
|
||||
if rootPid == uint32(os.Getpid()) {
|
||||
return 1 // Cửa sổ thuộc về WebView2 / app của ta, bỏ qua không quét
|
||||
}
|
||||
}
|
||||
|
||||
title := getWindowText(hwnd)
|
||||
if title == "" {
|
||||
return 1
|
||||
}
|
||||
|
||||
var pid uint32
|
||||
procGetWindowThreadProcessId.Call(hwnd, uintptr(unsafe.Pointer(&pid)))
|
||||
|
||||
procName := enumProcessMap[pid]
|
||||
if procName == "" {
|
||||
procName = "Unknown"
|
||||
}
|
||||
|
||||
enumWindowsList = append(enumWindowsList, WindowInfo{
|
||||
PID: pid,
|
||||
Title: title,
|
||||
ProcessName: procName,
|
||||
})
|
||||
|
||||
return 1
|
||||
})
|
||||
|
||||
func EnumerateGUIWindows() ([]WindowInfo, map[uint32]uint32, error) {
|
||||
pMap, parentMap, err := getProcessMap()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
enumWindowsMutex.Lock()
|
||||
defer enumWindowsMutex.Unlock()
|
||||
|
||||
enumWindowsList = make([]WindowInfo, 0, 100)
|
||||
enumProcessMap = pMap
|
||||
enumParentMap = parentMap
|
||||
|
||||
procEnumWindows.Call(enumWindowsCallback, 0)
|
||||
|
||||
// Clean up map reference so GC can reclaim it
|
||||
enumProcessMap = nil
|
||||
enumParentMap = nil
|
||||
|
||||
// Copy to a new slice to return safely
|
||||
res := make([]WindowInfo, len(enumWindowsList))
|
||||
copy(res, enumWindowsList)
|
||||
return res, parentMap, nil
|
||||
}
|
||||
|
||||
var systemAllowed = map[string]bool{
|
||||
"explorer.exe": true,
|
||||
"taskmgr.exe": true,
|
||||
"cmd.exe": true,
|
||||
"powershell.exe": true,
|
||||
"conhost.exe": true,
|
||||
"client.exe": true, // App Wails của ta
|
||||
"simple_care_v1.0.exe": true,
|
||||
"wails.exe": true,
|
||||
"msedgewebview2.exe": true, // WebView2 runtime của Wails
|
||||
"code.exe": true, // VSCode
|
||||
"cursor.exe": true, // Cursor
|
||||
"windsurf.exe": true, // Windsurf
|
||||
"goland.exe": true, // GoLand
|
||||
"goland64.exe": true, // GoLand
|
||||
"idea64.exe": true, // IntelliJ IDEA
|
||||
"clion64.exe": true, // CLion
|
||||
"webstorm64.exe": true, // WebStorm
|
||||
"pycharm64.exe": true, // PyCharm
|
||||
"rider64.exe": true, // Rider
|
||||
"studio64.exe": true, // Android Studio
|
||||
"eclipse.exe": true, // Eclipse
|
||||
"sublime_text.exe": true, // Sublime Text
|
||||
"notepad++.exe": true, // Notepad++
|
||||
"devenv.exe": true, // Visual Studio
|
||||
"git-bash.exe": true, // Git Bash
|
||||
"bash.exe": true, // Bash
|
||||
}
|
||||
|
||||
func isDescendant(pid, targetPid uint32, parentMap map[uint32]uint32) bool {
|
||||
curr := pid
|
||||
for i := 0; i < 16; i++ {
|
||||
parent, ok := parentMap[curr]
|
||||
if !ok || parent == 0 {
|
||||
return false
|
||||
}
|
||||
if parent == targetPid {
|
||||
return true
|
||||
}
|
||||
curr = parent
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (b *Blocker) checkAndKill() {
|
||||
b.mu.Lock()
|
||||
keywords := make([]string, len(b.allowedKeywords))
|
||||
copy(keywords, b.allowedKeywords)
|
||||
b.mu.Unlock()
|
||||
|
||||
// Nếu không cấu hình keyword thì không chặn gì cả
|
||||
if len(keywords) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
currentExec := ""
|
||||
if execPath, err := os.Executable(); err == nil {
|
||||
currentExec = strings.ToLower(filepath.Base(execPath))
|
||||
}
|
||||
|
||||
windows, parentMap, err := EnumerateGUIWindows()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
myPid := uint32(os.Getpid())
|
||||
for _, w := range windows {
|
||||
pNameLower := strings.ToLower(w.ProcessName)
|
||||
wTitleLower := strings.ToLower(w.Title)
|
||||
|
||||
// 1. Luôn cho phép hệ thống/app cốt lõi hoặc chính tiến trình này (bao gồm tiến trình con/cháu, và đổi tên)
|
||||
isOurApp := w.PID == myPid || isDescendant(w.PID, myPid, parentMap)
|
||||
if isOurApp || (currentExec != "" && pNameLower == currentExec) || systemAllowed[pNameLower] || strings.Contains(pNameLower, "antigravity") || strings.Contains(wTitleLower, "antigravity") {
|
||||
continue
|
||||
}
|
||||
|
||||
// 2. Kiểm tra xem có chứa bất kỳ từ khóa nào được cho phép không
|
||||
allowed := false
|
||||
for _, kw := range keywords {
|
||||
if matchesAllowedKeyword(kw, pNameLower, wTitleLower) {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Nếu không nằm trong whitelist, tắt ứng dụng
|
||||
if !allowed {
|
||||
if b.OnBlocked != nil {
|
||||
b.OnBlocked(w.ProcessName, w.Title)
|
||||
}
|
||||
log.Printf("[BLOCKER] KILLED unauthorized application: %s (PID: %d, Title: %s)", w.ProcessName, w.PID, w.Title)
|
||||
h, err := syscall.OpenProcess(syscall.PROCESS_TERMINATE, false, w.PID)
|
||||
if err == nil {
|
||||
errTerm := syscall.TerminateProcess(h, 0)
|
||||
_ = syscall.CloseHandle(h)
|
||||
if errTerm == nil && b.OnKill != nil {
|
||||
b.OnKill(w.ProcessName, w.Title)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
89
client/internal/guard/guard_darwin.go
Normal file
89
client/internal/guard/guard_darwin.go
Normal file
@@ -0,0 +1,89 @@
|
||||
//go:build darwin
|
||||
|
||||
package guard
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/kbinani/screenshot"
|
||||
)
|
||||
|
||||
var (
|
||||
guardOnce sync.Once
|
||||
guardViolation func(string)
|
||||
guardStop chan struct{}
|
||||
suppressMu sync.Mutex
|
||||
suppressViolationsUntil time.Time
|
||||
)
|
||||
|
||||
// SuppressFor tạm không thoát app khi WebView/Explorer chuyển màn hình nội bộ.
|
||||
func SuppressFor(d time.Duration) {
|
||||
if d <= 0 {
|
||||
return
|
||||
}
|
||||
suppressMu.Lock()
|
||||
next := time.Now().Add(d)
|
||||
if next.After(suppressViolationsUntil) {
|
||||
suppressViolationsUntil = next
|
||||
}
|
||||
suppressMu.Unlock()
|
||||
}
|
||||
|
||||
func violationsSuppressed() bool {
|
||||
suppressMu.Lock()
|
||||
defer suppressMu.Unlock()
|
||||
return time.Now().Before(suppressViolationsUntil)
|
||||
}
|
||||
|
||||
// Start giám sát môi trường macOS — vi phạm thì gọi onViolation (đa màn hình).
|
||||
func Start(onViolation func(reason string)) {
|
||||
guardOnce.Do(func() {
|
||||
if onViolation == nil {
|
||||
return
|
||||
}
|
||||
guardViolation = onViolation
|
||||
guardStop = make(chan struct{})
|
||||
|
||||
if screenshot.NumActiveDisplays() > 1 {
|
||||
onViolation("Phát hiện nhiều hơn 1 màn hình. Vui lòng chỉ dùng một màn hình khi chạy Simple Care.")
|
||||
return
|
||||
}
|
||||
|
||||
go pollLoop()
|
||||
})
|
||||
}
|
||||
|
||||
func Stop() {
|
||||
if guardStop != nil {
|
||||
select {
|
||||
case <-guardStop:
|
||||
// already closed
|
||||
default:
|
||||
close(guardStop)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func pollLoop() {
|
||||
ticker := time.NewTicker(3 * time.Second)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-guardStop:
|
||||
return
|
||||
case <-ticker.C:
|
||||
checkEnvironment()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkEnvironment() {
|
||||
if guardViolation == nil || violationsSuppressed() {
|
||||
return
|
||||
}
|
||||
if n := screenshot.NumActiveDisplays(); n > 1 {
|
||||
guardViolation("Phát hiện nhiều hơn 1 màn hình. Vui lòng rút/bật tắt màn hình phụ.")
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build !windows
|
||||
//go:build !windows && !darwin
|
||||
|
||||
package guard
|
||||
|
||||
|
||||
46
client/internal/winapi/wifi_darwin.go
Normal file
46
client/internal/winapi/wifi_darwin.go
Normal file
@@ -0,0 +1,46 @@
|
||||
//go:build darwin
|
||||
|
||||
package winapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GetWifiConnection đọc SSID và BSSID từ ipconfig (macOS)
|
||||
func GetWifiConnection() WifiConnection {
|
||||
// Find Wi-Fi interface dynamically
|
||||
wifiInterface := "en0"
|
||||
cmdPort := exec.Command("networksetup", "-listallhardwareports")
|
||||
if outPort, err := cmdPort.Output(); err == nil {
|
||||
lines := strings.Split(string(outPort), "\n")
|
||||
for i := 0; i < len(lines)-1; i++ {
|
||||
if strings.Contains(lines[i], "Hardware Port: Wi-Fi") {
|
||||
next := strings.TrimSpace(lines[i+1])
|
||||
if strings.HasPrefix(next, "Device:") {
|
||||
wifiInterface = strings.TrimSpace(strings.TrimPrefix(next, "Device:"))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cmd := exec.Command("ipconfig", "getsummary", wifiInterface)
|
||||
var out bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
if err := cmd.Run(); err != nil {
|
||||
return WifiConnection{}
|
||||
}
|
||||
|
||||
var conn WifiConnection
|
||||
for _, line := range strings.Split(out.String(), "\n") {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if strings.HasPrefix(trimmed, "SSID :") {
|
||||
conn.SSID = strings.TrimSpace(strings.TrimPrefix(trimmed, "SSID :"))
|
||||
} else if strings.HasPrefix(trimmed, "BSSID :") {
|
||||
conn.BSSID = normalizeMAC(strings.TrimSpace(strings.TrimPrefix(trimmed, "BSSID :")))
|
||||
}
|
||||
}
|
||||
return conn
|
||||
}
|
||||
8
client/internal/winapi/wifi_other.go
Normal file
8
client/internal/winapi/wifi_other.go
Normal file
@@ -0,0 +1,8 @@
|
||||
//go:build !windows && !darwin
|
||||
|
||||
package winapi
|
||||
|
||||
// GetWifiConnection returns an empty WifiConnection stub for other systems
|
||||
func GetWifiConnection() WifiConnection {
|
||||
return WifiConnection{}
|
||||
}
|
||||
40
client/internal/winapi/wifi_windows.go
Normal file
40
client/internal/winapi/wifi_windows.go
Normal file
@@ -0,0 +1,40 @@
|
||||
//go:build windows
|
||||
|
||||
package winapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// GetWifiConnection đọc SSID và BSSID từ netsh (Windows)
|
||||
func GetWifiConnection() WifiConnection {
|
||||
cmd := exec.Command("netsh", "wlan", "show", "interfaces")
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
||||
|
||||
var out bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
if err := cmd.Run(); err != nil {
|
||||
return WifiConnection{}
|
||||
}
|
||||
|
||||
var conn WifiConnection
|
||||
for _, line := range strings.Split(out.String(), "\n") {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
lower := strings.ToLower(trimmed)
|
||||
if strings.HasPrefix(lower, "ssid") && !strings.Contains(lower, "bssid") {
|
||||
if v := valueAfterColon(trimmed); v != "" {
|
||||
conn.SSID = v
|
||||
}
|
||||
}
|
||||
// Windows: "AP BSSID" (EN) hoặc dòng có chứa "bssid" (locale khác)
|
||||
if strings.Contains(lower, "bssid") {
|
||||
if v := valueAfterColon(trimmed); v != "" {
|
||||
conn.BSSID = normalizeMAC(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
return conn
|
||||
}
|
||||
@@ -1,10 +1,7 @@
|
||||
package winapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// WifiConnection — SSID + BSSID (MAC) của điểm phát đang kết nối
|
||||
@@ -18,36 +15,6 @@ func GetWifiSSID() string {
|
||||
return GetWifiConnection().SSID
|
||||
}
|
||||
|
||||
// GetWifiConnection đọc SSID và BSSID từ netsh (Windows)
|
||||
func GetWifiConnection() WifiConnection {
|
||||
cmd := exec.Command("netsh", "wlan", "show", "interfaces")
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
||||
|
||||
var out bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
if err := cmd.Run(); err != nil {
|
||||
return WifiConnection{}
|
||||
}
|
||||
|
||||
var conn WifiConnection
|
||||
for _, line := range strings.Split(out.String(), "\n") {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
lower := strings.ToLower(trimmed)
|
||||
if strings.HasPrefix(lower, "ssid") && !strings.Contains(lower, "bssid") {
|
||||
if v := valueAfterColon(trimmed); v != "" {
|
||||
conn.SSID = v
|
||||
}
|
||||
}
|
||||
// Windows: "AP BSSID" (EN) hoặc dòng có chứa "bssid" (locale khác)
|
||||
if strings.Contains(lower, "bssid") {
|
||||
if v := valueAfterColon(trimmed); v != "" {
|
||||
conn.BSSID = normalizeMAC(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
return conn
|
||||
}
|
||||
|
||||
func valueAfterColon(line string) string {
|
||||
idx := strings.Index(line, ":")
|
||||
if idx < 0 {
|
||||
|
||||
30
client/internal/winapi/window_darwin.go
Normal file
30
client/internal/winapi/window_darwin.go
Normal file
@@ -0,0 +1,30 @@
|
||||
//go:build darwin
|
||||
|
||||
package winapi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
// ActivateAppWindow — đưa cửa sổ app lên trước
|
||||
func ActivateAppWindow(titleHint string) {
|
||||
pid := os.Getpid()
|
||||
script := fmt.Sprintf(`tell application "System Events" to set frontmost of first process whose unix id is %d to true`, pid)
|
||||
cmd := exec.Command("osascript", "-e", script)
|
||||
_ = cmd.Run()
|
||||
}
|
||||
|
||||
// PlayNotifySound — beep hệ thống macOS
|
||||
func PlayNotifySound() {
|
||||
cmd := exec.Command("osascript", "-e", "beep")
|
||||
_ = cmd.Run()
|
||||
}
|
||||
|
||||
// ShowWarningMessageBox hiển thị hộp thoại cảnh báo macOS bất đồng bộ
|
||||
func ShowWarningMessageBox(title, message string) {
|
||||
script := fmt.Sprintf(`display alert %q message %q`, title, message)
|
||||
cmd := exec.Command("osascript", "-e", script)
|
||||
_ = cmd.Start() // Run asynchronously
|
||||
}
|
||||
9
client/internal/winapi/window_other.go
Normal file
9
client/internal/winapi/window_other.go
Normal file
@@ -0,0 +1,9 @@
|
||||
//go:build !windows && !darwin
|
||||
|
||||
package winapi
|
||||
|
||||
func ActivateAppWindow(titleHint string) {}
|
||||
|
||||
func PlayNotifySound() {}
|
||||
|
||||
func ShowWarningMessageBox(title, message string) {}
|
||||
BIN
client/test_build
Executable file
BIN
client/test_build
Executable file
Binary file not shown.
Reference in New Issue
Block a user