Compare commits

..

1 Commits

20 changed files with 92 additions and 1017 deletions

View File

@@ -1,79 +0,0 @@
# Log File Management
## Overview
RustDesk uses `flexi_logger` for logging with automatic log rotation. Logs are written to platform-specific directories and rotated to prevent excessive disk usage.
## Current Implementation
### Log Locations
- **macOS**: `~/Library/Logs/RustDesk/`
- **Linux**: `~/.local/share/logs/RustDesk/`
- **Android**: `{APP_HOME}/RustDesk/Logs/`
- **Windows**: Config directory → `log/` subdirectory
### Current Rotation Policy
The logging system (implemented in `libs/hbb_common/src/lib.rs`) uses size-based rotation:
```rust
.rotate(
// Rotate logs daily OR when they reach 100MB (whichever comes first)
Criterion::AgeOrSize(Age::Day, 100_000_000),
Naming::Timestamps,
Cleanup::KeepLogFiles(31),
)
```
### Benefits
1. **Bounded Disk Usage**: Maximum log storage becomes ~3.1GB (31 files × 100MB)
2. **Maintained Organization**: Daily rotation still occurs for time-based organization
3. **Prevents Runaway Logs**: Heavy activity days can't create multi-gigabyte single files
4. **Automatic Cleanup**: Old files still automatically deleted after 31 days
### Implementation
The implementation is in the `hbb_common` library:
**File**: `libs/hbb_common/src/lib.rs`
**Function**: `init_log()`
**Line**: ~407
```diff
- Criterion::Age(Age::Day),
+ // Rotate logs daily OR when they reach 100MB to prevent excessive disk usage
+ // With 31 files max, this limits total log storage to ~3.1GB
+ Criterion::AgeOrSize(Age::Day, 100_000_000),
```
## Tuning Parameters
The 100MB size limit can be adjusted based on deployment needs:
- **50MB** (`50_000_000`): More conservative, max ~1.5GB total
- **100MB** (`100_000_000`): Balanced approach, max ~3.1GB total ✅ Recommended
- **200MB** (`200_000_000`): Permissive, max ~6.2GB total
## Monitoring
Users can monitor log disk usage at the locations listed above. The rotation ensures:
1. Logs older than 31 days are automatically deleted
2. Individual files never exceed the configured size limit
3. Total disk usage is bounded and predictable
## Testing
To verify the rotation works correctly:
1. Check log directory before and after rotation
2. Verify old files are cleaned up after 31 days
3. Monitor file sizes don't exceed the configured limit
4. Ensure logs still contain all necessary debugging information
## References
- flexi_logger documentation: https://docs.rs/flexi_logger/
- RustDesk logging implementation: `libs/hbb_common/src/lib.rs`

View File

@@ -1,39 +0,0 @@
# Patches for hbb_common
This directory contains patches for reference. The changes described have already been applied to the `hbb_common` submodule in this PR.
## hbb_common-log-rotation.patch
**Status**: ✅ Already applied in this PR (submodule commit 0c401fd)
**Purpose**: Add size-based log rotation to prevent excessive disk usage
**Apply to**: `libs/hbb_common` submodule
**Target repository**: https://github.com/rustdesk/hbb_common
### Reference Information
This patch file is provided for:
- Documentation of the exact changes made
- Reference for maintainers
- Potential cherry-picking to other branches if needed
The changes have already been implemented in the submodule updated by this PR.
### What it does
- Changes log rotation from age-only to age-or-size based
- Rotates logs when they reach 100MB OR daily (whichever comes first)
- Limits total log storage to ~3.1GB (31 files × 100MB max each)
- Prevents runaway log files from consuming excessive disk space
### Testing
After applying the patch and rebuilding:
1. Verify logs are created in the standard location
2. Check that individual log files don't exceed 100MB
3. Confirm old files are still cleaned up after 31 days
4. Ensure log content is still complete and useful
See `../LOG_MANAGEMENT.md` for complete documentation.

View File

@@ -1,32 +0,0 @@
From: GitHub Copilot <github-copilot@github.com>
Subject: [PATCH] Add size-based log rotation to prevent excessive disk usage
This patch adds size-based rotation to the flexi_logger configuration
to prevent individual log files from growing unbounded. Logs will now
rotate when they reach 100MB OR daily (whichever comes first).
With the existing 31-file cleanup policy, this limits total log storage
to approximately 3.1GB, preventing excessive disk space consumption.
---
src/lib.rs | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/src/lib.rs b/src/lib.rs
index d9713b9..ba21e90 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -402,7 +402,9 @@ pub fn init_log(_is_async: bool, _name: &str) -> Option<flexi_logger::LoggerHand
})
.format(opt_format)
.rotate(
- Criterion::Age(Age::Day),
+ // Rotate logs daily OR when they reach 100MB to prevent excessive disk usage
+ // With 31 files max, this limits total log storage to ~3.1GB
+ Criterion::AgeOrSize(Age::Day, 100_000_000),
Naming::Timestamps,
Cleanup::KeepLogFiles(31),
)
--
2.43.0

View File

