fix build macos

This commit is contained in:
2026-07-01 09:49:27 +07:00
parent c3bea3bb46
commit 35d954ff0f
20 changed files with 697 additions and 318 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

BIN
client/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -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
View 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

Binary file not shown.

0
client/frontend/wailsjs/go/main/App.d.ts vendored Normal file → Executable file
View File

0
client/frontend/wailsjs/go/main/App.js Normal file → Executable file
View File

View 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() {

View 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)
}
}
}
}
}

View File

@@ -0,0 +1,5 @@
//go:build !windows && !darwin
package blocker
func (b *Blocker) checkAndKill() {}

View 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)
}
}
}
}
}

View 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
}
}

View File

@@ -1,4 +1,4 @@
//go:build !windows
//go:build !windows && !darwin
package guard

View 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
}

View 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{}
}

View 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
}

View File

@@ -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 {

View 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
}

View 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

Binary file not shown.