Files
rikkei_simple_care/client/internal/blocker/blocker_darwin.go
2026-07-06 11:54:47 +07:00

284 lines
12 KiB
Go

//go:build darwin
package blocker
import (
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
)
var systemAllowed = map[string]bool{
// ── Core macOS Desktop Infrastructure ─────────────────────────────────────
"finder": true, // macOS file manager / desktop
"dock": true, // macOS Dock — kill = dock disappears
"windowserver": true, // WindowServer — kill = instant logout
"loginwindow": true, // Login/session manager — kill = logout
"systemuiserver": true, // Menu bar icons (volume, wifi, battery, clock)
"controlcenter": true, // macOS Control Center (Monterey+)
"notificationcenter": true, // Notification Center
"spotlight": true, // Spotlight search
"launchpad": true,
"mission control": true,
"exposé": true,
"universalaccessd": true,
"accessibilityuiagent": true, // Accessibility helper
// ── Input Methods & Language (critical — kill = can't type) ───────────────
"inputmethodkit": true,
"ibus": true,
"hiragana kakomi input": true,
"kinput2": true,
"squirrel": true, // Rime input method
"scim": true,
"kotoeri": true, // Japanese IME
"pinyin - simplified": true, // macOS Chinese Pinyin
"zhuyin - traditional": true,
"vietnamese": true, // macOS built-in Vietnamese IME
"abc": true, // macOS ABC keyboard input
// ── Security / Keychain / Authentication ──────────────────────────────────
"securityagent": true, // macOS security agent — kill breaks sudo GUI, Keychain prompts
"keychain": true, // Keychain access
"keychainservicesagent": true,
"trustd": true,
"opendirectoryd": true,
"authorizationhost": true, // Authorization host — UAC equivalent
"securityd": true,
"coreauthenticationd": true,
"biometricd": true,
"touchidd": true,
// ── Audio / Media ─────────────────────────────────────────────────────────
"coreaudiod": true, // Core Audio daemon — kill = no sound
"audioundockhelper": true,
"audio midi setup": true,
"noiseremoval": true,
// ── Networking / VPN ──────────────────────────────────────────────────────
"networkd": true,
"nesessionmanager": true, // Network Extension — kill drops VPN
"scutil": true,
"configd": true,
"mDNSResponder": true, // Bonjour DNS
// ── Spotlight / File Indexing ──────────────────────────────────────────────
"mds": true, // Spotlight metadata server
"mds_stores": true,
"mdworker": true, // prefix match covers mdworker_shared
"mdworker_shared": true,
// ── iCloud / Apple Services ───────────────────────────────────────────────
"bird": true, // iCloud Drive daemon
"cloudd": true,
"com.apple.icloud": true, // prefix
"cloudphotod": true,
"nsurlsessiond": true,
// ── System Preferences / Settings ─────────────────────────────────────────
"system preferences": true, // macOS System Preferences (pre-Ventura)
"system settings": true, // macOS System Settings (Ventura+)
"software update": true,
"app store": true,
// ── Screen / Display ──────────────────────────────────────────────────────
"screensaver engine": true, // Screensaver
"com.apple.screensaver": true,
"colorsyncd": true,
"colorsync utility": true,
"nightshift": true,
"display menu": true,
// ── Clipboard / Pasteboard ────────────────────────────────────────────────
"pboard": true, // Pasteboard daemon — kill breaks copy/paste
// ── Printing ─────────────────────────────────────────────────────────────
"printingproxy": true,
"cupsd": true,
// ── Crash Reporting / Diagnostics ─────────────────────────────────────────
"crashreporter": true,
"diagnosticsd": true,
"spindump": true,
"reportmemoryexception": true,
// ── Webkit / App subprocesses ─────────────────────────────────────────────
"webkit": true, // prefix
"com.apple.webkit": true, // prefix
"com.apple.webkit.networking": true,
// ── Remote support ────────────────────────────────────────────────────────
"applescriptkit": true,
"applescript runner": true,
"rustdesk": true,
"anydesk": true,
"teamviewer": true,
"screen sharing": true,
"screensharingd": true, // macOS Screen Sharing
// ── Terminals ─────────────────────────────────────────────────────────────
"terminal": true, // macOS Terminal
"iterm": true,
"iterm2": true,
"wezterm": true,
"kitty": true,
"alacritty": true,
"hyper": true,
// ── Shells ────────────────────────────────────────────────────────────────
"bash": true,
"zsh": true,
"sh": true,
"fish": true,
// ── AppleScript / Automation ──────────────────────────────────────────────
"system events": true, // AppleScript System Events (used by our blocker itself)
"osascript": true, // AppleScript runner (used by our getVisibleProcesses)
// ── Git & Credential Helpers ──────────────────────────────────────────────
"git": true,
"git-credential-manager": true,
"git-credential-osxkeychain": true,
"github desktop": true,
"sourcetree": true,
"fork": true,
// ── Docker ───────────────────────────────────────────────────────────────
"docker": true,
"docker desktop": true,
"com.docker": true, // prefix
// ── Our app + IDE/dev tools ────────────────────────────────────────────────
"client": true,
"simple_care_v1.0": true,
"simple_care": true,
"wails": true,
"code": true, // VSCode
"cursor": true,
"windsurf": true,
"goland": true,
"idea": true,
"clion": true,
"webstorm": true,
"pycharm": true,
"rider": true,
"studio": true, // Android Studio
"eclipse": true,
"sublime text": true,
}
type ProcessInfo struct {
Name string
BundleID string
}
func getVisibleProcesses() (map[uint32]ProcessInfo, error) {
script := `tell application "System Events"
set out to ""
set procList to every process whose visible is true
repeat with p in procList
try
set nameStr to name of p
set pidVal to unix id of p
set bid to bundle identifier of p
if bid is missing value then
set bid to ""
end if
set out to out & nameStr & "|" & pidVal & "|" & bid & "\n"
on error
-- ignore
end try
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]ProcessInfo)
lines := strings.Split(string(out), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
parts := strings.Split(line, "|")
if len(parts) < 2 {
continue
}
pName := parts[0]
pIdStr := parts[1]
bundleID := ""
if len(parts) >= 3 {
bundleID = parts[2]
}
var pid uint32
if _, err := fmt.Sscanf(pIdStr, "%d", &pid); err == nil {
procs[pid] = ProcessInfo{
Name: pName,
BundleID: bundleID,
}
}
}
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, info := range procs {
pNameLower := strings.ToLower(info.Name)
// 1. Always allow our app, system/critical developer tools, or agent helpers
if pid == myPid || (currentExec != "" && pNameLower == currentExec) || systemAllowed[pNameLower] {
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(info.Name, info.Name)
}
log.Printf("[BLOCKER] KILLED unauthorized application: %s (PID: %d)", info.Name, pid)
proc, err := os.FindProcess(int(pid))
if err == nil {
errKill := proc.Kill()
if errKill == nil && b.OnKill != nil {
b.OnKill(info.Name, info.Name)
}
}
}
}
}