@@ -25,7 +25,6 @@ class CustomTouchGestureRecognizer extends ScaleGestureRecognizer {
GestureDragStartCallback? onOneFingerPanStart;
GestureDragUpdateCallback? onOneFingerPanUpdate;
GestureDragEndCallback? onOneFingerPanEnd;
GestureDragCancelCallback? onOneFingerPanCancel;
// twoFingerScale : scale + pan event
GestureScaleStartCallback? onTwoFingerScaleStart;
@@ -170,27 +169,6 @@ class CustomTouchGestureRecognizer extends ScaleGestureRecognizer {
DragEndDetails _getDragEndDetails(ScaleEndDetails d) =>
DragEndDetails(velocity: d.velocity);
@override
void rejectGesture(int pointer) {
super.rejectGesture(pointer);
switch (_currentState) {
case GestureState.oneFingerPan:
if (onOneFingerPanCancel != null) {
onOneFingerPanCancel!();
}
break;
case GestureState.twoFingerScale:
// Reset scale state if needed, currently self-contained
break;
case GestureState.threeFingerVerticalDrag:
// Reset drag state if needed, currently self-contained
break;
default:
break;
}
_currentState = GestureState.none;
}
}
class HoldTapMoveGestureRecognizer extends GestureRecognizer {
@@ -739,7 +717,6 @@ RawGestureDetector getMixinGestureDetector({
GestureDragStartCallback? onOneFingerPanStart,
GestureDragUpdateCallback? onOneFingerPanUpdate,
GestureDragEndCallback? onOneFingerPanEnd,
GestureDragCancelCallback? onOneFingerPanCancel,
GestureScaleUpdateCallback? onTwoFingerScaleUpdate,
GestureScaleEndCallback? onTwoFingerScaleEnd,
GestureDragUpdateCallback? onThreeFingerVerticalDragUpdate,
@@ -788,7 +765,6 @@ RawGestureDetector getMixinGestureDetector({
..onOneFingerPanStart = onOneFingerPanStart
..onOneFingerPanUpdate = onOneFingerPanUpdate
..onOneFingerPanEnd = onOneFingerPanEnd
..onOneFingerPanCancel = onOneFingerPanCancel
..onTwoFingerScaleUpdate = onTwoFingerScaleUpdate
..onTwoFingerScaleEnd = onTwoFingerScaleEnd
..onThreeFingerVerticalDragUpdate = onThreeFingerVerticalDragUpdate;

View File

@@ -158,8 +158,7 @@ class _RawTouchGestureDetectorRegionState
final isMoved =
await ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
if (isMoved) {
// If pan already handled 'down', don't send it again.
if (lastTapDownDetails != null && !_touchModePanStarted) {
if (lastTapDownDetails != null) {
await inputModel.tapDown(MouseButtons.left);
}
await inputModel.tapUp(MouseButtons.left);
@@ -425,14 +424,6 @@ class _RawTouchGestureDetectorRegionState
}
}
// Reset `_touchModePanStarted` if the one-finger pan gesture is cancelled
// or rejected by the gesture arena. Without this, the flag can remain
// stuck in the "started" state and cause issues such as the Magic Mouse
// double-click problem on iPad with magic mouse.
onOneFingerPanCancel() {
_touchModePanStarted = false;
}
// scale + pan event
onTwoFingerScaleStart(ScaleStartDetails d) {
_lastTapDownDetails = null;
@@ -566,7 +557,6 @@ class _RawTouchGestureDetectorRegionState
instance
..onOneFingerPanUpdate = onOneFingerPanUpdate
..onOneFingerPanEnd = onOneFingerPanEnd
..onOneFingerPanCancel = onOneFingerPanCancel
..onTwoFingerScaleStart = onTwoFingerScaleStart
..onTwoFingerScaleUpdate = onTwoFingerScaleUpdate
..onTwoFingerScaleEnd = onTwoFingerScaleEnd

View File

@@ -164,13 +164,6 @@ class _TerminalPageState extends State<TerminalPage>
autofocus: true,
textStyle: _getTerminalStyle(),
backgroundOpacity: 0.7,
// The following comment is from xterm.dart source code:
// Workaround to detect delete key for platforms and IMEs that do not
// emit a hardware delete event. Preferred on mobile platforms. [false] by
// default.
//
// Android works fine without this workaround.
deleteDetection: isIOS,
padding: _calculatePadding(heightPx),
onSecondaryTapDown: (details, offset) async {
final selection = _terminalModel.terminalController.selection;

View File

@@ -1048,14 +1048,6 @@ class InputModel {
if (isViewOnly && !showMyCursor) return;
if (e.kind != ui.PointerDeviceKind.mouse) return;
// May fix https://github.com/rustdesk/rustdesk/issues/13009
if (isIOS && e.synthesized && e.position == Offset.zero && e.buttons == 0) {
// iOS may emit a synthesized hover event at (0,0) when the mouse is disconnected.
// Ignore this event to prevent cursor jumping.
debugPrint('Ignored synthesized hover at (0,0) on iOS');
return;
}
// Only update pointer region when relative mouse mode is enabled.
// This avoids unnecessary tracking when not in relative mode.
if (_relativeMouse.enabled.value) {

View File

@@ -562,8 +562,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("id_input_tip", "Sie können eine ID, eine direkte IP oder eine Domäne mit einem Port (<domain>:<port>) eingeben.\nWenn Sie auf ein Gerät auf einem anderen Server zugreifen wollen, fügen Sie bitte die Serveradresse (<id>@<server_address>?key=<key_value>) hinzu, zum Beispiel\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nWenn Sie auf ein Gerät auf einem öffentlichen Server zugreifen wollen, geben Sie bitte \"<id>@public\" ein. Der Schlüssel wird für öffentliche Server nicht benötigt.\n\nWenn Sie bei der ersten Verbindung die Verwendung einer Relay-Verbindung erzwingen wollen, fügen Sie \"/r\" am Ende der ID hinzu, zum Beispiel \"9123456234/r\"."),
("privacy_mode_impl_mag_tip", "Modus 1"),
("privacy_mode_impl_virtual_display_tip", "Modus 2"),
("Enter privacy mode", "Datenschutzmodus aktiviert"),
("Exit privacy mode", "Datenschutzmodus beendet"),
("Enter privacy mode", "Datenschutzmodus aktivieren"),
("Exit privacy mode", "Datenschutzmodus beenden"),
("idd_not_support_under_win10_2004_tip", "Indirekter Grafiktreiber wird nicht unterstützt. Windows 10, Version 2004 oder neuer ist erforderlich."),
("input_source_1_tip", "Eingangsquelle 1"),
("input_source_2_tip", "Eingangsquelle 2"),
@@ -737,7 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("rel-mouse-exit-{}-tip", "Drücken Sie {} zum Beenden."),
("rel-mouse-permission-lost-tip", "Die Tastaturberechtigung wurde widerrufen. Der relative Mausmodus wurde deaktiviert."),
("Changelog", "Änderungsprotokoll"),
("keep-awake-during-outgoing-sessions-label", "Bildschirm während ausgehender Sitzungen aktiv halten"),
("keep-awake-during-incoming-sessions-label", "Bildschirm während eingehender Sitzungen aktiv halten"),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
].iter().cloned().collect();
}

View File

@@ -106,7 +106,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Are you sure you want to delete this empty directory?", "Voulez-vous vraiment supprimer ce répertoire vide ?"),
("Are you sure you want to delete the file of this directory?", "Voulez-vous vraiment supprimer le fichier de ce répertoire ?"),
("Do this for all conflicts", "Appliquer à tous les conflits"),
("This is irreversible!", "Cette action est irréversible !"),
("This is irreversible!", "Ceci est irréversible !"),
("Deleting", "Suppression"),
("files", "fichiers"),
("Waiting", "En attente"),
@@ -737,7 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("rel-mouse-exit-{}-tip", "Appuyez sur {} pour quitter."),
("rel-mouse-permission-lost-tip", "Lautorisation de contrôle du clavier a été révoquée. Le mode souris relative a été désactivé."),
("Changelog", "Journal des modifications"),
("keep-awake-during-outgoing-sessions-label", "Maintenir lécran allumé lors des sessions sortantes"),
("keep-awake-during-incoming-sessions-label", "Maintenir lécran allumé lors des sessions entrantes"),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
].iter().cloned().collect();
}

View File

@@ -149,7 +149,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Click to upgrade", "Kattintson ide a frissítés telepítéséhez"),
("Configure", "Beállítás"),
("config_acc", "A számítógép távoli vezérléséhez a RustDesknek hozzáférési jogokat kell adnia."),
("config_screen", "Ahhoz, hogy távolról hozzáférhessen a számítógépéhez, meg kell adnia a RustDesknek a \"Képernyőfelvétel\" jogosultságot."),
("config_screen", "Ahhoz, hogy távolról hozzáférhessen a számítógépéhez, meg kell adnia a RustDesknek a Képernyőfelvétel jogosultságot."),
("Installing ...", "Telepítés…"),
("Install", "Telepítés"),
("Installation", "Telepítés"),
@@ -276,13 +276,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Do you accept?", "Elfogadás?"),
("Open System Setting", "Rendszerbeállítások megnyitása"),
("How to get Android input permission?", "Hogyan állítható be az Androidos beviteli engedély?"),
("android_input_permission_tip1", "Ahhoz, hogy egy távoli eszköz vezérelhesse Android készülékét, engedélyeznie kell a RustDesk számára a \"Hozzáférhetőség\" szolgáltatás használatát."),
("android_input_permission_tip1", "Ahhoz, hogy egy távoli eszköz vezérelhesse Android készülékét, engedélyeznie kell a RustDesk számára a Hozzáférhetőség szolgáltatás használatát."),
("android_input_permission_tip2", "A következő rendszerbeállítások oldalon a letöltött alkalmazások menüponton belül, kapcsolja be a [RustDesk Input] szolgáltatást."),
("android_new_connection_tip", "Új kérés érkezett, mely vezérelni szeretné az eszközét"),
("android_service_will_start_tip", "A képernyőmegosztás aktiválása automatikusan elindítja a szolgáltatást, így más eszközök is vezérelhetik ezt az Android-eszközt."),
("android_stop_service_tip", "A szolgáltatás leállítása automatikusan szétkapcsol minden létező kapcsolatot."),
("android_version_audio_tip", "A jelenlegi Android verzió nem támogatja a hangrögzítést, frissítsen legalább Android 10-re, vagy egy újabb verzióra."),
("android_start_service_tip", "A képernyőmegosztó szolgáltatás elindításához koppintson a \"Kapcsolási szolgáltatás indítása\" gombra, vagy aktiválja a \"Képernyőfelvétel\" engedélyt."),
("android_start_service_tip", "A képernyőmegosztó szolgáltatás elindításához koppintson a Kapcsolási szolgáltatás indítása gombra, vagy aktiválja a Képernyőfelvétel engedélyt."),
("android_permission_may_not_change_tip", "A meglévő kapcsolatok engedélyei csak új kapcsolódás után módosulnak."),
("Account", "Fiók"),
("Overwrite", "Felülírás"),
@@ -408,15 +408,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Select local keyboard type", "Helyi billentyűzet típusának kiválasztása"),
("software_render_tip", "Ha Nvidia grafikus kártyát használ Linux alatt, és a távoli ablak a kapcsolat létrehozása után azonnal bezáródik, akkor a Nouveau nyílt forráskódú illesztőprogramra való váltás és a szoftveres leképezés alkalmazása segíthet. A szoftvert újra kell indítani."),
("Always use software rendering", "Mindig szoftveres leképezést használjon"),
("config_input", "Ahhoz, hogy a távoli asztalt a billentyűzettel vezérelhesse, a RustDesknek meg kell adnia a \"Bemenet figyelése\" jogosultságot."),
("config_microphone", "Ahhoz, hogy távolról beszélhessen, meg kell adnia a RustDesknek a \"Hangfelvétel\" jogosultságot."),
("config_input", "Ahhoz, hogy a távoli asztalt a billentyűzettel vezérelhesse, a RustDesknek meg kell adnia a Bemenet figyelése jogosultságot."),
("config_microphone", "Ahhoz, hogy távolról beszélhessen, meg kell adnia a RustDesknek a Hangfelvétel jogosultságot."),
("request_elevation_tip", "Akkor is kérhet megnövelt jogokat, ha valaki a partneroldalon van."),
("Wait", "Várjon"),
("Elevation Error", "Emelt szintű hozzáférési hiba"),
("Ask the remote user for authentication", "Hitelesítés kérése a távoli felhasználótól"),
("Choose this if the remote account is administrator", "Akkor válassza ezt, ha a távoli fiók rendszergazda"),
("Transmit the username and password of administrator", "Küldje el a rendszergazda felhasználónevét és jelszavát"),
("still_click_uac_tip", "A távoli felhasználónak továbbra is az \"Igen\" gombra kell kattintania a RustDesk UAC ablakában. Kattintson!"),
("still_click_uac_tip", "A távoli felhasználónak továbbra is az „Igen” gombra kell kattintania a RustDesk UAC ablakában. Kattintson!"),
("Request Elevation", "Emelt szintű jogok igénylése"),
("wait_accept_uac_tip", "Várjon, amíg a távoli felhasználó elfogadja az UAC párbeszédet."),
("Elevate successfully", "Emelt szintű jogok megadva"),
@@ -442,7 +442,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Voice call", "Hanghívás"),
("Text chat", "Szöveges csevegés"),
("Stop voice call", "Hanghívás leállítása"),
("relay_hint_tip", "Ha a közvetlen kapcsolat nem lehetséges, megpróbálhat kapcsolatot létesíteni egy továbbító-kiszolgálón keresztül.\nHa az első próbálkozáskor továbbító-kiszolgálón keresztüli kapcsolatot szeretne létrehozni, használhatja az \"/r\" utótagot. Az azonosítóhoz vagy a \"Mindig továbbító-kiszolgálón keresztül kapcsolódom\" opcióhoz a legutóbbi munkamenetek listájában, ha van ilyen."),
("relay_hint_tip", "Ha a közvetlen kapcsolat nem lehetséges, megpróbálhat kapcsolatot létesíteni egy továbbító-kiszolgálón keresztül.\nHa az első próbálkozáskor továbbító-kiszolgálón keresztüli kapcsolatot szeretne létrehozni, használhatja az „/r” utótagot. Az azonosítóhoz vagy a Mindig továbbító-kiszolgálón keresztül kapcsolódom opcióhoz a legutóbbi munkamenetek listájában, ha van ilyen."),
("Reconnect", "Újrakapcsolódás"),
("Codec", "Kodek"),
("Resolution", "Felbontás"),
@@ -490,7 +490,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Update", "Frissítés"),
("Enable", "Engedélyezés"),
("Disable", "Letiltás"),
("Options", "Opciók"),
("Options", "Beállítások"),
("resolution_original_tip", "Eredeti felbontás"),
("resolution_fit_local_tip", "Helyi felbontás beállítása"),
("resolution_custom_tip", "Testre szabható felbontás"),
@@ -559,7 +559,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Plug out all", "Kapcsolja ki az összeset"),
("True color (4:4:4)", "Valódi szín (4:4:4)"),
("Enable blocking user input", "Engedélyezze a felhasználói bevitel blokkolását"),
("id_input_tip", "Megadhat egy azonosítót, egy közvetlen IP-címet vagy egy tartományt egy porttal (<domain>:<port>).\nHa egy másik kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a kiszolgáló címét (<id>@<kiszolgáló_cím>?key=<key_value>), például\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHa egy nyilvános kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a \"<id>@public\" lehetőséget. A kulcsra nincs szükség nyilvános kiszolgálók esetén.\n\nHa az első kapcsolathoz továbbító-kiszolgálón keresztüli kapcsolatot akar kényszeríteni, adja hozzá az \"/r\" az azonosítót a végén, például \"9123456234/r\"."),
("id_input_tip", "Megadhat egy azonosítót, egy közvetlen IP-címet vagy egy tartományt egy porttal (<domain>:<port>).\nHa egy másik kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a kiszolgáló címét (<id>@<kiszolgáló_cím>?key=<key_value>), például\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHa egy nyilvános kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a <id>@public lehetőséget. A kulcsra nincs szükség nyilvános kiszolgálók esetén.\n\nHa az első kapcsolathoz továbbító-kiszolgálón keresztüli kapcsolatot akar kényszeríteni, adja hozzá az „/r” az azonosítót a végén, például 9123456234/r."),
("privacy_mode_impl_mag_tip", "1. mód"),
("privacy_mode_impl_virtual_display_tip", "2. mód"),
("Enter privacy mode", "Lépjen be az adatvédelmi módba"),
@@ -622,7 +622,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Power", "Főkapcsoló"),
("Telegram bot", "Telegram bot"),
("enable-bot-tip", "Ha aktiválja ezt a funkciót, akkor a 2FA-kódot a botjától kaphatja meg. Kapcsolati értesítésként is használható."),
("enable-bot-desc", "1. Nyisson csevegést @BotFather.\n2. Küldje el a \"/newbot\" parancsot. Miután ezt a lépést elvégezte, kap egy tokent.\n3. Indítson csevegést az újonnan létrehozott botjával. Küldjön egy olyan üzenetet, amely egy perjel (\"/\") kezdetű, pl. \"/hello\" az aktiváláshoz.\n"),
("enable-bot-desc", "1. Nyisson csevegést @BotFather.\n2. Küldje el a /newbot parancsot. Miután ezt a lépést elvégezte, kap egy tokent.\n3. Indítson csevegést az újonnan létrehozott botjával. Küldjön egy olyan üzenetet, amely egy perjel („/”) kezdetű, pl. „/hello” az aktiváláshoz.\n"),
("cancel-2fa-confirm-tip", "Biztosan vissza akarja vonni a 2FA-hitelesítést?"),
("cancel-bot-confirm-tip", "Biztosan le akarja mondani a Telegram botot?"),
("About RustDesk", "A RustDesk névjegye"),
@@ -643,7 +643,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("one-way-file-transfer-tip", "Az egyirányú fájlátvitel engedélyezve van a vezérelt oldalon."),
("Authentication Required", "Hitelesítés szükséges"),
("Authenticate", "Hitelesítés"),
("web_id_input_tip", "Azonos kiszolgálón lévő azonosítót adhat meg, a közvetlen IP elérés nem támogatott a webkliensben.\nHa egy másik kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a kiszolgáló címét (<id>@<kiszolgáló_cím>?key=<key_value>), például\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHa egy nyilvános kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a \"<id>@public\" betűt. A kulcsra nincs szükség a nyilvános kiszolgálók esetében."),
("web_id_input_tip", "Azonos kiszolgálón lévő azonosítót adhat meg, a közvetlen IP elérés nem támogatott a webkliensben.\nHa egy másik kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a kiszolgáló címét (<id>@<kiszolgáló_cím>?key=<key_value>), például\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHa egy nyilvános kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a <id>@public betűt. A kulcsra nincs szükség a nyilvános kiszolgálók esetében."),
("Download", "Letöltés"),
("Upload folder", "Mappa feltöltése"),
("Upload files", "Fájlok feltöltése"),
@@ -682,9 +682,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Downloading {}", "{} letöltése"),
("{} Update", "{} frissítés"),
("{}-to-update-tip", "A(z) {} bezárása és az új verzió telepítése."),
("download-new-version-failed-tip", "Ha a letöltés sikertelen, akkor vagy újrapróbálkozhat, vagy a \"Letöltés\" gombra kattintva letöltheti a kiadási oldalról, és manuálisan frissíthet."),
("download-new-version-failed-tip", "Ha a letöltés sikertelen, akkor vagy újrapróbálkozhat, vagy a Letöltés gombra kattintva letöltheti a kiadási oldalról, és manuálisan frissíthet."),
("Auto update", "Automatikus frissítés"),
("update-failed-check-msi-tip", "A telepítési módszer felismerése nem sikerült. Kattintson a \"Letöltés\" gombra, hogy letöltse a kiadási oldalról, és manuálisan frissítse."),
("update-failed-check-msi-tip", "A telepítési módszer felismerése nem sikerült. Kattintson a Letöltés gombra, hogy letöltse a kiadási oldalról, és manuálisan frissítse."),
("websocket_tip", "WebSocket használatakor csak a relé-kapcsolatok támogatottak."),
("Use WebSocket", "WebSocket használata"),
("Trackpad speed", "Érintőpad sebessége"),
@@ -730,14 +730,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("input note here", "Megjegyzés beírása"),
("note-at-conn-end-tip", "Kérjen megjegyzést a kapcsolat végén"),
("Show terminal extra keys", "További terminálgombok megjelenítése"),
("Relative mouse mode", "Relatív egér mód"),
("rel-mouse-not-supported-peer-tip", "A kapcsolódott partner nem támogatja a relatív egér módot."),
("rel-mouse-not-ready-tip", "A relatív egér mód még nem elérhető. Próbálja meg újra."),
("rel-mouse-lock-failed-tip", "Nem sikerült zárolni a kurzort. A relatív egér mód le lett tiltva."),
("Relative mouse mode", "Relatív egérmód"),
("rel-mouse-not-supported-peer-tip", "A kapcsolódott partner nem támogatja a relatív egérmódot."),
("rel-mouse-not-ready-tip", "A relatív egérmód még nem elérhető. Próbálja meg újra."),
("rel-mouse-lock-failed-tip", "Nem sikerült zárolni a kurzort. A relatív egérmód le lett tiltva."),
("rel-mouse-exit-{}-tip", "A kilépéshez nyomja meg a(z) {} gombot."),
("rel-mouse-permission-lost-tip", "A billentyűzet-hozzáférés vissza lett vonva. A relatív egér mód le lett tilva."),
("rel-mouse-permission-lost-tip", "A billentyűzet-hozzáférés vissza lett vonva. A relatív egérmód le lett tilva."),
("Changelog", "Változáslista"),
("keep-awake-during-outgoing-sessions-label", "Képernyő aktív állapotban tartása a kimenő munkamenetek során"),
("keep-awake-during-incoming-sessions-label", "Képernyő aktív állapotban tartása a bejövő munkamenetek során"),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
].iter().cloned().collect();
}

View File

@@ -737,7 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("rel-mouse-exit-{}-tip", "Premi {} per uscire."),
("rel-mouse-permission-lost-tip", "È stata revocato l'accesso alla tastiera. La modalità mouse relativa è stata disabilitata."),
("Changelog", "Novità programma"),
("keep-awake-during-outgoing-sessions-label", "Mantieni lo schermo attivo durante le sessioni in uscita"),
("keep-awake-during-incoming-sessions-label", "Mantieni lo schermo attivo durante le sessioni in ingresso"),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
].iter().cloned().collect();
}

View File

@@ -737,7 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("rel-mouse-exit-{}-tip", "종료하려면 {}을(를) 누르세요."),
("rel-mouse-permission-lost-tip", "키보드 권한이 취소되었습니다. 상대 마우스 모드가 비활성화되었습니다."),
("Changelog", "변경 기록"),
("keep-awake-during-outgoing-sessions-label", "발신 세션 중 화면 켜짐 유지"),
("keep-awake-during-incoming-sessions-label", "수신 세션 중 화면 켜짐 유지"),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
].iter().cloned().collect();
}

