diff --git a/client/app.go b/client/app.go index 00b7589..49e6d82 100644 --- a/client/app.go +++ b/client/app.go @@ -19,6 +19,7 @@ import ( "time" "client/internal/blocker" + "client/internal/guard" "client/internal/screen" "client/internal/winapi" @@ -100,7 +101,9 @@ type App struct { dashboard StudentDashboardSnapshot wifiEnforce bool acceptedWifi map[string]bool - wifiRejected bool + wifiRejected bool + quitDialogShown bool + monitoringTornDown bool } type LocalStats struct { @@ -173,6 +176,9 @@ func (a *App) startup(ctx context.Context) { }) } + // Giám sát môi trường Windows: đa màn hình, desktop ảo, đổi user + guard.Start(a.handleGuardViolation) + // Khởi chạy HTTP server nhận callback login thành công a.startLocalServer() @@ -183,6 +189,53 @@ func (a *App) startup(ctx context.Context) { go a.authStorageScanner() } +func (a *App) handleGuardViolation(reason string) { + a.showQuitDialog("Vi phạm giám sát", reason) +} + +// tearDownBeforeQuit ngắt mọi kênh giám sát ngay — trước khi hiện dialog (tránh treo OK để duy trì kết nối). +func (a *App) tearDownBeforeQuit() { + a.mu.Lock() + if a.monitoringTornDown { + a.mu.Unlock() + return + } + a.monitoringTornDown = true + a.mu.Unlock() + + blocker.Instance.Stop() + a.stopScreenshotStream() + runtime.EventsEmit(a.ctx, "stop_webcam_stream") + a.disconnectWS() + guard.Stop() +} + +func (a *App) isMonitoringActive() bool { + a.mu.Lock() + defer a.mu.Unlock() + return !a.monitoringTornDown +} + +// showQuitDialog — ngắt giám sát trước, hiện cảnh báo, sinh viên bấm OK rồi app mới thoát. +func (a *App) showQuitDialog(title, message string) { + a.mu.Lock() + if a.quitDialogShown { + a.mu.Unlock() + return + } + a.quitDialogShown = true + a.mu.Unlock() + + a.tearDownBeforeQuit() + + _, _ = runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{ + Type: runtime.WarningDialog, + Title: title, + Message: message + "\n\nNhấn OK để đóng ứng dụng.", + }) + runtime.Quit(a.ctx) +} + // startLocalServer khởi chạy server lắng nghe callback nhận thông tin sinh viên từ webview func (a *App) startLocalServer() { mux := http.NewServeMux() @@ -224,6 +277,7 @@ func (a *App) startLocalServer() { go a.fetchAllowedApps(st.SystemID) go a.fetchStudentStatus(st.StudentID) go a.fetchWifiPolicy() + go a.refreshNetworkStatus() // Chuyển hướng WebView về trang dashboard của app bằng cách reload app assets runtime.WindowReloadApp(a.ctx) @@ -264,6 +318,7 @@ func (a *App) loadSession() { go a.fetchAllowedApps(s.SystemID) go a.fetchStudentStatus(s.StudentID) go a.fetchWifiPolicy() + go a.refreshNetworkStatus() go a.syncProfileToServer(&s) } } @@ -433,6 +488,9 @@ func (a *App) GetStats() map[string]any { // SendWebcamFrame truyền webcam frame từ JS frontend lên máy chủ qua WS func (a *App) SendWebcamFrame(frameBase64 string) { + if !a.isMonitoringActive() { + return + } a.mu.Lock() conn := a.wsConn a.mu.Unlock() @@ -480,50 +538,69 @@ func (a *App) monitorLoop() { ticker := time.NewTicker(10 * time.Second) defer ticker.Stop() + a.runMonitorTick() + for { <-ticker.C + a.runMonitorTick() + } +} - if !a.CheckLoginStatus() { - continue - } +func (a *App) runMonitorTick() { + if !a.isMonitoringActive() || !a.CheckLoginStatus() { + return + } - wifi := winapi.GetWifiConnection() - a.mu.Lock() - a.wifiSSID = wifi.SSID - a.wifiBSSID = wifi.BSSID - a.mu.Unlock() + wifi := winapi.GetWifiConnection() + a.mu.Lock() + a.wifiSSID = wifi.SSID + a.wifiBSSID = wifi.BSSID + a.mu.Unlock() - backendOnline := a.pingBackend() + backendOnline := a.pingBackend() - if backendOnline && wifi.SSID != "" && wifi.BSSID != "" { - go a.reportWifi(wifi.SSID, wifi.BSSID) - a.fetchWifiPolicy() - if !a.isWifiAllowed(wifi.SSID, wifi.BSSID) { - a.rejectUnauthorizedWifi(wifi.SSID, wifi.BSSID) - continue - } - } - - a.mu.Lock() - a.backendOnline = backendOnline - if backendOnline { - a.onlineSecs += 10 - a.unsyncedOn += 10 - } else { - a.offlineSecs += 10 - a.unsyncedOff += 10 - } - a.mu.Unlock() - - a.saveStats() - - if backendOnline { - a.connectWS() - a.syncLogsToServer() - } else { - a.disconnectWS() + if backendOnline && wifi.SSID != "" && wifi.BSSID != "" { + go a.reportWifi(wifi.SSID, wifi.BSSID) + a.fetchWifiPolicy() + if !a.isWifiAllowed(wifi.SSID, wifi.BSSID) { + a.rejectUnauthorizedWifi(wifi.SSID, wifi.BSSID) + return } } + + a.mu.Lock() + a.backendOnline = backendOnline + if backendOnline { + a.onlineSecs += 10 + a.unsyncedOn += 10 + } else { + a.offlineSecs += 10 + a.unsyncedOff += 10 + } + a.mu.Unlock() + + a.saveStats() + + if backendOnline { + a.connectWS() + a.syncLogsToServer() + } else { + a.disconnectWS() + } +} + +// refreshNetworkStatus — đọc WiFi + ping server ngay (không cộng thời gian online/offline). +func (a *App) refreshNetworkStatus() { + if !a.isMonitoringActive() || !a.CheckLoginStatus() { + return + } + wifi := winapi.GetWifiConnection() + backendOnline := a.pingBackend() + a.mu.Lock() + a.wifiSSID = wifi.SSID + a.wifiBSSID = wifi.BSSID + a.backendOnline = backendOnline + a.mu.Unlock() } func (a *App) pingBackend() bool { @@ -537,6 +614,9 @@ func (a *App) pingBackend() bool { } func (a *App) syncLogsToServer() { + if !a.isMonitoringActive() { + return + } a.mu.Lock() student := a.student unsyncedOn := a.unsyncedOn @@ -580,6 +660,9 @@ func (a *App) syncLogsToServer() { } func (a *App) reportBlockedApp(procName string, title string) { + if !a.isMonitoringActive() { + return + } a.mu.Lock() student := a.student a.mu.Unlock() @@ -620,6 +703,9 @@ func (a *App) reportBlockedApp(procName string, title string) { } func (a *App) reportWifi(ssid, bssid string) { + if !a.isMonitoringActive() { + return + } a.mu.Lock() student := a.student a.mu.Unlock() @@ -709,15 +795,10 @@ func (a *App) rejectUnauthorizedWifi(ssid, bssid string) { a.wifiRejected = true a.mu.Unlock() - blocker.Instance.Stop() - a.disconnectWS() - - _, _ = runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{ - Type: runtime.WarningDialog, - Title: "WiFi không được phép", - Message: fmt.Sprintf("Điểm phát WiFi \"%s\" (%s) không được phép.\n\nCó thể là mạng giả mạo (hotspot trùng tên). Hãy kết nối đúng WiFi của trường rồi mở lại ứng dụng.", ssid, bssid), - }) - runtime.Quit(a.ctx) + a.showQuitDialog( + "WiFi không được phép", + fmt.Sprintf("Điểm phát WiFi \"%s\" (%s) không được phép.\n\nCó thể là mạng giả mạo (hotspot trùng tên). Hãy kết nối đúng WiFi của trường rồi mở lại ứng dụng.", ssid, bssid), + ) } func (a *App) fetchAllowedApps(classId int64) { @@ -807,6 +888,9 @@ func (a *App) fetchStudentStatus(studentID int64) { } func (a *App) connectWS() { + if !a.isMonitoringActive() { + return + } a.mu.Lock() if a.wsConnected || a.student == nil { a.mu.Unlock() diff --git a/client/build/appicon.png b/client/build/appicon.png index 63617fe..026b8f0 100644 Binary files a/client/build/appicon.png and b/client/build/appicon.png differ diff --git a/client/frontend/index.html b/client/frontend/index.html index 3addd1f..fa9205e 100644 --- a/client/frontend/index.html +++ b/client/frontend/index.html @@ -3,6 +3,7 @@
+