Compare commits

..

2 Commits

Author SHA1 Message Date
Your Name
4b8f4d288a add install guide for linux
All checks were successful
Deploy on Master Change / deploy (push) Successful in 40s
2026-07-02 13:35:53 +07:00
Your Name
2ad899d787 add linux 2026-07-02 13:34:40 +07:00
9 changed files with 451 additions and 9 deletions

86
client/INSTALL_LINUX.md Normal file
View File

@@ -0,0 +1,86 @@
# Hướng dẫn Cài đặt & Chạy ứng dụng Simple Care trên Linux
Tài liệu này hướng dẫn cách cài đặt công cụ lập trình, cài đặt thư viện chạy và tiến hành biên dịch ứng dụng Simple Care trên môi trường Linux.
---
## 1. Cho Nhà phát triển (Developer - Build ứng dụng)
### Bước 1: Cài đặt thư viện hệ thống
Cài đặt trình biên dịch C và các thư viện Webview (GTK/WebKit) tùy vào hệ điều hành đang dùng:
* **Ubuntu / Debian / Linux Mint:**
```bash
sudo apt update
sudo apt install -y build-essential libgtk-3-dev libwebkit2gtk-4.1-dev libx11-dev pkg-config zenity libcanberra-gtk3-module
```
* **Fedora / RHEL:**
```bash
sudo dnf groupinstall "Development Tools"
sudo dnf install -y gtk3-devel webkit2gtk4.1-devel libX11-devel pkgconf-pkg-config zenity
```
* **Arch Linux:**
```bash
sudo pacman -Syu --needed base-devel gtk3 webkit2gtk-4.1 libx11 pkgconf zenity
```
### Bước 2: Cài đặt Go & Node.js & Wails CLI
Nếu chưa cài đặt bộ công cụ biên dịch:
1. Tải và cài đặt **Go** (bản 1.23+): [golang.org](https://go.dev/dl/)
2. Tải và cài đặt **Node.js** (bản 18 hoặc 20 LTS): [nodejs.org](https://nodejs.org/)
3. Cài đặt **Wails CLI** thông qua Go:
```bash
go install github.com/wailsapp/wails/v2/cmd/wails@v2.12.0
```
4. Cài đặt công cụ mã hóa **Garble**:
```bash
go install mvdan.cc/garble@v0.13.0
```
### Bước 3: Tiến hành Biên dịch (Build)
Chạy script tự động phát hiện hệ điều hành để build bản bảo mật (obfuscated):
```bash
cd client
chmod +x build.sh
./build.sh
```
Sau khi build xong, file thực thi sẽ nằm tại: `client/build/bin/simple_care_v1.0`
---
## 2. Cho Người dùng cuối (End-user - Chỉ chạy ứng dụng)
Người dùng cuối **không cần** cài đặt Go, Node hay trình biên dịch C. Họ chỉ cần tệp thực thi `simple_care_v1.0` và cài đặt các thư viện đồ họa cơ bản của hệ thống:
* **Ubuntu / Debian / Linux Mint:**
```bash
sudo apt update
sudo apt install -y libwebkit2gtk-4.1-0 zenity
```
* **Fedora / RHEL:**
```bash
sudo dnf install -y webkit2gtk4.1 zenity
```
* **Arch Linux:**
```bash
sudo pacman -Sy webkit2gtk-4.1 zenity
```
### Lệnh chạy ứng dụng:
Cấp quyền chạy cho file và khởi chạy:
```bash
chmod +x simple_care_v1.0
./simple_care_v1.0
```
---
## 3. Lệnh dọn dẹp bộ nhớ đệm (Clean cache)
Nếu thư mục `client/build/` xuất hiện nhiều thư mục đệm tạm thời (dạng số hexa `00` đến `ff`), bạn có thể dọn dẹp bằng lệnh sau:
```bash
find build -mindepth 1 -maxdepth 1 -type d -name "[0-9a-f][0-9a-f]" -exec rm -rf {} +
```

View File

@@ -5,7 +5,7 @@ set -e
cd "$(dirname "$0")"
echo "============================================="
echo " BUILDING SECURE WAILS CLIENT FOR MACOS "
echo " BUILDING SECURE WAILS CLIENT "
echo "============================================="
# Setup Go path for tools like garble and wails
@@ -46,10 +46,21 @@ else
fi
BUILD_FLAGS+=(-ldflags "-s -w")
# Detect host OS
HOST_OS=$(go env GOOS)
if [ "$HOST_OS" = "darwin" ]; then
echo "[*] Building macOS Apple Silicon (arm64)..."
"$WAILS_CMD" build -platform darwin/arm64 "${BUILD_FLAGS[@]}"
echo "[*] Building macOS Intel (amd64)..."
"$WAILS_CMD" build -platform darwin/amd64 "${BUILD_FLAGS[@]}"
echo "[+] macOS arm64 and amd64 builds completed successfully!"
elif [ "$HOST_OS" = "linux" ]; then
echo "[*] Building Linux Intel/AMD64 (amd64)..."
"$WAILS_CMD" build -platform linux/amd64 -tags webkit2_41 "${BUILD_FLAGS[@]}"
echo "[+] Linux amd64 build completed successfully!"
else
echo "[!] Unsupported host OS: $HOST_OS. Please build manually using 'wails build'."
exit 1
fi

1
client/build/trim.txt Normal file
View File

@@ -0,0 +1 @@
1782971860

View File

@@ -0,0 +1,214 @@
//go:build linux
package blocker
import (
"fmt"
"log"
"os"
"path/filepath"
"strconv"
"strings"
)
var systemAllowed = map[string]bool{
"gnome-shell": true,
"mutter": true,
"xterm": true,
"gnome-terminal": true,
"konsole": true,
"ptyxis": true, // Ubuntu terminal
"bash": true,
"zsh": true,
"sh": true,
"wails": true,
"code": true,
"cursor": true,
"windsurf": true,
"goland": true,
"idea": true,
"client": true,
"simple_care_v1.0": true,
"xwayland": true,
"mutter-x11-fram": true,
"ibus-x11": true,
"ibus-daemon": true,
"gsd-": true, // prefix
"ibus-": true, // prefix
"xdg-": true, // prefix
"at-spi": true, // prefix
"evolution-": true, // prefix
"goa-daemon": true,
"gjs": true,
"snapd-": true, // prefix
"systemd": true,
"webkit": true, // WebKit subprocesses
"glycin": true, // GNOME image helper
"zenity": true, // Dialog utility
"rustdesk": true, // Remote support safety
"rustdesk-bin": true,
"anydesk": true,
"teamviewer": true,
"remmina": true,
}
func isSystemAllowed(name string) bool {
name = strings.ToLower(name)
if systemAllowed[name] {
return true
}
for pattern := range systemAllowed {
if strings.HasPrefix(name, pattern) && pattern != name {
return true
}
}
return false
}
func getPPID(pid uint32) uint32 {
statusBytes, err := os.ReadFile(fmt.Sprintf("/proc/%d/status", pid))
if err != nil {
return 0
}
lines := strings.Split(string(statusBytes), "\n")
for _, line := range lines {
if strings.HasPrefix(line, "PPid:") {
parts := strings.Fields(line)
if len(parts) >= 2 {
if ppid, err := strconv.ParseUint(parts[1], 10, 32); err == nil {
return uint32(ppid)
}
}
}
}
return 0
}
func isDescendantOf(pid, targetPid uint32) bool {
curr := pid
for i := 0; i < 10; i++ { // limits lookup to 10 ancestor levels
ppid := getPPID(curr)
if ppid == 0 {
return false
}
if ppid == targetPid {
return true
}
curr = ppid
}
return false
}
type ProcessInfo struct {
PID uint32
Name string
}
func getVisibleProcesses() (map[uint32]ProcessInfo, error) {
files, err := os.ReadDir("/proc")
if err != nil {
return nil, err
}
procs := make(map[uint32]ProcessInfo)
for _, f := range files {
if !f.IsDir() {
continue
}
pid, err := strconv.ParseUint(f.Name(), 10, 32)
if err != nil {
continue
}
mapsPath := fmt.Sprintf("/proc/%d/maps", pid)
mapsBytes, err := os.ReadFile(mapsPath)
if err != nil {
// Skip processes we don't own (permission denied)
continue
}
mapsStr := string(mapsBytes)
isGUI := strings.Contains(mapsStr, "libgtk") ||
strings.Contains(mapsStr, "libQt") ||
strings.Contains(mapsStr, "libX11") ||
strings.Contains(mapsStr, "libwayland-client")
if !isGUI {
continue
}
// Read process name from /proc/PID/comm
commBytes, err := os.ReadFile(fmt.Sprintf("/proc/%d/comm", pid))
if err != nil {
continue
}
procName := strings.TrimSpace(string(commBytes))
if procName != "" {
procs[uint32(pid)] = ProcessInfo{
PID: uint32(pid),
Name: procName,
}
}
}
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, our sub-processes, system/critical developer tools, or agent helpers
isOurSubprocess := pid == myPid || isDescendantOf(pid, myPid)
isAntigravity := strings.Contains(pNameLower, "antigravity")
if isOurSubprocess || (currentExec != "" && pNameLower == currentExec) || isSystemAllowed(pNameLower) || isAntigravity {
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)
}
}
}
}
}