View File

@@ -737,7 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("rel-mouse-exit-{}-tip", "Druk op {} om af te sluiten."),
("rel-mouse-permission-lost-tip", "De toetsenbordcontrole is uitgeschakeld. De relatieve muismodus is uitgeschakeld."),
("Changelog", "Wijzigingenlogboek"),
("keep-awake-during-outgoing-sessions-label", "Houd het scherm open tijdens de uitgaande sessies."),
("keep-awake-during-incoming-sessions-label", "Houd het scherm open tijdens de inkomende sessies."),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
].iter().cloned().collect();
}

View File

@@ -737,7 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("rel-mouse-exit-{}-tip", "Aby wyłączyć tryb przechwytywania myszy, naciśnij {}"),
("rel-mouse-permission-lost-tip", "Utracono uprawnienia do trybu przechwytywania myszy"),
("Changelog", "Dziennik zmian"),
("keep-awake-during-outgoing-sessions-label", "Utrzymuj urządzenie w stanie aktywnym podczas sesji wychodzących"),
("keep-awake-during-incoming-sessions-label", "Utrzymuj urządzenie w stanie aktywnym podczas sesji przychodzących"),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
].iter().cloned().collect();
}

View File

@@ -672,72 +672,72 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("remote-printing-disallowed-text-tip", "As configurações do dispositivo controlado não permitem impressão remota."),
("save-settings-tip", "Salvar configurações"),
("dont-show-again-tip", "Não mostrar novamente"),
("Take screenshot", "Capturar de tela"),
("Taking screenshot", "Capturando tela"),
("Take screenshot", ""),
("Taking screenshot", ""),
("screenshot-merged-screen-not-supported-tip", ""),
("screenshot-action-tip", ""),
("Save as", "Salvar como"),
("Copy to clipboard", "Copiar para área de transferência"),
("Enable remote printer", "Habilitar impressora remota"),
("Save as", ""),
("Copy to clipboard", ""),
("Enable remote printer", ""),
("Downloading {}", ""),
("{} Update", ""),
("{}-to-update-tip", ""),
("download-new-version-failed-tip", "Falha no download. Você pode tentar novamente ou clicar no botão \"Download\" para baixar da página releases e atualizar manualmente."),
("Auto update", "Atualização automática"),
("update-failed-check-msi-tip", "Falha na verificação do método de instalação. Clique no botão \"Download\" para baixar da página releases e atualizar manualmente."),
("websocket_tip", "Usando WebSocket, apenas conexões via relay são suportadas."),
("Use WebSocket", "Usar WebSocket"),
("Trackpad speed", "Velocidade do trackpad"),
("download-new-version-failed-tip", ""),
("Auto update", ""),
("update-failed-check-msi-tip", ""),
("websocket_tip", ""),
("Use WebSocket", ""),
("Trackpad speed", ""),
("Default trackpad speed", ""),
("Numeric one-time password", "Senha numérica de uso único"),
("Enable IPv6 P2P connection", "Habilitar conexão IPv6 P2P"),
("Enable UDP hole punching", "Habilitar UDP hole punching"),
("Numeric one-time password", ""),
("Enable IPv6 P2P connection", ""),
("Enable UDP hole punching", ""),
("View camera", "Visualizar câmera"),
("Enable camera", "Ativar câmera"),
("No cameras", "Sem câmeras"),
("view_camera_unsupported_tip", "O dispositivo remoto não suporta visualização da câmera."),
("Terminal", "Terminal"),
("Enable terminal", "Habilitar Terminal"),
("New tab", "Nova aba"),
("Keep terminal sessions on disconnect", "Manter sessões de terminal ao desconectar"),
("Terminal (Run as administrator)", "Terminal (Executar como administrador)"),
("terminal-admin-login-tip", "Insira o nome do usuário e senha de administrador do dispositivo controlado."),
("Failed to get user token.", "Falha ao obter token do usuário."),
("Incorrect username or password.", "Usuário ou senha incorretos"),
("The user is not an administrator.", "O usuário não é administrador"),
("Failed to check if the user is an administrator.", "Falha ao verificar se o usuário é administrador"),
("Supported only in the installed version.", "Funciona somente na versão instalada"),
("elevation_username_tip", "Insira o nome do usuário ou domínio\\usuário"),
("Preparing for installation ...", "Preparando para instalação ..."),
("Show my cursor", "Mostrar meu cursor"),
("Terminal", ""),
("Enable terminal", ""),
("New tab", ""),
("Keep terminal sessions on disconnect", ""),
("Terminal (Run as administrator)", ""),
("terminal-admin-login-tip", ""),
("Failed to get user token.", ""),
("Incorrect username or password.", ""),
("The user is not an administrator.", ""),
("Failed to check if the user is an administrator.", ""),
("Supported only in the installed version.", ""),
("elevation_username_tip", ""),
("Preparing for installation ...", ""),
("Show my cursor", ""),
("Scale custom", "Escala personalizada"),
("Custom scale slider", "Controle deslizante de escala personalizada"),
("Decrease", "Diminuir"),
("Increase", "Aumentar"),
("Show virtual mouse", "Mostrar mouse virtual"),
("Virtual mouse size", "Tamanho do mouse virtual"),
("Small", "Pequeno"),
("Large", "Grande"),
("Show virtual mouse", ""),
("Virtual mouse size", ""),
("Small", ""),
("Large", ""),
("Show virtual joystick", ""),
("Edit note", "Editar nota"),
("Alias", "Apelido"),
("ScrollEdge", "Rolagem nas bordas"),
("Edit note", ""),
("Alias", ""),
("ScrollEdge", ""),
("Allow insecure TLS fallback", ""),
("allow-insecure-tls-fallback-tip", "Por padrão, o RustDesk verifica o certificado do servidor para protocolos que usam TLS.\nCom esta opção habilitada, o RustDesk ignorará a verificação e prosseguirá em caso de falha."),
("Disable UDP", "Desabilitar UDP"),
("disable-udp-tip", "Controla se deve usar somente TCP.\nCom esta opção habilitada, o RustDesk não usará mais UDP 21116, TCP 21116 será usado no lugar."),
("server-oss-not-support-tip", "NOTA: O servidor RustDesk OSS não inclui este recurso."),
("input note here", "Insira uma nota aqui"),
("note-at-conn-end-tip", "Solicitar nota ao final da conexão"),
("Show terminal extra keys", "Mostrar teclas extras do terminal"),
("Relative mouse mode", "Modo de Mouse Relativo"),
("rel-mouse-not-supported-peer-tip", "O Modo de Mouse Relativo não é suportado pelo parceiro conectado."),
("rel-mouse-not-ready-tip", "O Modo de Mouse Relativo ainda não está pronto. Por favor, tente novamente."),
("rel-mouse-lock-failed-tip", "Falha ao bloquear o cursor. O Modo de Mouse Relativo foi desabilitado."),
("rel-mouse-exit-{}-tip", "Pressione {} para sair."),
("rel-mouse-permission-lost-tip", "Permissão de teclado revogada. O Modo Mouse Relativo foi desabilitado."),
("Changelog", "Registro de alterações"),
("keep-awake-during-outgoing-sessions-label", "Manter tela ativa durante sessões de saída"),
("keep-awake-during-incoming-sessions-label", "Manter tela ativa durante sessões de entrada"),
("allow-insecure-tls-fallback-tip", ""),
("Disable UDP", ""),
("disable-udp-tip", ""),
("server-oss-not-support-tip", ""),
("input note here", ""),
("note-at-conn-end-tip", ""),
("Show terminal extra keys", ""),
("Relative mouse mode", ""),
("rel-mouse-not-supported-peer-tip", ""),
("rel-mouse-not-ready-tip", ""),
("rel-mouse-lock-failed-tip", ""),
("rel-mouse-exit-{}-tip", ""),
("rel-mouse-permission-lost-tip", ""),
("Changelog", ""),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
].iter().cloned().collect();
}

View File

@@ -4,13 +4,6 @@
#include <Security/Authorization.h>
#include <Security/AuthorizationTags.h>
#include <CoreGraphics/CoreGraphics.h>
#include <vector>
#include <map>
#include <set>
#include <mutex>
#include <string>
extern "C" bool CanUseNewApiForScreenCaptureCheck() {
#ifdef NO_InputMonitoringAuthStatus
return false;
@@ -299,611 +292,3 @@ extern "C" bool MacSetMode(CGDirectDisplayID display, uint32_t width, uint32_t h
CFRelease(allModes);
return ret;
}
static CFMachPortRef g_eventTap = NULL;
static CFRunLoopSourceRef g_runLoopSource = NULL;
static std::mutex g_privacyModeMutex;
static bool g_privacyModeActive = false;
// Flag to request asynchronous shutdown of privacy mode.
// This is set by DisplayReconfigurationCallback when an error occurs, instead of calling
// TurnOffPrivacyModeInternal() directly from within the callback. This avoids potential
// issues with unregistering a callback from within itself, which is not explicitly
// guaranteed to be safe by Apple documentation.
static bool g_privacyModeShutdownRequested = false;
// Timestamp of the last display reconfiguration event (in milliseconds).
// Used for debouncing rapid successive changes (e.g., multiple resolution changes).
static uint64_t g_lastReconfigTimestamp = 0;
// Flag indicating whether a delayed blackout reapplication is already scheduled.
// Prevents multiple concurrent delayed tasks from being created.
static bool g_blackoutReapplicationScheduled = false;
// Use CFStringRef (UUID) as key instead of CGDirectDisplayID for stability across reconnections
// CGDirectDisplayID can change when displays are reconnected, but UUID remains stable
static std::map<std::string, std::vector<CGGammaValue>> g_originalGammas;
// The event source user data value used by enigo library for injected events.
// This allows us to distinguish remote input (which should be allowed) from local physical input.
// See: libs/enigo/src/macos/macos_impl.rs - ENIGO_INPUT_EXTRA_VALUE
static const int64_t ENIGO_INPUT_EXTRA_VALUE = 100;
// Duration in milliseconds to monitor and enforce blackout after display reconfiguration.
// macOS may restore default gamma (via ColorSync) at unpredictable times after display changes,
// so we need to actively monitor and reapply blackout during this period.
static const int64_t DISPLAY_RECONFIG_MONITOR_DURATION_MS = 5000;
// Interval in milliseconds between gamma checks during the monitoring period.
static const int64_t GAMMA_CHECK_INTERVAL_MS = 200;
// Helper function to get UUID string from DisplayID
static std::string GetDisplayUUID(CGDirectDisplayID displayId) {
CFUUIDRef uuid = CGDisplayCreateUUIDFromDisplayID(displayId);
if (uuid == NULL) {
return "";
}
CFStringRef uuidStr = CFUUIDCreateString(kCFAllocatorDefault, uuid);
CFRelease(uuid);
if (uuidStr == NULL) {
return "";
}
char buffer[128];
if (CFStringGetCString(uuidStr, buffer, sizeof(buffer), kCFStringEncodingUTF8)) {
CFRelease(uuidStr);
return std::string(buffer);
}
CFRelease(uuidStr);
return "";
}
// Helper function to find DisplayID by UUID from current online displays
static CGDirectDisplayID FindDisplayIdByUUID(const std::string& targetUuid) {
uint32_t count = 0;
CGGetOnlineDisplayList(0, NULL, &count);
if (count == 0) return kCGNullDirectDisplay;
std::vector<CGDirectDisplayID> displays(count);
CGGetOnlineDisplayList(count, displays.data(), &count);
for (uint32_t i = 0; i < count; i++) {
std::string uuid = GetDisplayUUID(displays[i]);
if (uuid == targetUuid) {
return displays[i];
}
}
return kCGNullDirectDisplay;
}
// Helper function to restore gamma values for all displays in g_originalGammas.
// Returns true if all displays were restored successfully, false if any failed.
// Note: This function does NOT clear g_originalGammas - caller should do that if needed.
static bool RestoreAllGammas() {
bool allSuccess = true;
for (auto const& [uuid, gamma] : g_originalGammas) {
CGDirectDisplayID d = FindDisplayIdByUUID(uuid);
if (d == kCGNullDirectDisplay) {
NSLog(@"Display with UUID %s no longer online, skipping gamma restore", uuid.c_str());
continue;
}
uint32_t sampleCount = gamma.size() / 3;
if (sampleCount > 0) {
const CGGammaValue* red = gamma.data();
const CGGammaValue* green = red + sampleCount;
const CGGammaValue* blue = green + sampleCount;
CGError error = CGSetDisplayTransferByTable(d, sampleCount, red, green, blue);
if (error != kCGErrorSuccess) {
NSLog(@"Failed to restore gamma for display (ID: %u, UUID: %s, error: %d)", (unsigned)d, uuid.c_str(), error);
allSuccess = false;
}
}
}
return allSuccess;
}
// Helper function to apply blackout to a single display
static bool ApplyBlackoutToDisplay(CGDirectDisplayID display) {
uint32_t capacity = CGDisplayGammaTableCapacity(display);
if (capacity > 0) {
std::vector<CGGammaValue> zeros(capacity, 0.0f);
CGError error = CGSetDisplayTransferByTable(display, capacity, zeros.data(), zeros.data(), zeros.data());
if (error != kCGErrorSuccess) {
NSLog(@"ApplyBlackoutToDisplay: Failed to set gamma for display %u (error %d)", (unsigned)display, error);
return false;
}
return true;
}
NSLog(@"ApplyBlackoutToDisplay: Display %u has zero gamma table capacity, blackout not supported", (unsigned)display);
return false;
}
// Forward declaration - defined later in the file
// Must be called while holding g_privacyModeMutex
static bool TurnOffPrivacyModeInternal();
// Helper function to schedule asynchronous shutdown of privacy mode.
// This is called from DisplayReconfigurationCallback when an error occurs,
// instead of calling TurnOffPrivacyModeInternal() directly. This avoids
// potential issues with unregistering a callback from within itself.
// Note: This function should be called while holding g_privacyModeMutex.
static void ScheduleAsyncPrivacyModeShutdown(const char* reason) {
if (g_privacyModeShutdownRequested) {
// Already requested, no need to schedule again
return;
}
g_privacyModeShutdownRequested = true;
NSLog(@"Privacy mode shutdown requested: %s", reason);
// Schedule the actual shutdown on the main queue asynchronously
// This ensures we're outside the callback when we unregister it
dispatch_async(dispatch_get_main_queue(), ^{
std::lock_guard<std::mutex> lock(g_privacyModeMutex);
if (g_privacyModeShutdownRequested && g_privacyModeActive) {
NSLog(@"Executing deferred privacy mode shutdown");
TurnOffPrivacyModeInternal();
}
g_privacyModeShutdownRequested = false;
});
}
// Helper function to apply blackout to all online displays.
// Must be called while holding g_privacyModeMutex.
static void ApplyBlackoutToAllDisplays() {
uint32_t onlineCount = 0;
CGGetOnlineDisplayList(0, NULL, &onlineCount);
std::vector<CGDirectDisplayID> onlineDisplays(onlineCount);
CGGetOnlineDisplayList(onlineCount, onlineDisplays.data(), &onlineCount);
for (uint32_t i = 0; i < onlineCount; i++) {
ApplyBlackoutToDisplay(onlineDisplays[i]);
}
}
// Helper function to get current timestamp in milliseconds
static uint64_t GetCurrentTimestampMs() {
return (uint64_t)(CFAbsoluteTimeGetCurrent() * 1000.0);
}
// Helper function to check if a display's gamma is currently blacked out (all zeros).
// Returns true if gamma appears to be blacked out, false otherwise.
static bool IsDisplayBlackedOut(CGDirectDisplayID display) {
uint32_t capacity = CGDisplayGammaTableCapacity(display);
if (capacity == 0) {
return true; // Can't check, assume it's fine
}
std::vector<CGGammaValue> red(capacity), green(capacity), blue(capacity);
uint32_t sampleCount = 0;
if (CGGetDisplayTransferByTable(display, capacity, red.data(), green.data(), blue.data(), &sampleCount) != kCGErrorSuccess) {
return true; // Can't read, assume it's fine
}
// Check if all values are zero (or very close to zero)
for (uint32_t i = 0; i < sampleCount; i++) {
if (red[i] > 0.01f || green[i] > 0.01f || blue[i] > 0.01f) {
return false; // Not blacked out
}
}
return true;
}
// Internal function that monitors and enforces blackout for a period after display reconfiguration.
// This function checks gamma values periodically and reapplies blackout if needed.
// Must NOT be called while holding g_privacyModeMutex (it acquires the lock internally).
static void RunBlackoutMonitor() {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(GAMMA_CHECK_INTERVAL_MS * NSEC_PER_MSEC)), dispatch_get_main_queue(), ^{
std::lock_guard<std::mutex> lock(g_privacyModeMutex);
if (!g_privacyModeActive) {
g_blackoutReapplicationScheduled = false;
return;
}
uint64_t now = GetCurrentTimestampMs();
// Calculate effective end time based on the last reconfig event
uint64_t effectiveEndTime = g_lastReconfigTimestamp + DISPLAY_RECONFIG_MONITOR_DURATION_MS;
// Check all displays and reapply blackout if any has been restored
uint32_t onlineCount = 0;
CGGetOnlineDisplayList(0, NULL, &onlineCount);
std::vector<CGDirectDisplayID> onlineDisplays(onlineCount);
CGGetOnlineDisplayList(onlineCount, onlineDisplays.data(), &onlineCount);
bool needsReapply = false;
for (uint32_t i = 0; i < onlineCount; i++) {
if (!IsDisplayBlackedOut(onlineDisplays[i])) {
needsReapply = true;
break;
}
}
if (needsReapply) {
NSLog(@"Gamma was restored by system, reapplying blackout");
ApplyBlackoutToAllDisplays();
}
// Continue monitoring if we haven't reached the end time
if (now < effectiveEndTime) {
RunBlackoutMonitor();
} else {
NSLog(@"Blackout monitoring period ended");
g_blackoutReapplicationScheduled = false;
}
});
}
// Helper function to start monitoring and enforcing blackout after display reconfiguration.
// This is used after display reconfiguration events because macOS may restore
// default gamma (via ColorSync) at unpredictable times after display changes.
// Note: This function should be called while holding g_privacyModeMutex.
static void ScheduleDelayedBlackoutReapplication(const char* reason) {
// Update timestamp to current time
g_lastReconfigTimestamp = GetCurrentTimestampMs();
NSLog(@"Starting blackout monitor: %s", reason);
// Only schedule if not already scheduled
if (!g_blackoutReapplicationScheduled) {
g_blackoutReapplicationScheduled = true;
RunBlackoutMonitor();
}
// If already scheduled, the running monitor will see the updated timestamp
// and extend its monitoring period
}
// Display reconfiguration callback to handle display connect/disconnect events
//
// IMPORTANT: When errors occur in this callback, we use ScheduleAsyncPrivacyModeShutdown()
// instead of calling TurnOffPrivacyModeInternal() directly. This is because:
// 1. TurnOffPrivacyModeInternal() calls CGDisplayRemoveReconfigurationCallback to unregister
// this callback, and unregistering a callback from within itself is not explicitly
// guaranteed to be safe by Apple documentation.
// 2. Using async dispatch ensures we're completely outside the callback context when
// performing the cleanup, avoiding any potential undefined behavior.
static void DisplayReconfigurationCallback(CGDirectDisplayID display, CGDisplayChangeSummaryFlags flags, void *userInfo) {
(void)userInfo;
// Note: We need to handle the callback carefully because:
// 1. macOS may call this callback multiple times during display reconfiguration
// 2. The system may restore ColorSync settings after our gamma change
// 3. We should not hold the lock for too long in the callback
// Skip begin configuration flag - wait for the actual change
if (flags & kCGDisplayBeginConfigurationFlag) {
return;
}
std::lock_guard<std::mutex> lock(g_privacyModeMutex);
if (!g_privacyModeActive) {
return;
}
if (flags & kCGDisplayAddFlag) {
// A display was added - apply blackout to it
NSLog(@"Display %u added during privacy mode, applying blackout", (unsigned)display);
std::string uuid = GetDisplayUUID(display);
if (uuid.empty()) {
NSLog(@"Failed to get UUID for newly added display %u, exiting privacy mode", (unsigned)display);
ScheduleAsyncPrivacyModeShutdown("Failed to get UUID for newly added display");
return;
}
// Save original gamma if not already saved for this UUID
if (g_originalGammas.find(uuid) == g_originalGammas.end()) {
uint32_t capacity = CGDisplayGammaTableCapacity(display);
if (capacity > 0) {
std::vector<CGGammaValue> red(capacity), green(capacity), blue(capacity);
uint32_t sampleCount = 0;
if (CGGetDisplayTransferByTable(display, capacity, red.data(), green.data(), blue.data(), &sampleCount) == kCGErrorSuccess) {
std::vector<CGGammaValue> all;
all.insert(all.end(), red.begin(), red.begin() + sampleCount);
all.insert(all.end(), green.begin(), green.begin() + sampleCount);
all.insert(all.end(), blue.begin(), blue.begin() + sampleCount);
g_originalGammas[uuid] = all;
} else {
NSLog(@"DisplayReconfigurationCallback: Failed to get gamma table for display %u (UUID: %s), exiting privacy mode", (unsigned)display, uuid.c_str());
ScheduleAsyncPrivacyModeShutdown("Failed to get gamma table for newly added display");
return;
}
} else {
NSLog(@"DisplayReconfigurationCallback: Display %u (UUID: %s) has zero gamma table capacity, exiting privacy mode", (unsigned)display, uuid.c_str());
ScheduleAsyncPrivacyModeShutdown("Newly added display has zero gamma table capacity");
return;
}
}
// Apply blackout to the new display immediately
if (!ApplyBlackoutToDisplay(display)) {
NSLog(@"DisplayReconfigurationCallback: Failed to blackout display %u (UUID: %s), exiting privacy mode", (unsigned)display, uuid.c_str());
ScheduleAsyncPrivacyModeShutdown("Failed to blackout newly added display");
return;
}
// Schedule a delayed re-application to handle ColorSync restoration
// macOS may restore default gamma for ALL displays after a new display is added,
// so we need to reapply blackout to all online displays, not just the new one
ScheduleDelayedBlackoutReapplication("after new display added");
} else if (flags & kCGDisplayRemoveFlag) {
// A display was removed - update our mapping and reapply blackout to remaining displays
NSLog(@"Display %u removed during privacy mode", (unsigned)display);
std::string uuid = GetDisplayUUID(display);
(void)uuid; // UUID retrieved for potential future use or logging
// When a display is removed, macOS may reconfigure other displays and restore their gamma.
// Schedule a delayed re-application of blackout to all remaining online displays.
ScheduleDelayedBlackoutReapplication("after display removal");
} else if (flags & kCGDisplaySetModeFlag) {
// Display mode changed (resolution change, ColorSync/Night Shift interference, etc.)
// macOS resets gamma to default when display mode changes, so we need to reapply blackout.
// Schedule a delayed re-application because ColorSync restoration happens asynchronously.
NSLog(@"Display %u mode changed during privacy mode, reapplying blackout", (unsigned)display);
ScheduleDelayedBlackoutReapplication("after display mode change");
}
}
CGEventRef MyEventTapCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) {
(void)proxy;
(void)refcon;
// Handle EventTap being disabled by system timeout
if (type == kCGEventTapDisabledByTimeout) {
NSLog(@"EventTap was disabled by timeout, re-enabling");
if (g_eventTap) {
CGEventTapEnable(g_eventTap, true);
}
return event;
}
// Handle EventTap being disabled by user input
if (type == kCGEventTapDisabledByUserInput) {
NSLog(@"EventTap was disabled by user input, re-enabling");
if (g_eventTap) {
CGEventTapEnable(g_eventTap, true);
}
return event;
}
// Allow events explicitly injected by enigo (remote input), identified via custom user data.
int64_t userData = CGEventGetIntegerValueField(event, kCGEventSourceUserData);
if (userData == ENIGO_INPUT_EXTRA_VALUE) {
return event;
}
// Block local physical HID input.
if (CGEventGetIntegerValueField(event, kCGEventSourceStateID) == kCGEventSourceStateHIDSystemState) {
return NULL;
}
return event;
}
// Helper function to set up EventTap on the main thread
// Returns true if EventTap was successfully created and enabled
static bool SetupEventTapOnMainThread() {
__block bool success = false;
void (^setupBlock)(void) = ^{
if (g_eventTap) {
// Already set up
success = true;
return;
}
// Note: kCGEventTapDisabledByTimeout and kCGEventTapDisabledByUserInput are special
// notification types (0xFFFFFFFE and 0xFFFFFFFF) that are delivered via the callback's
// type parameter, not through the event mask. They should NOT be included in eventMask
// as bit-shifting by these values causes undefined behavior.
CGEventMask eventMask = (1 << kCGEventKeyDown) | (1 << kCGEventKeyUp) |
(1 << kCGEventLeftMouseDown) | (1 << kCGEventLeftMouseUp) |
(1 << kCGEventRightMouseDown) | (1 << kCGEventRightMouseUp) |
(1 << kCGEventOtherMouseDown) | (1 << kCGEventOtherMouseUp) |
(1 << kCGEventLeftMouseDragged) | (1 << kCGEventRightMouseDragged) |
(1 << kCGEventOtherMouseDragged) |
(1 << kCGEventMouseMoved) | (1 << kCGEventScrollWheel);
g_eventTap = CGEventTapCreate(kCGHIDEventTap, kCGHeadInsertEventTap, kCGEventTapOptionDefault,
eventMask, MyEventTapCallback, NULL);
if (g_eventTap) {
g_runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, g_eventTap, 0);
CFRunLoopAddSource(CFRunLoopGetMain(), g_runLoopSource, kCFRunLoopCommonModes);
CGEventTapEnable(g_eventTap, true);
success = true;
} else {
NSLog(@"MacSetPrivacyMode: Failed to create CGEventTap; input blocking not enabled.");
success = false;
}
};
// Execute on main thread to ensure CFRunLoop operations are safe.
// Use dispatch_sync if not on main thread, otherwise execute directly to avoid deadlock.
//
// IMPORTANT: Potential deadlock consideration:
// Using dispatch_sync while holding g_privacyModeMutex could deadlock if the main thread
// tries to acquire g_privacyModeMutex. Currently this is safe because:
// 1. MacSetPrivacyMode (which holds the mutex) is only called from background threads
// 2. The main thread never directly calls MacSetPrivacyMode
// If this assumption changes in the future, consider releasing the mutex before dispatch_sync
// or restructuring the locking strategy.
if ([NSThread isMainThread]) {
setupBlock();
} else {
dispatch_sync(dispatch_get_main_queue(), setupBlock);
}
return success;
}
// Helper function to tear down EventTap on the main thread
static void TeardownEventTapOnMainThread() {
void (^teardownBlock)(void) = ^{
if (g_eventTap) {
CGEventTapEnable(g_eventTap, false);
CFRunLoopRemoveSource(CFRunLoopGetMain(), g_runLoopSource, kCFRunLoopCommonModes);
CFRelease(g_runLoopSource);
CFRelease(g_eventTap);
g_eventTap = NULL;
g_runLoopSource = NULL;
}
};
// Execute on main thread to ensure CFRunLoop operations are safe.
//
// NOTE: We use dispatch_sync here instead of dispatch_async because:
// 1. TurnOffPrivacyModeInternal() expects EventTap to be fully torn down before
// proceeding with gamma restoration - using async would cause race conditions.
// 2. The caller (MacSetPrivacyMode) needs deterministic cleanup order.
//
// IMPORTANT: Potential deadlock consideration (same as SetupEventTapOnMainThread):
// Using dispatch_sync while holding g_privacyModeMutex could deadlock if the main thread
// tries to acquire g_privacyModeMutex. Currently this is safe because:
// 1. MacSetPrivacyMode (which holds the mutex) is only called from background threads
// 2. The main thread never directly calls MacSetPrivacyMode
// If this assumption changes in the future, consider releasing the mutex before dispatch_sync
// or restructuring the locking strategy.
if ([NSThread isMainThread]) {
teardownBlock();
} else {
dispatch_sync(dispatch_get_main_queue(), teardownBlock);
}
}
// Internal function to turn off privacy mode without acquiring the mutex
// Must be called while holding g_privacyModeMutex
static bool TurnOffPrivacyModeInternal() {
if (!g_privacyModeActive) {
return true;
}
// 1. Unregister display reconfiguration callback
CGDisplayRemoveReconfigurationCallback(DisplayReconfigurationCallback, NULL);
// 2. Input - restore (tear down EventTap on main thread)
TeardownEventTapOnMainThread();
// 3. Gamma - restore using UUID to find current DisplayID
bool restoreSuccess = RestoreAllGammas();
// 4. Fallback: Always call CGDisplayRestoreColorSyncSettings as a safety net
// This ensures displays return to normal even if our restoration failed or
// if the system (ColorSync/Night Shift) modified gamma during privacy mode
CGDisplayRestoreColorSyncSettings();
// Clean up
g_originalGammas.clear();
g_privacyModeActive = false;
g_privacyModeShutdownRequested = false;
g_lastReconfigTimestamp = 0;
g_blackoutReapplicationScheduled = false;
return restoreSuccess;
}
extern "C" bool MacSetPrivacyMode(bool on) {
std::lock_guard<std::mutex> lock(g_privacyModeMutex);
if (on) {
// Already in privacy mode
if (g_privacyModeActive) {
return true;
}
// 1. Input Blocking - set up EventTap on main thread
if (!SetupEventTapOnMainThread()) {
return false;
}
// 2. Register display reconfiguration callback to handle hot-plug events
CGDisplayRegisterReconfigurationCallback(DisplayReconfigurationCallback, NULL);
// 3. Gamma Blackout
uint32_t count = 0;
CGGetOnlineDisplayList(0, NULL, &count);
std::vector<CGDirectDisplayID> displays(count);
CGGetOnlineDisplayList(count, displays.data(), &count);
uint32_t blackoutSuccessCount = 0;
uint32_t blackoutAttemptCount = 0;
for (uint32_t i = 0; i < count; i++) {
CGDirectDisplayID d = displays[i];
std::string uuid = GetDisplayUUID(d);
if (uuid.empty()) {
NSLog(@"MacSetPrivacyMode: Failed to get UUID for display %u, privacy mode requires all displays", (unsigned)d);
// Privacy mode requires ALL connected displays to be successfully blacked out
// to ensure user privacy. If we can't identify a display (no UUID),
// we can't safely manage its state or restore it later.
// Therefore, we must abort the entire operation and clean up any resources
// already allocated (like event taps and reconfiguration callbacks).
CGDisplayRemoveReconfigurationCallback(DisplayReconfigurationCallback, NULL);
TeardownEventTapOnMainThread();
// Restore gamma for displays that were already blacked out before this failure
if (!RestoreAllGammas()) {
// If any display failed to restore, use system reset as fallback
CGDisplayRestoreColorSyncSettings();
}
g_originalGammas.clear();
return false;
}
// Save original gamma using UUID as key (stable across reconnections)
if (g_originalGammas.find(uuid) == g_originalGammas.end()) {
uint32_t capacity = CGDisplayGammaTableCapacity(d);
if (capacity > 0) {
std::vector<CGGammaValue> red(capacity), green(capacity), blue(capacity);
uint32_t sampleCount = 0;
if (CGGetDisplayTransferByTable(d, capacity, red.data(), green.data(), blue.data(), &sampleCount) == kCGErrorSuccess) {
std::vector<CGGammaValue> all;
all.insert(all.end(), red.begin(), red.begin() + sampleCount);
all.insert(all.end(), green.begin(), green.begin() + sampleCount);
all.insert(all.end(), blue.begin(), blue.begin() + sampleCount);
g_originalGammas[uuid] = all;
} else {
NSLog(@"MacSetPrivacyMode: Failed to get gamma table for display %u (UUID: %s)", (unsigned)d, uuid.c_str());
}
} else {
NSLog(@"MacSetPrivacyMode: Display %u (UUID: %s) has zero gamma table capacity, not supported", (unsigned)d, uuid.c_str());
}
}
// Set to black only if we have saved original gamma for this display
if (g_originalGammas.find(uuid) != g_originalGammas.end()) {
uint32_t capacity = CGDisplayGammaTableCapacity(d);
if (capacity > 0) {
std::vector<CGGammaValue> zeros(capacity, 0.0f);
blackoutAttemptCount++;
CGError error = CGSetDisplayTransferByTable(d, capacity, zeros.data(), zeros.data(), zeros.data());
if (error != kCGErrorSuccess) {
NSLog(@"MacSetPrivacyMode: Failed to blackout display (ID: %u, UUID: %s, error: %d)", (unsigned)d, uuid.c_str(), error);
} else {
blackoutSuccessCount++;
}
} else {
NSLog(@"MacSetPrivacyMode: Display %u (UUID: %s) has zero gamma table capacity for blackout", (unsigned)d, uuid.c_str());
}
}
}
// Return false if any display failed to blackout - privacy mode requires ALL displays to be blacked out
if (blackoutAttemptCount > 0 && blackoutSuccessCount < blackoutAttemptCount) {
NSLog(@"MacSetPrivacyMode: Failed to blackout all displays (%u/%u succeeded)", blackoutSuccessCount, blackoutAttemptCount);
// Clean up: unregister callback and disable event tap since we're failing
CGDisplayRemoveReconfigurationCallback(DisplayReconfigurationCallback, NULL);
TeardownEventTapOnMainThread();
// Restore gamma for displays that were successfully blacked out
if (!RestoreAllGammas()) {
// If any display failed to restore, use system reset as fallback
NSLog(@"Some displays failed to restore gamma during cleanup, using CGDisplayRestoreColorSyncSettings as fallback");
CGDisplayRestoreColorSyncSettings();
}
g_originalGammas.clear();
return false;
}
g_privacyModeActive = true;
return true;
} else {
return TurnOffPrivacyModeInternal();
}
}