View File

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

View File

@@ -0,0 +1,90 @@
//go:build linux
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 temporarily suspends environment violation checks (e.g. when transition screen)
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 monitors the Linux desktop environment (multi-display check)
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()
})
}
// Stop stops the environment guard
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 && !darwin
//go:build !windows && !darwin && !linux
package guard

View File

@@ -0,0 +1,40 @@
//go:build linux
package winapi
import (
"log"
"os/exec"
)
// ActivateAppWindow attempts to focus the application window.
// On Linux standard X11/Wayland desktop, focus is managed by the WM.
func ActivateAppWindow(titleHint string) {
log.Printf("[WINDOW] ActivateAppWindow requested for: %s", titleHint)
}
// PlayNotifySound plays a system notification sound using canberra-gtk-play, falling back to pw-play or aplay.
func PlayNotifySound() {
log.Println("[WINDOW] Playing notification sound...")
go func() {
// Try canberra-gtk-play first
cmd := exec.Command("canberra-gtk-play", "-i", "bell")
if err := cmd.Run(); err != nil {
// Fallback to pw-play (Pipewire)
cmd = exec.Command("pw-play", "/usr/share/sounds/freedesktop/stereo/bell.oga")
if err := cmd.Run(); err != nil {
// Fallback to aplay (ALSA)
_ = exec.Command("aplay", "/usr/share/sounds/alsa/Front_Center.wav").Run()
}
}
}()
}
// ShowWarningMessageBox displays an asynchronous GUI warning dialog using zenity.
func ShowWarningMessageBox(title, message string) {
log.Printf("[WINDOW] Warning Message Box: %s - %s", title, message)
go func() {
cmd := exec.Command("zenity", "--warning", "--title="+title, "--text="+message, "--no-wrap")
_ = cmd.Run()
}()
}

View File

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