View File

@@ -23,9 +23,6 @@ pub mod win_mag;
#[cfg(windows)]
pub mod win_topmost_window;
#[cfg(target_os = "macos")]
pub mod macos;
#[cfg(windows)]
mod win_virtual_display;
#[cfg(windows)]
@@ -108,14 +105,7 @@ lazy_static::lazy_static! {
}
#[cfg(not(windows))]
{
#[cfg(target_os = "macos")]
{
macos::PRIVACY_MODE_IMPL.to_owned()
}
#[cfg(not(target_os = "macos"))]
{
"".to_owned()
}
"".to_owned()
}
};
@@ -137,13 +127,7 @@ pub type PrivacyModeCreator = fn(impl_key: &str) -> Box<dyn PrivacyMode>;
lazy_static::lazy_static! {
static ref PRIVACY_MODE_CREATOR: Arc<Mutex<HashMap<&'static str, PrivacyModeCreator>>> = {
#[cfg(not(windows))]
let mut map: HashMap<&'static str, PrivacyModeCreator> = HashMap::new();
#[cfg(target_os = "macos")]
{
map.insert(macos::PRIVACY_MODE_IMPL, |impl_key: &str| {
Box::new(macos::PrivacyModeImpl::new(impl_key))
});
}
let map: HashMap<&'static str, PrivacyModeCreator> = HashMap::new();
#[cfg(windows)]
let mut map: HashMap<&'static str, PrivacyModeCreator> = HashMap::new();
#[cfg(windows)]
@@ -349,14 +333,7 @@ pub fn get_supported_privacy_mode_impl() -> Vec<(&'static str, &'static str)> {
vec_impls
}
#[cfg(target_os = "macos")]
{
// No translation is intended for privacy_mode_impl_macos_tip as it is a
// placeholder for macOS specific privacy mode implementation which currently
// doesn't provide multiple modes like Windows does.
vec![(macos::PRIVACY_MODE_IMPL, "privacy_mode_impl_macos_tip")]
}
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
#[cfg(not(target_os = "windows"))]
{
Vec::new()
}

View File

@@ -1,81 +0,0 @@
use super::{PrivacyMode, PrivacyModeState};
use hbb_common::{anyhow::anyhow, ResultType};
extern "C" {
fn MacSetPrivacyMode(on: bool) -> bool;
}
pub const PRIVACY_MODE_IMPL: &str = "privacy_mode_impl_macos";
pub struct PrivacyModeImpl {
impl_key: String,
conn_id: i32,
}
impl PrivacyModeImpl {
pub fn new(impl_key: &str) -> Self {
Self {
impl_key: impl_key.to_owned(),
conn_id: 0,
}
}
}
impl PrivacyMode for PrivacyModeImpl {
fn is_async_privacy_mode(&self) -> bool {
false
}
fn init(&self) -> ResultType<()> {
Ok(())
}
fn clear(&mut self) {
unsafe {
MacSetPrivacyMode(false);
}
self.conn_id = 0;
}
fn turn_on_privacy(&mut self, conn_id: i32) -> ResultType<bool> {
if self.check_on_conn_id(conn_id)? {
return Ok(true);
}
let success = unsafe { MacSetPrivacyMode(true) };
if !success {
return Err(anyhow!("Failed to turn on privacy mode"));
}
self.conn_id = conn_id;
Ok(true)
}
fn turn_off_privacy(&mut self, conn_id: i32, _state: Option<PrivacyModeState>) -> ResultType<()> {
// Note: The `_state` parameter is intentionally ignored on macOS.
// On Windows, it's used to notify the connection manager about privacy mode state changes
// (see win_topmost_window.rs). macOS currently has a simpler single-mode implementation
// without the need for such cross-component state synchronization.
self.check_off_conn_id(conn_id)?;
let success = unsafe { MacSetPrivacyMode(false) };
if !success {
return Err(anyhow!("Failed to turn off privacy mode"));
}
self.conn_id = 0;
Ok(())
}
fn pre_conn_id(&self) -> i32 {
self.conn_id
}
fn get_impl_key(&self) -> &str {
&self.impl_key
}
}
impl Drop for PrivacyModeImpl {
fn drop(&mut self) {
// Use the same cleanup logic as other code paths to keep conn_id consistent
// and ensure all cleanup is centralized in one place.
self.clear();
}
}

View File

@@ -1420,7 +1420,7 @@ impl Connection {
pi.platform = "Android".into();
}
#[cfg(all(target_os = "macos", not(feature = "unix-file-copy-paste")))]
let mut platform_additions = serde_json::Map::new();
let platform_additions = serde_json::Map::new();
#[cfg(any(
target_os = "windows",
target_os = "linux",
@@ -1453,13 +1453,6 @@ impl Connection {
json!(privacy_mode::get_supported_privacy_mode_impl()),
);
}
#[cfg(target_os = "macos")]
{
platform_additions.insert(
"supported_privacy_mode_impl".into(),
json!(privacy_mode::get_supported_privacy_mode_impl()),
);
}
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))]
{