Compare commits

..

259 Commits

Author SHA1 Message Date
dijunkun eeb6a2a1ae [refactor] organize GUI code by responsibility 2026-07-19 21:43:19 +08:00
dijunkun 1f86c43458 [fix] replace clipboard handling with SDL and optimize threading 2026-07-18 23:31:33 +08:00
dijunkun c487d3a62c [fix] refine recent connections overflow layout, refs #85 2026-07-18 22:03:36 +08:00
dijunkun aee5be5ee6 [feat] refine recent connection card controls and tooltip 2026-07-18 20:59:29 +08:00
dijunkun 6ff2783105 [feat] add TURN relay mode configuration 2026-07-18 20:22:36 +08:00
dijunkun d6d30b6842 [fix] fix Windows UAC prompt input by routing Consent UI through secure input helper 2026-07-06 17:47:50 +08:00
dijunkun 5db309243b [fix] fix stuck connecting state when remote device is offline, refs #57 2026-07-05 00:52:21 +08:00
dijunkun 0681f6540d [fix] fix persistent scrollbar in recent connections area when empty, refs #85 2026-06-26 17:29:04 +08:00
dijunkun d843901550 [fix] fix intermittent crash on incoming connection by guarding shared connection status state across threads 2026-06-26 17:21:09 +08:00
dijunkun b41630ebb4 [fix] fix blank connection status popup by handling Gathering state 2026-06-26 16:23:15 +08:00
dijunkun a815eecc73 [fix] fix close-issue workflow env indentation, pagination, and activity time base 2026-06-26 16:14:09 +08:00
dijunkun 7e0984fe9c [chore] refine online status indicator with gradient glow and remove status text 2026-06-26 16:09:21 +08:00
dijunkun 9c28cd2ab2 [fix] fix crash when switching monitors due to race condition and missing bounds checks in screen capturer 2026-06-26 14:44:30 +08:00
dijunkun 3c4000bdbb [fix] add missing Xft development dependency for Linux tray build 2026-06-25 00:35:25 +08:00
dijunkun d8e9fa5bba [feat] support minimizing to the system tray on Linux when closing 2026-06-23 17:11:42 +08:00
dijunkun e026491b9f [feat] support minimizing to the system tray on macOS when closing, refs #87 2026-06-23 17:11:42 +08:00
dijunkun 009699b375 [chore] optimize online status indicator icon 2026-06-22 16:52:51 +08:00
dijunkun 3677588a3d [fix] align recent connection name footer 2026-06-18 10:53:31 +08:00
dijunkun 3d280053a7 [feat] add custom names for recent connection devices and improve panel display 2026-06-17 17:48:17 +08:00
dijunkun fbde3f6a47 [fix] prevent remote keyboard keys from getting stuck 2026-06-15 17:28:34 +08:00
dijunkun 1c1a33fdce [fix] use evdev keycodes for Wayland keyboard input 2026-06-12 11:25:00 +08:00
kunkundi fea238722d [fix] fix macOS remote double-click and triple-click handling, refs #86 2026-06-05 00:25:08 +08:00
kunkundi 178d958c08 [fix] show self-hosted TLS certificate errors in status bar 2026-06-01 00:31:45 +08:00
kunkundi f9633f366b [fix] update MiniRTC: support loading macOS self-hosted TLS roots, refs #79 2026-06-01 00:31:27 +08:00
dijunkun 7a81f3e767 Merge branch 'file-transfer' of https://github.com/kunkundi/crossdesk into file-transfer 2026-05-31 23:21:42 +08:00
dijunkun bbbbbf7927 [fix] update MiniRTC: support loading Windows self-signed root certificates from intermediate stores, refs #79 2026-05-31 23:04:13 +08:00
dijunkun 5735f84008 [ci] include version alias in version.json for compatibility 2026-05-29 11:28:09 +08:00
kunkundi fe0cf42e5d [fix] use Linux system CA paths for update checks 2026-05-29 10:51:22 +08:00
kunkundi 04100584ce [fix] restore latest_version update metadata 2026-05-29 02:11:46 +08:00
kunkundi 9d3a422916 [fix] log update check failures and use macOS trust store 2026-05-29 02:00:13 +08:00
kunkundi 65d8284fb8 [ci] standardize hotfix package version ordering across platforms 2026-05-29 00:52:07 +08:00
kunkundi eea107db66 [chore] adjust settings window button vertical spacing 2026-05-28 23:36:25 +08:00
kunkundi 67812957db [ci] fix GitHub Actions Linux build version parsing 2026-05-28 23:30:42 +08:00
dijunkun 69d77e59cc [fix] make portable Windows Service setup optional, refs #84 2026-05-28 18:59:27 +08:00
dijunkun efcebfd82c [feat] add explicit hotfix patch version support 2026-05-28 15:22:35 +08:00
dijunkun 32345f93bf [fix] validate macOS input display state before injecting events 2026-05-28 04:22:14 +08:00
dijunkun 193e4bd5bf [fix] guard macOS speaker capture callbacks against invalid buffers 2026-05-28 04:22:07 +08:00
dijunkun 53edf3d57e [fix] harden macOS screen capture lifecycle against late callbacks 2026-05-28 04:21:59 +08:00
dijunkun 895e297771 [fix] initialize render runtime state to avoid invalid SDL handles 2026-05-28 04:21:52 +08:00
dijunkun 8f3959e6c6 [fix] reset stale macOS permissions during install 2026-05-28 01:34:02 +08:00
dijunkun 5ff6b601c7 [fix] improve macOS permission request flow 2026-05-28 01:33:50 +08:00
kunkundi 4895ac9c23 [fix] bundle Windows Service binaries in Windows portable package 2026-05-27 19:37:06 +08:00
kunkundi f121aa47f7 [feat] add portable Windows Service install prompt with one-click setup 2026-05-27 19:32:58 +08:00
dijunkun 00a8d59284 [chore] update README 2026-05-27 17:22:51 +08:00
dijunkun a30489e05b [feat] update MiniRTC: report relay traversal when either ICE candidate is relayed 2026-05-27 16:37:08 +08:00
dijunkun dfbeb3ed20 [fix] request stream keyframes when capture resumes 2026-05-27 16:05:42 +08:00
dijunkun 2eed1c974e [fix] recover Windows capture backends after secure desktop exit 2026-05-27 16:05:00 +08:00
dijunkun 63a79a90ac [fix] refine Windows credential UI state detection 2026-05-27 16:04:38 +08:00
dijunkun a61e74d97b [feat] add video keyframe request APIs 2026-05-27 15:58:32 +08:00
kunkundi 54438a4aa1 [feat] refine control bar display index label sizing and alignment 2026-05-27 10:24:53 +08:00
dijunkun 7682ad63e4 [feat] add localized tooltips for control bar buttons 2026-05-27 00:37:34 +08:00
dijunkun 06c53fdc9c [fix] handle SAS secure desktop transitions and restore desktop capture promptly, refs #77 2026-05-26 04:38:07 +08:00
dijunkun 665f4e684c [feat] improve Windows secure desktop capture and input handling, refs #77 2026-05-26 03:26:37 +08:00
dijunkun 52b894fe0e [feat] improve secure desktop capture by streaming latest frames through shared memory 2026-05-26 01:28:12 +08:00
kunkundi 82c0cbbad4 [fix] fix C++17 WGC build with newer MSVC coroutine deprecation 2026-05-25 17:37:26 +08:00
dijunkun ce004af379 [feat] add a control bar shortcut menu for sending Ctrl+Alt+Del and remote lock commands 2026-05-25 15:57:31 +08:00
dijunkun 15bd9e9fdc [fix] enable repeated SPS/PPS on NVENC keyframes, fixes #78 2026-05-25 02:16:19 +08:00
dijunkun 37aabeaf72 [fix] reset display popup hover state after monitor switching to restore mouse control, fixes #83 2026-05-25 01:28:17 +08:00
dijunkun 473737ac9b [fix] fix Windows input forwarding and allow debug builds to run without admin, fixes #82 2026-05-25 00:40:38 +08:00
dijunkun 1e29ec708f [fix] fix macOS remote keyboard modifier injection, fixes #81 2026-05-21 00:23:50 +08:00
dijunkun 515d517a99 [feat] add portable build storage mode, refs #80 2026-05-21 00:13:27 +08:00
dijunkun a3aedcb624 [fix] fix incorrect new version notification display issue 2026-05-07 15:45:21 +08:00
dijunkun 98b7c6c966 [fix] preserve Linux keypad navigation semantics and Windows scan-code metadata for remote keyboard input 2026-05-07 14:50:00 +08:00
dijunkun b1d956af2c [fix] fix left/right modifier key injection while preserving scan code metadata 2026-05-06 17:52:31 +08:00
dijunkun b7a031bb7f [fix] make PipeWire and portal dependencies optional 2026-04-28 17:08:34 +08:00
dijunkun 15cce07b6e Merge branch 'desktop-unlock-win' into file-transfer 2026-04-28 15:53:18 +08:00
dijunkun 1d5d6f5121 [fix] fix Debian package dependencies for PipeWire and ALSA t64 transitions 2026-04-28 11:14:25 +08:00
dijunkun 5f541f5c8b [feat] make CrossDesk service start and stop with the app 2026-04-28 10:25:16 +08:00
dijunkun 71bce08549 [fix] select the correct X11 pixel format conversion to prevent green-tinted screen capture on ubuntu 2026-04-27 17:57:11 +08:00
dijunkun 37b9badb2a [ci] fix NSIS uninstall function naming for service cleanup 2026-04-22 00:16:58 +08:00
dijunkun 4089e80fe8 [feat] register and remove CrossDeskService in the Windows installer 2026-04-21 23:23:11 +08:00
dijunkun 2be6e727ce [fix] use SDL keyboard capture on Wayland only 2026-04-21 17:37:19 +08:00
dijunkun d3b886c3f6 [fix] fix blocking issue on controlled-side during shutdown 2026-04-21 16:52:59 +08:00
dijunkun 97e48bfe71 [fix] fix Wayland keyboard capture by using SDL key events 2026-04-21 14:47:10 +08:00
dijunkun a8769dee06 Merge branch 'file-transfer' of https://github.com/kunkundi/crossdesk into file-transfer 2026-04-21 09:28:22 +08:00
dijunkun ffa94986d5 [feat] add Windows secure desktop remote unlock support for locked sessions, refs #77 2026-04-21 04:10:08 +08:00
dijunkun e4dfb61509 [fix] fix wayland cursor mapping 2026-04-20 18:09:13 +08:00
dijunkun d42b6e3261 [feat] update MiniRTC 2026-04-20 18:02:56 +08:00
dijunkun 855b15025c [fix] fix file transfer window interactions issue 2026-04-14 14:25:16 +08:00
dijunkun 3701b2c0d9 [chore] add acknowledgements 2026-04-14 11:03:06 +08:00
dijunkun ecbec4d301 [ci] use rolling cache keys for xmake dependency caches 2026-04-14 02:16:17 +08:00
dijunkun 21425c7132 [fix] fix system tray icon display issue 2026-04-14 01:54:03 +08:00
dijunkun 3e95a7ba29 [fix] fix startup failure caused by wgc_plugin working directory change 2026-04-14 01:53:20 +08:00
dijunkun c1394db285 [fix] fix missing wgc_plugin.dll in Windows packaging process, refs #76 2026-04-13 23:51:39 +08:00
dijunkun eee6c588bd [fix] fix X11 odd-size captures by aligning dimensions to even values 2026-03-24 00:07:44 +08:00
dijunkun eca68f6c7a [fix] fix keyboard input injection on Linux Wayland sessions 2026-03-23 23:14:05 +08:00
dijunkun f4e28d8774 [fix] fix mac-to-windows symbol key input 2026-03-23 22:30:58 +08:00
dijunkun 21b179e01c [fix] release pressed modifier keys when stream window loses focus 2026-03-23 21:33:01 +08:00
dijunkun 83cacf6f51 [fix] split mouse and keyboard from data channel and use reliable transimission 2026-03-23 21:14:07 +08:00
dijunkun 13c37f01b1 [fix] fix cross-platform key capture and mapping issues 2026-03-23 20:42:48 +08:00
dijunkun 511831ced3 [fix] fix Wayland reconnect black screen by keeping capturer warm and also fix Wayland mouse control 2026-03-23 05:18:56 +08:00
dijunkun 518e1afa58 [feat] add Linux screen capture fallback support for DRM and Wayland 2026-03-22 21:33:50 +08:00
dijunkun 43d03ac081 [fix] fix font path lookup for wqy/dejavu by switching from OpenType to TrueType 2026-03-22 00:39:19 +08:00
dijunkun f7f62c5fe0 [fix] update MiniRTC: refactor IceAgent to improve stability 2026-03-20 15:58:59 +08:00
dijunkun 2bbddbca6b [fix] fix Linux audio fallback when audio devices are unavaliable 2026-03-20 15:14:45 +08:00
dijunkun f0f8f27f4c [fix] fix blocking join() in Linux clipboard monitor thread during shutdown 2026-03-20 15:02:34 +08:00
dijunkun 262af263f2 [fix] move keyboard capturer to a background thread and use poll-based X11 event handling to avoid main-thread blocking 2026-03-20 14:56:40 +08:00
dijunkun 38b7775b1b [fix] fix restart/shutdown races in process monitor 2026-03-20 14:50:42 +08:00
dijunkun 56c0bca62f [chore] adjust hyperlink spacing and alignment 2026-03-20 14:47:33 +08:00
dijunkun 4b1b09fd5b [fix] fix Linux fonts: use opentype instead of truetype 2026-03-20 13:01:13 +08:00
dijunkun 1d6425bbf4 [fix] update MiniRTC: fix compiler warnings by adding missing override specifiers 2026-03-20 04:36:58 +08:00
dijunkun 5ec6552d25 [fix] fix macOS intel CI build failure caused by python 3.13 PGO mismatch 2026-03-20 03:50:19 +08:00
dijunkun 79e4a0790a [fix] fix issue where wgc_plugin was not compiled 2026-03-20 02:59:31 +08:00
dijunkun 1d3cac54ab [feat] load wgc from wgc_plugin.dll at runtime and drop direct'windowsapp' linking, refs #74 2026-03-20 01:36:36 +08:00
dijunkun 2f26334775 [feat] unify UI font loading across platforms and prefer PingFang on macOS 2026-03-19 21:58:14 +08:00
dijunkun 9270d528e3 [feat] update miniRTC: fix compiler warnings caused by missing override specifiers 2026-03-19 21:57:52 +08:00
dijunkun 91db3a7e34 [feat] add Russian language support 2026-03-19 20:04:30 +08:00
dijunkun d017561e54 [fix] fix typo ImGuiChildFlags_Border to ImGuiChildFlags_Borders 2026-03-19 16:16:51 +08:00
dijunkun 8e8a85bae3 [feat] upgrade actions/checkout and actions/cache to v5 for Node 24 compatibility 2026-03-19 15:03:58 +08:00
dijunkun bea89e9111 [feat] crossdesk server image supports Linux ARM64, refs #72 2026-03-19 10:06:57 +08:00
dijunkun 499ce0190a [fix] process mouse events only from the stream window 2026-03-11 16:00:29 +08:00
dijunkun 91bde91238 [feat] probe presence before connect and show warning if offline 2026-03-10 17:46:44 +08:00
dijunkun 3e31ba102d [fix] prevent sending connection requests to offline devices 2026-03-10 10:53:58 +08:00
dijunkun 263c5eefd3 [fix] fix update button lag in release mode by using non-blocking URL opener. 2026-03-10 10:39:05 +08:00
dijunkun b230b851e4 [fix] fix cannot close connection from Server Window when the peer is a web client 2026-03-10 00:39:00 +08:00
dijunkun ff32477ffe [fix] update MiniRTC: fix crash on disconnect 2026-03-10 00:35:33 +08:00
dijunkun c6c60decdb [fix] fix incorrect online status of recently connections 2026-03-09 22:52:05 +08:00
dijunkun 7505adeca8 [feat] update MiniRTC 2026-03-09 22:50:42 +08:00
dijunkun 754f1fba88 [feat] show 'Receiving screen' text before the remote frame arrives 2026-03-09 22:37:50 +08:00
dijunkun 8be46b870a [feat] add cancel button during connecting 2026-03-09 21:35:21 +08:00
dijunkun 81cb1d6c0b [fix] disable clipboard sharing when not in control mode 2026-03-05 17:46:27 +08:00
dijunkun 319416f1b7 [feat] update MiniRTC: optimize video quality and smoothness 2026-03-05 17:30:05 +08:00
dijunkun d679c6251b [feat] update MiniRTC 2026-03-04 10:46:21 +08:00
dijunkun a14baafda7 [fix] fix keyboard event loss due to start_keyboard_capturer_ flag improper setting, fixes #65 2026-03-04 10:36:39 +08:00
dijunkun cfdc7d3106 [fix] update MiniRTC: fix bandwidth degradation caused by ALR-triggered resolution downgrade during static frames 2026-03-03 10:58:38 +08:00
dijunkun 33d51b8ce5 [fix] reset to initial monitor on connection close via ResetToInitialMonitor to fix black screen 2026-03-02 15:42:44 +08:00
dijunkun b13dac2093 [feat] refine display of recent connections presence tooltip 2026-03-02 10:48:16 +08:00
dijunkun a605c95e5a [fix] fix window rounding inconsistency under different DPI scales 2026-03-02 10:38:06 +08:00
dijunkun 9a5553a636 [chore] update fonts 2026-03-02 10:17:06 +08:00
dijunkun ef02403da6 [fix] fix incorrect sizing of the online status indicator on high-DPI displays 2026-03-01 16:47:09 +08:00
dijunkun adfab363c1 [feat] add online presence check before connecting and show offline warning dialog 2026-03-01 16:29:11 +08:00
dijunkun 123d4cf595 [fix] update MiniRTC: fix the macOS hardware decode fail when the server using openH264 encode 2026-03-01 15:40:50 +08:00
dijunkun 19feb8ff49 [feat] show device online/offline status in recent connection tooltip 2026-02-28 17:25:41 +08:00
dijunkun 9223bf9d2d [feat] add online status indicators for recent connections 2026-02-28 17:06:44 +08:00
dijunkun 11b5f87841 [feat] update MiniRTC: add signaling send/receive API support 2026-02-28 17:04:47 +08:00
dijunkun cea59fb453 [feat] update MiniRTC 2026-02-27 17:53:51 +08:00
ZongYangBigPolo 3a179bf480 [feat] add macOS installer icon and optimize packaging script (#70) 2026-02-27 17:37:54 +08:00
dijunkun b9c53024f1 [feat] set video quality to HIGH and enable hardware codec by default 2026-02-27 17:24:04 +08:00
dijunkun 62b37ad698 [fix] resolve failures in the WGC→DXGI→GDI fallback chain 2026-02-27 16:33:57 +08:00
dijunkun de56cd5d3b [feat] update MiniRTC 2026-02-27 16:30:08 +08:00
dijunkun 8d9d78185a [fix] fix issue where client display list was incorrectly merged into the server display list 2026-02-27 16:27:37 +08:00
dijunkun b10a6512fe [feat] add Windows DXGI/GDI screen capture with WGC→DXGI→GDI fallback support 2026-02-27 13:55:41 +08:00
dijunkun a94da8802f [feat] make MainWindow and ServerWindow use rounded corners 2026-02-26 18:06:07 +08:00
dijunkun 4e6f82d00c [feat] restrict the dragging range of the ControlWindow 2026-02-26 15:55:04 +08:00
dijunkun 5e2ad99ec0 [feat] update MiniRTC to resolve color distortion in the OpenH264 decoder 2026-02-25 17:48:30 +08:00
dijunkun 8ab50ea362 [feat] add video resolution and conection mode in NetTrafficStats window 2026-02-25 15:33:17 +08:00
dijunkun 25e9958a69 Merge branch 'file-transfer' of https://github.com/kunkundi/crossdesk into file-transfer 2026-02-24 17:56:21 +08:00
dijunkun 410ea8b96b [feat] update MiniRTC 2026-02-24 17:56:02 +08:00
dijunkun e656664cad [chore] adjust file transfer save path button pos 2026-02-24 14:36:03 +08:00
dijunkun 0e6cee0961 [fix] fix stream window rendering height 2026-02-24 14:31:34 +08:00
dijunkun 42506b8c1d [ci] combine Linux amd64 and arm64 builds into a single job using matrix 2026-02-13 02:31:58 +08:00
dijunkun e35365d162 [feat] disable and style minimize_to_tray checkbox for non-Windows platforms 2026-02-13 02:29:52 +08:00
dijunkun bf1c0f796d [fix] fix Linux system certificate loading failure 2026-02-13 01:56:49 +08:00
dijunkun 547532b28c [fix] fix server window scaling issues on high-DPI displays 2026-02-13 01:26:10 +08:00
dijunkun a91e23abf6 [fix] fix raw pointer issues when closing connections 2026-02-13 01:12:21 +08:00
dijunkun 2b324f636b [fix] fix macOS system certificate loading failure 2026-02-12 22:49:54 +08:00
dijunkun 103b8372e4 [chore] rename packaged executable to CrossDesk.exe in NSIS and portable artifacts 2026-02-12 16:45:41 +08:00
dijunkun f7f1724bf1 [feat] optimize hyperlink opening by replacing system start with CreateProcessW-based URL launch on Winodws 2026-02-12 16:22:57 +08:00
dijunkun 5d70e11f17 [feat] support Windows x64 portable build, refs #54 2026-02-12 16:03:06 +08:00
dijunkun fb7ae90d46 [feat] add configurable file transfer save path in settings window, refs #63 2026-02-12 14:30:14 +08:00
dijunkun 957792a7a0 [feat] remove client certificate dependency 2026-02-11 16:23:43 +08:00
dijunkun 2e8ce6a2f0 [fix] reset default cert fingerprint if mismatch 2026-02-05 18:59:28 +08:00
dijunkun 9927a56b78 [feat] update MiniRTC 2026-02-05 18:05:35 +08:00
dijunkun db3da52f83 [feat] clear cached fingerprint when verification fails 2026-02-05 17:15:59 +08:00
dijunkun 19a7c6978a [feat] update MiniRTC to resolve websocket reconnection and post task issues 2026-01-28 09:45:19 +08:00
dijunkun b5e9ba03a1 [fix] double-buffer video frames and handle stream cleanup on main thread 2026-01-28 09:44:54 +08:00
dijunkun cb5f8b91ad [feat] update update-notification icon 2026-01-27 21:11:26 +08:00
dijunkun f627f60f1a [feat] use tooltips to display server-side file transfer status information 2026-01-27 17:50:21 +08:00
dijunkun e9fce5b8b8 [feat] display remote controller hostname instead of remote id 2026-01-26 22:52:58 +08:00
dijunkun a7820a79db [fix] fix incorrect peer_ usage in SendReliableDataFrame 2026-01-26 21:47:10 +08:00
dijunkun b6a52dbcd4 [feat] add support for displaying multiple controller info and file transfer to controllers 2026-01-26 17:47:31 +08:00
dijunkun 7bbd10a50c [fix] fix rendering issues in stream and server windows when the main window is minimized 2026-01-22 17:56:00 +08:00
dijunkun ee08b231db [fix] fix height when server window is restored from collapsed state 2026-01-20 23:58:43 +08:00
dijunkun 619e54dc0e [feat] add controller info and file transfer in server window 2026-01-20 21:22:20 +08:00
dijunkun 9b69596af1 [fix] fix stream window size recalculation 2026-01-20 01:33:27 +08:00
dijunkun f6e169c013 [feat] add support for server window resizing and dragging 2026-01-20 01:22:14 +08:00
dijunkun fd242d50c1 [feat] show server window in the bottom-right corner of the screen 2026-01-19 17:42:22 +08:00
dijunkun d6d8ecd6c5 [feat] add server window 2026-01-19 00:47:34 +08:00
dijunkun 669fac7f50 [feat] support drag-and-drop file sending, refs #63 2026-01-14 18:13:22 +08:00
dijunkun 92d670916e [fix] fix incorrect data send function used for control data 2026-01-13 18:13:54 +08:00
dijunkun 0155413c12 [feat] use reliable transmission to send control info 2026-01-12 17:28:19 +08:00
dijunkun 8468be6532 [fix] update MiniRTC 2026-01-12 17:27:49 +08:00
dijunkun 78c82f778a [chore] remove SetOnFileComplete() in class FileReceiver 2025-12-29 12:48:14 +08:00
dijunkun ab89a3d41a [fix] fix clipboard copy from remote device to controller 2025-12-29 12:46:19 +08:00
dijunkun 5f320af6e6 [fix] fix macOS clipboard monitoring compilation error using changeCount polling 2025-12-29 01:01:08 +08:00
dijunkun 17b7ba6b72 [feat] support clipboard sharing, refs #35 2025-12-29 00:53:14 +08:00
dijunkun c70ebdfe15 [fix] correct file send rate calculation by using data channel feedback 2025-12-29 00:53:14 +08:00
dijunkun a3e564f160 [feat] support multi-file transfer, refs #45 2025-12-29 00:52:58 +08:00
dijunkun eea37424c9 [feat] use receiver ack to calculate file transfer progress 2025-12-25 18:13:18 +08:00
dijunkun b322181853 [feat] add file transfer window to show file sending info 2025-12-24 18:12:50 +08:00
dijunkun 3ad66f5e0b [fix] fix file transfer 2025-12-24 01:45:59 +08:00
dijunkun 4035e0dd13 [feat] upgrade MiniRTC and expose source ID in data callbacks 2025-12-23 00:53:47 +08:00
dijunkun 832b820096 [fix] update MiniRTC to latest version and add logging for sent file size 2025-12-22 18:33:53 +08:00
dijunkun d337971de0 [feat] update MiniRTC api calling 2025-12-19 18:22:34 +08:00
dijunkun a967dc72d7 [feat] use SendReliableDataFrame() instead of SendDataFrame() 2025-12-19 01:46:16 +08:00
dijunkun 5066fcda48 [feat] implement file transfer module 2025-12-18 18:30:51 +08:00
dijunkun e7bdf42694 [feat] add file transfer button to control bar 2025-12-18 01:54:03 +08:00
dijunkun 875fea88ee [chore] adjust main window title font size 2025-12-18 01:32:38 +08:00
dijunkun b2654ea9db [feat] use fingerprint-based verification for both default and self-hosted servers 2025-12-10 03:46:03 +08:00
dijunkun 8f8e415262 [chore] update README 2025-12-10 03:38:05 +08:00
dijunkun 5ff624f7b2 [feat] use fingerprint-based verification for TLS connection 2025-12-10 03:28:28 +08:00
dijunkun e09243f1ec [chore] update README 2025-12-10 00:23:04 +08:00
dijunkun f5941c7eda [chore] update README 2025-12-10 00:20:26 +08:00
dijunkun 3c2ebe602e [fix] do not save password info into logs 2025-12-10 00:20:10 +08:00
dijunkun 2f64172ead [feat] use new client id and password if server switched 2025-12-10 00:09:49 +08:00
Junkun Di a83206a346 [ci] update issue templates 2025-12-09 17:50:57 +08:00
dijunkun 46e769976f [chore] update README 2025-12-09 00:36:00 +08:00
dijunkun 58c24b798e [chore] update README: refresh self-hosted server setup guide 2025-12-09 00:19:25 +08:00
dijunkun 5cc31e5ba3 [fix] fix self-hosted server configuration being reset when disabling self-hosted mode 2025-12-08 18:27:18 +08:00
dijunkun 74fe9bebf5 [fix] fix server settings window height 2025-12-08 17:42:02 +08:00
dijunkun 1f6a2182be [fix] resolve tab bar dragging issue 2025-12-06 18:46:04 +08:00
dijunkun 1a883f0d6c [fix] fix menu BeginPopup & EndPopup pairing 2025-12-05 17:53:17 +08:00
dijunkun a560b4ca70 [feat] make thumbnail save asynchronous to prevent UI blocking 2025-12-05 17:07:55 +08:00
dijunkun 46f45ed216 [fix] fix tab close button not working in stream window 2025-12-05 15:18:35 +08:00
dijunkun 5c23f1c5e8 [fix] correct tab bar scaling and layout 2025-12-05 15:17:07 +08:00
dijunkun 70ae02549f [fix] correct title bar display 2025-12-05 04:28:33 +08:00
dijunkun 68de995c64 [fix] correct hardware codec setting item display 2025-12-05 00:57:33 +08:00
dijunkun ed5ddb96fd [refactor] update notification window rendering for high-DPI scaling support 2025-12-04 19:00:37 +08:00
dijunkun 436dfafc2a [refactor] update about window rendering for high-DPI scaling support 2025-12-04 17:51:03 +08:00
dijunkun 5221b193e5 [refactor] update settings window rendering for high-DPI scaling support 2025-12-04 17:23:14 +08:00
dijunkun fafced23c2 [refactor] update stream window rendering for high-DPI scaling support 2025-12-04 02:11:06 +08:00
dijunkun 1e48b645ca [refactor] update recent connections panel rendering for high-DPI scaling support 2025-12-04 00:14:09 +08:00
dijunkun 49ed0200e7 [refactor] update connection status window rendering for high-DPI scaling support 2025-12-03 21:54:10 +08:00
dijunkun 24873afe64 [refactor] update remote peer panel rendering for high-DPI scaling support 2025-12-03 21:17:11 +08:00
dijunkun d21e1bd422 [refactor] update remote peer panel rendering for high-DPI scaling support 2025-12-03 17:36:19 +08:00
dijunkun be044c248b [refactor] update local peer panel rendering for high-DPI scaling support 2025-12-03 17:12:35 +08:00
dijunkun 49cbbc3363 [refactor] update status bar rendering for high-DPI scaling support 2025-12-03 12:45:01 +08:00
dijunkun 1e20cb806b [refactor] update title bar rendering for high-DPI scaling support 2025-12-03 04:20:49 +08:00
dijunkun 2e52818f6f fix: correct array deletion and improve state management in WGC screen capturer 2025-12-01 23:06:46 +08:00
dijunkun b50f386713 [fix] fix system_chinese_font_ usage to avoid dangling font pointer after closing stream window 2025-12-01 23:03:45 +08:00
dijunkun 280e011ae4 [fix] update RecentConnectionsWindow layout 2025-12-01 17:16:28 +08:00
dijunkun 8d09bf53c3 [fix] fix UpdateNotificationWindow dpi scaling 2025-12-01 13:52:39 +08:00
dijunkun 131b4f1795 [fix] resolve compilation errors on Linux 2025-12-01 13:07:36 +08:00
dijunkun 7d3ecf789d [fix] fix control bar dpi scaling 2025-12-01 11:30:35 +08:00
dijunkun 37797bf873 [fix] fix DPI scaling issues 2025-12-01 04:54:30 +08:00
dijunkun 91d42b6561 [fix] macOS: fix audio capture, refs #29 2025-11-30 17:13:02 +08:00
dijunkun feb9f2f460 [revert] revert to the pre-lock version 2025-11-28 11:44:08 +08:00
Junkun Di 9c1753c78c [chroe] add issue templates 2025-11-28 11:34:22 +08:00
dijunkun 7370ff5b30 [chore] add HelloGitHub badge 2025-11-28 11:10:41 +08:00
dijunkun f6eda34dbd [fix] fix dead lock during connecting 2025-11-28 10:02:57 +08:00
dijunkun 5d9a0a3ea5 [fix] fix dead lock during peer init 2025-11-28 09:32:21 +08:00
dijunkun 3d8249bffa [fix] use lock and null pointer checks to prevent crashes 2025-11-28 00:41:29 +08:00
dijunkun 82f32cbe8f [fix] fix incorrect peer reference in AddVideoStream call 2025-11-27 23:53:27 +08:00
dijunkun 56da2f99f3 [chore] update Linux log path configuration 2025-11-27 23:20:14 +08:00
dijunkun e6c72fe558 [feat] remove screen capture and accessibility permission when reinstall 2025-11-27 17:35:15 +08:00
dijunkun a964c6bbf5 [fix] call log after log initialzation, refs #36, #29 2025-11-27 16:09:08 +08:00
dijunkun 239da373d0 [feat] attempt to rejoin once per second 2025-11-27 04:20:29 +08:00
dijunkun 217cfb091d [feat] display version info on startup 2025-11-27 04:18:08 +08:00
dijunkun 3c3c7b9ae0 [fix] fix crash when screen recording permission is not granted on macOS, refs #29 2025-11-27 03:32:16 +08:00
dijunkun f14bdb7fe8 [feat] optimize UpdateNotificationWindow UI appearance 2025-11-27 03:07:55 +08:00
dijunkun c0a98f97c3 [feat] optimize RequestPermissionWindow UI appearance 2025-11-27 02:40:33 +08:00
dijunkun 0ab6686eb8 [feat] use DrawToggleSwitch to request permission 2025-11-26 23:32:04 +08:00
dijunkun 76b475450b [feat] request macOS system permissions by showing a prompt on startup 2025-11-26 18:18:40 +08:00
dijunkun 5d1e1b5667 [fix] remove permissions on uninstall and request permissions during installation for MacOS 2025-11-26 16:11:49 +08:00
dijunkun c3b8b1374a [chore] update README 2025-11-25 16:30:14 +08:00
dijunkun 7c940d6b15 [ci] fix tag error in update-version-json.yml 2025-11-25 03:30:28 +08:00
dijunkun 86501b05dd [feat] show notification window if there is a new version avaliable 2025-11-25 01:20:16 +08:00
dijunkun 01ebed9b37 [ci] upload release notes to version.json 2025-11-25 01:19:12 +08:00
dijunkun 2188adb1f1 [chore] disable CROSSDESK_DEBUG in keyboard and mouse control 2025-11-24 16:47:33 +08:00
189 changed files with 36282 additions and 8243 deletions
+35
View File
@@ -0,0 +1,35 @@
---
name: 问题反馈
about: 请在此提交问题报告,以便持续优化产品。
title: ''
labels: bug
assignees: kunkundi
---
**描述问题**
清晰简洁地描述遇到的错误。
**复现步骤**
复现该问题的步骤:
1. 前往 '...'
2. 点击 '....'
3. 出现错误
**预期行为**
清晰简洁地描述你期望发生的行为。
**截图**
如果适用,请添加截图以帮助说明问题。
**桌面端信息(请填写以下内容):**
- 操作系统: [例如 Windows 11]
- 版本: [例如 v1.1.10]
**移动端信息(请填写以下内容):**
- 设备: [例如 iPhone 17]
- 操作系统: [例如 iOS 26.1]
- 浏览器: [例如 系统浏览器、Safari]
**补充信息**
在此添加与问题相关的其他上下文内容。
+28
View File
@@ -0,0 +1,28 @@
---
name: 需求建议
about: 请在此提交功能需求或改进建议,以便后续迭代参考。
title: ''
labels: enhancement
assignees: kunkundi
---
**功能/改进建议描述**
清晰简洁地描述希望新增的功能或改进的内容。
**使用场景 / 背景**
说明该功能或改进的使用场景,以及解决后带来的价值。
**预期效果**
描述你认为最理想的功能表现或改进效果。
**参考示例(可选)**
提供类似功能截图、参考链接或其他说明,帮助更好理解需求。
**优先级(可选)**
- [ ]
- [ ]
- [ ]
**补充信息(可选)**
其他相关信息或特殊要求。
+225 -120
View File
@@ -7,6 +7,11 @@ on:
tags:
- "*"
workflow_dispatch:
inputs:
patch:
description: "Hotfix patch number, for example 1 or 2. Use 0 for a normal build."
required: false
default: "0"
permissions:
contents: write
@@ -15,103 +20,82 @@ env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
jobs:
# Linux amd64
build-linux-amd64:
name: Build on Ubuntu 22.04 amd64
runs-on: ubuntu-22.04
container:
image: crossdesk/ubuntu20.04:latest
options: --user root
steps:
- name: Extract version number
id: version
run: |
VERSION="${GITHUB_REF##*/}"
VERSION_NUM="${VERSION#v}"
echo "VERSION_NUM=${VERSION_NUM}" >> $GITHUB_ENV
- name: Set legal Debian version
shell: bash
id: set_deb_version
run: |
SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-7)
BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d)
if [[ ! "${VERSION_NUM}" =~ ^[0-9] ]]; then
LEGAL_VERSION="v0.0.0-${VERSION_NUM}-${BUILD_DATE}-${SHORT_SHA}"
else
LEGAL_VERSION="v${VERSION_NUM}-${BUILD_DATE}-${SHORT_SHA}"
fi
echo "LEGAL_VERSION=${LEGAL_VERSION}" >> $GITHUB_ENV
echo "BUILD_DATE=${BUILD_DATE}" >> $GITHUB_ENV
- name: Checkout code
uses: actions/checkout@v4
with:
submodules: recursive
- name: Build CrossDesk
env:
CUDA_PATH: /usr/local/cuda
XMAKE_GLOBALDIR: /data
run: |
ls -la $XMAKE_GLOBALDIR
xmake f --CROSSDESK_VERSION=${LEGAL_VERSION} --USE_CUDA=true --root -y
xmake b -vy --root crossdesk
- name: Decode and save certificate
shell: bash
run: |
mkdir -p certs
echo "${{ secrets.CROSSDESK_CERT_BASE64 }}" | base64 --decode > certs/crossdesk.cn_root.crt
- name: Package
run: |
chmod +x ./scripts/linux/pkg_amd64.sh
./scripts/linux/pkg_amd64.sh ${LEGAL_VERSION}
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: crossdesk-linux-amd64-${{ env.LEGAL_VERSION }}
path: ${{ github.workspace }}/crossdesk-linux-amd64-${{ env.LEGAL_VERSION }}.deb
# Linux arm64
build-linux-arm64:
name: Build on Ubuntu 22.04 arm64
runs-on: ubuntu-22.04-arm
build-linux:
name: Build Linux (${{ matrix.arch }})
runs-on: ${{ matrix.runner }}
strategy:
matrix:
include:
- arch: amd64
runner: ubuntu-22.04
image: crossdesk/ubuntu20.04:latest
package_script: ./scripts/linux/pkg_amd64.sh
- arch: arm64
runner: ubuntu-22.04-arm
image: crossdesk/ubuntu20.04-arm64v8:latest
package_script: ./scripts/linux/pkg_arm64.sh
container:
image: ${{ matrix.image }}
options: --user root
steps:
- name: Extract version number
id: version
shell: bash
run: |
VERSION="${GITHUB_REF##*/}"
VERSION_NUM="${VERSION#v}"
echo "VERSION_NUM=${VERSION_NUM}" >> $GITHUB_ENV
VERSION_REF="${GITHUB_REF##*/}"
VERSION_BASE="${VERSION_REF#v}"
PATCH_NUMBER="${{ github.event.inputs.patch }}"
BUILD_DATE_OVERRIDE=""
if [[ ! "${PATCH_NUMBER}" =~ ^[0-9]+$ ]]; then
PATCH_NUMBER=0
fi
if [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)-([0-9]{8})$ ]]; then
VERSION_BASE="${BASH_REMATCH[1]}"
PATCH_NUMBER="${BASH_REMATCH[3]}"
BUILD_DATE_OVERRIDE="${BASH_REMATCH[4]}"
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
VERSION_BASE="${BASH_REMATCH[1]}"
BUILD_DATE_OVERRIDE="${BASH_REMATCH[3]}"
PATCH_NUMBER="${BASH_REMATCH[4]}"
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})$ ]]; then
VERSION_BASE="${BASH_REMATCH[1]}"
BUILD_DATE_OVERRIDE="${BASH_REMATCH[3]}"
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)$ && "${PATCH_NUMBER}" == "0" ]]; then
VERSION_BASE="${BASH_REMATCH[1]}"
PATCH_NUMBER="${BASH_REMATCH[3]}"
fi
echo "VERSION_BASE=${VERSION_BASE}" >> $GITHUB_ENV
echo "PATCH_NUMBER=${PATCH_NUMBER}" >> $GITHUB_ENV
echo "BUILD_DATE_OVERRIDE=${BUILD_DATE_OVERRIDE}" >> $GITHUB_ENV
- name: Set legal Debian version
shell: bash
id: set_deb_version
run: |
SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-7)
BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d)
if [[ ! "${VERSION_NUM}" =~ ^[0-9] ]]; then
LEGAL_VERSION="v0.0.0-${VERSION_NUM}-${BUILD_DATE}-${SHORT_SHA}"
else
LEGAL_VERSION="v${VERSION_NUM}-${BUILD_DATE}-${SHORT_SHA}"
BUILD_DATE="${BUILD_DATE_OVERRIDE}"
if [[ -z "${BUILD_DATE}" ]]; then
BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d)
fi
if [[ ! "${VERSION_BASE}" =~ ^[0-9] ]]; then
VERSION_BASE="0.0.0-${VERSION_BASE}"
fi
if [[ "${PATCH_NUMBER}" != "0" ]]; then
LEGAL_VERSION="v${VERSION_BASE}-${PATCH_NUMBER}-${BUILD_DATE}"
else
LEGAL_VERSION="v${VERSION_BASE}-${BUILD_DATE}"
fi
echo "LEGAL_VERSION=${LEGAL_VERSION}" >> $GITHUB_ENV
echo "BUILD_DATE=${BUILD_DATE}" >> $GITHUB_ENV
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
submodules: recursive
@@ -120,22 +104,17 @@ jobs:
CUDA_PATH: /usr/local/cuda
XMAKE_GLOBALDIR: /data
run: |
apt install -y libxft-dev
xmake f --CROSSDESK_VERSION=${LEGAL_VERSION} --USE_CUDA=true --root -y
xmake b -vy --root crossdesk
- name: Decode and save certificate
shell: bash
run: |
mkdir -p certs
echo "${{ secrets.CROSSDESK_CERT_BASE64 }}" | base64 --decode > certs/crossdesk.cn_root.crt
- name: Package
run: |
chmod +x ${{ matrix.package_script }}
${{ matrix.package_script }} ${LEGAL_VERSION}
- name: Upload artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: crossdesk-linux-${{ matrix.arch }}-${{ env.LEGAL_VERSION }}
path: ${{ github.workspace }}/crossdesk-linux-${{ matrix.arch }}-${{ env.LEGAL_VERSION }}.deb
@@ -161,20 +140,49 @@ jobs:
steps:
- name: Extract version number
id: version
shell: bash
run: |
VERSION="${GITHUB_REF##*/}"
SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-7)
VERSION_REF="${GITHUB_REF##*/}"
VERSION_BASE="${VERSION_REF#v}"
BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d)
VERSION_NUM="v${VERSION#v}-${BUILD_DATE}-${SHORT_SHA}"
PATCH_NUMBER="${{ github.event.inputs.patch }}"
if [[ ! "${PATCH_NUMBER}" =~ ^[0-9]+$ ]]; then
PATCH_NUMBER=0
fi
if [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)-([0-9]{8})$ ]]; then
VERSION_BASE="${BASH_REMATCH[1]}"
PATCH_NUMBER="${BASH_REMATCH[3]}"
BUILD_DATE="${BASH_REMATCH[4]}"
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
VERSION_BASE="${BASH_REMATCH[1]}"
BUILD_DATE="${BASH_REMATCH[3]}"
PATCH_NUMBER="${BASH_REMATCH[4]}"
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})$ ]]; then
VERSION_BASE="${BASH_REMATCH[1]}"
BUILD_DATE="${BASH_REMATCH[3]}"
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)$ && "${PATCH_NUMBER}" == "0" ]]; then
VERSION_BASE="${BASH_REMATCH[1]}"
PATCH_NUMBER="${BASH_REMATCH[3]}"
fi
if [[ "${PATCH_NUMBER}" != "0" ]]; then
VERSION_NUM="v${VERSION_BASE}-${PATCH_NUMBER}-${BUILD_DATE}"
else
VERSION_NUM="v${VERSION_BASE}-${BUILD_DATE}"
fi
echo "VERSION_NUM=${VERSION_NUM}" >> $GITHUB_ENV
echo "VERSION_NUM=${VERSION_NUM}"
echo "BUILD_DATE=${BUILD_DATE}" >> $GITHUB_ENV
echo "PATCH_NUMBER=${PATCH_NUMBER}" >> $GITHUB_ENV
- name: Cache xmake dependencies
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: ~/.xmake/packages
key: ${{ runner.os }}-xmake-deps-${{ matrix.cache-key }}-${{ github.sha }}
key: "${{ runner.os }}-xmake-deps-${{ matrix.cache-key }}-${{ github.run_id }}"
restore-keys: |
${{ runner.os }}-xmake-deps-${{ matrix.cache-key }}-
@@ -182,7 +190,7 @@ jobs:
run: brew install xmake
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Initialize submodules
run: git submodule update --init --recursive
@@ -192,19 +200,13 @@ jobs:
xmake f --CROSSDESK_VERSION=${VERSION_NUM} --USE_CUDA=true -y
xmake b -vy crossdesk
- name: Decode and save certificate
shell: bash
run: |
mkdir -p certs
echo "${{ secrets.CROSSDESK_CERT_BASE64 }}" | base64 --decode > certs/crossdesk.cn_root.crt
- name: Package CrossDesk app
run: |
chmod +x ${{ matrix.package_script }}
${{ matrix.package_script }} ${VERSION_NUM}
- name: Upload build artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: crossdesk-macos-${{ matrix.arch }}-${{ env.VERSION_NUM }}
path: crossdesk-macos-${{ matrix.arch }}-${{ env.VERSION_NUM }}.pkg
@@ -228,16 +230,44 @@ jobs:
$version = $ref -replace '^refs/(tags|heads)/', ''
$version = $version -replace '^v', ''
$version = $version -replace '/', '-'
$SHORT_SHA = $env:GITHUB_SHA.Substring(0,7)
$BUILD_DATE = ([System.TimeZoneInfo]::ConvertTimeBySystemTimeZoneId((Get-Date), "China Standard Time")).ToString("yyyyMMdd")
echo "VERSION_NUM=v$version-$BUILD_DATE-$SHORT_SHA" >> $env:GITHUB_ENV
$PATCH_NUMBER = "${{ github.event.inputs.patch }}"
if ($PATCH_NUMBER -notmatch '^[0-9]+$') {
$PATCH_NUMBER = "0"
}
if ($version -match '^([0-9]+(\.[0-9]+){1,3})-([0-9]+)-([0-9]{8})$') {
$version = $Matches[1]
$PATCH_NUMBER = $Matches[3]
$BUILD_DATE = $Matches[4]
} elseif ($version -match '^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$') {
$version = $Matches[1]
$BUILD_DATE = $Matches[3]
$PATCH_NUMBER = $Matches[4]
} elseif ($version -match '^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})$') {
$version = $Matches[1]
$BUILD_DATE = $Matches[3]
} elseif ($version -match '^([0-9]+(\.[0-9]+){1,3})-([0-9]+)$' -and $PATCH_NUMBER -eq "0") {
$version = $Matches[1]
$PATCH_NUMBER = $Matches[3]
}
if ($PATCH_NUMBER -ne "0") {
$VERSION_NUM = "v$version-$PATCH_NUMBER-$BUILD_DATE"
} else {
$VERSION_NUM = "v$version-$BUILD_DATE"
}
echo "VERSION_NUM=$VERSION_NUM" >> $env:GITHUB_ENV
echo "BUILD_DATE=$BUILD_DATE" >> $env:GITHUB_ENV
echo "PATCH_NUMBER=$PATCH_NUMBER" >> $env:GITHUB_ENV
- name: Cache xmake dependencies
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: D:\xmake_global\.xmake\packages
key: ${{ runner.os }}-xmake-deps-intel-${{ github.sha }}
key: "${{ runner.os }}-xmake-deps-intel-${{ github.run_id }}"
restore-keys: |
${{ runner.os }}-xmake-deps-intel-
@@ -286,7 +316,7 @@ jobs:
Copy-Item $source $target -Force
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Initialize submodules
run: git submodule update --init --recursive
@@ -301,55 +331,120 @@ jobs:
xmake f --CROSSDESK_VERSION=${{ env.VERSION_NUM }} --USE_CUDA=true -y
xmake b -vy crossdesk
- name: Decode and save certificate
shell: powershell
run: |
New-Item -ItemType Directory -Force -Path certs
[System.IO.File]::WriteAllBytes('certs\crossdesk.cn_root.crt', [Convert]::FromBase64String('${{ secrets.CROSSDESK_CERT_BASE64 }}'))
- name: Package
shell: pwsh
run: |
cd "${{ github.workspace }}\scripts\windows"
makensis /DVERSION=$env:VERSION_NUM nsis_script.nsi
& "${{ github.workspace }}\scripts\windows\pkg_x64.ps1" $env:VERSION_NUM
- name: Build Portable CrossDesk
run: |
xmake f --CROSSDESK_VERSION=${{ env.VERSION_NUM }} --USE_CUDA=true --CROSSDESK_PORTABLE=true -y
xmake b -vy crossdesk
- name: Package Portable
shell: pwsh
run: |
$buildDir = "${{ github.workspace }}\build\windows\x64\release"
$portableDir = "${{ github.workspace }}\portable"
New-Item -ItemType Directory -Force -Path $portableDir
$portableFiles = @(
@("crossdesk.exe", "CrossDesk.exe"),
@("crossdesk_service.exe", "crossdesk_service.exe"),
@("crossdesk_session_helper.exe", "crossdesk_session_helper.exe")
)
foreach ($file in $portableFiles) {
$source = Join-Path $buildDir $file[0]
$destination = Join-Path $portableDir $file[1]
if (!(Test-Path $source)) {
throw "Missing portable package file: $source"
}
Copy-Item $source $destination -Force
}
Copy-Item (Join-Path $buildDir "*.dll") $portableDir -Force
foreach ($file in $portableFiles) {
$packagedFile = Join-Path $portableDir $file[1]
if (!(Test-Path $packagedFile)) {
throw "Portable package is missing: $packagedFile"
}
}
Compress-Archive -Path "$portableDir\*" -DestinationPath "${{ github.workspace }}\crossdesk-win-x64-portable-${{ env.VERSION_NUM }}.zip"
- name: Upload artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: crossdesk-win-x64-${{ env.VERSION_NUM }}
path: ${{ github.workspace }}/scripts/windows/crossdesk-win-x64-${{ env.VERSION_NUM }}.exe
- name: Upload portable artifact
uses: actions/upload-artifact@v6
with:
name: crossdesk-win-x64-portable-${{ env.VERSION_NUM }}
path: ${{ github.workspace }}/crossdesk-win-x64-portable-${{ env.VERSION_NUM }}.zip
release:
name: Publish Release
if: startsWith(github.ref, 'refs/tags/v')
needs:
[build-linux-amd64, build-linux-arm64, build-macos, build-windows-x64]
[build-linux, build-macos, build-windows-x64]
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Download all artifacts
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
path: artifacts
- name: Extract version number
id: version
shell: bash
run: |
VERSION="${GITHUB_REF##*/}"
SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-7)
VERSION_REF="${GITHUB_REF##*/}"
VERSION_BASE="${VERSION_REF#v}"
BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d)
BUILD_DATE_ISO=$(TZ=Asia/Shanghai date +%Y-%m-%d)
VERSION_NUM="${VERSION#v}-${BUILD_DATE}-${SHORT_SHA}"
PATCH_NUMBER="${{ github.event.inputs.patch }}"
if [[ ! "${PATCH_NUMBER}" =~ ^[0-9]+$ ]]; then
PATCH_NUMBER=0
fi
if [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)-([0-9]{8})$ ]]; then
VERSION_BASE="${BASH_REMATCH[1]}"
PATCH_NUMBER="${BASH_REMATCH[3]}"
BUILD_DATE="${BASH_REMATCH[4]}"
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
VERSION_BASE="${BASH_REMATCH[1]}"
BUILD_DATE="${BASH_REMATCH[3]}"
PATCH_NUMBER="${BASH_REMATCH[4]}"
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})$ ]]; then
VERSION_BASE="${BASH_REMATCH[1]}"
BUILD_DATE="${BASH_REMATCH[3]}"
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)$ && "${PATCH_NUMBER}" == "0" ]]; then
VERSION_BASE="${BASH_REMATCH[1]}"
PATCH_NUMBER="${BASH_REMATCH[3]}"
fi
BUILD_DATE_ISO="${BUILD_DATE:0:4}-${BUILD_DATE:4:2}-${BUILD_DATE:6:2}"
if [[ "${PATCH_NUMBER}" != "0" ]]; then
VERSION_NUM="${VERSION_BASE}-${PATCH_NUMBER}-${BUILD_DATE}"
else
VERSION_NUM="${VERSION_BASE}-${BUILD_DATE}"
fi
VERSION_WITH_V="v${VERSION_NUM}"
VERSION_ONLY="${VERSION#v}"
echo "VERSION_NUM=${VERSION_NUM}" >> $GITHUB_OUTPUT
echo "VERSION_WITH_V=${VERSION_WITH_V}" >> $GITHUB_OUTPUT
echo "VERSION_ONLY=${VERSION_ONLY}" >> $GITHUB_OUTPUT
echo "VERSION_BASE=${VERSION_BASE}" >> $GITHUB_OUTPUT
echo "BUILD_DATE=${BUILD_DATE}" >> $GITHUB_OUTPUT
echo "BUILD_DATE_ISO=${BUILD_DATE_ISO}" >> $GITHUB_OUTPUT
echo "PATCH_NUMBER=${PATCH_NUMBER}" >> $GITHUB_OUTPUT
- name: Rename artifacts
run: |
@@ -359,6 +454,7 @@ jobs:
cp artifacts/crossdesk-linux-amd64-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-linux-amd64-${{ steps.version.outputs.VERSION_WITH_V }}.deb
cp artifacts/crossdesk-linux-arm64-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-linux-arm64-${{ steps.version.outputs.VERSION_WITH_V }}.deb
cp artifacts/crossdesk-win-x64-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-win-x64-${{ steps.version.outputs.VERSION_WITH_V }}.exe
cp artifacts/crossdesk-win-x64-portable-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-win-x64-portable-${{ steps.version.outputs.VERSION_WITH_V }}.zip
- name: List release files
run: ls -lh release/
@@ -406,13 +502,22 @@ jobs:
run: |
cat > version.json << EOF
{
"version": "${{ steps.version.outputs.VERSION_ONLY }}",
"latest_version": "${{ steps.version.outputs.VERSION_NUM }}",
"version": "${{ steps.version.outputs.VERSION_NUM }}",
"releaseDate": "${{ steps.version.outputs.BUILD_DATE_ISO }}",
"patch": ${{ steps.version.outputs.PATCH_NUMBER }},
"releaseName": "",
"releaseNotes": "",
"tagName": "${{ steps.version.outputs.VERSION_WITH_V }}",
"downloads": {
"windows-x64": {
"url": "https://downloads.crossdesk.cn/crossdesk-win-x64-${{ steps.version.outputs.VERSION_WITH_V }}.exe",
"filename": "crossdesk-win-x64-${{ steps.version.outputs.VERSION_WITH_V }}.exe"
},
"windows-x64-portable": {
"url": "https://downloads.crossdesk.cn/crossdesk-win-x64-portable-${{ steps.version.outputs.VERSION_WITH_V }}.zip",
"filename": "crossdesk-win-x64-portable-${{ steps.version.outputs.VERSION_WITH_V }}.zip"
},
"macos-x64": {
"url": "https://downloads.crossdesk.cn/crossdesk-macos-x64-${{ steps.version.outputs.VERSION_WITH_V }}.pkg",
"filename": "crossdesk-macos-x64-${{ steps.version.outputs.VERSION_WITH_V }}.pkg"
+14 -25
View File
@@ -4,6 +4,7 @@ on:
schedule:
# run every day at midnight
- cron: "0 0 * * *"
workflow_dispatch:
permissions:
issues: write
@@ -15,19 +16,21 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check inactive issues and close them
uses: actions/github-script@v6
uses: actions/github-script@v7
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
script: |
const { data: issues } = await github.rest.issues.listForRepo({
const inactivePeriod = 7 * 24 * 60 * 60 * 1000; // 7 days
const now = Date.now();
// paginate through all open issues (listForRepo also returns PRs)
const issues = await github.paginate(github.rest.issues.listForRepo, {
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
per_page: 100,
});
const now = new Date().getTime();
const inactivePeriod = 7 * 24 * 60 * 60 * 1000; // 7 days
for (const issue of issues) {
// skip pull requests (they are also returned by listForRepo)
if (issue.pull_request) continue;
@@ -38,26 +41,14 @@ jobs:
continue;
}
// fetch comments for this issue
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
per_page: 100,
});
// determine the "last activity" time
let lastActivityTime;
if (comments.length > 0) {
const lastComment = comments[comments.length - 1];
lastActivityTime = new Date(lastComment.updated_at).getTime();
} else {
lastActivityTime = new Date(issue.created_at).getTime();
}
// last activity time = the issue's own updated_at, which is
// refreshed on comments, labels, etc. This avoids relying on
// fetching comments and is accurate even when comments are edited.
const lastActivityTime = new Date(issue.updated_at).getTime();
// check inactivity
if (now - lastActivityTime > inactivePeriod) {
console.log(`Closing inactive issue: #${issue.number} (No recent replies for 7 days)`);
console.log(`Closing inactive issue: #${issue.number} (No activity for 7 days)`);
await github.rest.issues.createComment({
owner: context.repo.owner,
@@ -76,5 +67,3 @@ jobs:
console.log(`Skipping issue #${issue.number} (Active within 7 days).`);
}
}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+164
View File
@@ -0,0 +1,164 @@
name: Update version.json from Release
on:
release:
types: [published, edited]
permissions:
contents: write
jobs:
update-version-json:
name: Update version.json with release information
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Extract version from tag
id: version
shell: bash
run: |
TAG_NAME="${{ github.event.release.tag_name }}"
TAG_VERSION="${TAG_NAME#v}"
VERSION_FULL="${TAG_VERSION}"
VERSION_BASE="${TAG_VERSION}"
PATCH_NUMBER=0
# Extract date and patch from tags such as v1.2.3-1-20251113.
if [[ "${TAG_VERSION}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)-([0-9]{8})$ ]]; then
VERSION_BASE="${BASH_REMATCH[1]}"
PATCH_NUMBER="${BASH_REMATCH[3]}"
DATE_STR="${BASH_REMATCH[4]}"
BUILD_DATE_ISO="${DATE_STR:0:4}-${DATE_STR:4:2}-${DATE_STR:6:2}"
elif [[ "${TAG_VERSION}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
VERSION_BASE="${BASH_REMATCH[1]}"
DATE_STR="${BASH_REMATCH[3]}"
PATCH_NUMBER="${BASH_REMATCH[4]}"
BUILD_DATE_ISO="${DATE_STR:0:4}-${DATE_STR:4:2}-${DATE_STR:6:2}"
elif [[ "${TAG_VERSION}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})$ ]]; then
VERSION_BASE="${BASH_REMATCH[1]}"
DATE_STR="${BASH_REMATCH[3]}"
BUILD_DATE_ISO="${DATE_STR:0:4}-${DATE_STR:4:2}-${DATE_STR:6:2}"
else
# Use release published date
BUILD_DATE_ISO=$(echo "${{ github.event.release.published_at }}" | cut -d'T' -f1)
fi
echo "TAG_NAME=${TAG_NAME}" >> $GITHUB_OUTPUT
echo "VERSION_FULL=${VERSION_FULL}" >> $GITHUB_OUTPUT
echo "VERSION_BASE=${VERSION_BASE}" >> $GITHUB_OUTPUT
echo "BUILD_DATE_ISO=${BUILD_DATE_ISO}" >> $GITHUB_OUTPUT
echo "PATCH_NUMBER=${PATCH_NUMBER}" >> $GITHUB_OUTPUT
- name: Install jq
run: sudo apt-get update && sudo apt-get install -y jq
- name: Get release information
id: release_info
run: |
# Use jq to properly escape JSON
RELEASE_BODY="${{ github.event.release.body }}"
RELEASE_NAME="${{ github.event.release.name }}"
# Handle empty values
if [ -z "$RELEASE_BODY" ]; then
RELEASE_BODY=""
fi
if [ -z "$RELEASE_NAME" ]; then
RELEASE_NAME=""
fi
# Save to temporary files for proper handling
echo -n "$RELEASE_BODY" > /tmp/release_body.txt
echo -n "$RELEASE_NAME" > /tmp/release_name.txt
# Use jq to escape JSON strings
RELEASE_BODY_JSON=$(jq -Rs . < /tmp/release_body.txt)
RELEASE_NAME_JSON=$(jq -Rs . < /tmp/release_name.txt)
echo "RELEASE_BODY=${RELEASE_BODY_JSON}" >> $GITHUB_OUTPUT
echo "RELEASE_NAME=${RELEASE_NAME_JSON}" >> $GITHUB_OUTPUT
- name: Download current version.json from server
id: download_version
continue-on-error: true
run: |
# Try to download current version.json from server
curl -f -s "https://version.crossdesk.cn/version.json" -o version.json || echo "Failed to download, will create new one"
- name: Generate or update version.json
run: |
# If version.json exists, try to preserve downloads section
if [ -f version.json ] && jq -e '.downloads' version.json > /dev/null 2>&1; then
EXISTING_DOWNLOADS=$(jq -c '.downloads' version.json)
if [ "$EXISTING_DOWNLOADS" != "null" ] && [ "$EXISTING_DOWNLOADS" != "{}" ]; then
DOWNLOADS_JSON="$EXISTING_DOWNLOADS"
else
DOWNLOADS_JSON=""
fi
else
DOWNLOADS_JSON=""
fi
# If downloads is empty, use default structure
if [ -z "$DOWNLOADS_JSON" ]; then
DOWNLOADS_JSON=$(cat << DOWNLOADS_EOF
{
"windows-x64": {
"url": "https://downloads.crossdesk.cn/crossdesk-win-x64-${{ steps.version.outputs.TAG_NAME }}.exe",
"filename": "crossdesk-win-x64-${{ steps.version.outputs.TAG_NAME }}.exe"
},
"windows-x64-portable": {
"url": "https://downloads.crossdesk.cn/crossdesk-win-x64-portable-${{ steps.version.outputs.TAG_NAME }}.zip",
"filename": "crossdesk-win-x64-portable-${{ steps.version.outputs.TAG_NAME }}.zip"
},
"macos-x64": {
"url": "https://downloads.crossdesk.cn/crossdesk-macos-x64-${{ steps.version.outputs.TAG_NAME }}.pkg",
"filename": "crossdesk-macos-x64-${{ steps.version.outputs.TAG_NAME }}.pkg"
},
"macos-arm64": {
"url": "https://downloads.crossdesk.cn/crossdesk-macos-arm64-${{ steps.version.outputs.TAG_NAME }}.pkg",
"filename": "crossdesk-macos-arm64-${{ steps.version.outputs.TAG_NAME }}.pkg"
},
"linux-amd64": {
"url": "https://downloads.crossdesk.cn/crossdesk-linux-amd64-${{ steps.version.outputs.TAG_NAME }}.deb",
"filename": "crossdesk-linux-amd64-${{ steps.version.outputs.TAG_NAME }}.deb"
},
"linux-arm64": {
"url": "https://downloads.crossdesk.cn/crossdesk-linux-arm64-${{ steps.version.outputs.TAG_NAME }}.deb",
"filename": "crossdesk-linux-arm64-${{ steps.version.outputs.TAG_NAME }}.deb"
}
}
DOWNLOADS_EOF
)
fi
# Generate version.json using cat and heredoc
cat > version.json << EOF
{
"latest_version": "${{ steps.version.outputs.VERSION_FULL }}",
"version": "${{ steps.version.outputs.VERSION_FULL }}",
"releaseDate": "${{ steps.version.outputs.BUILD_DATE_ISO }}",
"patch": ${{ steps.version.outputs.PATCH_NUMBER }},
"releaseName": ${{ steps.release_info.outputs.RELEASE_NAME }},
"releaseNotes": ${{ steps.release_info.outputs.RELEASE_BODY }},
"tagName": "${{ steps.version.outputs.TAG_NAME }}",
"downloads": ${DOWNLOADS_JSON}
}
EOF
cat version.json
- name: Upload version.json to server
uses: burnett01/rsync-deployments@5.2
with:
switches: -avzr --delete
path: version.json
remote_path: /var/www/html/version/
remote_host: ${{ secrets.SERVER_HOST }}
remote_user: ${{ secrets.SERVER_USER }}
remote_key: ${{ secrets.SERVER_KEY }}
+1 -2
View File
@@ -6,5 +6,4 @@ build/
.DS_Store
# VSCode cache
.vscode
continuous-desk.code-workspace
.vscode
+91 -134
View File
@@ -1,5 +1,6 @@
# CrossDesk
<a href="https://hellogithub.com/repository/kunkundi/crossdesk" target="_blank"><img src="https://api.hellogithub.com/v1/widgets/recommend.svg?rid=55d41367570345f1838e02fd12be7961&claim_uid=cb0OpZRrBuGVAfL&theme=small" alt="FeaturedHelloGitHub" /></a>
[![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20Linux%20%7C%20macOS-brightgreen.svg)]()
[![License: LGPL v3](https://img.shields.io/badge/License-LGPL%20v3-blue.svg)](https://www.gnu.org/licenses/lgpl-3.0)
[![GitHub last commit](https://img.shields.io/github/last-commit/kunkundi/crossdesk)](https://github.com/kunkundi/crossdesk/commits/web-client)
@@ -52,6 +53,29 @@ CrossDesk 是 [MiniRTC](https://github.com/kunkundi/minirtc.git) 实时音视频
<img width="645" height="300" alt="_cgi-bin_mmwebwx-bin_webwxgetmsgimg__ MsgID=932911462648581698 skey=@crypt_1f5153b1_b550ca7462b5009ce03c991cca2a92a7 mmweb_appid=wx_webfilehelper" src="https://github.com/user-attachments/assets/a5109e6f-752c-4654-9f4e-7e161bddf43e" />
### Windows 服务(CrossDesk Service
CrossDesk 在 Windows 平台提供本地辅助服务 **CrossDesk Service**,服务名为 `CrossDeskService`。该服务用于锁屏、登录界面和安全桌面等受保护场景下的远程控制增强能力,包括:
- 上报远端当前是否处于锁屏、登录、凭据输入或安全桌面状态;
- 支持从控制端发送 `Ctrl+Alt+Del`SAS);
- 在锁屏、登录和安全桌面阶段转发键盘、鼠标输入。
Windows 安装包会自动打包 `crossdesk_service.exe``crossdesk_session_helper.exe`,并在安装时注册为按需启动的 Windows 服务。CrossDesk 客户端启动时会尝试启动已安装的服务;当本机没有 CrossDesk 客户端进程运行时,服务会自动退出。卸载客户端时会同步停止并移除该服务。
如果是手动编译或手动部署 Windows 版本,请确保 `CrossDesk.exe``crossdesk_service.exe``crossdesk_session_helper.exe` 位于同一目录。安装或卸载服务需要使用管理员权限打开 PowerShell
```
# 安装
.\CrossDesk.exe --service-install
# 启动
.\CrossDesk.exe --service-start
# 查看状态
.\CrossDesk.exe --service-status
.\CrossDesk.exe --service-stop
# 卸载
.\CrossDesk.exe --service-uninstall
```
如果远端 Windows 服务未安装、未启动或暂时不可用,基础远程桌面连接仍可使用,但锁屏、登录界面和安全桌面阶段的控制能力会受限,客户端会提示“远端Windows服务不可用”。
## 如何编译
依赖:
@@ -168,7 +192,7 @@ xmake r -d crossdesk
## 自托管服务器
推荐使用Docker部署CrossDesk Server。
```
```bash
sudo docker run -d \
--name crossdesk_server \
--network host \
@@ -178,158 +202,91 @@ sudo docker run -d \
-e COTURN_PORT=xxxx \
-e MIN_PORT=xxxxx \
-e MAX_PORT=xxxxx \
-v /path/to/your/certs:/crossdesk-server/certs \
-v /path/to/your/db:/crossdesk-server/db \
-v /path/to/your/logs:/crossdesk-server/logs \
crossdesk/crossdesk-server:v1.1.1
-v /var/lib/crossdesk:/var/lib/crossdesk \
-v /var/log/crossdesk:/var/log/crossdesk \
crossdesk/crossdesk-server:v1.1.6
```
上述命令中,用户需注意的参数如下:
**参数**
- EXTERNAL_IP:服务器公网 IP , 对应 CrossDesk 客户端**自托管服务器配置**中填写的**服务器地址**
- INTERNAL_IP:服务器内网 IP
- CROSSDESK_SERVER_PORT:自托管服务使用的端口,对应 CrossDesk 客户端**自托管服务器配置**中填写的**服务器端口**
- COTURN_PORT: COTURN 服务使用的端口, 对应 CrossDesk 客户端**自托管服务器配置**中填写的**中继服务端口**
- MIN_PORT/MAX_PORTCOTURN 服务使用的端口范围,例如:MIN_PORT=50000, MAX_PORT=60000,范围可根据客户端数量调整。
- `-v /var/lib/crossdesk:/var/lib/crossdesk`:持久化数据库和证书文件到宿主机
- `-v /var/log/crossdesk:/var/log/crossdesk`:持久化日志文件到宿主机
- /path/to/your/certs:证书文件目录
- /path/to/your/dbCrossDesk Server 设备管理数据库
- /path/to/your/logs:日志目录
**示例**
```bash
sudo docker run -d \
--name crossdesk_server \
--network host \
-e EXTERNAL_IP=114.114.114.114 \
-e INTERNAL_IP=10.0.0.1 \
-e CROSSDESK_SERVER_PORT=9099 \
-e COTURN_PORT=3478 \
-e MIN_PORT=50000 \
-e MAX_PORT=60000 \
-v /var/lib/crossdesk:/var/lib/crossdesk \
-v /var/log/crossdesk:/var/log/crossdesk \
crossdesk/crossdesk-server:v1.1.6
```
**注意**
- **/path/to/your/ 是示例路径,请替换为你自己的实际路径。挂载的目录必须事先创建好,否则容器会报错。**
- **服务器需开放端口:3478/udp3478/tcpMIN_PORT-MAX_PORT/udpCROSSDESK_SERVER_PORT/tcp。**
- **服务器需开放端口:COTURN_PORT/udpCOTURN_PORT/tcpMIN_PORT-MAX_PORT/udpCROSSDESK_SERVER_PORT/tcp。**
- 如果不挂载 volume,容器删除后数据会丢失
- 证书文件会在首次启动时自动生成并持久化到宿主机的 `/var/lib/crossdesk/certs` 路径下。由于默认使用的是自签证书,无法保障安全性,建议在云服务商申请正式证书放到该目录下并重启服务。
- 数据库文件会自动创建并持久化到宿主机的 `/var/lib/crossdesk/db/crossdesk-server.db` 路径下
- 日志文件会自动创建并持久化到宿主机的 `/var/log/crossdesk/` 路径下
## 证书文件
客户端需加载根证书文件,服务端需加载服务器私钥和服务器证书文件。
如果已有SSL证书的用户,可以忽略下面的证书生成步骤。
对于无证书的用户,可使用下面的脚本自行生成证书文件
```
# 创建证书生成脚本
vim generate_certs.sh
```
拷贝到脚本中
```
#!/bin/bash
set -e
# 检查参数
if [ "$#" -ne 1 ]; then
echo "Usage: $0 <SERVER_IP>"
exit 1
fi
SERVER_IP="$1"
# 文件名
ROOT_KEY="crossdesk.cn_root.key"
ROOT_CERT="crossdesk.cn_root.crt"
SERVER_KEY="crossdesk.cn.key"
SERVER_CSR="crossdesk.cn.csr"
SERVER_CERT="crossdesk.cn_bundle.crt"
FULLCHAIN_CERT="crossdesk.cn_fullchain.crt"
# 证书主题
SUBJ="/C=CN/ST=Zhejiang/L=Hangzhou/O=CrossDesk/OU=CrossDesk/CN=$SERVER_IP"
# 1. 生成根证书
echo "Generating root private key..."
openssl genrsa -out "$ROOT_KEY" 4096
echo "Generating self-signed root certificate..."
openssl req -x509 -new -nodes -key "$ROOT_KEY" -sha256 -days 3650 -out "$ROOT_CERT" -subj "$SUBJ"
# 2. 生成服务器私钥
echo "Generating server private key..."
openssl genrsa -out "$SERVER_KEY" 2048
# 3. 生成服务器 CSR
echo "Generating server CSR..."
openssl req -new -key "$SERVER_KEY" -out "$SERVER_CSR" -subj "$SUBJ"
# 4. 生成临时 OpenSSL 配置文件,加入 SAN
SAN_CONF="san.cnf"
cat > $SAN_CONF <<EOL
[ req ]
default_bits = 2048
distinguished_name = req_distinguished_name
req_extensions = req_ext
prompt = no
[ req_distinguished_name ]
C = CN
ST = Zhejiang
L = Hangzhou
O = CrossDesk
OU = CrossDesk
CN = $SERVER_IP
[ req_ext ]
subjectAltName = IP:$SERVER_IP
EOL
# 5. 用根证书签发服务器证书(包含 SAN)
echo "Signing server certificate with root certificate..."
openssl x509 -req -in "$SERVER_CSR" -CA "$ROOT_CERT" -CAkey "$ROOT_KEY" -CAcreateserial \
-out "$SERVER_CERT" -days 3650 -sha256 -extfile "$SAN_CONF" -extensions req_ext
# 6. 生成完整链证书
cat "$SERVER_CERT" "$ROOT_CERT" > "$FULLCHAIN_CERT"
# 7. 清理中间文件
rm -f "$ROOT_CERT.srl" "$SAN_CONF" "$ROOT_KEY" "$SERVER_CSR" "FULLCHAIN_CERT"
echo "Generation complete. Deployment files:"
echo " Client root certificate: $ROOT_CERT"
echo " Server private key: $SERVER_KEY"
echo " Server certificate: $SERVER_CERT"
```
执行
```
chmod +x generate_certs.sh
./generate_certs.sh 服务器公网IP
# 例如 ./generate_certs.sh 111.111.111.111
```
输出如下:
```
Generating root private key...
Generating self-signed root certificate...
Generating server private key...
Generating server CSR...
Signing server certificate with root certificate...
Certificate request self-signature ok
subject=C = CN, ST = Zhejiang, L = Hangzhou, O = CrossDesk, OU = CrossDesk, CN = xxx.xxx.xxx.xxx
cleaning up intermediate files...
Generation complete. Deployment files::
Client root certificate:: crossdesk.cn_root.crt
Server private key: crossdesk.cn.key
Server certificate: crossdesk.cn_bundle.crt
**权限注意**:如果 Docker 自动创建的目录权限不足(属于 root),容器内用户无法写入,会导致:
- 证书生成失败,容器启动脚本会报错退出
- 数据库目录创建失败,程序会抛出异常并崩溃
- 日志目录创建失败,日志文件无法写入(但程序可能继续运行)
**解决方案**:在启动容器前手动设置权限
```bash
sudo mkdir -p /var/lib/crossdesk /var/log/crossdesk
sudo chown -R $(id -u):$(id -g) /var/lib/crossdesk /var/log/crossdesk
```
#### 服务端
**crossdesk.cn.key****crossdesk.cn_bundle.crt** 放置到 **/path/to/your/certs** 目录下。
#### 客户端
1. 点击右上角设置进入设置页面。<br>
<img width="600" height="210" alt="image" src="https://github.com/user-attachments/assets/6431131d-b32a-4726-8783-6788f47baa3b" /><br><br>
### 客户端
1. 点击右上角设置进入设置页面。<br><br>
<img width="600" height="210" alt="image" src="https://github.com/user-attachments/assets/6431131d-b32a-4726-8783-6788f47baa3b" /><br>
3. 点击点击**自托管服务器配置**。<br><br>
<img width="600" height="160" alt="image" src="https://github.com/user-attachments/assets/24c761a3-1985-4d7e-84be-787383c2afb8" /><br><br>
2. 点击`自托管服务器配置`按钮。<br><br>
<img width="600" height="160" alt="image" src="https://github.com/user-attachments/assets/24c761a3-1985-4d7e-84be-787383c2afb8" /><br>
5. 在**证书文件路径**选择框中找到 **crossdesk.cn_root.crt** 的存放路径,选中 **crossdesk.cn_root.crt**,点击确认。<br><br>
<img width="600" height="220" alt="image" src="https://github.com/user-attachments/assets/4af7cd3a-c72e-44fb-b032-30e050019c2a" /><br><br>
3. 输入`服务器地址`(**EXTERNAL_IP**)、`信令服务端口`(**CROSSDESK_SERVER_PORT**)、`中继服务端口`(**COTURN_PORT**),点击确认按钮
4. 勾选`自托管服务器配置`选项,点击确认按钮保存设置。如果服务端使用的是正式证书,则到此步骤为止,客户端即可显示已连接服务器。
7. 勾选使用**自托管服务器配置**,点击确认配置生效。<br><br>
<img width="600" height="160" alt="image" src="https://github.com/user-attachments/assets/1e455dc3-4087-4f37-a544-1ff9f8789383" /><br><br>
5. 如果使用默认证书(正式证书忽略此步骤),则需要将服务端`/var/lib/crossdesk/certs/`目录下的`api.crossdesk.cn_root.crt`自签根证书下载到运行客户端的机器,并执行下述命令安装证书:
Windows 平台使用**管理员权限**打开 PowerShell 执行
```
certutil -addstore "Root" "C:\path\to\api.crossdesk.cn_root.crt"
```
Linux
```
sudo cp /path/to/api.crossdesk.cn_root.crt /usr/local/share/ca-certificates/api.crossdesk.cn_root.crt
sudo update-ca-certificates
```
macOS
```
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain path/to/api.crossdesk.cn_root.crt
```
### Web 客户端
详情见项目 [CrossDesk Web Client](https://github.com/kunkundi/crossdesk-web-client)。
# 常见问题
见 [常见问题](https://github.com/kunkundi/crossdesk/blob/self-hosted-server/docs/FAQ.md) 。
# 致谢
- 感谢 [HelloGitHub](https://hellogithub.com/) 的推荐与关注。
- 感谢 [阮一峰的科技爱好者周刊](https://github.com/ruanyf/weekly) 的收录与推荐。
- 感谢 [LinuxDo](https://linux.do) 社区的关注、交流与支持,为 CrossDesk 项目的完善提供了帮助。
+103 -140
View File
@@ -1,5 +1,6 @@
# CrossDesk
<a href="https://hellogithub.com/repository/kunkundi/crossdesk" target="_blank"><img src="https://api.hellogithub.com/v1/widgets/recommend.svg?rid=55d41367570345f1838e02fd12be7961&claim_uid=cb0OpZRrBuGVAfL&theme=small" alt="FeaturedHelloGitHub" /></a>
[![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20Linux%20%7C%20macOS-brightgreen.svg)]()
[![License: LGPL v3](https://img.shields.io/badge/License-LGPL%20v3-blue.svg)](https://www.gnu.org/licenses/lgpl-3.0)
[![GitHub last commit](https://img.shields.io/github/last-commit/kunkundi/crossdesk)](https://github.com/kunkundi/crossdesk/commits/web-client)
@@ -55,6 +56,31 @@ Enter the **Remote Device ID** and **Password**, then click Connect to access th
<img width="645" height="300" alt="_cgi-bin_mmwebwx-bin_webwxgetmsgimg__ MsgID=932911462648581698 skey=@crypt_1f5153b1_b550ca7462b5009ce03c991cca2a92a7 mmweb_appid=wx_webfilehelper" src="https://github.com/user-attachments/assets/a5109e6f-752c-4654-9f4e-7e161bddf43e" />
### Windows Service (CrossDesk Service)
CrossDesk provides a local helper service on Windows named **CrossDesk Service**. Its service name is `CrossDeskService`. The service improves remote control in protected Windows states such as the lock screen, sign-in UI, credential UI, and secure desktop. It provides:
- Remote status reporting for lock screen, sign-in, credential, and secure desktop states.
- Remote `Ctrl+Alt+Del` (SAS) delivery.
- Keyboard and mouse input forwarding while the remote Windows device is on the lock screen, sign-in UI, or secure desktop.
The Windows installer bundles `crossdesk_service.exe` and `crossdesk_session_helper.exe`, then registers the service as an on-demand Windows service during installation. When the CrossDesk client starts, it tries to start the installed service automatically. When no CrossDesk client process is running on the machine, the service exits automatically. Uninstalling the client also stops and removes the service.
For manual Windows builds or deployments, make sure `CrossDesk.exe`, `crossdesk_service.exe`, and `crossdesk_session_helper.exe` are placed in the same directory. Open PowerShell with administrator privileges to install or uninstall the service:
```
# install
.\CrossDesk.exe --service-install
# start
.\CrossDesk.exe --service-start
# check status
.\CrossDesk.exe --service-status
.\CrossDesk.exe --service-ping
# stop
.\CrossDesk.exe --service-stop
# uninstall
.\CrossDesk.exe --service-uninstall
```
If the remote Windows service is not installed, not running, or temporarily unavailable, the basic remote desktop connection still works, but remote control on the lock screen, sign-in UI, and secure desktop is limited. The client will show “Remote Windows service unavailable”.
## How to build
Requirements:
@@ -186,158 +212,95 @@ sudo docker run -d \
-e COTURN_PORT=xxxx \
-e MIN_PORT=xxxxx \
-e MAX_PORT=xxxxx \
-v /path/to/your/certs:/crossdesk-server/certs \
-v /path/to/your/db:/crossdesk-server/db \
-v /path/to/your/logs:/crossdesk-server/logs \
crossdesk/crossdesk-server:v1.1.1
-v /var/lib/crossdesk:/var/lib/crossdesk \
-v /var/log/crossdesk:/var/log/crossdesk \
crossdesk/crossdesk-server:v1.1.6
```
The parameters you need to pay attention to are as follows:
- **EXTERNAL_IP**: The server's public IP, corresponding to the **Server Address** in the CrossDesk client **Self-Hosted Server Configuration**.
**Parameters**
- **EXTERNAL_IP**: The servers public IP. This corresponds to **Server Address** in the CrossDesk clients **Self-Hosted Server Configuration**.
- **INTERNAL_IP**: The servers internal IP.
- **CROSSDESK_SERVER_PORT**: The port used by the self-hosted service. This corresponds to **Server Port** in the CrossDesk clients **Self-Hosted Server Configuration**.
- **COTURN_PORT**: The port used by the COTURN service. This corresponds to **Relay Service Port** in the CrossDesk clients **Self-Hosted Server Configuration**.
- **MIN_PORT / MAX_PORT**: The port range used by the COTURN service. Example: `MIN_PORT=50000`, `MAX_PORT=60000`. Adjust the range depending on the number of clients.
- `-v /var/lib/crossdesk:/var/lib/crossdesk`: Persists database and certificate files on the host machine.
- `-v /var/log/crossdesk:/var/log/crossdesk`: Persists log files on the host machine.
- **INTERNAL_IP**: The server's internal IP.
- **CROSSDESK_SERVER_PORT**: The port used by the self-hosted server, corresponding to the **Server Port** in the CrossDesk client **Self-Hosted Server Configuration**.
- **COTURN_PORT**: The port used by Coturn, corresponding to the **Relay Server Port** in the CrossDesk client **Self-Hosted Server Configuration**.
- **MIN_PORT** and **MAX_PORT**: The range of ports used by the self-hosted server, corresponding to the **Minimum Port** and **Maximum Port** in the CrossDesk client **Self-Hosted Server Configuration**. Example: 50000-60000. It depends on the number of devices connected to the server.
- **/path/to/your/certs**: Directory for certificate files.
- **/path/to/your/db**: CrossDesk Server device management database.
- **/path/to/your/logs**: Log directory.
**Note**:
- **/path/to/your/ is an example path; please replace it with your actual path. The mounted directories must be created in advance, otherwise the container will fail.**
- **The server must open the following ports: 3478/udp, 3478/tcp, 30000-60000/udp, CROSSDESK_SERVER_PORT/tcp.**
## Certificate Files
The client needs to load the root certificate, and the server needs to load the server private key and server certificate.
If you already have an SSL certificate, you can skip the following certificate generation steps.
For users without a certificate, you can use the script below to generate the certificate files:
```
# Create certificate generation script
vim generate_certs.sh
```
Copy the following into the script:
```
#!/bin/bash
set -e
# Check arguments
if [ "$#" -ne 1 ]; then
echo "Usage: $0 <SERVER_IP>"
exit 1
fi
SERVER_IP="$1"
# Filenames
ROOT_KEY="crossdesk.cn_root.key"
ROOT_CERT="crossdesk.cn_root.crt"
SERVER_KEY="crossdesk.cn.key"
SERVER_CSR="crossdesk.cn.csr"
SERVER_CERT="crossdesk.cn_bundle.crt"
FULLCHAIN_CERT="crossdesk.cn_fullchain.crt"
# Certificate subject
SUBJ="/C=CN/ST=Zhejiang/L=Hangzhou/O=CrossDesk/OU=CrossDesk/CN=$SERVER_IP"
# 1. Generate root certificate
echo "Generating root private key..."
openssl genrsa -out "$ROOT_KEY" 4096
echo "Generating self-signed root certificate..."
openssl req -x509 -new -nodes -key "$ROOT_KEY" -sha256 -days 3650 -out "$ROOT_CERT" -subj "$SUBJ"
# 2. Generate server private key
echo "Generating server private key..."
openssl genrsa -out "$SERVER_KEY" 2048
# 3. Generate server CSR
echo "Generating server CSR..."
openssl req -new -key "$SERVER_KEY" -out "$SERVER_CSR" -subj "$SUBJ"
# 4. Create temporary OpenSSL config file with SAN
SAN_CONF="san.cnf"
cat > $SAN_CONF <<EOL
[ req ]
default_bits = 2048
distinguished_name = req_distinguished_name
req_extensions = req_ext
prompt = no
[ req_distinguished_name ]
C = CN
ST = Zhejiang
L = Hangzhou
O = CrossDesk
OU = CrossDesk
CN = $SERVER_IP
[ req_ext ]
subjectAltName = IP:$SERVER_IP
EOL
# 5. Sign server certificate with root certificate (including SAN)
echo "Signing server certificate with root certificate..."
openssl x509 -req -in "$SERVER_CSR" -CA "$ROOT_CERT" -CAkey "$ROOT_KEY" -CAcreateserial \
-out "$SERVER_CERT" -days 3650 -sha256 -extfile "$SAN_CONF" -extensions req_ext
# 6. Generate full chain certificate
cat "$SERVER_CERT" "$ROOT_CERT" > "$FULLCHAIN_CERT"
# 7. Clean up intermediate files
rm -f "$ROOT_CERT.srl" "$SAN_CONF" "$ROOT_KEY" "$SERVER_CSR" "FULLCHAIN_CERT"
echo "Generation complete. Deployment files:"
echo " Client root certificate: $ROOT_CERT"
echo " Server private key: $SERVER_KEY"
echo " Server certificate: $SERVER_CERT"
```
Execute:
```
chmod +x generate_certs.sh
./generate_certs.sh EXTERNAL_IP
# example ./generate_certs.sh 111.111.111.111
```
Expected output:
```
Generating root private key...
Generating self-signed root certificate...
Generating server private key...
Generating server CSR...
Signing server certificate with root certificate...
Certificate request self-signature ok
subject=C = CN, ST = Zhejiang, L = Hangzhou, O = CrossDesk, OU = CrossDesk, CN = xxx.xxx.xxx.xxx
cleaning up intermediate files...
Generation complete. Deployment files::
Client root certificate:: crossdesk.cn_root.crt
Server private key: crossdesk.cn.key
Server certificate: crossdesk.cn_bundle.crt
**Example**:
```bash
sudo docker run -d \
--name crossdesk_server \
--network host \
-e EXTERNAL_IP=114.114.114.114 \
-e INTERNAL_IP=10.0.0.1 \
-e CROSSDESK_SERVER_PORT=9099 \
-e COTURN_PORT=3478 \
-e MIN_PORT=50000 \
-e MAX_PORT=60000 \
-v /var/lib/crossdesk:/var/lib/crossdesk \
-v /var/log/crossdesk:/var/log/crossdesk \
crossdesk/crossdesk-server:v1.1.6
```
#### Server Side
**Notes**
- **The server must open the following ports: COTURN_PORT/udp, COTURN_PORT/tcp, MIN_PORTMAX_PORT/udp, and CROSSDESK_SERVER_PORT/tcp.**
- If you dont mount volumes, all data will be lost when the container is removed.
- Certificate files will be automatically generated on first startup and persisted to the host at `/var/lib/crossdesk/certs`.As the default certificates are self-signed and cannot guarantee security, it is strongly recommended to apply for a trusted certificate from a cloud provider, deploy it to this directory, and restart the service.
- The database file will be automatically created and stored at `/var/lib/crossdesk/db/crossdesk-server.db`.
- Log files will be created and stored at `/var/log/crossdesk/`.
**Permission Notice**
If the directories automatically created by Docker belong to root and have insufficient write permissions, the container user may not be able to write to them. This can cause:
- Certificate generation failure, leading to startup script errors and container exit.
- Database directory creation failure, causing the program to throw exceptions and crash.
- Log directory creation failure, preventing logs from being written (though the program may continue running).
**Solution:** Manually set permissions before starting the container:
```bash
sudo mkdir -p /var/lib/crossdesk /var/log/crossdesk
sudo chown -R $(id -u):$(id -g) /var/lib/crossdesk /var/log/crossdesk
```
### Server Side
Place **crossdesk.cn.key** and **crossdesk.cn_bundle.crt** into the **/path/to/your/certs** directory.
#### Client Side
1. Click the settings icon in the top-right corner to enter the settings page.<br>
<img width="600" height="210" alt="image" src="https://github.com/user-attachments/assets/6431131d-b32a-4726-8783-6788f47baa3b" /><br><br>
### Client Side
1. Click the settings icon in the top-right corner to enter the settings page.<br><br>
<img width="600" height="210" alt="image" src="https://github.com/user-attachments/assets/6431131d-b32a-4726-8783-6788f47baa3b" /><br>
2. Click **Self-Hosted Server Configuration**.<br><br>
<img width="600" height="160" alt="image" src="https://github.com/user-attachments/assets/24c761a3-1985-4d7e-84be-787383c2afb8" /><br><br>
2. Click `Self-Hosted Server Configuration` button.<br><br>
<img width="600" height="160" alt="image" src="https://github.com/user-attachments/assets/24c761a3-1985-4d7e-84be-787383c2afb8" /><br>
3. In the **Certificate File Path** selection, locate and select the **crossdesk.cn_root.crt** file.<br><br>
<img width="600" height="220" alt="image" src="https://github.com/user-attachments/assets/4af7cd3a-c72e-44fb-b032-30e050019c2a" /><br><br>
3. Enter the `Server Address` (**EXTERNAL_IP**), `Signaling Service Port` (**CROSSDESK_SERVER_PORT**), and `Relay Service Port` (**COTURN_PORT**) and click OK button.
4. Check the option to use **Self-Hosted Server Configuration**.<br><br>
<img width="600" height="160" alt="image" src="https://github.com/user-attachments/assets/1e455dc3-4087-4f37-a544-1ff9f8789383" /><br><br>
4. Check the `Self-hosted server configuration` option and click the OK button to save the settings. If the server is using a valid (official) certificate, the process ends here and the client will show that it is connected to the server.
5. If the default certificate is used (skip this step if an official certificate is used), download the self-signed root certificate `api.crossdesk.cn_root.crt` from the server directory /var/lib/crossdesk/certs/ to the machine running the client, and install the certificate by executing the following command:
On Windows, open PowerShell with **administrator privileges** and execute:
```
certutil -addstore "Root" "C:\path\to\api.crossdesk.cn_root.crt"
```
Linux
```
sudo cp /path/to/api.crossdesk.cn_root.crt /usr/local/share/ca-certificates/api.crossdesk.cn_root.crt
sudo update-ca-certificates
```
macOS
```
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain path/to/api.crossdesk.cn_root.crt
```
### Web Client
See [CrossDesk Web Client](https://github.com/kunkundi/crossdesk-web-client)。
# FAQ
See [FAQ](https://github.com/kunkundi/crosssesk/blob/self-hosted-server/docs/FAQ.md) .
# Acknowledgements
- Thanks to [HelloGitHub](https://hellogithub.com/) for the recommendation and exposure.
- Thanks to [Ruanyf Weekly](https://github.com/ruanyf/weekly) for featuring CrossDesk.
- Thanks to the [LinuxDo](https://linux.do) community for the attention, discussions, and support that helped improve CrossDesk.
+207
View File
@@ -0,0 +1,207 @@
# GUI 目录结构与职责说明
本文档说明 `src/gui` 的目录结构、核心类型、依赖方向和代码归属规则,用于避免 `Render``GuiApplication``GuiRuntime` 再次演变成职责混杂的大类。
## 目录结构
```text
src/gui/
├── render.h # 对外稳定入口 Render
├── render.cpp # 创建 GuiApplication 并转发 Run
├── application/ # SDL/ImGui 应用外壳
│ ├── gui_application.h # GuiApplication 声明
│ ├── gui_application.cpp # 初始化、主循环和清理
│ ├── application_state.h # 窗口、交互和 UI 状态
│ ├── sdl_event_dispatch.cpp # SDL 窗口及应用事件分发
│ ├── sdl_events.cpp # 键盘和鼠标事件转换
│ ├── window_lifecycle.cpp # 原生窗口及 ImGui 上下文生命周期
│ └── window_rendering.cpp # 三类窗口的渲染流程
├── runtime/ # 非界面的 GUI 运行时
│ ├── gui_runtime.h/.cpp # 运行时协调接口与公共实现
│ ├── connection_runtime.cpp # 连接、在线探测、超时及会话清理
│ ├── windows_service_runtime.cpp # Windows 服务和安全桌面集成
│ ├── mac_permission_runtime.mm # macOS 权限检查和系统设置调用
│ ├── gui_state.h # ApplicationState 与 RuntimeState 组合点
│ ├── runtime_state.h # 配置、连接、平台和通信状态
│ ├── remote_session.h # 单个远端会话 RemoteSession
│ ├── device_presence_cache.h # 设备在线状态缓存
│ ├── peer_event_handler.h/.cpp # 信令和连接状态回调
│ ├── peer_media_callbacks.cpp # 视频和音频回调
│ ├── peer_data_callbacks.cpp # 控制、剪贴板和文件数据回调
│ └── remote_action_codec.h/.cpp # RemoteAction 编解码
├── features/ # 可独立演进的功能模块
│ ├── clipboard/ # 本地与远端剪贴板同步
│ ├── devices/ # 媒体及输入设备生命周期
│ ├── file_transfer/ # 文件队列、发送、确认和进度
│ ├── input/ # 键盘状态、命令和超时处理
│ └── settings/ # 配置缓存和最近连接别名
├── views/ # ImGui 视图实现
│ ├── panels/ # 主窗口内嵌面板
│ ├── toolbars/ # 标题栏、状态栏和控制栏
│ └── windows/ # 独立窗口和模态对话框
├── platform/ # 原生桌面平台适配
│ └── tray/ # Windows、macOS 和 Linux 托盘
└── assets/ # 字体、图标、布局和本地化资源
```
## 分层关系
```text
Render
└── GuiApplication
├── SDL/ImGui 生命周期
├── views/panels / views/toolbars / views/windows
└── GuiRuntime
├── SessionDeviceManager
├── KeyboardController
├── ClipboardController
├── FileTransferManager
├── SettingsManager
└── PeerEventHandler
```
依赖方向应保持从上向下:
```text
公开入口 → 应用层 → 运行时协调层 → 功能模块 → 底层库
↘ 视图层
```
底层功能模块不应反向依赖具体面板、工具栏或窗口。
## 核心类型
### Render
`Render` 是应用其他部分可见的稳定入口,只负责:
- 管理 `GuiApplication` 生命周期;
-`Run()` 转发给 `GuiApplication`
- 隔离 GUI 内部类型,避免实现细节扩散到其他模块。
不要向 `Render` 添加窗口状态、连接状态或业务方法。
### GuiApplication
`GuiApplication` 是 SDL/ImGui 应用外壳,负责:
- 初始化日志、配置、SDL 和功能模块;
- 创建、销毁原生窗口及 ImGui 上下文;
- 执行主事件循环;
- 分发 SDL 事件;
- 调用面板、工具栏和窗口的绘制方法;
- 在程序退出时按顺序清理资源。
它不应实现连接协议、文件传输、剪贴板或设备控制细节。
### GuiRuntime
`GuiRuntime` 是 GUI 进程的非界面协调层,负责:
- 初始化 MiniRTC Peer 及回调参数;
- 协调连接、远端会话和在线状态;
- 持有各功能 Manager/Controller
- 处理跨功能模块的调用顺序;
- 提供平台集成功能所需的运行时上下文。
`GuiRuntime` 应保持为协调器。新的独立功能应优先创建 Manager 或 Controller,不应直接继续堆积到 `GuiRuntime`
### RemoteSession
`RemoteSession` 表示一个连接中或已连接的远端端点,其生命周期覆盖:
- MiniRTC Peer 和连接状态;
- 视频帧、纹理和渲染区域;
- 远端显示器和控制栏状态;
- 音频、鼠标和键盘控制状态;
- 文件传输状态;
- Windows 服务及安全桌面状态。
`remote_sessions_` 是按远端 ID 索引的会话表。会话查找、连接和清理统一由运行时处理。
## 状态划分
状态定义按生命周期和使用范围划分:
| 文件 | 状态范围 |
| --- | --- |
| `application/application_state.h` | SDL 窗口、渲染上下文、交互标志和 UI 显示状态 |
| `runtime/runtime_state.h` | 配置、Peer、连接表、在线探测和平台集成状态 |
| `runtime/remote_session.h` | 单个远端会话独占的连接、媒体、控制和文件状态 |
| `runtime/gui_state.h` | 仅组合 `ApplicationState``RuntimeState` |
添加状态前应先判断其生命周期:
- 只属于某个远端连接:放入 `RemoteSession`
- 只属于窗口或 UI:放入 `ApplicationState` 对应子状态;
- 属于整个 GUI 运行期:放入 `RuntimeState` 对应子状态;
- 只属于某个功能模块:优先作为 Manager/Controller 的私有成员。
## 主要运行流程
### 启动
```text
Render::Run
→ GuiApplication::Run
→ 初始化路径、日志和配置
→ 初始化 SDL 和功能模块
→ GuiRuntime::CreateConnectionPeer
→ 创建主窗口
→ GuiApplication::MainLoop
```
### 建立远端连接
```text
remote_peer_panel
→ GuiRuntime::ConnectTo
→ 在线状态探测
→ 创建或复用 RemoteSession
→ MiniRTC JoinConnection
→ PeerEventHandler 接收连接状态
→ 创建串流窗口并开始渲染
```
### 接收远端数据
```text
MiniRTC callback
→ PeerEventHandler
├── peer_media_callbacks:视频、音频
├── peer_data_callbacks:控制、文件、剪贴板
└── peer_event_handler:信令、连接状态、网络统计
→ 对应 Manager/Controller
→ 更新 RemoteSession 或运行时状态
```
### 退出和清理
```text
SDL quit / tray exit
→ GuiApplication::Cleanup
→ CloseAllRemoteSessions
→ 停止设备及后台任务
→ 销毁 Peer
→ 销毁 ImGui 上下文和 SDL 窗口
→ SDL_Quit
```
## 新代码归属规则
新增代码时按以下规则选择目录:
| 功能 | 目录 |
| --- | --- |
| SDL 初始化、事件循环、窗口生命周期 | `application/` |
| 连接、会话、Peer 回调和平台运行时 | `runtime/` |
| 视频、音频、鼠标、键盘设备生命周期 | `features/devices/` |
| 键盘协议和按键状态 | `features/input/` |
| 剪贴板同步 | `features/clipboard/` |
| 文件传输 | `features/file_transfer/` |
| 配置持久化 | `features/settings/` |
| 主窗口内嵌区域 | `views/panels/` |
| 标题栏、状态栏、控制栏 | `views/toolbars/` |
| 独立窗口和对话框 | `views/windows/` |
| 系统托盘 | `platform/tray/` |
| 字体、图标、布局和本地化数据 | `assets/` |
+27 -34
View File
@@ -4,10 +4,30 @@ set -e
PKG_NAME="crossdesk"
APP_NAME="CrossDesk"
APP_VERSION="$1"
ARCHITECTURE="amd64"
MAINTAINER="Junkun Di <junkun.di@hotmail.com>"
DESCRIPTION="A simple cross-platform remote desktop client."
ALSA_RUNTIME_DEP="libasound2 | libasound2t64"
PORTAL_RUNTIME_RECOMMENDS="xdg-desktop-portal, xdg-desktop-portal-gtk | xdg-desktop-portal-kde | xdg-desktop-portal-wlr"
normalize_app_version() {
local input="$1"
local prefix=""
local body="$input"
if [[ "$body" == v* ]]; then
prefix="v"
body="${body#v}"
fi
if [[ "$body" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
echo "${prefix}${BASH_REMATCH[1]}-${BASH_REMATCH[4]}-${BASH_REMATCH[3]}"
else
echo "$input"
fi
}
APP_VERSION="$(normalize_app_version "$1")"
# Remove 'v' prefix from version for Debian package (Debian version must start with digit)
DEB_VERSION="${APP_VERSION#v}"
@@ -15,21 +35,18 @@ DEB_VERSION="${APP_VERSION#v}"
DEB_DIR="${PKG_NAME}-${DEB_VERSION}"
DEBIAN_DIR="$DEB_DIR/DEBIAN"
BIN_DIR="$DEB_DIR/usr/bin"
CERT_SRC_DIR="$DEB_DIR/opt/$PKG_NAME/certs"
ICON_BASE_DIR="$DEB_DIR/usr/share/icons/hicolor"
DESKTOP_DIR="$DEB_DIR/usr/share/applications"
rm -rf "$DEB_DIR"
mkdir -p "$DEBIAN_DIR" "$BIN_DIR" "$CERT_SRC_DIR" "$DESKTOP_DIR"
mkdir -p "$DEBIAN_DIR" "$BIN_DIR" "$DESKTOP_DIR"
cp build/linux/x86_64/release/crossdesk "$BIN_DIR/$PKG_NAME"
chmod +x "$BIN_DIR/$PKG_NAME"
ln -s "$PKG_NAME" "$BIN_DIR/$APP_NAME"
cp certs/crossdesk.cn_root.crt "$CERT_SRC_DIR/crossdesk.cn_root.crt"
for size in 16 24 32 48 64 96 128 256; do
mkdir -p "$ICON_BASE_DIR/${size}x${size}/apps"
cp "icons/linux/crossdesk_${size}x${size}.png" \
@@ -44,9 +61,9 @@ Maintainer: $MAINTAINER
Description: $DESCRIPTION
Depends: libc6 (>= 2.29), libstdc++6 (>= 9), libx11-6, libxcb1,
libxcb-randr0, libxcb-xtest0, libxcb-xinerama0, libxcb-shape0,
libxcb-xkb1, libxcb-xfixes0, libxv1, libxtst6, libasound2,
libsndio7.0, libxcb-shm0, libpulse0
Recommends: nvidia-cuda-toolkit
libxcb-xkb1, libxcb-xfixes0, libxv1, libxtst6, $ALSA_RUNTIME_DEP,
libsndio7.0, libxcb-shm0, libpulse0, libdrm2, libdbus-1-3
Recommends: $PORTAL_RUNTIME_RECOMMENDS, nvidia-cuda-toolkit
Priority: optional
Section: utils
EOF
@@ -71,7 +88,6 @@ if [ "\$1" = "remove" ] || [ "\$1" = "purge" ]; then
rm -f /usr/bin/$PKG_NAME || true
rm -f /usr/bin/$APP_NAME || true
rm -f /usr/share/applications/$PKG_NAME.desktop || true
rm -rf /opt/$PKG_NAME/certs || true
for size in 16 24 32 48 64 96 128 256; do
rm -f /usr/share/icons/hicolor/\${size}x\${size}/apps/$PKG_NAME.png || true
done
@@ -85,32 +101,9 @@ cat > "$DEBIAN_DIR/postinst" << 'EOF'
#!/bin/bash
set -e
CERT_SRC="/opt/crossdesk/certs"
CERT_FILE="crossdesk.cn_root.crt"
for user_home in /home/*; do
[ -d "$user_home" ] || continue
username=$(basename "$user_home")
config_dir="$user_home/.config/CrossDesk/certs"
target="$config_dir/$CERT_FILE"
if [ ! -f "$target" ]; then
mkdir -p "$config_dir" || true
cp "$CERT_SRC/$CERT_FILE" "$target" || true
chown -R "$username:$username" "$user_home/.config/CrossDesk" || true
echo "✔ Installed cert for $username at $target"
fi
done
if [ -d "/root" ]; then
config_dir="/root/.config/CrossDesk/certs"
mkdir -p "$config_dir" || true
cp "$CERT_SRC/$CERT_FILE" "$config_dir/$CERT_FILE" || true
chown -R root:root /root/.config/CrossDesk || true
fi
exit 0
EOF
chmod +x "$DEBIAN_DIR/postinst"
dpkg-deb --build "$DEB_DIR"
@@ -120,4 +113,4 @@ mv "$DEB_DIR.deb" "$OUTPUT_FILE"
rm -rf "$DEB_DIR"
echo "✅ Deb package created: $OUTPUT_FILE"
echo "✅ Deb package created: $OUTPUT_FILE"
+26 -33
View File
@@ -4,10 +4,30 @@ set -e
PKG_NAME="crossdesk"
APP_NAME="CrossDesk"
APP_VERSION="$1"
ARCHITECTURE="arm64"
MAINTAINER="Junkun Di <junkun.di@hotmail.com>"
DESCRIPTION="A simple cross-platform remote desktop client."
ALSA_RUNTIME_DEP="libasound2 | libasound2t64"
PORTAL_RUNTIME_RECOMMENDS="xdg-desktop-portal, xdg-desktop-portal-gtk | xdg-desktop-portal-kde | xdg-desktop-portal-wlr"
normalize_app_version() {
local input="$1"
local prefix=""
local body="$input"
if [[ "$body" == v* ]]; then
prefix="v"
body="${body#v}"
fi
if [[ "$body" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
echo "${prefix}${BASH_REMATCH[1]}-${BASH_REMATCH[4]}-${BASH_REMATCH[3]}"
else
echo "$input"
fi
}
APP_VERSION="$(normalize_app_version "$1")"
# Remove 'v' prefix from version for Debian package (Debian version must start with digit)
DEB_VERSION="${APP_VERSION#v}"
@@ -15,21 +35,18 @@ DEB_VERSION="${APP_VERSION#v}"
DEB_DIR="${PKG_NAME}-${DEB_VERSION}"
DEBIAN_DIR="$DEB_DIR/DEBIAN"
BIN_DIR="$DEB_DIR/usr/bin"
CERT_SRC_DIR="$DEB_DIR/opt/$PKG_NAME/certs"
ICON_BASE_DIR="$DEB_DIR/usr/share/icons/hicolor"
DESKTOP_DIR="$DEB_DIR/usr/share/applications"
rm -rf "$DEB_DIR"
mkdir -p "$DEBIAN_DIR" "$BIN_DIR" "$CERT_SRC_DIR" "$DESKTOP_DIR"
mkdir -p "$DEBIAN_DIR" "$BIN_DIR" "$DESKTOP_DIR"
cp build/linux/arm64/release/crossdesk "$BIN_DIR"
chmod +x "$BIN_DIR/$PKG_NAME"
ln -s "$PKG_NAME" "$BIN_DIR/$APP_NAME"
cp certs/crossdesk.cn_root.crt "$CERT_SRC_DIR/crossdesk.cn_root.crt"
for size in 16 24 32 48 64 96 128 256; do
mkdir -p "$ICON_BASE_DIR/${size}x${size}/apps"
cp "icons/linux/crossdesk_${size}x${size}.png" \
@@ -44,8 +61,9 @@ Maintainer: $MAINTAINER
Description: $DESCRIPTION
Depends: libc6 (>= 2.29), libstdc++6 (>= 9), libx11-6, libxcb1,
libxcb-randr0, libxcb-xtest0, libxcb-xinerama0, libxcb-shape0,
libxcb-xkb1, libxcb-xfixes0, libxv1, libxtst6, libasound2,
libsndio7.0, libxcb-shm0, libpulse0
libxcb-xkb1, libxcb-xfixes0, libxv1, libxtst6, $ALSA_RUNTIME_DEP,
libsndio7.0, libxcb-shm0, libpulse0, libdrm2, libdbus-1-3
Recommends: $PORTAL_RUNTIME_RECOMMENDS
Priority: optional
Section: utils
EOF
@@ -70,7 +88,6 @@ if [ "\$1" = "remove" ] || [ "\$1" = "purge" ]; then
rm -f /usr/bin/$PKG_NAME || true
rm -f /usr/bin/$APP_NAME || true
rm -f /usr/share/applications/$PKG_NAME.desktop || true
rm -rf /opt/$PKG_NAME/certs || true
for size in 16 24 32 48 64 96 128 256; do
rm -f /usr/share/icons/hicolor/\${size}x\${size}/apps/$PKG_NAME.png || true
done
@@ -84,30 +101,6 @@ cat > "$DEBIAN_DIR/postinst" << 'EOF'
#!/bin/bash
set -e
CERT_SRC="/opt/crossdesk/certs"
CERT_FILE="crossdesk.cn_root.crt"
for user_home in /home/*; do
[ -d "$user_home" ] || continue
username=$(basename "$user_home")
config_dir="$user_home/.config/CrossDesk/certs"
target="$config_dir/$CERT_FILE"
if [ ! -f "$target" ]; then
mkdir -p "$config_dir" || true
cp "$CERT_SRC/$CERT_FILE" "$target" || true
chown -R "$username:$username" "$user_home/.config/CrossDesk" || true
echo "✔ Installed cert for $username at $target"
fi
done
if [ -d "/root" ]; then
config_dir="/root/.config/CrossDesk/certs"
mkdir -p "$config_dir" || true
cp "$CERT_SRC/$CERT_FILE" "$config_dir/$CERT_FILE" || true
chown -R root:root /root/.config/CrossDesk || true
fi
exit 0
EOF
@@ -120,4 +113,4 @@ mv "$DEB_DIR.deb" "$OUTPUT_FILE"
rm -rf "$DEB_DIR"
echo "✅ Deb package created: $OUTPUT_FILE"
echo "✅ Deb package created: $OUTPUT_FILE"
Regular → Executable
+117 -30
View File
@@ -4,20 +4,36 @@ set -e
APP_NAME="crossdesk"
APP_NAME_UPPER="CrossDesk"
EXECUTABLE_PATH="./build/macosx/arm64/release/crossdesk"
APP_VERSION="$1"
PLATFORM="macos"
ARCH="arm64"
IDENTIFIER="cn.crossdesk.app"
ICON_PATH="icons/macos/crossdesk.icns"
MACOS_MIN_VERSION="10.12"
CERTS_SOURCE="certs"
CERT_NAME="crossdesk.cn_root.crt"
normalize_app_version() {
local input="$1"
local prefix=""
local body="$input"
if [[ "$body" == v* ]]; then
prefix="v"
body="${body#v}"
fi
if [[ "$body" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
echo "${prefix}${BASH_REMATCH[1]}-${BASH_REMATCH[4]}-${BASH_REMATCH[3]}"
else
echo "$input"
fi
}
APP_VERSION="$(normalize_app_version "$1")"
APP_BUNDLE="${APP_NAME_UPPER}.app"
CONTENTS_DIR="${APP_BUNDLE}/Contents"
MACOS_DIR="${CONTENTS_DIR}/MacOS"
RESOURCES_DIR="${CONTENTS_DIR}/Resources"
INSTALLER_TITLE="${APP_NAME_UPPER} ${APP_VERSION}"
PKG_NAME="${APP_NAME}-${PLATFORM}-${ARCH}-${APP_VERSION}.pkg"
DMG_NAME="${APP_NAME}-${PLATFORM}-${ARCH}-${APP_VERSION}.dmg"
@@ -76,50 +92,121 @@ cat > "${CONTENTS_DIR}/Info.plist" <<EOF
</plist>
EOF
xattr -cr "${APP_BUNDLE}" 2>/dev/null || true
find "${APP_BUNDLE}" -name '._*' -delete
echo ".app created successfully."
mkdir -p build_pkg_scripts
cp scripts/macosx/tcc_postinstall.sh build_pkg_scripts/postinstall
chmod +x build_pkg_scripts/postinstall
mkdir -p build_pkg_resources
cat > build_pkg_resources/welcome.html <<EOF
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", sans-serif;
font-size: 13px;
line-height: 1.5;
color: #222;
margin: 0;
padding: 0;
}
h1 {
font-size: 20px;
font-weight: 600;
margin: 0 0 12px;
}
p {
margin: 0 0 10px;
}
</style>
</head>
<body>
<h1>欢迎安装 ${INSTALLER_TITLE}</h1>
<p>CrossDesk 将安装到“应用程序”文件夹。</p>
<p>首次启动时,CrossDesk 会引导你在系统设置中授予必要权限,包括辅助功能、录屏与系统录音等。</p>
<p>为避免旧版本授权残留造成状态误判,安装后可能需要重新授权。</p>
<p>安装完成后,请从“应用程序”文件夹启动 CrossDesk。</p>
</body>
</html>
EOF
echo "building pkg..."
pkgbuild \
--identifier "${IDENTIFIER}" \
--version "${APP_VERSION}" \
--install-location "/Applications" \
--component "${APP_BUNDLE}" \
--scripts build_pkg_scripts \
build_pkg_temp/${APP_NAME}-component.pkg
mkdir -p scripts
cat > scripts/postinstall <<'EOF'
#!/bin/bash
USER_HOME=$( /usr/bin/stat -f "%Su" /dev/console )
HOME_DIR=$( /usr/bin/dscl . -read /Users/$USER_HOME NFSHomeDirectory | awk '{print $2}' )
DEST="$HOME_DIR/Library/Application Support/CrossDesk/certs"
mkdir -p "$DEST"
cp -R "/Library/Application Support/CrossDesk/certs/"* "$DEST/"
exit 0
cat > build_pkg_temp/Distribution <<EOF
<?xml version="1.0" encoding="utf-8"?>
<installer-gui-script minSpecVersion="1">
<title>${INSTALLER_TITLE}</title>
<welcome file="welcome.html" mime-type="text/html"/>
<options customize="never" require-scripts="false" hostArchitectures="arm64"/>
<choices-outline>
<line choice="default">
<line choice="${IDENTIFIER}"/>
</line>
</choices-outline>
<choice id="default" title="${INSTALLER_TITLE}"/>
<choice id="${IDENTIFIER}" title="${INSTALLER_TITLE}" visible="false">
<pkg-ref id="${IDENTIFIER}"/>
</choice>
<pkg-ref id="${IDENTIFIER}" version="${APP_VERSION}" onConclusion="none">crossdesk-component.pkg</pkg-ref>
</installer-gui-script>
EOF
chmod +x scripts/postinstall
pkgbuild \
--root "${CERTS_SOURCE}" \
--identifier "${IDENTIFIER}.certs" \
--version "${APP_VERSION}" \
--install-location "/Library/Application Support/CrossDesk/certs" \
--scripts scripts \
build_pkg_temp/${APP_NAME}-certs.pkg
productbuild \
--package build_pkg_temp/${APP_NAME}-component.pkg \
--package build_pkg_temp/${APP_NAME}-certs.pkg \
--distribution build_pkg_temp/Distribution \
--package-path build_pkg_temp \
--resources build_pkg_resources \
"${PKG_NAME}"
echo "PKG package created: ${PKG_NAME}"
rm -rf build_pkg_temp scripts ${APP_BUNDLE}
# Set custom icon for PKG file
if [ -f "${ICON_PATH}" ]; then
echo "Setting custom icon for PKG file..."
# Create a temporary iconset from icns
TEMP_ICON_DIR=$(mktemp -d)
cp "${ICON_PATH}" "${TEMP_ICON_DIR}/icon.icns"
# Use sips to create a png from icns for the icon
sips -s format png "${TEMP_ICON_DIR}/icon.icns" --out "${TEMP_ICON_DIR}/icon.png" 2>/dev/null || true
# Method: Use osascript to set file icon (works on macOS)
osascript <<APPLESCRIPT
use framework "Foundation"
use framework "AppKit"
set iconPath to POSIX file "${TEMP_ICON_DIR}/icon.icns"
set targetPath to POSIX file "$(pwd)/${PKG_NAME}"
set iconImage to current application's NSImage's alloc()'s initWithContentsOfFile:(POSIX path of iconPath)
set workspace to current application's NSWorkspace's sharedWorkspace()
workspace's setIcon:iconImage forFile:(POSIX path of targetPath) options:0
APPLESCRIPT
if [ $? -eq 0 ]; then
echo "Custom icon set successfully for ${PKG_NAME}"
else
echo "Warning: Failed to set custom icon (this is optional)"
fi
rm -rf "${TEMP_ICON_DIR}"
fi
echo "Set icon finished"
rm -rf build_pkg_temp build_pkg_scripts build_pkg_resources ${APP_BUNDLE}
echo "PKG package created successfully."
echo "package ${APP_BUNDLE}"
echo "installer ${PKG_NAME}"
echo "installer ${PKG_NAME}"
Regular → Executable
+117 -30
View File
@@ -4,20 +4,36 @@ set -e
APP_NAME="crossdesk"
APP_NAME_UPPER="CrossDesk"
EXECUTABLE_PATH="build/macosx/x86_64/release/crossdesk"
APP_VERSION="$1"
PLATFORM="macos"
ARCH="x64"
IDENTIFIER="cn.crossdesk.app"
ICON_PATH="icons/macos/crossdesk.icns"
MACOS_MIN_VERSION="10.12"
CERTS_SOURCE="certs"
CERT_NAME="crossdesk.cn_root.crt"
normalize_app_version() {
local input="$1"
local prefix=""
local body="$input"
if [[ "$body" == v* ]]; then
prefix="v"
body="${body#v}"
fi
if [[ "$body" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
echo "${prefix}${BASH_REMATCH[1]}-${BASH_REMATCH[4]}-${BASH_REMATCH[3]}"
else
echo "$input"
fi
}
APP_VERSION="$(normalize_app_version "$1")"
APP_BUNDLE="${APP_NAME_UPPER}.app"
CONTENTS_DIR="${APP_BUNDLE}/Contents"
MACOS_DIR="${CONTENTS_DIR}/MacOS"
RESOURCES_DIR="${CONTENTS_DIR}/Resources"
INSTALLER_TITLE="${APP_NAME_UPPER} ${APP_VERSION}"
PKG_NAME="${APP_NAME}-${PLATFORM}-${ARCH}-${APP_VERSION}.pkg"
DMG_NAME="${APP_NAME}-${PLATFORM}-${ARCH}-${APP_VERSION}.dmg"
@@ -76,50 +92,121 @@ cat > "${CONTENTS_DIR}/Info.plist" <<EOF
</plist>
EOF
xattr -cr "${APP_BUNDLE}" 2>/dev/null || true
find "${APP_BUNDLE}" -name '._*' -delete
echo ".app created successfully."
mkdir -p build_pkg_scripts
cp scripts/macosx/tcc_postinstall.sh build_pkg_scripts/postinstall
chmod +x build_pkg_scripts/postinstall
mkdir -p build_pkg_resources
cat > build_pkg_resources/welcome.html <<EOF
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", sans-serif;
font-size: 13px;
line-height: 1.5;
color: #222;
margin: 0;
padding: 0;
}
h1 {
font-size: 20px;
font-weight: 600;
margin: 0 0 12px;
}
p {
margin: 0 0 10px;
}
</style>
</head>
<body>
<h1>欢迎安装 ${INSTALLER_TITLE}</h1>
<p>CrossDesk 将安装到“应用程序”文件夹。</p>
<p>首次启动时,CrossDesk 会引导你在系统设置中授予必要权限,包括辅助功能、录屏与系统录音等。</p>
<p>为避免旧版本授权残留造成状态误判,安装后可能需要重新授权。</p>
<p>安装完成后,请从“应用程序”文件夹启动 CrossDesk。</p>
</body>
</html>
EOF
echo "building pkg..."
pkgbuild \
--identifier "${IDENTIFIER}" \
--version "${APP_VERSION}" \
--install-location "/Applications" \
--component "${APP_BUNDLE}" \
--scripts build_pkg_scripts \
build_pkg_temp/${APP_NAME}-component.pkg
mkdir -p scripts
cat > scripts/postinstall <<'EOF'
#!/bin/bash
USER_HOME=$( /usr/bin/stat -f "%Su" /dev/console )
HOME_DIR=$( /usr/bin/dscl . -read /Users/$USER_HOME NFSHomeDirectory | awk '{print $2}' )
DEST="$HOME_DIR/Library/Application Support/CrossDesk/certs"
mkdir -p "$DEST"
cp -R "/Library/Application Support/CrossDesk/certs/"* "$DEST/"
exit 0
cat > build_pkg_temp/Distribution <<EOF
<?xml version="1.0" encoding="utf-8"?>
<installer-gui-script minSpecVersion="1">
<title>${INSTALLER_TITLE}</title>
<welcome file="welcome.html" mime-type="text/html"/>
<options customize="never" require-scripts="false" hostArchitectures="x86_64"/>
<choices-outline>
<line choice="default">
<line choice="${IDENTIFIER}"/>
</line>
</choices-outline>
<choice id="default" title="${INSTALLER_TITLE}"/>
<choice id="${IDENTIFIER}" title="${INSTALLER_TITLE}" visible="false">
<pkg-ref id="${IDENTIFIER}"/>
</choice>
<pkg-ref id="${IDENTIFIER}" version="${APP_VERSION}" onConclusion="none">crossdesk-component.pkg</pkg-ref>
</installer-gui-script>
EOF
chmod +x scripts/postinstall
pkgbuild \
--root "${CERTS_SOURCE}" \
--identifier "${IDENTIFIER}.certs" \
--version "${APP_VERSION}" \
--install-location "/Library/Application Support/CrossDesk/certs" \
--scripts scripts \
build_pkg_temp/${APP_NAME}-certs.pkg
productbuild \
--package build_pkg_temp/${APP_NAME}-component.pkg \
--package build_pkg_temp/${APP_NAME}-certs.pkg \
--distribution build_pkg_temp/Distribution \
--package-path build_pkg_temp \
--resources build_pkg_resources \
"${PKG_NAME}"
echo "PKG package created: ${PKG_NAME}"
rm -rf build_pkg_temp scripts ${APP_BUNDLE}
# Set custom icon for PKG file
if [ -f "${ICON_PATH}" ]; then
echo "Setting custom icon for PKG file..."
# Create a temporary iconset from icns
TEMP_ICON_DIR=$(mktemp -d)
cp "${ICON_PATH}" "${TEMP_ICON_DIR}/icon.icns"
# Use sips to create a png from icns for the icon
sips -s format png "${TEMP_ICON_DIR}/icon.icns" --out "${TEMP_ICON_DIR}/icon.png" 2>/dev/null || true
# Method: Use osascript to set file icon (works on macOS)
osascript <<APPLESCRIPT
use framework "Foundation"
use framework "AppKit"
set iconPath to POSIX file "${TEMP_ICON_DIR}/icon.icns"
set targetPath to POSIX file "$(pwd)/${PKG_NAME}"
set iconImage to current application's NSImage's alloc()'s initWithContentsOfFile:(POSIX path of iconPath)
set workspace to current application's NSWorkspace's sharedWorkspace()
workspace's setIcon:iconImage forFile:(POSIX path of targetPath) options:0
APPLESCRIPT
if [ $? -eq 0 ]; then
echo "Custom icon set successfully for ${PKG_NAME}"
else
echo "Warning: Failed to set custom icon (this is optional)"
fi
rm -rf "${TEMP_ICON_DIR}"
fi
echo "Set icon finished"
rm -rf build_pkg_temp build_pkg_scripts build_pkg_resources ${APP_BUNDLE}
echo "PKG package created successfully."
echo "package ${APP_BUNDLE}"
echo "installer ${PKG_NAME}"
echo "installer ${PKG_NAME}"
+112
View File
@@ -0,0 +1,112 @@
#!/bin/bash
set -e
APP_IDENTIFIER="cn.crossdesk.app"
# Keep known historical identifiers here. tccutil only resets identifiers that
# Launch Services can currently resolve, so path/db cleanup below remains a
# best-effort fallback for stale entries from unsigned or removed builds.
BUNDLE_IDENTIFIERS=(
"cn.crossdesk.app"
"cn.crossdesk.CrossDesk"
"com.crossdesk.app"
"com.crossdesk.CrossDesk"
"com.kunkundi.crossdesk"
"com.kunkundi.CrossDesk"
)
TCC_SERVICES=(
"ScreenCapture"
"Accessibility"
"Microphone"
"AudioCapture"
)
run_tccutil() {
local user_name="$1"
local user_id="$2"
local service="$3"
local bundle_id="$4"
if [ -n "$user_name" ] && [ -n "$user_id" ]; then
/bin/launchctl asuser "$user_id" \
/usr/bin/sudo -u "$user_name" \
/usr/bin/tccutil reset "$service" "$bundle_id" >/dev/null 2>&1
else
/usr/bin/tccutil reset "$service" "$bundle_id" >/dev/null 2>&1
fi
}
reset_bundle_tcc() {
local user_name="$1"
local user_id="$2"
local bundle_id
local service
for bundle_id in "${BUNDLE_IDENTIFIERS[@]}"; do
if run_tccutil "$user_name" "$user_id" "All" "$bundle_id"; then
continue
fi
for service in "${TCC_SERVICES[@]}"; do
run_tccutil "$user_name" "$user_id" "$service" "$bundle_id" || true
done
done
}
cleanup_tcc_db() {
local db_path="$1"
if [ ! -f "$db_path" ] || ! command -v sqlite3 >/dev/null 2>&1; then
return
fi
/usr/bin/sqlite3 "$db_path" <<'SQL' >/dev/null 2>&1 || true
DELETE FROM access
WHERE service IN (
'kTCCServiceScreenCapture',
'kTCCServiceAccessibility',
'kTCCServiceMicrophone',
'kTCCServiceAudioCapture'
)
AND (
client IN (
'cn.crossdesk.app',
'cn.crossdesk.CrossDesk',
'com.crossdesk.app',
'com.crossdesk.CrossDesk',
'com.kunkundi.crossdesk',
'com.kunkundi.CrossDesk'
)
OR lower(client) LIKE '%crossdesk%'
);
SQL
}
cleanup_user_tcc_db() {
local user_name="$1"
local home_dir
home_dir=$(/usr/bin/dscl . -read "/Users/${user_name}" NFSHomeDirectory 2>/dev/null |
/usr/bin/awk '{print $2}')
if [ -z "$home_dir" ]; then
return
fi
cleanup_tcc_db "${home_dir}/Library/Application Support/com.apple.TCC/TCC.db"
}
CONSOLE_USER=$(/usr/bin/stat -f "%Su" /dev/console 2>/dev/null || true)
if [ -n "$CONSOLE_USER" ] &&
[ "$CONSOLE_USER" != "root" ] &&
[ "$CONSOLE_USER" != "loginwindow" ]; then
CONSOLE_UID=$(/usr/bin/id -u "$CONSOLE_USER" 2>/dev/null || true)
reset_bundle_tcc "$CONSOLE_USER" "$CONSOLE_UID"
cleanup_user_tcc_db "$CONSOLE_USER"
fi
# Also clear any system/root-scoped decisions as a harmless fallback.
reset_bundle_tcc "" ""
cleanup_tcc_db "/Library/Application Support/com.apple.TCC/TCC.db"
exit 0
+3 -14
View File
@@ -1,17 +1,13 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<!-- 应用程序标识 -->
<assemblyIdentity
version="1.0.0.0"
processorArchitecture="*"
name="CrossDesk"
type="win32" />
<!-- 描述信息 -->
<description>CrossDesk Application</description>
<!-- 权限:要求管理员运行 -->
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
@@ -20,24 +16,17 @@
</security>
</trustInfo>
<!-- DPI 感知设置:支持高分屏 -->
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<!-- Windows Vista/7 风格 DPI 感知 -->
<dpiAware>true/pm</dpiAware>
<!-- Windows 10/11 高级 DPI 感知 -->
<dpiAwareness>PerMonitorV2</dpiAwareness>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
<!-- Windows 兼容性声明 -->
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- 支持 Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
<!-- 支持 Windows 11(向下兼容 Win10 GUID -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
</application>
</compatibility>
</assembly>
</assembly>
+11
View File
@@ -0,0 +1,11 @@
// Application icon resource; load by the resource name IDI_ICON1.
IDI_ICON1 ICON "..\\..\\icons\\windows\\crossdesk.ico"
#define CREATEPROCESS_MANIFEST_RESOURCE_ID 1
#define RT_MANIFEST 24
#ifdef CROSSDESK_DEBUG
CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "crossdesk_debug.manifest"
#else
CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "crossdesk.manifest"
#endif
+32
View File
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity
version="1.0.0.0"
name="CrossDesk"
type="win32" />
<description>CrossDesk Application</description>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
</application>
</compatibility>
</assembly>
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity
version="1.0.0.0"
name="CrossDesk"
type="win32" />
<description>CrossDesk Portable Application</description>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
</application>
</compatibility>
</assembly>
+8
View File
@@ -0,0 +1,8 @@
// Portable build resource. The app itself runs as the current user; service
// installation raises a separate UAC prompt only when the user chooses it.
IDI_ICON1 ICON "..\\..\\icons\\windows\\crossdesk.ico"
#define CREATEPROCESS_MANIFEST_RESOURCE_ID 1
#define RT_MANIFEST 24
CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "crossdesk_portable.manifest"
+97 -24
View File
@@ -8,13 +8,11 @@
!define PRODUCT_WEB_SITE "https://www.crossdesk.cn/"
!define APP_NAME "CrossDesk"
!define UNINSTALL_REG_KEY "CrossDesk"
!define PRODUCT_SERVICE_NAME "CrossDeskService"
; Installer icon path
!define MUI_ICON "${__FILEDIR__}\..\..\icons\windows\crossdesk.ico"
; Certificate path
!define CERT_FILE "${__FILEDIR__}\..\..\certs\crossdesk.cn_root.crt"
; Compression settings
SetCompressor /FINAL lzma
@@ -49,7 +47,7 @@ ShowInstDetails show
Section "MainSection"
; Check if CrossDesk is running
StrCpy $1 "crossdesk.exe"
StrCpy $1 "CrossDesk.exe"
nsProcess::_FindProcess "$1"
Pop $R0
@@ -71,14 +69,20 @@ cancelInstall:
Abort
installApp:
Call StopInstalledService
SetOutPath "$INSTDIR"
SetOverwrite ifnewer
; Main application executable path
File /oname=crossdesk.exe "..\..\build\windows\x64\release\crossdesk.exe"
; Copy icon file to installation directory
File "${MUI_ICON}"
File /oname=CrossDesk.exe "..\..\build\windows\x64\release\crossdesk.exe"
; Bundle service-side binaries required by the Windows service flow
File "..\..\build\windows\x64\release\crossdesk_service.exe"
File "..\..\build\windows\x64\release\crossdesk_session_helper.exe"
; Bundle runtime DLLs from the release output directory
File "..\..\build\windows\x64\release\*.dll"
Call RegisterInstalledService
; Write uninstall information
WriteUninstaller "$INSTDIR\uninstall.exe"
@@ -88,33 +92,23 @@ installApp:
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "DisplayVersion" "${PRODUCT_VERSION}"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "Publisher" "${PRODUCT_PUBLISHER}"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "URLInfoAbout" "${PRODUCT_WEB_SITE}"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "DisplayIcon" "$INSTDIR\crossdesk.ico"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "DisplayIcon" "$INSTDIR\CrossDesk.exe"
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "NoModify" 1
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "NoRepair" 1
WriteRegStr HKCU "Software\${PRODUCT_NAME}" "InstallDir" "$INSTDIR"
SectionEnd
; After installation
Section -Post
ExecWait '"C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x86\mt.exe" -manifest "$INSTDIR\crossdesk.manifest" -outputresource:"$INSTDIR\crossdesk.exe";1'
SectionEnd
Section "Cert"
SetOutPath "$APPDATA\CrossDesk\certs"
File /r "${CERT_FILE}"
SectionEnd
Section -AdditionalIcons
; Desktop shortcut
CreateShortCut "$DESKTOP\${PRODUCT_NAME}.lnk" "$INSTDIR\crossdesk.exe" "" "$INSTDIR\crossdesk.ico"
CreateShortCut "$DESKTOP\${PRODUCT_NAME}.lnk" "$INSTDIR\CrossDesk.exe" "" "$INSTDIR\CrossDesk.exe"
; Start menu shortcut
CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}.lnk" "$INSTDIR\crossdesk.exe" "" "$INSTDIR\crossdesk.ico"
CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}.lnk" "$INSTDIR\CrossDesk.exe" "" "$INSTDIR\CrossDesk.exe"
SectionEnd
Section "Uninstall"
; Check if CrossDesk is running
StrCpy $1 "crossdesk.exe"
StrCpy $1 "CrossDesk.exe"
nsProcess::_FindProcess "$1"
Pop $R0
@@ -136,8 +130,12 @@ cancelUninstall:
Abort
uninstallApp:
Call un.UnregisterInstalledService
; Delete main executable and uninstaller
Delete "$INSTDIR\crossdesk.exe"
Delete "$INSTDIR\CrossDesk.exe"
Delete "$INSTDIR\crossdesk_service.exe"
Delete "$INSTDIR\crossdesk_session_helper.exe"
Delete "$INSTDIR\uninstall.exe"
; Recursively delete installation directory
@@ -160,5 +158,80 @@ SectionEnd
; ------ Functions ------
Function LaunchApp
Exec "$INSTDIR\crossdesk.exe"
Exec "$INSTDIR\CrossDesk.exe"
FunctionEnd
Function StopInstalledService
IfFileExists "$INSTDIR\CrossDesk.exe" 0 stop_with_sc
IfFileExists "$INSTDIR\crossdesk_service.exe" 0 stop_with_sc
DetailPrint "Stopping existing CrossDesk service"
ExecWait '"$INSTDIR\CrossDesk.exe" --service-stop' $0
${If} $0 = 0
Return
${EndIf}
stop_with_sc:
DetailPrint "Stopping existing CrossDesk service via Service Control Manager"
ExecWait '"$SYSDIR\sc.exe" stop ${PRODUCT_SERVICE_NAME}' $0
${If} $0 != 0
${AndIf} $0 != 1060
${AndIf} $0 != 1062
MessageBox MB_ICONSTOP|MB_OK "Failed to stop the existing CrossDesk service. The installation will be aborted."
Abort
${EndIf}
Sleep 1500
FunctionEnd
Function RegisterInstalledService
IfFileExists "$INSTDIR\CrossDesk.exe" 0 missing_service_binary
IfFileExists "$INSTDIR\crossdesk_service.exe" 0 missing_service_binary
IfFileExists "$INSTDIR\crossdesk_session_helper.exe" 0 missing_service_binary
DetailPrint "Registering CrossDesk service"
ExecWait '"$INSTDIR\CrossDesk.exe" --service-install' $0
${If} $0 != 0
MessageBox MB_ICONSTOP|MB_OK "Failed to register the CrossDesk service. The installation will be aborted."
Abort
${EndIf}
DetailPrint "CrossDesk service registered for on-demand start"
Return
missing_service_binary:
MessageBox MB_ICONSTOP|MB_OK "CrossDesk service files are missing from the installer package. The installation will be aborted."
Abort
FunctionEnd
Function un.UnregisterInstalledService
IfFileExists "$INSTDIR\CrossDesk.exe" 0 unregister_with_sc
DetailPrint "Stopping CrossDesk service"
ExecWait '"$INSTDIR\CrossDesk.exe" --service-stop' $0
${If} $0 = 0
DetailPrint "Removing CrossDesk service"
ExecWait '"$INSTDIR\CrossDesk.exe" --service-uninstall' $0
${If} $0 = 0
Return
${EndIf}
${EndIf}
unregister_with_sc:
DetailPrint "Removing CrossDesk service via Service Control Manager"
ExecWait '"$SYSDIR\sc.exe" stop ${PRODUCT_SERVICE_NAME}' $0
${If} $0 != 0
${AndIf} $0 != 1060
${AndIf} $0 != 1062
MessageBox MB_ICONSTOP|MB_OK "Failed to stop the CrossDesk service. Uninstall will be aborted."
Abort
${EndIf}
Sleep 1500
ExecWait '"$SYSDIR\sc.exe" delete ${PRODUCT_SERVICE_NAME}' $0
${If} $0 != 0
${AndIf} $0 != 1060
MessageBox MB_ICONSTOP|MB_OK "Failed to remove the CrossDesk service. Uninstall will be aborted."
Abort
${EndIf}
FunctionEnd
+40
View File
@@ -0,0 +1,40 @@
param(
[Parameter(Mandatory = $true)]
[string]$Version
)
$ErrorActionPreference = "Stop"
function Normalize-AppVersion {
param(
[Parameter(Mandatory = $true)]
[string]$InputVersion
)
$prefix = ""
$body = $InputVersion
if ($body.StartsWith("v")) {
$prefix = "v"
$body = $body.Substring(1)
}
if ($body -match '^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$') {
return "${prefix}$($Matches[1])-$($Matches[4])-$($Matches[3])"
}
return $InputVersion
}
$normalizedVersion = Normalize-AppVersion -InputVersion $Version
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
Push-Location $scriptDir
try {
& makensis "/DVERSION=$normalizedVersion" "nsis_script.nsi"
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
} finally {
Pop-Location
}
+81 -52
View File
@@ -34,9 +34,16 @@
#endif
#ifndef _WIN32
Daemon* Daemon::instance_ = nullptr;
volatile std::sig_atomic_t Daemon::stop_requested_ = 0;
#endif
namespace {
constexpr int kRestartDelayMs = 1000;
#ifndef _WIN32
constexpr int kWaitPollIntervalMs = 200;
#endif
} // namespace
// get executable file path
static std::string GetExecutablePath() {
#ifdef _WIN32
@@ -66,33 +73,35 @@ static std::string GetExecutablePath() {
return "";
}
Daemon::Daemon(const std::string& name)
: name_(name)
#ifdef _WIN32
,
running_(false)
#else
,
running_(true)
Daemon::Daemon(const std::string& name) : name_(name), running_(false) {}
void Daemon::stop() {
running_.store(false);
#ifndef _WIN32
stop_requested_ = 1;
#endif
{
}
void Daemon::stop() { running_ = false; }
bool Daemon::isRunning() const { return running_; }
bool Daemon::isRunning() const {
#ifndef _WIN32
return running_.load() && (stop_requested_ == 0);
#else
return running_.load();
#endif
}
bool Daemon::start(MainLoopFunc loop) {
#ifdef _WIN32
running_ = true;
running_.store(true);
return runWithRestart(loop);
#elif __APPLE__
// macOS: Use child process monitoring (like Windows) to preserve GUI
running_ = true;
stop_requested_ = 0;
running_.store(true);
return runWithRestart(loop);
#else
// linux: Daemonize first, then run with restart monitoring
instance_ = this;
stop_requested_ = 0;
// check if running from terminal before fork
bool from_terminal =
@@ -119,7 +128,11 @@ bool Daemon::start(MainLoopFunc loop) {
if (pid > 0) _exit(0);
umask(0);
chdir("/");
if (chdir("/") != 0) {
std::cerr << "Failed to change daemon working directory to /: "
<< std::strerror(errno) << std::endl;
return false;
}
// redirect file descriptors: keep stdout/stderr if from terminal, else
// redirect to /dev/null
@@ -134,29 +147,13 @@ bool Daemon::start(MainLoopFunc loop) {
}
// set up signal handlers
signal(SIGTERM, [](int) {
if (instance_) instance_->stop();
});
signal(SIGINT, [](int) {
if (instance_) instance_->stop();
});
signal(SIGTERM, [](int) { stop_requested_ = 1; });
signal(SIGINT, [](int) { stop_requested_ = 1; });
// ignore SIGPIPE
signal(SIGPIPE, SIG_IGN);
// set up SIGCHLD handler to reap zombie processes
struct sigaction sa_chld;
sa_chld.sa_handler = [](int) {
// reap zombie processes
while (waitpid(-1, nullptr, WNOHANG) > 0) {
// continue until no more zombie children
}
};
sigemptyset(&sa_chld.sa_mask);
sa_chld.sa_flags = SA_RESTART | SA_NOCLDSTOP;
sigaction(SIGCHLD, &sa_chld, nullptr);
running_ = true;
running_.store(true);
return runWithRestart(loop);
#endif
}
@@ -204,8 +201,7 @@ bool Daemon::runWithRestart(MainLoopFunc loop) {
restart_count++;
std::cerr << "Exception caught, restarting... (attempt "
<< restart_count << ")" << std::endl;
std::this_thread::sleep_for(
std::chrono::milliseconds(DAEMON_DEFAULT_RESTART_DELAY_MS));
std::this_thread::sleep_for(std::chrono::milliseconds(kRestartDelayMs));
}
}
return true;
@@ -237,27 +233,41 @@ bool Daemon::runWithRestart(MainLoopFunc loop) {
if (!success) {
std::cerr << "Failed to create child process, error: " << GetLastError()
<< std::endl;
std::this_thread::sleep_for(
std::chrono::milliseconds(DAEMON_DEFAULT_RESTART_DELAY_MS));
std::this_thread::sleep_for(std::chrono::milliseconds(kRestartDelayMs));
restart_count++;
continue;
}
while (isRunning()) {
DWORD wait_result = WaitForSingleObject(pi.hProcess, 200);
if (wait_result == WAIT_OBJECT_0) {
break;
}
if (wait_result == WAIT_FAILED) {
std::cerr << "Failed waiting child process, error: " << GetLastError()
<< std::endl;
break;
}
}
if (!isRunning()) {
TerminateProcess(pi.hProcess, 1);
WaitForSingleObject(pi.hProcess, 3000);
}
DWORD exit_code = 0;
WaitForSingleObject(pi.hProcess, INFINITE);
GetExitCodeProcess(pi.hProcess, &exit_code);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
if (exit_code == 0) {
if (!isRunning() || exit_code == 0) {
break; // normal exit
}
restart_count++;
std::cerr << "Child process exited with code " << exit_code
<< ", restarting... (attempt " << restart_count << ")"
<< std::endl;
std::this_thread::sleep_for(
std::chrono::milliseconds(DAEMON_DEFAULT_RESTART_DELAY_MS));
std::this_thread::sleep_for(std::chrono::milliseconds(kRestartDelayMs));
#else
// linux: use fork + exec to create child process
pid_t pid = fork();
@@ -266,21 +276,39 @@ bool Daemon::runWithRestart(MainLoopFunc loop) {
_exit(1); // exec failed
} else if (pid > 0) {
int status = 0;
pid_t waited_pid = waitpid(pid, &status, 0);
pid_t waited_pid = -1;
while (isRunning()) {
waited_pid = waitpid(pid, &status, WNOHANG);
if (waited_pid == pid) {
break;
}
if (waited_pid < 0 && errno != EINTR) {
break;
}
std::this_thread::sleep_for(
std::chrono::milliseconds(kWaitPollIntervalMs));
}
if (!isRunning() && waited_pid != pid) {
kill(pid, SIGTERM);
waited_pid = waitpid(pid, &status, 0);
}
if (waited_pid < 0) {
if (!isRunning()) {
break;
}
restart_count++;
std::cerr << "waitpid failed, errno: " << errno
<< ", restarting... (attempt " << restart_count << ")"
<< std::endl;
std::this_thread::sleep_for(
std::chrono::milliseconds(DAEMON_DEFAULT_RESTART_DELAY_MS));
std::this_thread::sleep_for(std::chrono::milliseconds(kRestartDelayMs));
continue;
}
if (WIFEXITED(status)) {
int exit_code = WEXITSTATUS(status);
if (exit_code == 0) {
if (!isRunning() || exit_code == 0) {
break; // normal exit
}
restart_count++;
@@ -288,6 +316,9 @@ bool Daemon::runWithRestart(MainLoopFunc loop) {
<< ", restarting... (attempt " << restart_count << ")"
<< std::endl;
} else if (WIFSIGNALED(status)) {
if (!isRunning()) {
break;
}
restart_count++;
std::cerr << "Child process crashed with signal " << WTERMSIG(status)
<< ", restarting... (attempt " << restart_count << ")"
@@ -298,12 +329,10 @@ bool Daemon::runWithRestart(MainLoopFunc loop) {
"(attempt "
<< restart_count << ")" << std::endl;
}
std::this_thread::sleep_for(
std::chrono::milliseconds(DAEMON_DEFAULT_RESTART_DELAY_MS));
std::this_thread::sleep_for(std::chrono::milliseconds(kRestartDelayMs));
} else {
std::cerr << "Failed to fork child process" << std::endl;
std::this_thread::sleep_for(
std::chrono::milliseconds(DAEMON_DEFAULT_RESTART_DELAY_MS));
std::this_thread::sleep_for(std::chrono::milliseconds(kRestartDelayMs));
restart_count++;
}
#endif
+6 -8
View File
@@ -7,11 +7,11 @@
#ifndef _DAEMON_H_
#define _DAEMON_H_
#include <atomic>
#include <csignal>
#include <functional>
#include <string>
#define DAEMON_DEFAULT_RESTART_DELAY_MS 1000
class Daemon {
public:
using MainLoopFunc = std::function<void()>;
@@ -28,12 +28,10 @@ class Daemon {
std::string name_;
bool runWithRestart(MainLoopFunc loop);
#ifdef _WIN32
bool running_;
#else
static Daemon* instance_;
volatile bool running_;
#ifndef _WIN32
static volatile std::sig_atomic_t stop_requested_;
#endif
std::atomic<bool> running_;
};
#endif
#endif
+169 -4
View File
@@ -7,15 +7,179 @@
#endif
#include <cstring>
#include <filesystem>
#include <iostream>
#include <memory>
#include <string>
#ifdef _WIN32
#include <cstdio>
#include "service_host.h"
#endif
#include "config_center.h"
#include "daemon.h"
#include "path_manager.h"
#include "render.h"
#ifdef _WIN32
namespace {
void EnsureConsoleForCli() {
static bool console_ready = false;
if (console_ready) {
return;
}
if (!AttachConsole(ATTACH_PARENT_PROCESS)) {
DWORD error = GetLastError();
if (error != ERROR_ACCESS_DENIED) {
AllocConsole();
}
}
FILE* stream = nullptr;
freopen_s(&stream, "CONOUT$", "w", stdout);
freopen_s(&stream, "CONOUT$", "w", stderr);
freopen_s(&stream, "CONIN$", "r", stdin);
SetConsoleOutputCP(CP_UTF8);
console_ready = true;
}
void PrintServiceCliUsage() {
std::cout
<< "CrossDesk service management commands\n"
<< " --service-install Install the sibling crossdesk_service.exe\n"
<< " --service-uninstall Remove the installed Windows service\n"
<< " --service-start Start the Windows service\n"
<< " --service-stop Stop the Windows service\n"
<< " --service-sas Ask the service to send Secure Attention "
"Sequence\n"
<< " --service-ping Ping the service over named pipe IPC\n"
<< " --service-status Query service runtime status\n"
<< " --service-help Show this help\n";
}
std::wstring GetCurrentExecutablePathW() {
wchar_t path[MAX_PATH] = {0};
DWORD length = GetModuleFileNameW(nullptr, path, MAX_PATH);
if (length == 0 || length >= MAX_PATH) {
return L"";
}
return std::wstring(path, length);
}
std::filesystem::path GetSiblingServiceExecutablePath() {
std::wstring current_executable = GetCurrentExecutablePathW();
if (current_executable.empty()) {
return {};
}
return std::filesystem::path(current_executable).parent_path() /
L"crossdesk_service.exe";
}
bool IsServiceCliCommand(const char* arg) {
if (arg == nullptr) {
return false;
}
return std::strcmp(arg, "--service-install") == 0 ||
std::strcmp(arg, "--service-uninstall") == 0 ||
std::strcmp(arg, "--service-start") == 0 ||
std::strcmp(arg, "--service-stop") == 0 ||
std::strcmp(arg, "--service-sas") == 0 ||
std::strcmp(arg, "--service-ping") == 0 ||
std::strcmp(arg, "--service-status") == 0 ||
std::strcmp(arg, "--service-help") == 0;
}
void TryStartManagedWindowsService() {
std::filesystem::path service_path = GetSiblingServiceExecutablePath();
if (service_path.empty() || !std::filesystem::exists(service_path)) {
return;
}
if (!crossdesk::IsCrossDeskServiceInstalled()) {
return;
}
crossdesk::StartCrossDeskService();
}
int HandleServiceCliCommand(const std::string& command) {
EnsureConsoleForCli();
if (command == "--service-help") {
PrintServiceCliUsage();
return 0;
}
if (command == "--service-install") {
std::filesystem::path service_path = GetSiblingServiceExecutablePath();
if (service_path.empty()) {
std::cerr << "Failed to locate crossdesk_service.exe" << std::endl;
return 1;
}
if (!std::filesystem::exists(service_path)) {
std::cerr << "Service binary not found: " << service_path.string()
<< std::endl;
return 1;
}
bool success = crossdesk::InstallCrossDeskService(service_path.wstring());
std::cout << (success ? "install ok" : "install failed") << std::endl;
return success ? 0 : 1;
}
if (command == "--service-uninstall") {
bool success = crossdesk::UninstallCrossDeskService();
std::cout << (success ? "uninstall ok" : "uninstall failed") << std::endl;
return success ? 0 : 1;
}
if (command == "--service-start") {
bool success = crossdesk::StartCrossDeskService();
std::cout << (success ? "start ok" : "start failed") << std::endl;
return success ? 0 : 1;
}
if (command == "--service-stop") {
bool success = crossdesk::StopCrossDeskService();
std::cout << (success ? "stop ok" : "stop failed") << std::endl;
return success ? 0 : 1;
}
if (command == "--service-sas") {
std::cout << crossdesk::QueryCrossDeskService("sas") << std::endl;
return 0;
}
if (command == "--service-ping") {
std::cout << crossdesk::QueryCrossDeskService("ping") << std::endl;
return 0;
}
if (command == "--service-status") {
std::cout << crossdesk::QueryCrossDeskService("status") << std::endl;
return 0;
}
PrintServiceCliUsage();
return 1;
}
} // namespace
#endif
int main(int argc, char* argv[]) {
#ifdef _WIN32
if (argc > 1 && IsServiceCliCommand(argv[1])) {
return HandleServiceCliCommand(argv[1]);
}
#endif
// check if running as child process
bool is_child = false;
for (int i = 1; i < argc; i++) {
@@ -32,14 +196,15 @@ int main(int argc, char* argv[]) {
return 0;
}
#ifdef _WIN32
TryStartManagedWindowsService();
#endif
bool enable_daemon = false;
auto path_manager = std::make_unique<crossdesk::PathManager>("CrossDesk");
if (path_manager) {
std::string cert_path =
(path_manager->GetCertPath() / "crossdesk.cn_root.crt").string();
std::string cache_path = path_manager->GetCachePath().string();
crossdesk::ConfigCenter config_center(cache_path + "/config.ini",
cert_path);
crossdesk::ConfigCenter config_center(cache_path + "/config.ini");
enable_daemon = config_center.IsEnableDaemon();
}
+26 -2
View File
@@ -1,5 +1,8 @@
#include "platform.h"
#include <cstdlib>
#include <cstring>
#include "rd_log.h"
#ifdef _WIN32
@@ -108,7 +111,7 @@ std::string GetHostName() {
#ifdef _WIN32
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
std::cerr << "WSAStartup failed." << std::endl;
LOG_ERROR("WSAStartup failed");
return "";
}
if (gethostname(hostname, sizeof(hostname)) == SOCKET_ERROR) {
@@ -125,4 +128,25 @@ std::string GetHostName() {
#endif
return hostname;
}
} // namespace crossdesk
bool IsWaylandSession() {
#if defined(__linux__) && !defined(__APPLE__)
const char* session_type = std::getenv("XDG_SESSION_TYPE");
if (session_type) {
if (std::strcmp(session_type, "wayland") == 0 ||
std::strcmp(session_type, "Wayland") == 0) {
return true;
}
if (std::strcmp(session_type, "x11") == 0 ||
std::strcmp(session_type, "X11") == 0) {
return false;
}
}
const char* wayland_display = std::getenv("WAYLAND_DISPLAY");
return wayland_display && wayland_display[0] != '\0';
#else
return false;
#endif
}
} // namespace crossdesk
+2 -1
View File
@@ -13,6 +13,7 @@ namespace crossdesk {
std::string GetMac();
std::string GetHostName();
bool IsWaylandSession();
} // namespace crossdesk
#endif
#endif
+63
View File
@@ -0,0 +1,63 @@
#include "rounded_corner_button.h"
namespace crossdesk {
bool RoundedCornerButton(const char* label, const ImVec2& size, float rounding,
ImDrawFlags round_flags, bool enabled,
ImU32 normal_col, ImU32 hover_col, ImU32 active_col,
ImU32 border_col) {
ImGuiWindow* current_window = ImGui::GetCurrentWindow();
if (current_window->SkipItems) return false;
const ImGuiStyle& style = ImGui::GetStyle();
ImGuiID button_id = current_window->GetID(label);
ImVec2 cursor_pos = current_window->DC.CursorPos;
ImVec2 button_size = ImGui::CalcItemSize(size, 0.0f, 0.0f);
ImRect button_rect(cursor_pos, ImVec2(cursor_pos.x + button_size.x,
cursor_pos.y + button_size.y));
ImGui::ItemSize(button_rect);
if (!ImGui::ItemAdd(button_rect, button_id)) return false;
bool is_hovered = false, is_held = false;
bool is_pressed = false;
if (enabled) {
is_pressed =
ImGui::ButtonBehavior(button_rect, button_id, &is_hovered, &is_held);
}
if (normal_col == 0) normal_col = ImGui::GetColorU32(ImGuiCol_Button);
if (hover_col == 0) hover_col = ImGui::GetColorU32(ImGuiCol_ButtonHovered);
if (active_col == 0) active_col = ImGui::GetColorU32(ImGuiCol_ButtonActive);
if (border_col == 0) border_col = ImGui::GetColorU32(ImGuiCol_Border);
ImU32 fill_color = normal_col;
if (is_held && is_hovered)
fill_color = active_col;
else if (is_hovered)
fill_color = hover_col;
if (!enabled) fill_color = IM_COL32(120, 120, 120, 180);
ImDrawList* window_draw_list = ImGui::GetWindowDrawList();
window_draw_list->AddRectFilled(button_rect.Min, button_rect.Max, fill_color,
rounding, round_flags);
if (style.FrameBorderSize > 0.0f) {
window_draw_list->AddRect(button_rect.Min, button_rect.Max, border_col,
rounding, round_flags, style.FrameBorderSize);
}
ImU32 text_color =
ImGui::GetColorU32(enabled ? ImGuiCol_Text : ImGuiCol_TextDisabled);
const char* label_end = ImGui::FindRenderedTextEnd(label);
ImGui::PushStyleColor(ImGuiCol_Text,
ImGui::ColorConvertU32ToFloat4(text_color));
ImGui::RenderTextClipped(button_rect.Min, button_rect.Max, label, label_end,
nullptr, ImVec2(0.5f, 0.5f), &button_rect);
ImGui::PopStyleColor();
return is_pressed;
}
} // namespace crossdesk
+20
View File
@@ -0,0 +1,20 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-02-26
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _ROUNDED_CORNER_BUTTON_H_
#define _ROUNDED_CORNER_BUTTON_H_
#include "imgui.h"
#include "imgui_internal.h"
namespace crossdesk {
bool RoundedCornerButton(const char* label, const ImVec2& size, float rounding,
ImDrawFlags round_flags, bool enabled = true,
ImU32 normal_col = 0, ImU32 hover_col = 0,
ImU32 active_col = 0, ImU32 border_col = 0);
} // namespace crossdesk
#endif
+279
View File
@@ -0,0 +1,279 @@
#include "wayland_portal_shared.h"
#include <chrono>
#include <mutex>
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
#include <dbus/dbus.h>
#endif
#include "rd_log.h"
namespace crossdesk {
namespace {
std::mutex& SharedSessionMutex() {
static std::mutex mutex;
return mutex;
}
SharedWaylandPortalSessionInfo& SharedSessionInfo() {
static SharedWaylandPortalSessionInfo info;
return info;
}
bool& SharedSessionActive() {
static bool active = false;
return active;
}
int& SharedSessionRefs() {
static int refs = 0;
return refs;
}
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
constexpr const char* kPortalBusName = "org.freedesktop.portal.Desktop";
constexpr const char* kPortalSessionInterface =
"org.freedesktop.portal.Session";
constexpr int kPortalCloseWaitMs = 100;
void LogCloseDbusError(const char* action, DBusError* error) {
if (error && dbus_error_is_set(error)) {
LOG_ERROR("{} failed: {} ({})", action,
error->message ? error->message : "unknown",
error->name ? error->name : "unknown");
} else {
LOG_ERROR("{} failed", action);
}
}
struct SessionClosedState {
std::string session_handle;
bool received = false;
};
DBusHandlerResult HandleSessionClosedSignal(DBusConnection* connection,
DBusMessage* message,
void* user_data) {
(void)connection;
auto* state = static_cast<SessionClosedState*>(user_data);
if (!state || !message) {
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
}
if (!dbus_message_is_signal(message, kPortalSessionInterface, "Closed")) {
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
}
const char* path = dbus_message_get_path(message);
if (!path || state->session_handle != path) {
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
}
state->received = true;
return DBUS_HANDLER_RESULT_HANDLED;
}
bool BeginSessionClosedWatch(DBusConnection* connection,
const std::string& session_handle,
SessionClosedState* state,
std::string* match_rule_out) {
if (!connection || session_handle.empty() || !state || !match_rule_out) {
return false;
}
state->session_handle = session_handle;
state->received = false;
DBusError error;
dbus_error_init(&error);
const std::string match_rule =
"type='signal',interface='" + std::string(kPortalSessionInterface) +
"',member='Closed',path='" + session_handle + "'";
dbus_bus_add_match(connection, match_rule.c_str(), &error);
if (dbus_error_is_set(&error)) {
LogCloseDbusError("dbus_bus_add_match(Session.Closed)", &error);
dbus_error_free(&error);
return false;
}
dbus_connection_add_filter(connection, HandleSessionClosedSignal, state,
nullptr);
*match_rule_out = match_rule;
return true;
}
void EndSessionClosedWatch(DBusConnection* connection, SessionClosedState* state,
const std::string& match_rule) {
if (!connection || !state || match_rule.empty()) {
return;
}
dbus_connection_remove_filter(connection, HandleSessionClosedSignal, state);
DBusError remove_error;
dbus_error_init(&remove_error);
dbus_bus_remove_match(connection, match_rule.c_str(), &remove_error);
if (dbus_error_is_set(&remove_error)) {
dbus_error_free(&remove_error);
}
}
void WaitForSessionClosed(DBusConnection* connection, SessionClosedState* state,
int timeout_ms = kPortalCloseWaitMs) {
if (!connection || !state) {
return;
}
const auto deadline =
std::chrono::steady_clock::now() + std::chrono::milliseconds(timeout_ms);
while (!state->received && std::chrono::steady_clock::now() < deadline) {
dbus_connection_read_write(connection, 100);
while (dbus_connection_dispatch(connection) == DBUS_DISPATCH_DATA_REMAINS) {
}
}
}
#endif
} // namespace
bool PublishSharedWaylandPortalSession(
const SharedWaylandPortalSessionInfo& info) {
if (!info.connection || info.session_handle.empty() || info.stream_id == 0) {
return false;
}
std::lock_guard<std::mutex> lock(SharedSessionMutex());
if (SharedSessionActive()) {
const auto& active_info = SharedSessionInfo();
if (active_info.session_handle != info.session_handle &&
SharedSessionRefs() > 0) {
return false;
}
}
const bool same_session =
SharedSessionActive() &&
SharedSessionInfo().session_handle == info.session_handle;
SharedSessionInfo() = info;
SharedSessionActive() = true;
if (!same_session || SharedSessionRefs() <= 0) {
SharedSessionRefs() = 1;
}
return true;
}
bool AcquireSharedWaylandPortalSession(bool require_pointer,
SharedWaylandPortalSessionInfo* out) {
if (!out) {
return false;
}
std::lock_guard<std::mutex> lock(SharedSessionMutex());
if (!SharedSessionActive()) {
return false;
}
const auto& info = SharedSessionInfo();
if (require_pointer && !info.pointer_granted) {
return false;
}
++SharedSessionRefs();
*out = info;
return true;
}
bool ReleaseSharedWaylandPortalSession(DBusConnection** connection_out,
std::string* session_handle_out) {
if (connection_out) {
*connection_out = nullptr;
}
if (session_handle_out) {
session_handle_out->clear();
}
std::lock_guard<std::mutex> lock(SharedSessionMutex());
if (!SharedSessionActive()) {
return false;
}
if (SharedSessionRefs() > 0) {
--SharedSessionRefs();
}
if (SharedSessionRefs() > 0) {
return true;
}
if (connection_out) {
*connection_out = SharedSessionInfo().connection;
}
if (session_handle_out) {
*session_handle_out = SharedSessionInfo().session_handle;
}
SharedSessionInfo() = SharedWaylandPortalSessionInfo{};
SharedSessionActive() = false;
SharedSessionRefs() = 0;
return true;
}
void CloseWaylandPortalSessionAndConnection(DBusConnection* connection,
const std::string& session_handle,
const char* close_action) {
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
if (!connection) {
return;
}
if (!session_handle.empty()) {
SessionClosedState close_state;
std::string close_match_rule;
const bool watching_closed = BeginSessionClosedWatch(
connection, session_handle, &close_state, &close_match_rule);
DBusMessage* message = dbus_message_new_method_call(
kPortalBusName, session_handle.c_str(), kPortalSessionInterface,
"Close");
if (message) {
DBusError error;
dbus_error_init(&error);
DBusMessage* reply = dbus_connection_send_with_reply_and_block(
connection, message, 1000, &error);
if (!reply && dbus_error_is_set(&error)) {
LogCloseDbusError(close_action, &error);
dbus_error_free(&error);
}
if (reply) {
dbus_message_unref(reply);
}
dbus_message_unref(message);
}
if (watching_closed) {
WaitForSessionClosed(connection, &close_state);
if (!close_state.received) {
LOG_WARN("Timed out waiting for portal session to close: {}",
session_handle);
LOG_WARN("Forcing local teardown without waiting for Session.Closed: {}",
session_handle);
EndSessionClosedWatch(connection, &close_state, close_match_rule);
} else {
EndSessionClosedWatch(connection, &close_state, close_match_rule);
LOG_INFO("Portal session closed: {}", session_handle);
}
}
}
dbus_connection_close(connection);
dbus_connection_unref(connection);
#else
(void)connection;
(void)session_handle;
(void)close_action;
#endif
}
} // namespace crossdesk
+37
View File
@@ -0,0 +1,37 @@
/*
* Shared Wayland portal session state used by the Linux Wayland capturer and
* mouse controller so they can reuse one RemoteDesktop session.
*/
#ifndef _WAYLAND_PORTAL_SHARED_H_
#define _WAYLAND_PORTAL_SHARED_H_
#include <cstdint>
#include <string>
struct DBusConnection;
namespace crossdesk {
struct SharedWaylandPortalSessionInfo {
DBusConnection* connection = nullptr;
std::string session_handle;
uint32_t stream_id = 0;
int width = 0;
int height = 0;
bool pointer_granted = false;
};
bool PublishSharedWaylandPortalSession(
const SharedWaylandPortalSessionInfo& info);
bool AcquireSharedWaylandPortalSession(bool require_pointer,
SharedWaylandPortalSessionInfo* out);
bool ReleaseSharedWaylandPortalSession(DBusConnection** connection_out,
std::string* session_handle_out);
void CloseWaylandPortalSessionAndConnection(DBusConnection* connection,
const std::string& session_handle,
const char* close_action);
} // namespace crossdesk
#endif
+25
View File
@@ -0,0 +1,25 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-01-20
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _WINDOW_UTIL_MAC_H_
#define _WINDOW_UTIL_MAC_H_
struct SDL_Window;
namespace crossdesk {
// Best-effort: keep an SDL window above normal windows on macOS.
// No-op on non-macOS builds.
void MacSetWindowAlwaysOnTop(::SDL_Window* window, bool always_on_top);
// Best-effort: exclude an SDL window from the Window menu and window cycling.
// Note: Cmd-Tab switches apps (not individual windows), so this primarily
// affects the Window menu and Cmd-` window cycling.
void MacSetWindowExcludedFromWindowMenu(::SDL_Window* window, bool excluded);
} // namespace crossdesk
#endif
+64
View File
@@ -0,0 +1,64 @@
#include "window_util_mac.h"
#if defined(__APPLE__)
#include <SDL3/SDL.h>
#import <Cocoa/Cocoa.h>
namespace crossdesk {
static NSWindow* GetNSWindowFromSDL(::SDL_Window* window) {
if (!window) {
return nil;
}
#if !defined(SDL_PROP_WINDOW_COCOA_WINDOW_POINTER)
return nil;
#else
SDL_PropertiesID props = SDL_GetWindowProperties(window);
void* cocoa_window_ptr =
SDL_GetPointerProperty(props, SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, NULL);
if (!cocoa_window_ptr) {
return nil;
}
return (__bridge NSWindow*)cocoa_window_ptr;
#endif
}
void MacSetWindowAlwaysOnTop(::SDL_Window* window, bool always_on_top) {
NSWindow* ns_window = GetNSWindowFromSDL(window);
if (!ns_window) {
(void)always_on_top;
return;
}
// Keep above normal windows.
const NSInteger level = always_on_top ? NSFloatingWindowLevel : NSNormalWindowLevel;
[ns_window setLevel:level];
// Optional: keep visible across Spaces/fullscreen. Safe as best-effort.
NSWindowCollectionBehavior behavior = [ns_window collectionBehavior];
behavior |= NSWindowCollectionBehaviorCanJoinAllSpaces;
behavior |= NSWindowCollectionBehaviorFullScreenAuxiliary;
[ns_window setCollectionBehavior:behavior];
}
void MacSetWindowExcludedFromWindowMenu(::SDL_Window* window, bool excluded) {
NSWindow* ns_window = GetNSWindowFromSDL(window);
if (!ns_window) {
(void)excluded;
return;
}
[ns_window setExcludedFromWindowsMenu:excluded];
NSWindowCollectionBehavior behavior = [ns_window collectionBehavior];
behavior |= NSWindowCollectionBehaviorIgnoresCycle;
behavior |= NSWindowCollectionBehaviorTransient;
[ns_window setCollectionBehavior:behavior];
}
} // namespace crossdesk
#endif // __APPLE__
+192 -42
View File
@@ -5,11 +5,17 @@
namespace crossdesk {
ConfigCenter::ConfigCenter(const std::string& config_path,
const std::string& cert_file_path)
: config_path_(config_path),
cert_file_path_(cert_file_path),
cert_file_path_default_(cert_file_path) {
namespace {
bool IsValidTurnModeValue(long value) {
return value >= static_cast<long>(ConfigCenter::TURN_MODE::DISABLED) &&
value <= static_cast<long>(ConfigCenter::TURN_MODE::FORCE_TCP);
}
} // namespace
ConfigCenter::ConfigCenter(const std::string& config_path)
: config_path_(config_path) {
ini_.SetUnicode(true);
Load();
}
@@ -23,8 +29,16 @@ int ConfigCenter::Load() {
return -1;
}
language_ = static_cast<LANGUAGE>(
ini_.GetLongValue(section_, "language", static_cast<long>(language_)));
bool persist_turn_mode_migration = false;
const long language_value =
ini_.GetLongValue(section_, "language", static_cast<long>(language_));
if (language_value < static_cast<long>(LANGUAGE::CHINESE) ||
language_value > static_cast<long>(LANGUAGE::RUSSIAN)) {
language_ = LANGUAGE::ENGLISH;
} else {
language_ = static_cast<LANGUAGE>(language_value);
}
video_quality_ = static_cast<VIDEO_QUALITY>(ini_.GetLongValue(
section_, "video_quality", static_cast<long>(video_quality_)));
@@ -39,23 +53,78 @@ int ConfigCenter::Load() {
hardware_video_codec_ = ini_.GetBoolValue(section_, "hardware_video_codec",
hardware_video_codec_);
enable_turn_ = ini_.GetBoolValue(section_, "enable_turn", enable_turn_);
const char* turn_mode_value = ini_.GetValue(section_, "turn_mode", nullptr);
if (turn_mode_value != nullptr && strlen(turn_mode_value) > 0) {
const long parsed_turn_mode = ini_.GetLongValue(
section_, "turn_mode", static_cast<long>(turn_mode_));
if (IsValidTurnModeValue(parsed_turn_mode)) {
turn_mode_ = static_cast<TURN_MODE>(parsed_turn_mode);
} else {
LOG_WARN("Invalid TURN mode [{}], using auto UDP/TCP",
parsed_turn_mode);
turn_mode_ = TURN_MODE::AUTO_UDP_TCP;
}
} else {
const bool legacy_enable_turn = ini_.GetBoolValue(
section_, "enable_turn", turn_mode_ != TURN_MODE::DISABLED);
turn_mode_ = legacy_enable_turn ? TURN_MODE::AUTO_UDP_TCP
: TURN_MODE::DISABLED;
ini_.SetLongValue(section_, "turn_mode", static_cast<long>(turn_mode_));
persist_turn_mode_migration = true;
}
enable_srtp_ = ini_.GetBoolValue(section_, "enable_srtp", enable_srtp_);
signal_server_host_ = ini_.GetValue(section_, "signal_server_host",
signal_server_host_.c_str());
signal_server_port_ = static_cast<int>(
ini_.GetLongValue(section_, "signal_server_port", signal_server_port_));
coturn_server_port_ = static_cast<int>(
ini_.GetLongValue(section_, "coturn_server_port", coturn_server_port_));
cert_file_path_ =
ini_.GetValue(section_, "cert_file_path", cert_file_path_.c_str());
enable_self_hosted_ =
ini_.GetBoolValue(section_, "enable_self_hosted", enable_self_hosted_);
const char* signal_server_host_value =
ini_.GetValue(section_, "signal_server_host", nullptr);
if (signal_server_host_value != nullptr &&
strlen(signal_server_host_value) > 0) {
signal_server_host_ = signal_server_host_value;
} else {
signal_server_host_ = "";
}
const char* signal_server_port_value =
ini_.GetValue(section_, "signal_server_port", nullptr);
if (signal_server_port_value != nullptr &&
strlen(signal_server_port_value) > 0) {
signal_server_port_ =
static_cast<int>(ini_.GetLongValue(section_, "signal_server_port", 0));
} else {
signal_server_port_ = 0;
}
const char* coturn_server_port_value =
ini_.GetValue(section_, "coturn_server_port", nullptr);
if (coturn_server_port_value != nullptr &&
strlen(coturn_server_port_value) > 0) {
coturn_server_port_ =
static_cast<int>(ini_.GetLongValue(section_, "coturn_server_port", 0));
} else {
coturn_server_port_ = 0;
}
enable_autostart_ =
ini_.GetBoolValue(section_, "enable_autostart", enable_autostart_);
enable_daemon_ = ini_.GetBoolValue(section_, "enable_daemon", enable_daemon_);
enable_minimize_to_tray_ = ini_.GetBoolValue(
section_, "enable_minimize_to_tray", enable_minimize_to_tray_);
portable_service_prompt_suppressed_ =
ini_.GetBoolValue(section_, "portable_service_prompt_suppressed",
portable_service_prompt_suppressed_);
const char* file_transfer_save_path_value =
ini_.GetValue(section_, "file_transfer_save_path", nullptr);
if (file_transfer_save_path_value != nullptr &&
strlen(file_transfer_save_path_value) > 0) {
file_transfer_save_path_ = file_transfer_save_path_value;
} else {
file_transfer_save_path_ = "";
}
if (persist_turn_mode_migration &&
ini_.SaveFile(config_path_.c_str()) < 0) {
return -1;
}
return 0;
}
@@ -69,17 +138,30 @@ int ConfigCenter::Save() {
ini_.SetLongValue(section_, "video_encode_format",
static_cast<long>(video_encode_format_));
ini_.SetBoolValue(section_, "hardware_video_codec", hardware_video_codec_);
ini_.SetBoolValue(section_, "enable_turn", enable_turn_);
ini_.SetLongValue(section_, "turn_mode", static_cast<long>(turn_mode_));
ini_.SetBoolValue(section_, "enable_turn",
turn_mode_ != TURN_MODE::DISABLED);
ini_.SetBoolValue(section_, "enable_srtp", enable_srtp_);
ini_.SetValue(section_, "signal_server_host", signal_server_host_.c_str());
ini_.SetLongValue(section_, "signal_server_port",
static_cast<long>(signal_server_port_));
ini_.SetValue(section_, "cert_file_path", cert_file_path_.c_str());
ini_.SetBoolValue(section_, "enable_self_hosted", enable_self_hosted_);
// only save when self hosted
if (enable_self_hosted_) {
ini_.SetValue(section_, "signal_server_host", signal_server_host_.c_str());
ini_.SetLongValue(section_, "signal_server_port",
static_cast<long>(signal_server_port_));
ini_.SetLongValue(section_, "coturn_server_port",
static_cast<long>(coturn_server_port_));
}
ini_.SetBoolValue(section_, "enable_autostart", enable_autostart_);
ini_.SetBoolValue(section_, "enable_daemon", enable_daemon_);
ini_.SetBoolValue(section_, "enable_minimize_to_tray",
enable_minimize_to_tray_);
ini_.SetBoolValue(section_, "portable_service_prompt_suppressed",
portable_service_prompt_suppressed_);
ini_.SetValue(section_, "file_transfer_save_path",
file_transfer_save_path_.c_str());
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
@@ -145,9 +227,15 @@ int ConfigCenter::SetHardwareVideoCodec(bool hardware_video_codec) {
return 0;
}
int ConfigCenter::SetTurn(bool enable_turn) {
enable_turn_ = enable_turn;
ini_.SetBoolValue(section_, "enable_turn", enable_turn_);
int ConfigCenter::SetTurnMode(TURN_MODE turn_mode) {
if (!IsValidTurnModeValue(static_cast<long>(turn_mode))) {
return -1;
}
turn_mode_ = turn_mode;
ini_.SetLongValue(section_, "turn_mode", static_cast<long>(turn_mode_));
ini_.SetBoolValue(section_, "enable_turn",
turn_mode_ != TURN_MODE::DISABLED);
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
@@ -155,6 +243,16 @@ int ConfigCenter::SetTurn(bool enable_turn) {
return 0;
}
int ConfigCenter::SetTurn(bool enable_turn) {
if (!enable_turn) {
return SetTurnMode(TURN_MODE::DISABLED);
}
if (turn_mode_ == TURN_MODE::DISABLED) {
return SetTurnMode(TURN_MODE::AUTO_UDP_TCP);
}
return SetTurnMode(turn_mode_);
}
int ConfigCenter::SetSrtp(bool enable_srtp) {
enable_srtp_ = enable_srtp;
ini_.SetBoolValue(section_, "enable_srtp", enable_srtp_);
@@ -197,19 +295,40 @@ int ConfigCenter::SetCoturnServerPort(int coturn_server_port) {
return 0;
}
int ConfigCenter::SetCertFilePath(const std::string& cert_file_path) {
cert_file_path_ = cert_file_path;
ini_.SetValue(section_, "cert_file_path", cert_file_path_.c_str());
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
return 0;
}
int ConfigCenter::SetSelfHosted(bool enable_self_hosted) {
enable_self_hosted_ = enable_self_hosted;
ini_.SetBoolValue(section_, "enable_self_hosted", enable_self_hosted_);
// load from config if self hosted is enabled
if (enable_self_hosted_) {
const char* signal_server_host_value =
ini_.GetValue(section_, "signal_server_host", nullptr);
if (signal_server_host_value != nullptr &&
strlen(signal_server_host_value) > 0) {
signal_server_host_ = signal_server_host_value;
}
const char* signal_server_port_value =
ini_.GetValue(section_, "signal_server_port", nullptr);
if (signal_server_port_value != nullptr &&
strlen(signal_server_port_value) > 0) {
signal_server_port_ = static_cast<int>(
ini_.GetLongValue(section_, "signal_server_port", 0));
}
const char* coturn_server_port_value =
ini_.GetValue(section_, "coturn_server_port", nullptr);
if (coturn_server_port_value != nullptr &&
strlen(coturn_server_port_value) > 0) {
coturn_server_port_ = static_cast<int>(
ini_.GetLongValue(section_, "coturn_server_port", 0));
}
ini_.SetValue(section_, "signal_server_host", signal_server_host_.c_str());
ini_.SetLongValue(section_, "signal_server_port",
static_cast<long>(signal_server_port_));
ini_.SetLongValue(section_, "coturn_server_port",
static_cast<long>(coturn_server_port_));
}
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
@@ -263,6 +382,18 @@ int ConfigCenter::SetDaemon(bool enable_daemon) {
return 0;
}
int ConfigCenter::SetPortableServicePromptSuppressed(bool suppressed) {
portable_service_prompt_suppressed_ = suppressed;
ini_.SetBoolValue(section_, "portable_service_prompt_suppressed",
portable_service_prompt_suppressed_);
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
return 0;
}
// getters
ConfigCenter::LANGUAGE ConfigCenter::GetLanguage() const { return language_; }
@@ -283,7 +414,13 @@ bool ConfigCenter::IsHardwareVideoCodec() const {
return hardware_video_codec_;
}
bool ConfigCenter::IsEnableTurn() const { return enable_turn_; }
ConfigCenter::TURN_MODE ConfigCenter::GetTurnMode() const {
return turn_mode_;
}
bool ConfigCenter::IsEnableTurn() const {
return turn_mode_ != TURN_MODE::DISABLED;
}
bool ConfigCenter::IsEnableSrtp() const { return enable_srtp_; }
@@ -295,8 +432,6 @@ int ConfigCenter::GetSignalServerPort() const { return signal_server_port_; }
int ConfigCenter::GetCoturnServerPort() const { return coturn_server_port_; }
std::string ConfigCenter::GetCertFilePath() const { return cert_file_path_; }
std::string ConfigCenter::GetDefaultServerHost() const {
return signal_server_host_default_;
}
@@ -309,10 +444,6 @@ int ConfigCenter::GetDefaultCoturnServerPort() const {
return coturn_server_port_default_;
}
std::string ConfigCenter::GetDefaultCertFilePath() const {
return cert_file_path_default_;
}
bool ConfigCenter::IsSelfHosted() const { return enable_self_hosted_; }
bool ConfigCenter::IsMinimizeToTray() const { return enable_minimize_to_tray_; }
@@ -320,4 +451,23 @@ bool ConfigCenter::IsMinimizeToTray() const { return enable_minimize_to_tray_; }
bool ConfigCenter::IsEnableAutostart() const { return enable_autostart_; }
bool ConfigCenter::IsEnableDaemon() const { return enable_daemon_; }
} // namespace crossdesk
bool ConfigCenter::IsPortableServicePromptSuppressed() const {
return portable_service_prompt_suppressed_;
}
int ConfigCenter::SetFileTransferSavePath(const std::string& path) {
file_transfer_save_path_ = path;
ini_.SetValue(section_, "file_transfer_save_path",
file_transfer_save_path_.c_str());
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
return 0;
}
std::string ConfigCenter::GetFileTransferSavePath() const {
return file_transfer_save_path_;
}
} // namespace crossdesk
+22 -15
View File
@@ -15,15 +15,19 @@ namespace crossdesk {
class ConfigCenter {
public:
enum class LANGUAGE { CHINESE = 0, ENGLISH = 1 };
enum class LANGUAGE { CHINESE = 0, ENGLISH = 1, RUSSIAN = 2 };
enum class VIDEO_QUALITY { LOW = 0, MEDIUM = 1, HIGH = 2 };
enum class VIDEO_FRAME_RATE { FPS_30 = 0, FPS_60 = 1 };
enum class VIDEO_ENCODE_FORMAT { H264 = 0, AV1 = 1 };
enum class TURN_MODE {
DISABLED = 0,
AUTO_UDP_TCP = 1,
FORCE_UDP = 2,
FORCE_TCP = 3
};
public:
explicit ConfigCenter(
const std::string& config_path = "config.ini",
const std::string& cert_file_path = "crossdesk.cn_root.crt");
explicit ConfigCenter(const std::string& config_path = "config.ini");
~ConfigCenter();
// write config
@@ -32,16 +36,18 @@ class ConfigCenter {
int SetVideoFrameRate(VIDEO_FRAME_RATE video_frame_rate);
int SetVideoEncodeFormat(VIDEO_ENCODE_FORMAT video_encode_format);
int SetHardwareVideoCodec(bool hardware_video_codec);
int SetTurnMode(TURN_MODE turn_mode);
int SetTurn(bool enable_turn);
int SetSrtp(bool enable_srtp);
int SetServerHost(const std::string& signal_server_host);
int SetServerPort(int signal_server_port);
int SetCoturnServerPort(int coturn_server_port);
int SetCertFilePath(const std::string& cert_file_path);
int SetSelfHosted(bool enable_self_hosted);
int SetMinimizeToTray(bool enable_minimize_to_tray);
int SetAutostart(bool enable_autostart);
int SetDaemon(bool enable_daemon);
int SetPortableServicePromptSuppressed(bool suppressed);
int SetFileTransferSavePath(const std::string& path);
// read config
@@ -50,48 +56,49 @@ class ConfigCenter {
VIDEO_FRAME_RATE GetVideoFrameRate() const;
VIDEO_ENCODE_FORMAT GetVideoEncodeFormat() const;
bool IsHardwareVideoCodec() const;
TURN_MODE GetTurnMode() const;
bool IsEnableTurn() const;
bool IsEnableSrtp() const;
std::string GetSignalServerHost() const;
int GetSignalServerPort() const;
int GetCoturnServerPort() const;
std::string GetCertFilePath() const;
std::string GetDefaultServerHost() const;
int GetDefaultSignalServerPort() const;
int GetDefaultCoturnServerPort() const;
std::string GetDefaultCertFilePath() const;
bool IsSelfHosted() const;
bool IsMinimizeToTray() const;
bool IsEnableAutostart() const;
bool IsEnableDaemon() const;
bool IsPortableServicePromptSuppressed() const;
std::string GetFileTransferSavePath() const;
int Load();
int Save();
private:
std::string config_path_;
std::string cert_file_path_;
CSimpleIniA ini_;
const char* section_ = "Settings";
LANGUAGE language_ = LANGUAGE::CHINESE;
VIDEO_QUALITY video_quality_ = VIDEO_QUALITY::MEDIUM;
VIDEO_QUALITY video_quality_ = VIDEO_QUALITY::HIGH;
VIDEO_FRAME_RATE video_frame_rate_ = VIDEO_FRAME_RATE::FPS_60;
VIDEO_ENCODE_FORMAT video_encode_format_ = VIDEO_ENCODE_FORMAT::H264;
bool hardware_video_codec_ = false;
bool enable_turn_ = true;
TURN_MODE turn_mode_ = TURN_MODE::AUTO_UDP_TCP;
bool enable_srtp_ = false;
std::string signal_server_host_ = "api.crossdesk.cn";
std::string signal_server_host_ = "";
std::string signal_server_host_default_ = "api.crossdesk.cn";
int signal_server_port_ = 9099;
int signal_server_port_ = 0;
int server_port_default_ = 9099;
int coturn_server_port_ = 3478;
int coturn_server_port_ = 0;
int coturn_server_port_default_ = 3478;
std::string cert_file_path_default_ = "";
bool enable_self_hosted_ = false;
bool enable_minimize_to_tray_ = false;
bool enable_autostart_ = false;
bool enable_daemon_ = false;
bool portable_service_prompt_suppressed_ = false;
std::string file_transfer_save_path_ = "";
};
} // namespace crossdesk
#endif
#endif
+110 -8
View File
@@ -9,6 +9,8 @@
#include <stdio.h>
#include <cstdint>
#include <cstring>
#include <nlohmann/json.hpp>
#include <string>
@@ -19,10 +21,13 @@ namespace crossdesk {
typedef enum {
mouse = 0,
keyboard,
audio_capture,
host_infomation,
display_id,
keyboard = 1,
audio_capture = 2,
host_infomation = 3,
display_id = 4,
service_status = 5,
service_command = 6,
keyboard_state = 7,
} ControlType;
typedef enum {
move = 0,
@@ -36,6 +41,7 @@ typedef enum {
wheel_horizontal
} MouseFlag;
typedef enum { key_down = 0, key_up } KeyFlag;
typedef enum { send_sas = 0, lock_workstation } ServiceCommandFlag;
typedef struct {
float x;
float y;
@@ -45,9 +51,25 @@ typedef struct {
typedef struct {
size_t key_value;
uint32_t scan_code;
bool extended;
KeyFlag flag;
} Key;
inline constexpr size_t kMaxKeyboardStateKeys = 32;
typedef struct {
size_t key_value;
uint32_t scan_code;
bool extended;
} KeyboardStateKey;
typedef struct {
uint32_t seq;
size_t pressed_count;
KeyboardStateKey pressed_keys[kMaxKeyboardStateKeys];
} KeyboardState;
typedef struct {
char host_name[64];
size_t host_name_size;
@@ -59,14 +81,26 @@ typedef struct {
int* bottom;
} HostInfo;
typedef struct {
bool available;
char interactive_stage[32];
} ServiceStatus;
typedef struct {
ServiceCommandFlag flag;
} ServiceCommand;
struct RemoteAction {
ControlType type;
union {
Mouse m;
Key k;
KeyboardState ks;
HostInfo i;
bool a;
int d;
ServiceStatus ss;
ServiceCommand c;
};
// parse
@@ -88,14 +122,38 @@ struct RemoteAction {
{"x", a.m.x}, {"y", a.m.y}, {"s", a.m.s}, {"flag", a.m.flag}};
break;
case ControlType::keyboard:
j["keyboard"] = {{"key_value", a.k.key_value}, {"flag", a.k.flag}};
j["keyboard"] = {{"key_value", a.k.key_value},
{"scan_code", a.k.scan_code},
{"extended", a.k.extended},
{"flag", a.k.flag}};
break;
case ControlType::keyboard_state: {
json keys = json::array();
const size_t pressed_count =
a.ks.pressed_count < kMaxKeyboardStateKeys
? a.ks.pressed_count
: kMaxKeyboardStateKeys;
for (size_t idx = 0; idx < pressed_count; ++idx) {
keys.push_back({{"key_value", a.ks.pressed_keys[idx].key_value},
{"scan_code", a.ks.pressed_keys[idx].scan_code},
{"extended", a.ks.pressed_keys[idx].extended}});
}
j["keyboard_state"] = {{"seq", a.ks.seq}, {"pressed_keys", keys}};
break;
}
case ControlType::audio_capture:
j["audio_capture"] = a.a;
break;
case ControlType::display_id:
j["display_id"] = a.d;
break;
case ControlType::service_status:
j["service_status"] = {{"available", a.ss.available},
{"interactive_stage", a.ss.interactive_stage}};
break;
case ControlType::service_command:
j["service_command"] = {{"flag", a.c.flag}};
break;
case ControlType::host_infomation: {
json displays = json::array();
for (size_t idx = 0; idx < a.i.display_num; idx++) {
@@ -129,14 +187,58 @@ struct RemoteAction {
break;
case ControlType::keyboard:
out.k.key_value = j.at("keyboard").at("key_value").get<size_t>();
out.k.scan_code =
j.at("keyboard").value("scan_code", static_cast<uint32_t>(0));
out.k.extended = j.at("keyboard").value("extended", false);
out.k.flag = (KeyFlag)j.at("keyboard").at("flag").get<int>();
break;
case ControlType::keyboard_state: {
const auto& keyboard_state_json = j.at("keyboard_state");
out.ks.seq = keyboard_state_json.value("seq", 0u);
out.ks.pressed_count = 0;
const auto keys_json =
keyboard_state_json.value("pressed_keys", json::array());
if (!keys_json.is_array()) {
break;
}
const size_t count =
keys_json.size() < kMaxKeyboardStateKeys
? keys_json.size()
: kMaxKeyboardStateKeys;
for (size_t idx = 0; idx < count; ++idx) {
const auto& key_json = keys_json[idx];
out.ks.pressed_keys[idx].key_value =
key_json.at("key_value").get<size_t>();
out.ks.pressed_keys[idx].scan_code =
key_json.value("scan_code", static_cast<uint32_t>(0));
out.ks.pressed_keys[idx].extended =
key_json.value("extended", false);
}
out.ks.pressed_count = count;
break;
}
case ControlType::audio_capture:
out.a = j.at("audio_capture").get<bool>();
break;
case ControlType::display_id:
out.d = j.at("display_id").get<int>();
break;
case ControlType::service_status: {
const auto& service_status_json = j.at("service_status");
out.ss.available = service_status_json.value("available", false);
std::string interactive_stage =
service_status_json.value("interactive_stage", std::string());
std::strncpy(out.ss.interactive_stage, interactive_stage.c_str(),
sizeof(out.ss.interactive_stage) - 1);
out.ss.interactive_stage[sizeof(out.ss.interactive_stage) - 1] = '\0';
break;
}
case ControlType::service_command:
out.c.flag = static_cast<ServiceCommandFlag>(
j.at("service_command").at("flag").get<int>());
break;
case ControlType::host_infomation: {
std::string host_name =
j.at("host_info").at("host_name").get<std::string>();
@@ -174,8 +276,8 @@ struct RemoteAction {
}
};
// int key_code, bool is_down
typedef void (*OnKeyAction)(int, bool, void*);
// int key_code, bool is_down, uint32_t scan_code, bool extended
typedef void (*OnKeyAction)(int, bool, uint32_t, bool, void*);
class DeviceController {
public:
@@ -190,4 +292,4 @@ class DeviceController {
// virtual int Unhook();
};
} // namespace crossdesk
#endif
#endif
@@ -1,27 +1,60 @@
#include "keyboard_capturer.h"
#include <errno.h>
#include <poll.h>
#include "keyboard_converter.h"
#include "platform.h"
#include "rd_log.h"
#include "windows_key_metadata.h"
namespace crossdesk {
static OnKeyAction g_on_key_action = nullptr;
static void* g_user_ptr = nullptr;
static KeySym NormalizeKeySym(KeySym key_sym) {
if (key_sym >= XK_a && key_sym <= XK_z) {
return key_sym - XK_a + XK_A;
}
return key_sym;
}
static int KeyboardEventHandler(Display* display, XEvent* event) {
(void)display;
if (event->xkey.type == KeyPress || event->xkey.type == KeyRelease) {
KeySym keySym = XKeycodeToKeysym(display, event->xkey.keycode, 0);
int key_code = XKeysymToKeycode(display, keySym);
KeySym key_sym = NormalizeKeySym(XLookupKeysym(&event->xkey, 0));
auto key_it = x11KeySymToVkCode.find(static_cast<int>(key_sym));
if (key_it == x11KeySymToVkCode.end()) {
key_sym = NormalizeKeySym(XLookupKeysym(&event->xkey, 1));
key_it = x11KeySymToVkCode.find(static_cast<int>(key_sym));
}
if (key_it == x11KeySymToVkCode.end()) {
return 0;
}
int key_code = key_it->second;
bool is_key_down = (event->xkey.type == KeyPress);
uint32_t scan_code = 0;
bool extended = false;
LookupWindowsKeyMetadataFromVk(key_code, &scan_code, &extended);
if (g_on_key_action) {
g_on_key_action(key_code, is_key_down, g_user_ptr);
g_on_key_action(key_code, is_key_down, scan_code, extended, g_user_ptr);
}
}
return 0;
}
KeyboardCapturer::KeyboardCapturer() : display_(nullptr), running_(true) {
KeyboardCapturer::KeyboardCapturer()
: display_(nullptr),
root_(0),
running_(false),
use_wayland_portal_(false),
wayland_init_attempted_(false),
dbus_connection_(nullptr) {
XInitThreads();
display_ = XOpenDisplay(nullptr);
if (!display_) {
LOG_ERROR("Failed to open X display.");
@@ -29,42 +62,116 @@ KeyboardCapturer::KeyboardCapturer() : display_(nullptr), running_(true) {
}
KeyboardCapturer::~KeyboardCapturer() {
Unhook();
CleanupWaylandPortal();
if (display_) {
XCloseDisplay(display_);
display_ = nullptr;
}
}
int KeyboardCapturer::Hook(OnKeyAction on_key_action, void* user_ptr) {
if (!display_) {
LOG_ERROR("Display not initialized.");
return -1;
}
g_on_key_action = on_key_action;
g_user_ptr = user_ptr;
XSelectInput(display_, DefaultRootWindow(display_),
KeyPressMask | KeyReleaseMask);
while (running_) {
XEvent event;
XNextEvent(display_, &event);
KeyboardEventHandler(display_, &event);
if (running_) {
return 0;
}
root_ = DefaultRootWindow(display_);
XSelectInput(display_, root_, KeyPressMask | KeyReleaseMask);
XFlush(display_);
running_ = true;
const int x11_fd = ConnectionNumber(display_);
event_thread_ = std::thread([this, x11_fd]() {
while (running_) {
while (running_ && XPending(display_) > 0) {
XEvent event;
XNextEvent(display_, &event);
KeyboardEventHandler(display_, &event);
}
if (!running_) {
break;
}
struct pollfd pfd = {x11_fd, POLLIN, 0};
int poll_ret = poll(&pfd, 1, 50);
if (poll_ret < 0) {
if (errno == EINTR) {
continue;
}
LOG_ERROR("poll for X11 events failed.");
running_ = false;
break;
}
if (poll_ret == 0) {
continue;
}
if ((pfd.revents & (POLLERR | POLLHUP | POLLNVAL)) != 0) {
LOG_ERROR("poll got invalid X11 event fd state.");
running_ = false;
break;
}
if ((pfd.revents & POLLIN) == 0) {
continue;
}
}
});
return 0;
}
int KeyboardCapturer::Unhook() {
running_ = false;
if (event_thread_.joinable()) {
event_thread_.join();
}
g_on_key_action = nullptr;
g_user_ptr = nullptr;
running_ = false;
if (display_) {
XSelectInput(display_, DefaultRootWindow(display_), 0);
if (display_ && root_ != 0) {
XSelectInput(display_, root_, 0);
XFlush(display_);
}
return 0;
}
int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down) {
int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down,
uint32_t scan_code, bool extended) {
(void)scan_code;
(void)extended;
if (IsWaylandSession()) {
if (!use_wayland_portal_ && !wayland_init_attempted_) {
wayland_init_attempted_ = true;
if (InitWaylandPortal()) {
use_wayland_portal_ = true;
LOG_INFO("Keyboard controller initialized with Wayland portal backend");
} else {
LOG_WARN(
"Wayland keyboard control init failed, falling back to X11/XTest "
"backend");
}
}
if (use_wayland_portal_) {
return SendWaylandKeyboardCommand(key_code, is_down, scan_code, extended);
}
}
if (!display_) {
LOG_ERROR("Display not initialized.");
return -1;
@@ -78,4 +185,4 @@ int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down) {
}
return 0;
}
} // namespace crossdesk
} // namespace crossdesk
@@ -11,8 +11,17 @@
#include <X11/extensions/XTest.h>
#include <X11/keysym.h>
#include <atomic>
#include <cstdint>
#include <functional>
#include <string>
#include <thread>
#include "device_controller.h"
struct DBusConnection;
struct DBusMessageIter;
namespace crossdesk {
class KeyboardCapturer : public DeviceController {
@@ -23,12 +32,30 @@ class KeyboardCapturer : public DeviceController {
public:
virtual int Hook(OnKeyAction on_key_action, void* user_ptr);
virtual int Unhook();
virtual int SendKeyboardCommand(int key_code, bool is_down);
virtual int SendKeyboardCommand(int key_code, bool is_down,
uint32_t scan_code = 0,
bool extended = false);
private:
bool InitWaylandPortal();
void CleanupWaylandPortal();
int SendWaylandKeyboardCommand(int key_code, bool is_down, uint32_t scan_code,
bool extended);
bool NotifyWaylandKeyboardKeysym(int keysym, uint32_t state);
bool NotifyWaylandKeyboardKeycode(int keycode, uint32_t state);
bool SendWaylandPortalVoidCall(const char* method_name,
const std::function<void(DBusMessageIter*)>&
append_args);
private:
Display* display_;
Window root_;
bool running_;
std::atomic<bool> running_;
std::thread event_thread_;
bool use_wayland_portal_ = false;
bool wayland_init_attempted_ = false;
DBusConnection* dbus_connection_ = nullptr;
std::string wayland_session_handle_;
};
} // namespace crossdesk
#endif
#endif
@@ -0,0 +1,731 @@
#include "keyboard_capturer.h"
#include <chrono>
#include <cstring>
#include <map>
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
#include <dbus/dbus.h>
#endif
#include "linux_evdev_keycode.h"
#include "rd_log.h"
#include "wayland_portal_shared.h"
namespace crossdesk {
extern std::map<int, int> vkCodeToX11KeySym;
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
namespace {
constexpr const char* kPortalBusName = "org.freedesktop.portal.Desktop";
constexpr const char* kPortalObjectPath = "/org/freedesktop/portal/desktop";
constexpr const char* kPortalRemoteDesktopInterface =
"org.freedesktop.portal.RemoteDesktop";
constexpr const char* kPortalRequestInterface =
"org.freedesktop.portal.Request";
constexpr const char* kPortalRequestPathPrefix =
"/org/freedesktop/portal/desktop/request/";
constexpr const char* kPortalSessionPathPrefix =
"/org/freedesktop/portal/desktop/session/";
constexpr uint32_t kRemoteDesktopDeviceKeyboard = 1u;
constexpr uint32_t kKeyboardReleased = 0u;
constexpr uint32_t kKeyboardPressed = 1u;
int NormalizeFallbackKeysym(int keysym) {
if (keysym >= XK_A && keysym <= XK_Z) {
return keysym - XK_A + XK_a;
}
return keysym;
}
std::string MakeToken(const char* prefix) {
const auto now = std::chrono::steady_clock::now().time_since_epoch().count();
return std::string(prefix) + "_" + std::to_string(now);
}
void LogDbusError(const char* action, DBusError* error) {
if (error && dbus_error_is_set(error)) {
LOG_ERROR("{} failed: {} ({})", action,
error->message ? error->message : "unknown",
error->name ? error->name : "unknown");
} else {
LOG_ERROR("{} failed", action);
}
}
void AppendDictEntryString(DBusMessageIter* dict, const char* key,
const std::string& value) {
DBusMessageIter entry;
DBusMessageIter variant;
const char* key_cstr = key;
const char* value_cstr = value.c_str();
dbus_message_iter_open_container(dict, DBUS_TYPE_DICT_ENTRY, nullptr, &entry);
dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &key_cstr);
dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT, "s", &variant);
dbus_message_iter_append_basic(&variant, DBUS_TYPE_STRING, &value_cstr);
dbus_message_iter_close_container(&entry, &variant);
dbus_message_iter_close_container(dict, &entry);
}
void AppendDictEntryUint32(DBusMessageIter* dict, const char* key,
uint32_t value) {
DBusMessageIter entry;
DBusMessageIter variant;
const char* key_cstr = key;
dbus_message_iter_open_container(dict, DBUS_TYPE_DICT_ENTRY, nullptr, &entry);
dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &key_cstr);
dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT, "u", &variant);
dbus_message_iter_append_basic(&variant, DBUS_TYPE_UINT32, &value);
dbus_message_iter_close_container(&entry, &variant);
dbus_message_iter_close_container(dict, &entry);
}
void AppendEmptyOptionsDict(DBusMessageIter* iter) {
DBusMessageIter options;
dbus_message_iter_open_container(iter, DBUS_TYPE_ARRAY, "{sv}", &options);
dbus_message_iter_close_container(iter, &options);
}
bool ReadPathLikeVariant(DBusMessageIter* variant, std::string* value) {
if (!variant || !value) {
return false;
}
const int type = dbus_message_iter_get_arg_type(variant);
if (type == DBUS_TYPE_OBJECT_PATH || type == DBUS_TYPE_STRING) {
const char* temp = nullptr;
dbus_message_iter_get_basic(variant, &temp);
if (temp && temp[0] != '\0') {
*value = temp;
return true;
}
}
return false;
}
bool ReadUint32Like(DBusMessageIter* iter, uint32_t* value) {
if (!iter || !value) {
return false;
}
const int type = dbus_message_iter_get_arg_type(iter);
if (type == DBUS_TYPE_UINT32) {
uint32_t temp = 0;
dbus_message_iter_get_basic(iter, &temp);
*value = temp;
return true;
}
if (type == DBUS_TYPE_INT32) {
int32_t temp = 0;
dbus_message_iter_get_basic(iter, &temp);
if (temp < 0) {
return false;
}
*value = static_cast<uint32_t>(temp);
return true;
}
return false;
}
std::string BuildSessionHandleFromRequestPath(
const std::string& request_path, const std::string& session_handle_token) {
if (request_path.rfind(kPortalRequestPathPrefix, 0) != 0 ||
session_handle_token.empty()) {
return "";
}
const size_t sender_start = strlen(kPortalRequestPathPrefix);
const size_t token_sep = request_path.find('/', sender_start);
if (token_sep == std::string::npos || token_sep <= sender_start) {
return "";
}
const std::string sender =
request_path.substr(sender_start, token_sep - sender_start);
if (sender.empty()) {
return "";
}
return std::string(kPortalSessionPathPrefix) + sender + "/" +
session_handle_token;
}
struct PortalResponseState {
std::string request_path;
bool received = false;
DBusMessage* message = nullptr;
};
DBusHandlerResult HandlePortalResponseSignal(DBusConnection* connection,
DBusMessage* message,
void* user_data) {
(void)connection;
auto* state = static_cast<PortalResponseState*>(user_data);
if (!state || !message) {
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
}
if (!dbus_message_is_signal(message, kPortalRequestInterface, "Response")) {
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
}
const char* path = dbus_message_get_path(message);
if (!path || state->request_path != path) {
return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
}
if (state->message) {
dbus_message_unref(state->message);
state->message = nullptr;
}
state->message = dbus_message_ref(message);
state->received = true;
return DBUS_HANDLER_RESULT_HANDLED;
}
DBusMessage* WaitForPortalResponse(DBusConnection* connection,
const std::string& request_path,
int timeout_ms = 120000) {
if (!connection || request_path.empty()) {
return nullptr;
}
PortalResponseState state;
state.request_path = request_path;
DBusError error;
dbus_error_init(&error);
const std::string match_rule =
"type='signal',interface='" + std::string(kPortalRequestInterface) +
"',member='Response',path='" + request_path + "'";
dbus_bus_add_match(connection, match_rule.c_str(), &error);
if (dbus_error_is_set(&error)) {
LogDbusError("dbus_bus_add_match", &error);
dbus_error_free(&error);
return nullptr;
}
dbus_connection_add_filter(connection, HandlePortalResponseSignal, &state,
nullptr);
auto deadline =
std::chrono::steady_clock::now() + std::chrono::milliseconds(timeout_ms);
while (!state.received && std::chrono::steady_clock::now() < deadline) {
dbus_connection_read_write(connection, 100);
while (dbus_connection_dispatch(connection) == DBUS_DISPATCH_DATA_REMAINS) {
}
}
dbus_connection_remove_filter(connection, HandlePortalResponseSignal, &state);
DBusError remove_error;
dbus_error_init(&remove_error);
dbus_bus_remove_match(connection, match_rule.c_str(), &remove_error);
if (dbus_error_is_set(&remove_error)) {
dbus_error_free(&remove_error);
}
return state.message;
}
bool ExtractRequestPath(DBusMessage* reply, std::string* request_path) {
if (!reply || !request_path) {
return false;
}
const char* path = nullptr;
DBusError error;
dbus_error_init(&error);
const dbus_bool_t ok = dbus_message_get_args(
reply, &error, DBUS_TYPE_OBJECT_PATH, &path, DBUS_TYPE_INVALID);
if (!ok || !path) {
LogDbusError("dbus_message_get_args(request_path)", &error);
dbus_error_free(&error);
return false;
}
*request_path = path;
return true;
}
bool ExtractPortalResponse(DBusMessage* message, uint32_t* response_code,
DBusMessageIter* results_array) {
if (!message || !response_code || !results_array) {
return false;
}
DBusMessageIter iter;
if (!dbus_message_iter_init(message, &iter) ||
dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_UINT32) {
return false;
}
dbus_message_iter_get_basic(&iter, response_code);
if (!dbus_message_iter_next(&iter) ||
dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_ARRAY) {
return false;
}
*results_array = iter;
return true;
}
bool SendPortalRequestAndHandleResponse(
DBusConnection* connection, const char* interface_name,
const char* method_name, const char* action_name,
const std::function<bool(DBusMessage*)>& append_message_args,
const std::function<bool(uint32_t, DBusMessageIter*)>& handle_results,
std::string* request_path_out = nullptr) {
if (!connection || !interface_name || interface_name[0] == '\0' ||
!method_name || method_name[0] == '\0') {
return false;
}
DBusMessage* message = dbus_message_new_method_call(
kPortalBusName, kPortalObjectPath, interface_name, method_name);
if (!message) {
LOG_ERROR("Failed to allocate {} message", method_name);
return false;
}
if (append_message_args && !append_message_args(message)) {
dbus_message_unref(message);
LOG_ERROR("{} arguments are malformed", method_name);
return false;
}
DBusError error;
dbus_error_init(&error);
DBusMessage* reply =
dbus_connection_send_with_reply_and_block(connection, message, -1, &error);
dbus_message_unref(message);
if (!reply) {
LogDbusError(action_name ? action_name : method_name, &error);
dbus_error_free(&error);
return false;
}
std::string request_path;
const bool got_request_path = ExtractRequestPath(reply, &request_path);
dbus_message_unref(reply);
if (!got_request_path) {
return false;
}
if (request_path_out) {
*request_path_out = request_path;
}
DBusMessage* response = WaitForPortalResponse(connection, request_path);
if (!response) {
LOG_ERROR("Timed out waiting for {} response", method_name);
return false;
}
uint32_t response_code = 1;
DBusMessageIter results;
const bool parsed = ExtractPortalResponse(response, &response_code, &results);
if (!parsed) {
dbus_message_unref(response);
LOG_ERROR("{} response was malformed", method_name);
return false;
}
const bool ok = handle_results ? handle_results(response_code, &results)
: (response_code == 0);
dbus_message_unref(response);
return ok;
}
} // namespace
#endif
bool KeyboardCapturer::InitWaylandPortal() {
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
CleanupWaylandPortal();
DBusError error;
dbus_error_init(&error);
DBusConnection* check_connection = dbus_bus_get(DBUS_BUS_SESSION, &error);
if (!check_connection) {
LogDbusError("dbus_bus_get", &error);
dbus_error_free(&error);
return false;
}
const dbus_bool_t has_owner =
dbus_bus_name_has_owner(check_connection, kPortalBusName, &error);
if (dbus_error_is_set(&error)) {
LogDbusError("dbus_bus_name_has_owner", &error);
dbus_error_free(&error);
dbus_connection_unref(check_connection);
return false;
}
dbus_connection_unref(check_connection);
if (!has_owner) {
LOG_ERROR("xdg-desktop-portal is not available on session bus");
return false;
}
dbus_connection_ = dbus_bus_get_private(DBUS_BUS_SESSION, &error);
if (!dbus_connection_) {
LogDbusError("dbus_bus_get_private", &error);
dbus_error_free(&error);
return false;
}
dbus_connection_set_exit_on_disconnect(dbus_connection_, FALSE);
const std::string session_handle_token =
MakeToken("crossdesk_keyboard_session");
std::string request_path;
const bool create_ok = SendPortalRequestAndHandleResponse(
dbus_connection_, kPortalRemoteDesktopInterface, "CreateSession",
"CreateSession",
[&](DBusMessage* message) {
DBusMessageIter iter;
DBusMessageIter options;
dbus_message_iter_init_append(message, &iter);
dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}",
&options);
AppendDictEntryString(&options, "session_handle_token",
session_handle_token);
AppendDictEntryString(&options, "handle_token",
MakeToken("crossdesk_keyboard_req"));
dbus_message_iter_close_container(&iter, &options);
return true;
},
[&](uint32_t response_code, DBusMessageIter* results) {
if (response_code != 0) {
LOG_ERROR("RemoteDesktop.CreateSession denied, response={}",
response_code);
return false;
}
DBusMessageIter dict;
dbus_message_iter_recurse(results, &dict);
while (dbus_message_iter_get_arg_type(&dict) != DBUS_TYPE_INVALID) {
if (dbus_message_iter_get_arg_type(&dict) == DBUS_TYPE_DICT_ENTRY) {
DBusMessageIter entry;
dbus_message_iter_recurse(&dict, &entry);
const char* key = nullptr;
dbus_message_iter_get_basic(&entry, &key);
if (key && dbus_message_iter_next(&entry) &&
dbus_message_iter_get_arg_type(&entry) == DBUS_TYPE_VARIANT &&
strcmp(key, "session_handle") == 0) {
DBusMessageIter variant;
std::string parsed_handle;
dbus_message_iter_recurse(&entry, &variant);
if (ReadPathLikeVariant(&variant, &parsed_handle) &&
!parsed_handle.empty()) {
wayland_session_handle_ = parsed_handle;
break;
}
}
}
dbus_message_iter_next(&dict);
}
return true;
},
&request_path);
if (!create_ok) {
CleanupWaylandPortal();
return false;
}
if (wayland_session_handle_.empty()) {
wayland_session_handle_ =
BuildSessionHandleFromRequestPath(request_path, session_handle_token);
}
if (wayland_session_handle_.empty()) {
LOG_ERROR("RemoteDesktop.CreateSession did not return session handle");
CleanupWaylandPortal();
return false;
}
const char* session_handle = wayland_session_handle_.c_str();
const bool select_ok = SendPortalRequestAndHandleResponse(
dbus_connection_, kPortalRemoteDesktopInterface, "SelectDevices",
"SelectDevices",
[&](DBusMessage* message) {
DBusMessageIter iter;
DBusMessageIter options;
dbus_message_iter_init_append(message, &iter);
dbus_message_iter_append_basic(&iter, DBUS_TYPE_OBJECT_PATH,
&session_handle);
dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}",
&options);
AppendDictEntryUint32(&options, "types", kRemoteDesktopDeviceKeyboard);
AppendDictEntryString(&options, "handle_token",
MakeToken("crossdesk_keyboard_req"));
dbus_message_iter_close_container(&iter, &options);
return true;
},
[](uint32_t response_code, DBusMessageIter*) {
if (response_code != 0) {
LOG_ERROR("RemoteDesktop.SelectDevices denied, response={}",
response_code);
return false;
}
return true;
});
if (!select_ok) {
CleanupWaylandPortal();
return false;
}
const char* parent_window = "";
bool keyboard_granted = false;
const bool start_ok = SendPortalRequestAndHandleResponse(
dbus_connection_, kPortalRemoteDesktopInterface, "Start", "Start",
[&](DBusMessage* message) {
DBusMessageIter iter;
DBusMessageIter options;
dbus_message_iter_init_append(message, &iter);
dbus_message_iter_append_basic(&iter, DBUS_TYPE_OBJECT_PATH,
&session_handle);
dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &parent_window);
dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}",
&options);
AppendDictEntryString(&options, "handle_token",
MakeToken("crossdesk_keyboard_req"));
dbus_message_iter_close_container(&iter, &options);
return true;
},
[&](uint32_t response_code, DBusMessageIter* results) {
if (response_code != 0) {
LOG_ERROR("RemoteDesktop.Start denied, response={}", response_code);
return false;
}
uint32_t granted_devices = 0;
DBusMessageIter dict;
dbus_message_iter_recurse(results, &dict);
while (dbus_message_iter_get_arg_type(&dict) != DBUS_TYPE_INVALID) {
if (dbus_message_iter_get_arg_type(&dict) == DBUS_TYPE_DICT_ENTRY) {
DBusMessageIter entry;
dbus_message_iter_recurse(&dict, &entry);
const char* key = nullptr;
dbus_message_iter_get_basic(&entry, &key);
if (key && dbus_message_iter_next(&entry) &&
dbus_message_iter_get_arg_type(&entry) == DBUS_TYPE_VARIANT) {
DBusMessageIter variant;
dbus_message_iter_recurse(&entry, &variant);
if (strcmp(key, "devices") == 0) {
ReadUint32Like(&variant, &granted_devices);
}
}
}
dbus_message_iter_next(&dict);
}
keyboard_granted =
(granted_devices & kRemoteDesktopDeviceKeyboard) != 0;
if (!keyboard_granted) {
LOG_ERROR(
"RemoteDesktop.Start granted devices mask={}, keyboard not allowed",
granted_devices);
return false;
}
return true;
});
if (!start_ok) {
CleanupWaylandPortal();
return false;
}
if (!keyboard_granted) {
LOG_ERROR("RemoteDesktop session started without keyboard permission");
CleanupWaylandPortal();
return false;
}
return true;
#else
return false;
#endif
}
void KeyboardCapturer::CleanupWaylandPortal() {
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
if (dbus_connection_) {
CloseWaylandPortalSessionAndConnection(dbus_connection_,
wayland_session_handle_,
"RemoteDesktop.Session.Close");
dbus_connection_ = nullptr;
}
#endif
use_wayland_portal_ = false;
wayland_session_handle_.clear();
}
int KeyboardCapturer::SendWaylandKeyboardCommand(int key_code, bool is_down,
uint32_t scan_code,
bool extended) {
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
if (!dbus_connection_ || wayland_session_handle_.empty()) {
return -1;
}
const uint32_t key_state = is_down ? kKeyboardPressed : kKeyboardReleased;
const int evdev_keycode =
ResolveLinuxEvdevKeycodeFromWindowsKey(key_code, scan_code, extended);
if (evdev_keycode >= 0 &&
NotifyWaylandKeyboardKeycode(evdev_keycode, key_state)) {
return 0;
}
const auto key_it = vkCodeToX11KeySym.find(key_code);
if (key_it == vkCodeToX11KeySym.end()) {
if (evdev_keycode >= 0) {
LOG_ERROR(
"Failed to send Wayland keyboard keycode event, vk_code={}, "
"evdev_keycode={}, is_down={}",
key_code, evdev_keycode, is_down);
return -3;
}
return 0;
}
// Prefer keycode injection to preserve physical-key semantics and avoid
// implicit Shift interpretation for uppercase keysyms.
if (display_) {
const int keysym = key_it->second;
const KeyCode x11_keycode =
XKeysymToKeycode(display_, static_cast<KeySym>(keysym));
if (x11_keycode > 8) {
const int x11_evdev_keycode = static_cast<int>(x11_keycode) - 8;
if (NotifyWaylandKeyboardKeycode(x11_evdev_keycode, key_state)) {
return 0;
}
}
}
const int keysym = key_it->second;
const int fallback_keysym = NormalizeFallbackKeysym(keysym);
if (NotifyWaylandKeyboardKeysym(fallback_keysym, key_state)) {
return 0;
}
LOG_ERROR("Failed to send Wayland keyboard event, vk_code={}, is_down={}",
key_code, is_down);
return -3;
#else
(void)key_code;
(void)is_down;
(void)scan_code;
(void)extended;
return -1;
#endif
}
bool KeyboardCapturer::NotifyWaylandKeyboardKeysym(int keysym, uint32_t state) {
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
return SendWaylandPortalVoidCall(
"NotifyKeyboardKeysym", [&](DBusMessageIter* iter) {
const char* session_handle = wayland_session_handle_.c_str();
int32_t key_sym = keysym;
uint32_t key_state = state;
dbus_message_iter_append_basic(iter, DBUS_TYPE_OBJECT_PATH,
&session_handle);
AppendEmptyOptionsDict(iter);
dbus_message_iter_append_basic(iter, DBUS_TYPE_INT32, &key_sym);
dbus_message_iter_append_basic(iter, DBUS_TYPE_UINT32, &key_state);
});
#else
(void)keysym;
(void)state;
return false;
#endif
}
bool KeyboardCapturer::NotifyWaylandKeyboardKeycode(int keycode,
uint32_t state) {
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
return SendWaylandPortalVoidCall(
"NotifyKeyboardKeycode", [&](DBusMessageIter* iter) {
const char* session_handle = wayland_session_handle_.c_str();
int32_t key_code = keycode;
uint32_t key_state = state;
dbus_message_iter_append_basic(iter, DBUS_TYPE_OBJECT_PATH,
&session_handle);
AppendEmptyOptionsDict(iter);
dbus_message_iter_append_basic(iter, DBUS_TYPE_INT32, &key_code);
dbus_message_iter_append_basic(iter, DBUS_TYPE_UINT32, &key_state);
});
#else
(void)keycode;
(void)state;
return false;
#endif
}
bool KeyboardCapturer::SendWaylandPortalVoidCall(
const char* method_name,
const std::function<void(DBusMessageIter*)>& append_args) {
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
if (!dbus_connection_ || !method_name || method_name[0] == '\0') {
return false;
}
DBusMessage* message = dbus_message_new_method_call(
kPortalBusName, kPortalObjectPath, kPortalRemoteDesktopInterface,
method_name);
if (!message) {
LOG_ERROR("Failed to allocate {} message", method_name);
return false;
}
DBusMessageIter iter;
dbus_message_iter_init_append(message, &iter);
if (append_args) {
append_args(&iter);
}
DBusError error;
dbus_error_init(&error);
DBusMessage* reply = dbus_connection_send_with_reply_and_block(
dbus_connection_, message, 5000, &error);
dbus_message_unref(message);
if (!reply) {
LogDbusError(method_name, &error);
dbus_error_free(&error);
return false;
}
if (dbus_message_get_type(reply) == DBUS_MESSAGE_TYPE_ERROR) {
const char* error_name = dbus_message_get_error_name(reply);
LOG_ERROR("{} returned DBus error: {}", method_name,
error_name ? error_name : "unknown");
dbus_message_unref(reply);
return false;
}
dbus_message_unref(reply);
return true;
#else
(void)method_name;
(void)append_args;
return false;
#endif
}
} // namespace crossdesk
@@ -1,5 +1,7 @@
#include "keyboard_capturer.h"
#include <unordered_map>
#include "keyboard_converter.h"
#include "rd_log.h"
@@ -7,9 +9,100 @@ namespace crossdesk {
static OnKeyAction g_on_key_action = nullptr;
static void* g_user_ptr = nullptr;
static std::unordered_map<int, int> g_unmapped_keycode_to_vk;
static int VkCodeFromUnicode(UniChar ch) {
if (ch >= 'a' && ch <= 'z') {
return static_cast<int>(ch - 'a' + 'A');
}
if (ch >= 'A' && ch <= 'Z') {
return static_cast<int>(ch);
}
if (ch >= '0' && ch <= '9') {
return static_cast<int>(ch);
}
switch (ch) {
case ' ':
return 0x20; // VK_SPACE
case '-':
case '_':
return 0xBD; // VK_OEM_MINUS
case '=':
case '+':
return 0xBB; // VK_OEM_PLUS
case '[':
case '{':
return 0xDB; // VK_OEM_4
case ']':
case '}':
return 0xDD; // VK_OEM_6
case '\\':
case '|':
return 0xDC; // VK_OEM_5
case ';':
case ':':
return 0xBA; // VK_OEM_1
case '\'':
case '"':
return 0xDE; // VK_OEM_7
case ',':
case '<':
return 0xBC; // VK_OEM_COMMA
case '.':
case '>':
return 0xBE; // VK_OEM_PERIOD
case '/':
case '?':
return 0xBF; // VK_OEM_2
case '`':
case '~':
return 0xC0; // VK_OEM_3
default:
return -1;
}
}
static int ResolveVkCodeFromMacEvent(CGEventRef event, CGKeyCode key_code,
bool is_key_down) {
auto key_it = CGKeyCodeToVkCode.find(key_code);
if (key_it != CGKeyCodeToVkCode.end()) {
if (is_key_down) {
g_unmapped_keycode_to_vk.erase(static_cast<int>(key_code));
}
return key_it->second;
}
int vk_code = -1;
UniChar chars[4] = {0};
UniCharCount char_count = 0;
CGEventKeyboardGetUnicodeString(event, 4, &char_count, chars);
if (char_count > 0) {
vk_code = VkCodeFromUnicode(chars[0]);
}
if (vk_code < 0) {
auto fallback_it =
g_unmapped_keycode_to_vk.find(static_cast<int>(key_code));
if (fallback_it != g_unmapped_keycode_to_vk.end()) {
vk_code = fallback_it->second;
}
}
if (vk_code >= 0) {
if (is_key_down) {
g_unmapped_keycode_to_vk[static_cast<int>(key_code)] = vk_code;
} else {
g_unmapped_keycode_to_vk.erase(static_cast<int>(key_code));
}
}
return vk_code;
}
CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type,
CGEventRef event, void* userInfo) {
(void)proxy;
if (!g_on_key_action) {
return event;
}
@@ -20,84 +113,79 @@ CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type,
return event;
}
int vk_code = 0;
if (type == kCGEventKeyDown || type == kCGEventKeyUp) {
const bool is_key_down = (type == kCGEventKeyDown);
CGKeyCode key_code = static_cast<CGKeyCode>(
CGEventGetIntegerValueField(event, kCGKeyboardEventKeycode));
if (CGKeyCodeToVkCode.find(key_code) != CGKeyCodeToVkCode.end()) {
g_on_key_action(CGKeyCodeToVkCode[key_code], type == kCGEventKeyDown,
g_user_ptr);
int vk_code = ResolveVkCodeFromMacEvent(event, key_code, is_key_down);
if (vk_code >= 0) {
g_on_key_action(vk_code, is_key_down, 0, false, g_user_ptr);
}
} else if (type == kCGEventFlagsChanged) {
CGEventFlags current_flags = CGEventGetFlags(event);
CGKeyCode key_code = static_cast<CGKeyCode>(
CGEventGetIntegerValueField(event, kCGKeyboardEventKeycode));
auto key_it = CGKeyCodeToVkCode.find(key_code);
if (key_it == CGKeyCodeToVkCode.end()) {
return nullptr;
}
const int vk_code = key_it->second;
// caps lock
bool caps_lock_state = (current_flags & kCGEventFlagMaskAlphaShift) != 0;
if (caps_lock_state != keyboard_capturer->caps_lock_flag_) {
keyboard_capturer->caps_lock_flag_ = caps_lock_state;
if (keyboard_capturer->caps_lock_flag_) {
g_on_key_action(CGKeyCodeToVkCode[key_code], true, g_user_ptr);
} else {
g_on_key_action(CGKeyCodeToVkCode[key_code], false, g_user_ptr);
}
g_on_key_action(vk_code, keyboard_capturer->caps_lock_flag_, 0, false,
g_user_ptr);
}
// shift
bool shift_state = (current_flags & kCGEventFlagMaskShift) != 0;
if (shift_state != keyboard_capturer->shift_flag_) {
keyboard_capturer->shift_flag_ = shift_state;
if (keyboard_capturer->shift_flag_) {
g_on_key_action(CGKeyCodeToVkCode[key_code], true, g_user_ptr);
} else {
g_on_key_action(CGKeyCodeToVkCode[key_code], false, g_user_ptr);
}
g_on_key_action(vk_code, keyboard_capturer->shift_flag_, 0, false,
g_user_ptr);
}
// control
bool control_state = (current_flags & kCGEventFlagMaskControl) != 0;
if (control_state != keyboard_capturer->control_flag_) {
keyboard_capturer->control_flag_ = control_state;
if (keyboard_capturer->control_flag_) {
g_on_key_action(CGKeyCodeToVkCode[key_code], true, g_user_ptr);
} else {
g_on_key_action(CGKeyCodeToVkCode[key_code], false, g_user_ptr);
}
g_on_key_action(vk_code, keyboard_capturer->control_flag_, 0, false,
g_user_ptr);
}
// option
bool option_state = (current_flags & kCGEventFlagMaskAlternate) != 0;
if (option_state != keyboard_capturer->option_flag_) {
keyboard_capturer->option_flag_ = option_state;
if (keyboard_capturer->option_flag_) {
g_on_key_action(CGKeyCodeToVkCode[key_code], true, g_user_ptr);
} else {
g_on_key_action(CGKeyCodeToVkCode[key_code], false, g_user_ptr);
}
g_on_key_action(vk_code, keyboard_capturer->option_flag_, 0, false,
g_user_ptr);
}
// command
bool command_state = (current_flags & kCGEventFlagMaskCommand) != 0;
if (command_state != keyboard_capturer->command_flag_) {
keyboard_capturer->command_flag_ = command_state;
if (keyboard_capturer->command_flag_) {
g_on_key_action(CGKeyCodeToVkCode[key_code], true, g_user_ptr);
} else {
g_on_key_action(CGKeyCodeToVkCode[key_code], false, g_user_ptr);
}
g_on_key_action(vk_code, keyboard_capturer->command_flag_, 0, false,
g_user_ptr);
}
}
return nullptr;
}
KeyboardCapturer::KeyboardCapturer() {}
KeyboardCapturer::KeyboardCapturer()
: event_tap_(nullptr), run_loop_source_(nullptr) {}
KeyboardCapturer::~KeyboardCapturer() {}
KeyboardCapturer::~KeyboardCapturer() { Unhook(); }
int KeyboardCapturer::Hook(OnKeyAction on_key_action, void* user_ptr) {
if (event_tap_) {
return 0;
}
g_unmapped_keycode_to_vk.clear();
g_on_key_action = on_key_action;
g_user_ptr = user_ptr;
@@ -115,15 +203,30 @@ int KeyboardCapturer::Hook(OnKeyAction on_key_action, void* user_ptr) {
run_loop_source_ =
CFMachPortCreateRunLoopSource(kCFAllocatorDefault, event_tap_, 0);
if (!run_loop_source_) {
LOG_ERROR("CFMachPortCreateRunLoopSource failed");
CFRelease(event_tap_);
event_tap_ = nullptr;
return -1;
}
CFRunLoopAddSource(CFRunLoopGetCurrent(), run_loop_source_,
kCFRunLoopCommonModes);
const CGEventFlags current_flags =
CGEventSourceFlagsState(kCGEventSourceStateCombinedSessionState);
caps_lock_flag_ = (current_flags & kCGEventFlagMaskAlphaShift) != 0;
shift_flag_ = (current_flags & kCGEventFlagMaskShift) != 0;
control_flag_ = (current_flags & kCGEventFlagMaskControl) != 0;
option_flag_ = (current_flags & kCGEventFlagMaskAlternate) != 0;
command_flag_ = (current_flags & kCGEventFlagMaskCommand) != 0;
CGEventTapEnable(event_tap_, true);
return 0;
}
int KeyboardCapturer::Unhook() {
g_unmapped_keycode_to_vk.clear();
g_on_key_action = nullptr;
g_user_ptr = nullptr;
@@ -166,13 +269,39 @@ inline bool IsFunctionKey(int key_code) {
}
}
int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down) {
CGEventFlags ToCGEventFlags(uint32_t injected_flags) {
CGEventFlags flags = 0;
if ((injected_flags & kMacInjectedModifierShift) != 0) {
flags |= kCGEventFlagMaskShift;
}
if ((injected_flags & kMacInjectedModifierControl) != 0) {
flags |= kCGEventFlagMaskControl;
}
if ((injected_flags & kMacInjectedModifierOption) != 0) {
flags |= kCGEventFlagMaskAlternate;
}
if ((injected_flags & kMacInjectedModifierCommand) != 0) {
flags |= kCGEventFlagMaskCommand;
}
return flags;
}
int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down,
uint32_t scan_code, bool extended) {
(void)scan_code;
(void)extended;
const uint32_t injected_flags =
injected_modifier_state_.Update(key_code, is_down);
if (vkCodeToCGKeyCode.find(key_code) != vkCodeToCGKeyCode.end()) {
CGKeyCode cg_key_code = vkCodeToCGKeyCode[key_code];
CGEventRef event = CGEventCreateKeyboardEvent(NULL, cg_key_code, is_down);
CGEventRef clearFlags =
CGEventCreateKeyboardEvent(NULL, (CGKeyCode)0, true);
CGEventSetFlags(clearFlags, 0);
if (!event) {
LOG_ERROR("CGEventCreateKeyboardEvent failed");
return -1;
}
CGEventSetFlags(event, ToCGEventFlags(injected_flags));
CGEventPost(kCGHIDEventTap, event);
CFRelease(event);
@@ -181,6 +310,10 @@ int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down) {
if (IsFunctionKey(cg_key_code) && !is_down) {
CGEventRef fn_release_event =
CGEventCreateKeyboardEvent(NULL, fn_key_code_, false);
if (!fn_release_event) {
LOG_ERROR("CGEventCreateKeyboardEvent failed for fn release");
return -1;
}
CGEventPost(kCGHIDEventTap, fn_release_event);
CFRelease(fn_release_event);
}
@@ -188,4 +321,4 @@ int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down) {
return 0;
}
} // namespace crossdesk
} // namespace crossdesk
@@ -10,6 +10,7 @@
#include <ApplicationServices/ApplicationServices.h>
#include "device_controller.h"
#include "macos_keyboard_modifier_state.h"
namespace crossdesk {
@@ -21,11 +22,13 @@ class KeyboardCapturer : public DeviceController {
public:
virtual int Hook(OnKeyAction on_key_action, void* user_ptr);
virtual int Unhook();
virtual int SendKeyboardCommand(int key_code, bool is_down);
virtual int SendKeyboardCommand(int key_code, bool is_down,
uint32_t scan_code = 0,
bool extended = false);
private:
CFMachPortRef event_tap_;
CFRunLoopSourceRef run_loop_source_;
CFMachPortRef event_tap_ = nullptr;
CFRunLoopSourceRef run_loop_source_ = nullptr;
public:
bool caps_lock_flag_ = false;
@@ -34,6 +37,7 @@ class KeyboardCapturer : public DeviceController {
bool option_flag_ = false;
bool command_flag_ = false;
int fn_key_code_ = 0x3F;
MacKeyboardModifierState injected_modifier_state_;
};
} // namespace crossdesk
#endif
#endif
@@ -7,14 +7,56 @@ namespace crossdesk {
static OnKeyAction g_on_key_action = nullptr;
static void* g_user_ptr = nullptr;
static int NormalizeModifierVkCode(const KBDLLHOOKSTRUCT* kb_data) {
if (kb_data == nullptr) {
return -1;
}
if (kb_data->vkCode != VK_SHIFT && kb_data->vkCode != VK_CONTROL &&
kb_data->vkCode != VK_MENU) {
return static_cast<int>(kb_data->vkCode);
}
UINT scan_code = static_cast<UINT>(kb_data->scanCode & 0xFF);
if ((kb_data->flags & LLKHF_EXTENDED) != 0) {
scan_code |= 0xE000;
}
const UINT normalized_vk = MapVirtualKeyW(scan_code, MAPVK_VSC_TO_VK_EX);
if (normalized_vk != 0) {
return static_cast<int>(normalized_vk);
}
return static_cast<int>(kb_data->vkCode);
}
static bool PreferSideSpecificVkInjection(int key_code) {
switch (key_code) {
case VK_LSHIFT:
case VK_RSHIFT:
case VK_LCONTROL:
case VK_RCONTROL:
case VK_LMENU:
case VK_RMENU:
case VK_LWIN:
case VK_RWIN:
return true;
default:
return false;
}
}
LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) {
if (nCode == HC_ACTION && g_on_key_action) {
KBDLLHOOKSTRUCT* kbData = reinterpret_cast<KBDLLHOOKSTRUCT*>(lParam);
const int key_code = NormalizeModifierVkCode(kbData);
if (wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN) {
g_on_key_action(kbData->vkCode, true, g_user_ptr);
g_on_key_action(key_code, true, kbData->scanCode,
(kbData->flags & LLKHF_EXTENDED) != 0, g_user_ptr);
} else if (wParam == WM_KEYUP || wParam == WM_SYSKEYUP) {
g_on_key_action(kbData->vkCode, false, g_user_ptr);
g_on_key_action(key_code, false, kbData->scanCode,
(kbData->flags & LLKHF_EXTENDED) != 0, g_user_ptr);
}
return 1;
}
@@ -49,16 +91,53 @@ int KeyboardCapturer::Unhook() {
}
// apply remote keyboard commands to the local machine
int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down) {
int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down,
uint32_t scan_code, bool extended) {
INPUT input = {0};
input.type = INPUT_KEYBOARD;
input.ki.wVk = (WORD)key_code;
const bool prefer_vk = PreferSideSpecificVkInjection(key_code);
const UINT resolved_scan_code =
scan_code != 0
? static_cast<UINT>(scan_code & 0xFF) | (extended ? 0xE000u : 0u)
: MapVirtualKeyW(static_cast<UINT>(key_code), MAPVK_VK_TO_VSC_EX);
if (scan_code != 0 && !prefer_vk) {
input.ki.wVk = 0;
input.ki.wScan = static_cast<WORD>(scan_code & 0xFF);
input.ki.dwFlags |= KEYEVENTF_SCANCODE;
if (extended) {
input.ki.dwFlags |= KEYEVENTF_EXTENDEDKEY;
}
} else {
input.ki.wVk = (WORD)key_code;
if (prefer_vk && resolved_scan_code != 0) {
input.ki.wScan = static_cast<WORD>(resolved_scan_code & 0xFF);
if ((resolved_scan_code & 0xFF00) != 0) {
input.ki.dwFlags |= KEYEVENTF_EXTENDEDKEY;
}
} else if (resolved_scan_code != 0) {
input.ki.wVk = 0;
input.ki.wScan = static_cast<WORD>(resolved_scan_code & 0xFF);
input.ki.dwFlags |= KEYEVENTF_SCANCODE;
if ((resolved_scan_code & 0xFF00) != 0) {
input.ki.dwFlags |= KEYEVENTF_EXTENDEDKEY;
}
}
}
if (!is_down) {
input.ki.dwFlags = KEYEVENTF_KEYUP;
input.ki.dwFlags |= KEYEVENTF_KEYUP;
}
UINT sent = SendInput(1, &input, sizeof(INPUT));
if (sent != 1) {
LOG_WARN("SendInput failed for key_code={}, is_down={}, err={}", key_code,
is_down, GetLastError());
return -1;
}
SendInput(1, &input, sizeof(INPUT));
return 0;
}
} // namespace crossdesk
} // namespace crossdesk
@@ -21,7 +21,9 @@ class KeyboardCapturer : public DeviceController {
public:
virtual int Hook(OnKeyAction on_key_action, void* user_ptr);
virtual int Unhook();
virtual int SendKeyboardCommand(int key_code, bool is_down);
virtual int SendKeyboardCommand(int key_code, bool is_down,
uint32_t scan_code = 0,
bool extended = false);
private:
HHOOK keyboard_hook_ = nullptr;
+79 -73
View File
@@ -73,13 +73,13 @@ std::map<int, int> vkCodeToCGKeyCode = {
{0x20, 0x31}, // Space
{0x08, 0x33}, // Backspace
{0x09, 0x30}, // Tab
{0x2C, 0x74}, // Print Screen
{0x2C, 0x69}, // Print Screen(F13)
{0x2D, 0x72}, // Insert
{0x2E, 0x75}, // Delete
{0x24, 0x73}, // Home
{0x23, 0x77}, // End
{0x21, 0x79}, // Page Up
{0x22, 0x7A}, // Page Down
{0x21, 0x74}, // Page Up
{0x22, 0x79}, // Page Down
// arrow keys
{0x25, 0x7B}, // Left Arrow
@@ -98,6 +98,7 @@ std::map<int, int> vkCodeToCGKeyCode = {
{0x67, 0x59}, // Numpad 7
{0x68, 0x5B}, // Numpad 8
{0x69, 0x5C}, // Numpad 9
{0x90, 0x47}, // Num Lock / Keypad Clear
{0x6E, 0x41}, // Numpad .
{0x6F, 0x4B}, // Numpad /
{0x6A, 0x43}, // Numpad *
@@ -191,13 +192,13 @@ std::map<int, int> CGKeyCodeToVkCode = {
{0x31, 0x20}, // Space
{0x33, 0x08}, // Backspace
{0x30, 0x09}, // Tab
{0x74, 0x2C}, // Print Screen
{0x69, 0x2C}, // Print Screen(F13)
{0x72, 0x2D}, // Insert
{0x75, 0x2E}, // Delete
{0x73, 0x24}, // Home
{0x77, 0x23}, // End
{0x79, 0x21}, // Page Up
{0x7A, 0x22}, // Page Down
{0x74, 0x21}, // Page Up
{0x79, 0x22}, // Page Down
// arrow keys
{0x7B, 0x25}, // Left Arrow
@@ -216,6 +217,7 @@ std::map<int, int> CGKeyCodeToVkCode = {
{0x59, 0x67}, // Numpad 7
{0x5B, 0x68}, // Numpad 8
{0x5C, 0x69}, // Numpad 9
{0x47, 0x90}, // Num Lock / Keypad Clear
{0x41, 0x6E}, // Numpad .
{0x4B, 0x6F}, // Numpad /
{0x43, 0x6A}, // Numpad *
@@ -326,21 +328,22 @@ std::map<int, int> vkCodeToX11KeySym = {
{0x28, 0xFF54}, // Down Arrow
// numpad
{0x60, 0x0030}, // Numpad 0
{0x61, 0x0031}, // Numpad 1
{0x62, 0x0032}, // Numpad 2
{0x63, 0x0033}, // Numpad 3
{0x64, 0x0034}, // Numpad 4
{0x65, 0x0035}, // Numpad 5
{0x66, 0x0036}, // Numpad 6
{0x67, 0x0037}, // Numpad 7
{0x68, 0x0038}, // Numpad 8
{0x69, 0x0039}, // Numpad 9
{0x6E, 0x003A}, // Numpad .
{0x6F, 0x002F}, // Numpad /
{0x6A, 0x002A}, // Numpad *
{0x6D, 0x002D}, // Numpad -
{0x6B, 0x002B}, // Numpad +
{0x60, 0xFFB0}, // Numpad 0
{0x61, 0xFFB1}, // Numpad 1
{0x62, 0xFFB2}, // Numpad 2
{0x63, 0xFFB3}, // Numpad 3
{0x64, 0xFFB4}, // Numpad 4
{0x65, 0xFFB5}, // Numpad 5
{0x66, 0xFFB6}, // Numpad 6
{0x67, 0xFFB7}, // Numpad 7
{0x68, 0xFFB8}, // Numpad 8
{0x69, 0xFFB9}, // Numpad 9
{0x90, 0xFF7F}, // Num Lock
{0x6E, 0xFFAE}, // Numpad .
{0x6F, 0xFFAF}, // Numpad /
{0x6A, 0xFFAA}, // Numpad *
{0x6D, 0xFFAD}, // Numpad -
{0x6B, 0xFFAB}, // Numpad +
// symbol keys
{0xBA, 0x003B}, // ; (Semicolon)
@@ -454,21 +457,22 @@ std::map<int, int> x11KeySymToVkCode = {
{0xFF54, 0x28}, // Down Arrow
// numpad
{0x0030, 0x60}, // Numpad 0
{0x0031, 0x61}, // Numpad 1
{0x0032, 0x62}, // Numpad 2
{0x0033, 0x63}, // Numpad 3
{0x0034, 0x64}, // Numpad 4
{0x0035, 0x65}, // Numpad 5
{0x0036, 0x66}, // Numpad 6
{0x0037, 0x67}, // Numpad 7
{0x0038, 0x68}, // Numpad 8
{0x0039, 0x69}, // Numpad 9
{0x003A, 0x6E}, // Numpad .
{0x002F, 0x6F}, // Numpad /
{0x002A, 0x6A}, // Numpad *
{0x002D, 0x6D}, // Numpad -
{0x002B, 0x6B}, // Numpad +
{0xFFB0, 0x60}, // Numpad 0
{0xFFB1, 0x61}, // Numpad 1
{0xFFB2, 0x62}, // Numpad 2
{0xFFB3, 0x63}, // Numpad 3
{0xFFB4, 0x64}, // Numpad 4
{0xFFB5, 0x65}, // Numpad 5
{0xFFB6, 0x66}, // Numpad 6
{0xFFB7, 0x67}, // Numpad 7
{0xFFB8, 0x68}, // Numpad 8
{0xFFB9, 0x69}, // Numpad 9
{0xFF7F, 0x90}, // Num Lock
{0xFFAE, 0x6E}, // Numpad .
{0xFFAF, 0x6F}, // Numpad /
{0xFFAA, 0x6A}, // Numpad *
{0xFFAD, 0x6D}, // Numpad -
{0xFFAB, 0x6B}, // Numpad +
// symbol keys
{0x003B, 0xBA}, // ; (Semicolon)
@@ -557,13 +561,13 @@ std::map<int, int> cgKeyCodeToX11KeySym = {
{0x31, 0x0020}, // Space
{0x33, 0xFF08}, // Backspace
{0x30, 0xFF09}, // Tab
{0x74, 0xFF15}, // Print Screen
{0x69, 0xFF15}, // Print Screen(F13)
{0x72, 0xFF63}, // Insert
{0x75, 0xFFFF}, // Delete
{0x73, 0xFF50}, // Home
{0x77, 0xFF57}, // End
{0x79, 0xFF55}, // Page Up
{0x7A, 0xFF56}, // Page Down
{0x74, 0xFF55}, // Page Up
{0x79, 0xFF56}, // Page Down
// arrow keys
{0x7B, 0xFF51}, // Left Arrow
@@ -572,21 +576,22 @@ std::map<int, int> cgKeyCodeToX11KeySym = {
{0x7D, 0xFF54}, // Down Arrow
// numpad
{0x52, 0x0030}, // Numpad 0
{0x53, 0x0031}, // Numpad 1
{0x54, 0x0032}, // Numpad 2
{0x55, 0x0033}, // Numpad 3
{0x56, 0x0034}, // Numpad 4
{0x57, 0x0035}, // Numpad 5
{0x58, 0x0036}, // Numpad 6
{0x59, 0x0037}, // Numpad 7
{0x5B, 0x0038}, // Numpad 8
{0x5C, 0x0039}, // Numpad 9
{0x41, 0x003A}, // Numpad .
{0x4B, 0x002F}, // Numpad /
{0x43, 0x002A}, // Numpad *
{0x4E, 0x002D}, // Numpad -
{0x45, 0x002B}, // Numpad +
{0x52, 0xFFB0}, // Numpad 0
{0x53, 0xFFB1}, // Numpad 1
{0x54, 0xFFB2}, // Numpad 2
{0x55, 0xFFB3}, // Numpad 3
{0x56, 0xFFB4}, // Numpad 4
{0x57, 0xFFB5}, // Numpad 5
{0x58, 0xFFB6}, // Numpad 6
{0x59, 0xFFB7}, // Numpad 7
{0x5B, 0xFFB8}, // Numpad 8
{0x5C, 0xFFB9}, // Numpad 9
{0x47, 0xFF7F}, // Num Lock / Keypad Clear
{0x41, 0xFFAE}, // Numpad .
{0x4B, 0xFFAF}, // Numpad /
{0x43, 0xFFAA}, // Numpad *
{0x4E, 0xFFAD}, // Numpad -
{0x45, 0xFFAB}, // Numpad +
// symbol keys
{0x29, 0x003B}, // ; (Semicolon)
@@ -683,13 +688,13 @@ std::map<int, int> x11KeySymToCgKeyCode = {
{0x0020, 0x31}, // Space
{0xFF08, 0x33}, // Backspace
{0xFF09, 0x30}, // Tab
{0xFF15, 0x74}, // Print Screen
{0xFF15, 0x69}, // Print Screen(F13)
{0xFF63, 0x72}, // Insert
{0xFFFF, 0x75}, // Delete
{0xFF50, 0x73}, // Home
{0xFF57, 0x77}, // End
{0xFF55, 0x79}, // Page Up
{0xFF56, 0x7A}, // Page Down
{0xFF55, 0x74}, // Page Up
{0xFF56, 0x79}, // Page Down
// arrow keys
{0xFF51, 0x7B}, // Left Arrow
@@ -698,21 +703,22 @@ std::map<int, int> x11KeySymToCgKeyCode = {
{0xFF54, 0x7D}, // Down Arrow
// numpad
{0x0030, 0x52}, // Numpad 0
{0x0031, 0x53}, // Numpad 1
{0x0032, 0x54}, // Numpad 2
{0x0033, 0x55}, // Numpad 3
{0x0034, 0x56}, // Numpad 4
{0x0035, 0x57}, // Numpad 5
{0x0036, 0x58}, // Numpad 6
{0x0037, 0x59}, // Numpad 7
{0x0038, 0x5B}, // Numpad 8
{0x0039, 0x5C}, // Numpad 9
{0x003A, 0x41}, // Numpad .
{0x002F, 0x4B}, // Numpad /
{0x002A, 0x43}, // Numpad *
{0x002D, 0x4E}, // Numpad -
{0x002B, 0x45}, // Numpad +
{0xFFB0, 0x52}, // Numpad 0
{0xFFB1, 0x53}, // Numpad 1
{0xFFB2, 0x54}, // Numpad 2
{0xFFB3, 0x55}, // Numpad 3
{0xFFB4, 0x56}, // Numpad 4
{0xFFB5, 0x57}, // Numpad 5
{0xFFB6, 0x58}, // Numpad 6
{0xFFB7, 0x59}, // Numpad 7
{0xFFB8, 0x5B}, // Numpad 8
{0xFFB9, 0x5C}, // Numpad 9
{0xFF7F, 0x47}, // Num Lock / Keypad Clear
{0xFFAE, 0x41}, // Numpad .
{0xFFAF, 0x4B}, // Numpad /
{0xFFAA, 0x43}, // Numpad *
{0xFFAD, 0x4E}, // Numpad -
{0xFFAB, 0x45}, // Numpad +
// symbol keys
{0x003B, 0x29}, // ; (Semicolon)
@@ -739,4 +745,4 @@ std::map<int, int> x11KeySymToCgKeyCode = {
{0xFFEC, 0x36}, // Right Command
};
} // namespace crossdesk
#endif
#endif
+295
View File
@@ -0,0 +1,295 @@
#ifndef _LINUX_EVDEV_KEYCODE_H_
#define _LINUX_EVDEV_KEYCODE_H_
#include <linux/input-event-codes.h>
#include <cstdint>
namespace crossdesk {
inline int LinuxEvdevKeycodeFromWindowsScanCode(uint32_t scan_code,
bool extended) {
const uint32_t base_scan_code = scan_code & 0xFFu;
if (base_scan_code == 0) {
return -1;
}
if (extended) {
switch (base_scan_code) {
case 0x1C:
return KEY_KPENTER;
case 0x1D:
return KEY_RIGHTCTRL;
case 0x35:
return KEY_KPSLASH;
case 0x38:
return KEY_RIGHTALT;
case 0x47:
return KEY_HOME;
case 0x48:
return KEY_UP;
case 0x49:
return KEY_PAGEUP;
case 0x4B:
return KEY_LEFT;
case 0x4D:
return KEY_RIGHT;
case 0x4F:
return KEY_END;
case 0x50:
return KEY_DOWN;
case 0x51:
return KEY_PAGEDOWN;
case 0x52:
return KEY_INSERT;
case 0x53:
return KEY_DELETE;
case 0x5B:
return KEY_LEFTMETA;
case 0x5C:
return KEY_RIGHTMETA;
default:
return -1;
}
}
// For the common PC set-1 keys, Linux evdev key codes intentionally line up
// with the low byte of the Windows scan code.
if ((base_scan_code >= 0x01 && base_scan_code <= 0x53) ||
base_scan_code == 0x56 || base_scan_code == 0x57 ||
base_scan_code == 0x58) {
return static_cast<int>(base_scan_code);
}
return -1;
}
inline int LinuxEvdevKeycodeFromWindowsVk(int key_code) {
switch (key_code) {
case 0x08:
return KEY_BACKSPACE;
case 0x09:
return KEY_TAB;
case 0x0D:
return KEY_ENTER;
case 0x10:
case 0xA0:
return KEY_LEFTSHIFT;
case 0x11:
case 0xA2:
return KEY_LEFTCTRL;
case 0x12:
case 0xA4:
return KEY_LEFTALT;
case 0x13:
return KEY_PAUSE;
case 0x14:
return KEY_CAPSLOCK;
case 0x1B:
return KEY_ESC;
case 0x20:
return KEY_SPACE;
case 0x21:
return KEY_PAGEUP;
case 0x22:
return KEY_PAGEDOWN;
case 0x23:
return KEY_END;
case 0x24:
return KEY_HOME;
case 0x25:
return KEY_LEFT;
case 0x26:
return KEY_UP;
case 0x27:
return KEY_RIGHT;
case 0x28:
return KEY_DOWN;
case 0x2C:
return KEY_SYSRQ;
case 0x2D:
return KEY_INSERT;
case 0x2E:
return KEY_DELETE;
case 0x30:
return KEY_0;
case 0x31:
return KEY_1;
case 0x32:
return KEY_2;
case 0x33:
return KEY_3;
case 0x34:
return KEY_4;
case 0x35:
return KEY_5;
case 0x36:
return KEY_6;
case 0x37:
return KEY_7;
case 0x38:
return KEY_8;
case 0x39:
return KEY_9;
case 0x41:
return KEY_A;
case 0x42:
return KEY_B;
case 0x43:
return KEY_C;
case 0x44:
return KEY_D;
case 0x45:
return KEY_E;
case 0x46:
return KEY_F;
case 0x47:
return KEY_G;
case 0x48:
return KEY_H;
case 0x49:
return KEY_I;
case 0x4A:
return KEY_J;
case 0x4B:
return KEY_K;
case 0x4C:
return KEY_L;
case 0x4D:
return KEY_M;
case 0x4E:
return KEY_N;
case 0x4F:
return KEY_O;
case 0x50:
return KEY_P;
case 0x51:
return KEY_Q;
case 0x52:
return KEY_R;
case 0x53:
return KEY_S;
case 0x54:
return KEY_T;
case 0x55:
return KEY_U;
case 0x56:
return KEY_V;
case 0x57:
return KEY_W;
case 0x58:
return KEY_X;
case 0x59:
return KEY_Y;
case 0x5A:
return KEY_Z;
case 0x5B:
return KEY_LEFTMETA;
case 0x5C:
return KEY_RIGHTMETA;
case 0x60:
return KEY_KP0;
case 0x61:
return KEY_KP1;
case 0x62:
return KEY_KP2;
case 0x63:
return KEY_KP3;
case 0x64:
return KEY_KP4;
case 0x65:
return KEY_KP5;
case 0x66:
return KEY_KP6;
case 0x67:
return KEY_KP7;
case 0x68:
return KEY_KP8;
case 0x69:
return KEY_KP9;
case 0x6A:
return KEY_KPASTERISK;
case 0x6B:
return KEY_KPPLUS;
case 0x6D:
return KEY_KPMINUS;
case 0x6E:
return KEY_KPDOT;
case 0x6F:
return KEY_KPSLASH;
case 0x70:
return KEY_F1;
case 0x71:
return KEY_F2;
case 0x72:
return KEY_F3;
case 0x73:
return KEY_F4;
case 0x74:
return KEY_F5;
case 0x75:
return KEY_F6;
case 0x76:
return KEY_F7;
case 0x77:
return KEY_F8;
case 0x78:
return KEY_F9;
case 0x79:
return KEY_F10;
case 0x7A:
return KEY_F11;
case 0x7B:
return KEY_F12;
case 0x90:
return KEY_NUMLOCK;
case 0x91:
return KEY_SCROLLLOCK;
case 0xA1:
return KEY_RIGHTSHIFT;
case 0xA3:
return KEY_RIGHTCTRL;
case 0xA5:
return KEY_RIGHTALT;
case 0xBA:
return KEY_SEMICOLON;
case 0xBB:
return KEY_EQUAL;
case 0xBC:
return KEY_COMMA;
case 0xBD:
return KEY_MINUS;
case 0xBE:
return KEY_DOT;
case 0xBF:
return KEY_SLASH;
case 0xC0:
return KEY_GRAVE;
case 0xDB:
return KEY_LEFTBRACE;
case 0xDC:
return KEY_BACKSLASH;
case 0xDD:
return KEY_RIGHTBRACE;
case 0xDE:
return KEY_APOSTROPHE;
default:
return -1;
}
}
inline int ResolveLinuxEvdevKeycodeFromWindowsKey(int key_code,
uint32_t scan_code,
bool extended) {
const int scan_keycode =
LinuxEvdevKeycodeFromWindowsScanCode(scan_code, extended);
if (scan_keycode >= 0) {
return scan_keycode;
}
return LinuxEvdevKeycodeFromWindowsVk(key_code);
}
} // namespace crossdesk
#endif
@@ -0,0 +1,93 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-05-21
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _MACOS_KEYBOARD_MODIFIER_STATE_H_
#define _MACOS_KEYBOARD_MODIFIER_STATE_H_
#include <cstdint>
namespace crossdesk {
inline constexpr uint32_t kMacInjectedModifierShift = 1u << 0;
inline constexpr uint32_t kMacInjectedModifierControl = 1u << 1;
inline constexpr uint32_t kMacInjectedModifierOption = 1u << 2;
inline constexpr uint32_t kMacInjectedModifierCommand = 1u << 3;
class MacKeyboardModifierState {
public:
uint32_t Update(int key_code, bool is_down) {
bool* state = MutableStateForVk(key_code);
if (state != nullptr) {
*state = is_down;
}
return flags();
}
uint32_t flags() const {
uint32_t result = 0;
if (left_shift_down_ || right_shift_down_) {
result |= kMacInjectedModifierShift;
}
if (left_control_down_ || right_control_down_) {
result |= kMacInjectedModifierControl;
}
if (left_option_down_ || right_option_down_) {
result |= kMacInjectedModifierOption;
}
if (left_command_down_ || right_command_down_) {
result |= kMacInjectedModifierCommand;
}
return result;
}
void Clear() {
left_shift_down_ = false;
right_shift_down_ = false;
left_control_down_ = false;
right_control_down_ = false;
left_option_down_ = false;
right_option_down_ = false;
left_command_down_ = false;
right_command_down_ = false;
}
private:
bool* MutableStateForVk(int key_code) {
switch (key_code) {
case 0xA0: // VK_LSHIFT
return &left_shift_down_;
case 0xA1: // VK_RSHIFT
return &right_shift_down_;
case 0xA2: // VK_LCONTROL
return &left_control_down_;
case 0xA3: // VK_RCONTROL
return &right_control_down_;
case 0xA4: // VK_LMENU / left Option
return &left_option_down_;
case 0xA5: // VK_RMENU / right Option
return &right_option_down_;
case 0x5B: // VK_LWIN / left Command
return &left_command_down_;
case 0x5C: // VK_RWIN / right Command
return &right_command_down_;
default:
return nullptr;
}
}
bool left_shift_down_ = false;
bool right_shift_down_ = false;
bool left_control_down_ = false;
bool right_control_down_ = false;
bool left_option_down_ = false;
bool right_option_down_ = false;
bool left_command_down_ = false;
bool right_command_down_ = false;
};
} // namespace crossdesk
#endif
@@ -2,6 +2,7 @@
#include <X11/extensions/XTest.h>
#include "platform.h"
#include "rd_log.h"
namespace crossdesk {
@@ -12,6 +13,17 @@ MouseController::~MouseController() { Destroy(); }
int MouseController::Init(std::vector<DisplayInfo> display_info_list) {
display_info_list_ = display_info_list;
if (IsWaylandSession()) {
if (InitWaylandPortal()) {
use_wayland_portal_ = true;
LOG_INFO("Mouse controller initialized with Wayland portal backend");
return 0;
}
LOG_WARN(
"Wayland mouse control init failed, falling back to X11/XTest backend");
}
display_ = XOpenDisplay(NULL);
if (!display_) {
LOG_ERROR("Cannot connect to X server");
@@ -25,26 +37,68 @@ int MouseController::Init(std::vector<DisplayInfo> display_info_list) {
&minor_version)) {
LOG_ERROR("XTest extension not available");
XCloseDisplay(display_);
display_ = nullptr;
return -2;
}
return 0;
}
void MouseController::UpdateDisplayInfoList(
const std::vector<DisplayInfo>& display_info_list) {
if (display_info_list.empty()) {
return;
}
display_info_list_ = display_info_list;
if (use_wayland_portal_) {
OnWaylandDisplayInfoListUpdated();
}
if (last_display_index_ < 0 ||
last_display_index_ >= static_cast<int>(display_info_list_.size())) {
last_display_index_ = -1;
last_norm_x_ = -1.0;
last_norm_y_ = -1.0;
}
}
int MouseController::Destroy() {
CleanupWaylandPortal();
if (display_) {
XCloseDisplay(display_);
display_ = nullptr;
}
return 0;
}
int MouseController::SendMouseCommand(RemoteAction remote_action,
int display_index) {
if (remote_action.type != ControlType::mouse) {
return 0;
}
if (use_wayland_portal_) {
return SendWaylandMouseCommand(remote_action, display_index);
}
if (!display_) {
LOG_ERROR("X11 display not initialized");
return -1;
}
switch (remote_action.type) {
case mouse:
switch (remote_action.m.flag) {
case MouseFlag::move:
case MouseFlag::move: {
if (display_index < 0 ||
display_index >= static_cast<int>(display_info_list_.size())) {
LOG_ERROR("Invalid display index: {}", display_index);
return -2;
}
SetMousePosition(
static_cast<int>(remote_action.m.x *
display_info_list_[display_index].width +
@@ -53,6 +107,7 @@ int MouseController::SendMouseCommand(RemoteAction remote_action,
display_info_list_[display_index].height +
display_info_list_[display_index].top));
break;
}
case MouseFlag::left_down:
XTestFakeButtonEvent(display_, 1, True, CurrentTime);
XFlush(display_);
@@ -103,25 +158,39 @@ int MouseController::SendMouseCommand(RemoteAction remote_action,
}
void MouseController::SetMousePosition(int x, int y) {
if (!display_) {
return;
}
XWarpPointer(display_, None, root_, 0, 0, 0, 0, x, y);
XFlush(display_);
}
void MouseController::SimulateKeyDown(int kval) {
if (!display_) {
return;
}
XTestFakeKeyEvent(display_, kval, True, CurrentTime);
XFlush(display_);
}
void MouseController::SimulateKeyUp(int kval) {
if (!display_) {
return;
}
XTestFakeKeyEvent(display_, kval, False, CurrentTime);
XFlush(display_);
}
void MouseController::SimulateMouseWheel(int direction_button, int count) {
if (!display_) {
return;
}
for (int i = 0; i < count; ++i) {
XTestFakeButtonEvent(display_, direction_button, True, CurrentTime);
XTestFakeButtonEvent(display_, direction_button, False, CurrentTime);
}
XFlush(display_);
}
} // namespace crossdesk
} // namespace crossdesk
@@ -11,10 +11,16 @@
#include <X11/Xutil.h>
#include <unistd.h>
#include <cstdint>
#include <functional>
#include <string>
#include <vector>
#include "device_controller.h"
struct DBusConnection;
struct DBusMessageIter;
namespace crossdesk {
class MouseController : public DeviceController {
@@ -26,18 +32,49 @@ class MouseController : public DeviceController {
virtual int Init(std::vector<DisplayInfo> display_info_list);
virtual int Destroy();
virtual int SendMouseCommand(RemoteAction remote_action, int display_index);
void UpdateDisplayInfoList(const std::vector<DisplayInfo>& display_info_list);
private:
void SimulateKeyDown(int kval);
void SimulateKeyUp(int kval);
void SetMousePosition(int x, int y);
void SimulateMouseWheel(int direction_button, int count);
bool InitWaylandPortal();
void CleanupWaylandPortal();
int SendWaylandMouseCommand(RemoteAction remote_action, int display_index);
void OnWaylandDisplayInfoListUpdated();
bool NotifyWaylandPointerMotion(double dx, double dy);
bool NotifyWaylandPointerMotionAbsolute(uint32_t stream, double x, double y);
bool NotifyWaylandPointerButton(int button, uint32_t state);
bool NotifyWaylandPointerAxisDiscrete(uint32_t axis, int32_t steps);
bool SendWaylandPortalVoidCall(
const char* method_name,
const std::function<void(DBusMessageIter*)>& append_args);
enum class WaylandAbsoluteMode { kUnknown, kPixels, kNormalized, kDisabled };
Display* display_ = nullptr;
Window root_ = 0;
std::vector<DisplayInfo> display_info_list_;
int screen_width_ = 0;
int screen_height_ = 0;
bool use_wayland_portal_ = false;
DBusConnection* dbus_connection_ = nullptr;
std::string wayland_session_handle_;
int last_display_index_ = -1;
double last_norm_x_ = -1.0;
double last_norm_y_ = -1.0;
bool logged_wayland_display_info_ = false;
uintptr_t last_logged_wayland_stream_ = 0;
int last_logged_wayland_width_ = 0;
int last_logged_wayland_height_ = 0;
WaylandAbsoluteMode wayland_absolute_mode_ = WaylandAbsoluteMode::kUnknown;
bool wayland_absolute_disabled_logged_ = false;
uint32_t wayland_absolute_stream_id_ = 0;
int wayland_portal_space_width_ = 0;
int wayland_portal_space_height_ = 0;
bool using_shared_wayland_session_ = false;
};
} // namespace crossdesk
#endif
#endif
File diff suppressed because it is too large Load Diff
@@ -2,9 +2,35 @@
#include <ApplicationServices/ApplicationServices.h>
#include <algorithm>
#include <chrono>
#include <cstdlib>
#include "rd_log.h"
namespace crossdesk {
namespace {
constexpr auto kDoubleClickInterval = std::chrono::milliseconds(500);
constexpr int kDoubleClickMaxDistance = 8;
constexpr int kMaxClickState = 3;
bool IsWithinClickDistance(int x1, int y1, int x2, int y2) {
return std::abs(x1 - x2) <= kDoubleClickMaxDistance &&
std::abs(y1 - y2) <= kDoubleClickMaxDistance;
}
void SetClickState(CGEventRef event, int click_state) {
if (!event) {
return;
}
CGEventSetIntegerValueField(
event, kCGMouseEventClickState,
std::max(1, std::min(click_state, kMaxClickState)));
}
} // namespace
MouseController::MouseController() {}
@@ -18,87 +44,144 @@ int MouseController::Init(std::vector<DisplayInfo> display_info_list) {
int MouseController::Destroy() { return 0; }
int MouseController::BeginClick(ClickTracker& tracker, int x, int y) {
const auto now = std::chrono::steady_clock::now();
const bool continues_previous_click =
tracker.has_last_down &&
now - tracker.last_down_time <= kDoubleClickInterval &&
IsWithinClickDistance(tracker.last_down_x, tracker.last_down_y, x, y);
tracker.click_state = continues_previous_click
? std::min(tracker.click_state + 1, kMaxClickState)
: 1;
tracker.active_click_state = tracker.click_state;
tracker.has_last_down = true;
tracker.last_down_time = now;
tracker.last_down_x = x;
tracker.last_down_y = y;
return tracker.active_click_state;
}
int MouseController::EndClick(ClickTracker& tracker, int x, int y) {
const int click_state = tracker.active_click_state;
if (!IsWithinClickDistance(tracker.last_down_x, tracker.last_down_y, x, y)) {
tracker.has_last_down = false;
tracker.click_state = 0;
tracker.active_click_state = 1;
}
return click_state;
}
int MouseController::SendMouseCommand(RemoteAction remote_action,
int display_index) {
int mouse_pos_x =
remote_action.m.x * display_info_list_[display_index].width +
display_info_list_[display_index].left;
int mouse_pos_y =
remote_action.m.y * display_info_list_[display_index].height +
display_info_list_[display_index].top;
if (remote_action.type != ControlType::mouse) {
return 0;
}
if (remote_action.type == ControlType::mouse) {
CGEventRef mouse_event = nullptr;
CGEventType mouse_type;
CGMouseButton mouse_button;
CGPoint mouse_point = CGPointMake(mouse_pos_x, mouse_pos_y);
if (display_index < 0 ||
display_index >= static_cast<int>(display_info_list_.size())) {
LOG_WARN("Mouse command skipped, invalid display_index={}, displays={}",
display_index, display_info_list_.size());
return -1;
}
switch (remote_action.m.flag) {
case MouseFlag::left_down:
mouse_type = kCGEventLeftMouseDown;
left_dragging_ = true;
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
kCGMouseButtonLeft);
break;
case MouseFlag::left_up:
mouse_type = kCGEventLeftMouseUp;
left_dragging_ = false;
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
kCGMouseButtonLeft);
break;
case MouseFlag::right_down:
mouse_type = kCGEventRightMouseDown;
right_dragging_ = true;
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
kCGMouseButtonRight);
break;
case MouseFlag::right_up:
mouse_type = kCGEventRightMouseUp;
right_dragging_ = false;
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
kCGMouseButtonRight);
break;
case MouseFlag::middle_down:
mouse_type = kCGEventOtherMouseDown;
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
kCGMouseButtonCenter);
break;
case MouseFlag::middle_up:
mouse_type = kCGEventOtherMouseUp;
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
kCGMouseButtonCenter);
break;
case MouseFlag::wheel_vertical:
mouse_event = CGEventCreateScrollWheelEvent(
NULL, kCGScrollEventUnitLine, 2, remote_action.m.s, 0);
break;
case MouseFlag::wheel_horizontal:
mouse_event = CGEventCreateScrollWheelEvent(
NULL, kCGScrollEventUnitLine, 2, 0, remote_action.m.s);
break;
default:
if (left_dragging_) {
mouse_type = kCGEventLeftMouseDragged;
mouse_button = kCGMouseButtonLeft;
} else if (right_dragging_) {
mouse_type = kCGEventRightMouseDragged;
mouse_button = kCGMouseButtonRight;
} else {
mouse_type = kCGEventMouseMoved;
mouse_button = kCGMouseButtonLeft;
}
const DisplayInfo& display_info = display_info_list_[display_index];
if (display_info.width <= 0 || display_info.height <= 0) {
LOG_WARN("Mouse command skipped, invalid display geometry: {}x{}",
display_info.width, display_info.height);
return -1;
}
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
mouse_button);
break;
}
const float normalized_x = std::clamp(remote_action.m.x, 0.0f, 1.0f);
const float normalized_y = std::clamp(remote_action.m.y, 0.0f, 1.0f);
int mouse_pos_x = normalized_x * display_info.width + display_info.left;
int mouse_pos_y = normalized_y * display_info.height + display_info.top;
if (mouse_event) {
CGEventPost(kCGHIDEventTap, mouse_event);
CFRelease(mouse_event);
}
CGEventRef mouse_event = nullptr;
CGEventType mouse_type;
CGMouseButton mouse_button;
CGPoint mouse_point = CGPointMake(mouse_pos_x, mouse_pos_y);
int click_state = 1;
switch (remote_action.m.flag) {
case MouseFlag::left_down:
mouse_type = kCGEventLeftMouseDown;
left_dragging_ = true;
click_state = BeginClick(left_click_tracker_, mouse_pos_x, mouse_pos_y);
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
kCGMouseButtonLeft);
SetClickState(mouse_event, click_state);
break;
case MouseFlag::left_up:
mouse_type = kCGEventLeftMouseUp;
left_dragging_ = false;
click_state = EndClick(left_click_tracker_, mouse_pos_x, mouse_pos_y);
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
kCGMouseButtonLeft);
SetClickState(mouse_event, click_state);
break;
case MouseFlag::right_down:
mouse_type = kCGEventRightMouseDown;
right_dragging_ = true;
click_state = BeginClick(right_click_tracker_, mouse_pos_x, mouse_pos_y);
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
kCGMouseButtonRight);
SetClickState(mouse_event, click_state);
break;
case MouseFlag::right_up:
mouse_type = kCGEventRightMouseUp;
right_dragging_ = false;
click_state = EndClick(right_click_tracker_, mouse_pos_x, mouse_pos_y);
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
kCGMouseButtonRight);
SetClickState(mouse_event, click_state);
break;
case MouseFlag::middle_down:
mouse_type = kCGEventOtherMouseDown;
click_state = BeginClick(middle_click_tracker_, mouse_pos_x, mouse_pos_y);
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
kCGMouseButtonCenter);
SetClickState(mouse_event, click_state);
break;
case MouseFlag::middle_up:
mouse_type = kCGEventOtherMouseUp;
click_state = EndClick(middle_click_tracker_, mouse_pos_x, mouse_pos_y);
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
kCGMouseButtonCenter);
SetClickState(mouse_event, click_state);
break;
case MouseFlag::wheel_vertical:
mouse_event = CGEventCreateScrollWheelEvent(NULL, kCGScrollEventUnitLine,
2, remote_action.m.s, 0);
break;
case MouseFlag::wheel_horizontal:
mouse_event = CGEventCreateScrollWheelEvent(NULL, kCGScrollEventUnitLine,
2, 0, remote_action.m.s);
break;
default:
if (left_dragging_) {
mouse_type = kCGEventLeftMouseDragged;
mouse_button = kCGMouseButtonLeft;
} else if (right_dragging_) {
mouse_type = kCGEventRightMouseDragged;
mouse_button = kCGMouseButtonRight;
} else {
mouse_type = kCGEventMouseMoved;
mouse_button = kCGMouseButtonLeft;
}
mouse_event =
CGEventCreateMouseEvent(NULL, mouse_type, mouse_point, mouse_button);
break;
}
if (mouse_event) {
CGEventPost(kCGHIDEventTap, mouse_event);
CFRelease(mouse_event);
}
return 0;
}
} // namespace crossdesk
} // namespace crossdesk
@@ -7,6 +7,7 @@
#ifndef _MOUSE_CONTROLLER_H_
#define _MOUSE_CONTROLLER_H_
#include <chrono>
#include <vector>
#include "device_controller.h"
@@ -24,9 +25,24 @@ class MouseController : public DeviceController {
virtual int SendMouseCommand(RemoteAction remote_action, int display_index);
private:
struct ClickTracker {
bool has_last_down = false;
std::chrono::steady_clock::time_point last_down_time{};
int last_down_x = 0;
int last_down_y = 0;
int click_state = 0;
int active_click_state = 1;
};
int BeginClick(ClickTracker& tracker, int x, int y);
int EndClick(ClickTracker& tracker, int x, int y);
std::vector<DisplayInfo> display_info_list_;
bool left_dragging_ = false;
bool right_dragging_ = false;
ClickTracker left_click_tracker_;
ClickTracker right_click_tracker_;
ClickTracker middle_click_tracker_;
};
} // namespace crossdesk
#endif
#endif
@@ -1,5 +1,7 @@
#include "mouse_controller.h"
#include <Windows.h>
#include "rd_log.h"
namespace crossdesk {
@@ -18,7 +20,14 @@ int MouseController::Destroy() { return 0; }
int MouseController::SendMouseCommand(RemoteAction remote_action,
int display_index) {
INPUT ip;
if (display_index < 0 ||
display_index >= static_cast<int>(display_info_list_.size())) {
LOG_WARN("Mouse command skipped, invalid display_index={}, displays={}",
display_index, display_info_list_.size());
return -1;
}
INPUT ip = {0};
if (remote_action.type == ControlType::mouse) {
ip.type = INPUT_MOUSE;
@@ -63,13 +72,25 @@ int MouseController::SendMouseCommand(RemoteAction remote_action,
ip.mi.time = 0;
SetCursorPos(ip.mi.dx, ip.mi.dy);
if (!SetCursorPos(ip.mi.dx, ip.mi.dy)) {
LOG_WARN("SetCursorPos failed for mouse x={}, y={}, flag={}, err={}",
ip.mi.dx, ip.mi.dy, static_cast<int>(remote_action.m.flag),
GetLastError());
return -1;
}
if (ip.mi.dwFlags != MOUSEEVENTF_MOVE) {
SendInput(1, &ip, sizeof(INPUT));
UINT sent = SendInput(1, &ip, sizeof(INPUT));
if (sent != 1) {
LOG_WARN(
"SendInput failed for mouse x={}, y={}, wheel={}, flag={}, err={}",
ip.mi.dx, ip.mi.dy, remote_action.m.s,
static_cast<int>(remote_action.m.flag), GetLastError());
return -1;
}
}
}
return 0;
}
} // namespace crossdesk
} // namespace crossdesk
@@ -0,0 +1,89 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-05-07
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _WINDOWS_KEY_METADATA_H_
#define _WINDOWS_KEY_METADATA_H_
#include <cstdint>
namespace crossdesk {
inline bool LookupWindowsKeyMetadataFromVk(int key_code,
uint32_t* scan_code_out,
bool* extended_out) {
if (scan_code_out == nullptr || extended_out == nullptr) {
return false;
}
switch (key_code) {
case 0x21: // Page Up
*scan_code_out = 0x49;
*extended_out = true;
return true;
case 0x22: // Page Down
*scan_code_out = 0x51;
*extended_out = true;
return true;
case 0x23: // End
*scan_code_out = 0x4F;
*extended_out = true;
return true;
case 0x24: // Home
*scan_code_out = 0x47;
*extended_out = true;
return true;
case 0x25: // Left Arrow
*scan_code_out = 0x4B;
*extended_out = true;
return true;
case 0x26: // Up Arrow
*scan_code_out = 0x48;
*extended_out = true;
return true;
case 0x27: // Right Arrow
*scan_code_out = 0x4D;
*extended_out = true;
return true;
case 0x28: // Down Arrow
*scan_code_out = 0x50;
*extended_out = true;
return true;
case 0x2D: // Insert
*scan_code_out = 0x52;
*extended_out = true;
return true;
case 0x2E: // Delete
*scan_code_out = 0x53;
*extended_out = true;
return true;
case 0x6F: // Numpad /
*scan_code_out = 0x35;
*extended_out = true;
return true;
case 0xA3: // Right Ctrl
*scan_code_out = 0x1D;
*extended_out = true;
return true;
case 0xA5: // Right Alt
*scan_code_out = 0x38;
*extended_out = true;
return true;
case 0x5B: // Left Win
*scan_code_out = 0x5B;
*extended_out = true;
return true;
case 0x5C: // Right Win
*scan_code_out = 0x5C;
*extended_out = true;
return true;
default:
return false;
}
}
} // namespace crossdesk
#endif
+228
View File
@@ -0,0 +1,228 @@
#ifndef CROSSDESK_GUI_APPLICATION_STATE_H_
#define CROSSDESK_GUI_APPLICATION_STATE_H_
#include <SDL3/SDL.h>
#include <chrono>
#include <cstdint>
#include <memory>
#include <nlohmann/json.hpp>
#include <string>
#include "imgui.h"
#if _WIN32
#include "platform/tray/win_tray.h"
#elif defined(__APPLE__)
#include "platform/tray/mac_tray.h"
#elif defined(__linux__)
#include "platform/tray/linux_tray.h"
#endif
namespace crossdesk::gui_detail {
struct MainWindowState {
float title_bar_width_ = 640;
float title_bar_height_ = 30;
float title_bar_button_width_ = 30;
float title_bar_button_height_ = 30;
SDL_Window *main_window_ = nullptr;
SDL_Renderer *main_renderer_ = nullptr;
ImGuiContext *main_ctx_ = nullptr;
ImFont *main_windows_system_chinese_font_ = nullptr;
ImFont *stream_windows_system_chinese_font_ = nullptr;
ImFont *server_windows_system_chinese_font_ = nullptr;
bool exit_ = false;
const int sdl_refresh_ms_ = 16;
#if _WIN32
std::unique_ptr<WinTray> tray_;
#elif defined(__APPLE__)
std::unique_ptr<MacTray> tray_;
#elif defined(__linux__)
std::unique_ptr<LinuxTray> tray_;
#endif
bool main_window_minimized_ = false;
uint32_t last_main_minimize_request_tick_ = 0;
uint32_t last_stream_minimize_request_tick_ = 0;
int main_window_width_real_ = 720;
int main_window_height_real_ = 540;
float main_window_dpi_scaling_w_ = 1.0f;
float main_window_dpi_scaling_h_ = 1.0f;
float dpi_scale_ = 1.0f;
float main_window_width_default_ = 640;
float main_window_height_default_ = 480;
float main_window_width_ = 640;
float main_window_height_ = 480;
float main_window_width_last_ = 640;
float main_window_height_last_ = 480;
float local_window_width_ = 320;
float local_window_height_ = 235;
float remote_window_width_ = 320;
float remote_window_height_ = 235;
float local_child_window_width_ = 266;
float local_child_window_height_ = 180;
float remote_child_window_width_ = 266;
float remote_child_window_height_ = 180;
float main_window_text_y_padding_ = 10;
float main_child_window_x_padding_ = 27;
float main_child_window_y_padding_ = 45;
float status_bar_height_ = 22;
float connection_status_window_width_ = 200;
float connection_status_window_height_ = 150;
float notification_window_width_ = 200;
float notification_window_height_ = 80;
float about_window_width_ = 300;
float about_window_height_ = 170;
float update_notification_window_width_ = 400;
float update_notification_window_height_ = 320;
uint32_t STREAM_REFRESH_EVENT = 0;
uint32_t APP_EXIT_EVENT = 0;
};
// Application-global interaction flags that coordinate SDL events with media
// and remote-control devices.
struct InteractionState {
bool start_mouse_controller_ = false;
bool mouse_controller_is_started_ = false;
bool start_screen_capturer_ = false;
bool screen_capturer_is_started_ = false;
bool start_speaker_capturer_ = false;
bool speaker_capturer_is_started_ = false;
bool start_keyboard_capturer_ = false;
bool show_cursor_ = false;
bool keyboard_capturer_is_started_ = false;
bool keyboard_capturer_uses_sdl_events_ = false;
bool foucs_on_main_window_ = false;
bool focus_on_stream_window_ = false;
bool audio_capture_ = false;
int screen_width_ = 1280;
int screen_height_ = 720;
int selected_display_ = 0;
std::string connect_button_label_ = "Connect";
char input_password_tmp_[7] = "";
char input_password_[7] = "";
std::string random_password_;
char new_password_[7] = "";
char remote_id_display_[12] = "";
unsigned char audio_buffer_[720]{};
int audio_len_ = 0;
bool audio_buffer_fresh_ = false;
bool need_to_rejoin_ = false;
std::chrono::steady_clock::time_point last_rejoin_check_time_ =
std::chrono::steady_clock::now();
bool just_created_ = false;
std::string controlled_remote_id_;
std::string focused_remote_id_;
std::string remote_client_id_;
SDL_Event last_mouse_event{};
};
struct UpdateState {
nlohmann::json latest_version_info_ = nlohmann::json{};
bool update_available_ = false;
std::string latest_version_;
std::string release_notes_;
bool show_new_version_icon_ = false;
bool show_new_version_icon_in_menu_ = true;
double new_version_icon_last_trigger_time_ = 0.0;
double new_version_icon_render_start_time_ = 0.0;
};
struct StreamWindowState {
SDL_Window *stream_window_ = nullptr;
SDL_Renderer *stream_renderer_ = nullptr;
ImGuiContext *stream_ctx_ = nullptr;
bool need_to_create_stream_window_ = false;
bool stream_window_created_ = false;
bool stream_window_inited_ = false;
bool window_maximized_ = false;
bool stream_window_grabbed_ = false;
bool control_mouse_ = false;
int stream_window_width_default_ = 1280;
int stream_window_height_default_ = 720;
float stream_window_width_ = 1280;
float stream_window_height_ = 720;
SDL_PixelFormat stream_pixformat_ = SDL_PIXELFORMAT_NV12;
int stream_window_width_real_ = 1280;
int stream_window_height_real_ = 720;
float stream_window_dpi_scaling_w_ = 1.0f;
float stream_window_dpi_scaling_h_ = 1.0f;
};
struct ServerWindowState {
SDL_Window *server_window_ = nullptr;
SDL_Renderer *server_renderer_ = nullptr;
ImGuiContext *server_ctx_ = nullptr;
bool need_to_create_server_window_ = false;
bool need_to_destroy_server_window_ = false;
bool server_window_created_ = false;
bool server_window_inited_ = false;
int server_window_width_default_ = 250;
int server_window_height_default_ = 150;
float server_window_width_ = 250;
float server_window_height_ = 150;
float server_window_title_bar_height_ = 30.0f;
SDL_PixelFormat server_pixformat_ = SDL_PIXELFORMAT_NV12;
int server_window_normal_width_ = 250;
int server_window_normal_height_ = 150;
float server_window_dpi_scaling_w_ = 1.0f;
float server_window_dpi_scaling_h_ = 1.0f;
float window_rounding_ = 6.0f;
float window_rounding_default_ = 6.0f;
bool server_window_collapsed_ = false;
bool server_window_collapsed_dragging_ = false;
float server_window_collapsed_drag_start_mouse_x_ = 0.0f;
float server_window_collapsed_drag_start_mouse_y_ = 0.0f;
int server_window_collapsed_drag_start_win_x_ = 0;
int server_window_collapsed_drag_start_win_y_ = 0;
bool server_window_dragging_ = false;
float server_window_drag_start_mouse_x_ = 0.0f;
float server_window_drag_start_mouse_y_ = 0.0f;
int server_window_drag_start_win_x_ = 0;
int server_window_drag_start_win_y_ = 0;
};
struct UiState {
bool label_inited_ = false;
bool connect_button_pressed_ = false;
bool password_validating_ = false;
uint32_t password_validating_time_ = 0;
bool show_settings_window_ = false;
bool show_self_hosted_server_config_window_ = false;
bool rejoin_ = false;
bool local_id_copied_ = false;
bool show_password_ = true;
bool show_about_window_ = false;
bool show_connection_status_window_ = false;
bool show_reset_password_window_ = false;
bool show_update_notification_window_ = false;
bool fullscreen_button_pressed_ = false;
bool focus_on_input_widget_ = true;
bool is_client_mode_ = false;
bool is_server_mode_ = false;
bool reload_recent_connections_ = true;
bool show_confirm_delete_connection_ = false;
bool show_edit_connection_alias_window_ = false;
bool show_offline_warning_window_ = false;
bool delete_connection_ = false;
bool is_tab_bar_hovered_ = false;
std::string delete_connection_name_;
std::string edit_connection_alias_remote_id_;
char edit_connection_alias_[128] = "";
std::string offline_warning_text_;
bool re_enter_remote_id_ = false;
double copy_start_time_ = 0;
};
struct ApplicationState : MainWindowState,
InteractionState,
UpdateState,
StreamWindowState,
ServerWindowState,
UiState {};
} // namespace crossdesk::gui_detail
#endif // CROSSDESK_GUI_APPLICATION_STATE_H_
+295
View File
@@ -0,0 +1,295 @@
#include "application/gui_application.h"
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <memory>
#include <mutex>
#include <string>
#include <vector>
#include "localization.h"
#include "rd_log.h"
#include "version_checker.h"
namespace crossdesk {
GuiApplication::GuiApplication() = default;
GuiApplication::~GuiApplication() = default;
int GuiApplication::Run() {
path_manager_ = std::make_unique<PathManager>("CrossDesk");
if (path_manager_) {
exec_log_path_ = path_manager_->GetLogPath().string();
dll_log_path_ = path_manager_->GetLogPath().string();
cache_path_ = path_manager_->GetCachePath().string();
config_center_ =
std::make_unique<ConfigCenter>(cache_path_ + "/config.ini");
strncpy(signal_server_ip_self_,
config_center_->GetSignalServerHost().c_str(),
sizeof(signal_server_ip_self_) - 1);
signal_server_ip_self_[sizeof(signal_server_ip_self_) - 1] = '\0';
int signal_port_init = config_center_->GetSignalServerPort();
if (signal_port_init > 0) {
strncpy(signal_server_port_self_,
std::to_string(signal_port_init).c_str(),
sizeof(signal_server_port_self_) - 1);
signal_server_port_self_[sizeof(signal_server_port_self_) - 1] = '\0';
} else {
signal_server_port_self_[0] = '\0';
}
} else {
std::cerr << "Failed to create PathManager" << std::endl;
return -1;
}
InitializeLogger();
LOG_INFO("CrossDesk version: {}", CROSSDESK_VERSION);
latest_version_info_ = CheckUpdate();
if (!latest_version_info_.empty()) {
std::string version;
if (latest_version_info_.contains("latest_version") &&
latest_version_info_["latest_version"].is_string()) {
version = latest_version_info_["latest_version"].get<std::string>();
} else if (latest_version_info_.contains("version") &&
latest_version_info_["version"].is_string()) {
version = latest_version_info_["version"].get<std::string>();
}
if (!version.empty()) {
latest_version_ = 'v' + version;
} else {
latest_version_ = "";
}
if (latest_version_info_.contains("releaseNotes") &&
latest_version_info_["releaseNotes"].is_string()) {
release_notes_ = latest_version_info_["releaseNotes"].get<std::string>();
} else {
release_notes_ = "";
}
update_available_ =
!version.empty() && IsNewerVersion(CROSSDESK_VERSION, latest_version_);
LOG_INFO("Update check: current={}, latest={}, available={}",
CROSSDESK_VERSION, latest_version_, update_available_);
if (update_available_) {
show_update_notification_window_ = true;
}
} else {
latest_version_ = "";
update_available_ = false;
LOG_WARN("Update check skipped: version.json is empty or missing "
"latest_version");
}
InitializeSettings();
InitializeSDL();
InitializeModules();
InitializeMainWindow();
#if _WIN32 && CROSSDESK_PORTABLE
CheckPortableWindowsService();
#endif
const int scaled_video_width_ = 160;
const int scaled_video_height_ = 90;
MainLoop();
Cleanup();
return 0;
}
void GuiApplication::InitializeLogger() { InitLogger(exec_log_path_); }
void GuiApplication::InitializeSettings() {
settings_.Load();
settings_.LoadRecentConnectionAliases();
localization_language_index_ =
localization::detail::ClampLanguageIndex(language_button_value_);
language_button_value_ = localization_language_index_;
if (localization_language_index_ == 0) {
localization_language_ = ConfigCenter::LANGUAGE::CHINESE;
} else if (localization_language_index_ == 1) {
localization_language_ = ConfigCenter::LANGUAGE::ENGLISH;
} else {
localization_language_ = ConfigCenter::LANGUAGE::RUSSIAN;
}
}
void GuiApplication::InitializeSDL() {
#if defined(__linux__) && !defined(__APPLE__)
if (!getenv("SDL_AUDIODRIVER")) {
// Prefer PulseAudio first on Linux to avoid hard ALSA plugin dependency.
setenv("SDL_AUDIODRIVER", "pulseaudio", 0);
}
#endif
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO)) {
LOG_ERROR("Error: {}", SDL_GetError());
return;
}
const SDL_DisplayMode *dm = SDL_GetCurrentDisplayMode(0);
if (dm) {
screen_width_ = dm->w;
screen_height_ = dm->h;
}
const uint32_t custom_event_base = SDL_RegisterEvents(3);
if (custom_event_base == static_cast<uint32_t>(-1)) {
LOG_ERROR("Failed to register custom SDL events");
} else {
STREAM_REFRESH_EVENT = custom_event_base;
APP_EXIT_EVENT = custom_event_base + 1;
clipboard_.SetEventType(custom_event_base + 2);
}
LOG_INFO("Screen resolution: [{}x{}]", screen_width_, screen_height_);
}
void GuiApplication::InitializeModules() {
if (!modules_inited_) {
devices_.Initialize();
CreateConnectionPeer();
modules_inited_ = true;
}
}
void GuiApplication::InitializeMainWindow() {
CreateMainWindow();
clipboard_.Initialize();
if (SDL_WINDOW_HIDDEN & SDL_GetWindowFlags(main_window_)) {
SDL_ShowWindow(main_window_);
}
}
void GuiApplication::MainLoop() {
while (!exit_) {
if (!peer_) {
CreateConnectionPeer();
}
SDL_Event event;
if (SDL_WaitEventTimeout(&event, sdl_refresh_ms_)) {
ProcessSdlEvent(event);
}
// Also drain the pending value after a timeout so a full SDL event queue
// cannot indefinitely delay a clipboard update received from the network.
clipboard_.ApplyPendingRemoteText();
#if _WIN32
MSG msg;
while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
#elif defined(__linux__) && !defined(__APPLE__)
if (tray_) {
tray_->ProcessEvents();
}
#endif
UpdateLabels();
HandleRecentConnections();
HandleConnectionStatusChange();
HandlePendingPresenceProbe();
HandleConnectionTimeouts();
HandleStreamWindow();
HandleServerWindow();
HandleWindowsServiceIntegration();
const bool main_window_visible =
main_window_ && !(SDL_GetWindowFlags(main_window_) & SDL_WINDOW_HIDDEN);
if (main_window_visible) {
DrawMainWindow();
}
if (stream_window_inited_) {
DrawStreamWindow();
}
if (is_server_mode_) {
DrawServerWindow();
}
devices_.UpdateInteractions();
}
}
void GuiApplication::HandleStreamWindow() {
if (need_to_create_stream_window_) {
CreateStreamWindow();
need_to_create_stream_window_ = false;
}
if (stream_window_inited_) {
if (!stream_window_grabbed_ && control_mouse_) {
SDL_SetWindowMouseGrab(stream_window_, true);
stream_window_grabbed_ = true;
} else if (stream_window_grabbed_ && !control_mouse_) {
SDL_SetWindowMouseGrab(stream_window_, false);
stream_window_grabbed_ = false;
}
}
}
void GuiApplication::HandleServerWindow() {
if (need_to_create_server_window_) {
CreateServerWindow();
need_to_create_server_window_ = false;
}
if (need_to_destroy_server_window_) {
DestroyServerWindow();
DestroyServerWindowContext();
need_to_destroy_server_window_ = false;
}
}
void GuiApplication::Cleanup() {
clipboard_.Shutdown();
devices_.DestroyDevices();
devices_.DestroyFactories();
CloseAllRemoteSessions();
#if _WIN32 && CROSSDESK_PORTABLE
JoinPortableWindowsServiceInstallThread();
#endif
WaitForThumbnailSaveTasks();
devices_.DestroyAudioOutput();
#if defined(_WIN32) || defined(__APPLE__) || defined(__linux__)
tray_.reset();
#endif
if (stream_window_created_) {
if (stream_window_) {
SDL_SetWindowMouseGrab(stream_window_, false);
}
DestroyStreamWindow();
}
if (stream_ctx_) {
DestroyStreamWindowContext();
}
if (server_window_created_) {
DestroyServerWindow();
}
if (server_ctx_) {
DestroyServerWindowContext();
}
DestroyMainWindowContext();
DestroyMainWindow();
SDL_Quit();
}
} // namespace crossdesk
+112
View File
@@ -0,0 +1,112 @@
#ifndef CROSSDESK_GUI_APPLICATION_H_
#define CROSSDESK_GUI_APPLICATION_H_
#include <filesystem>
#include <string>
#include <vector>
#include "IconsFontAwesome6.h"
#include "imgui_impl_sdl3.h"
#include "imgui_impl_sdlrenderer3.h"
#include "imgui_internal.h"
#include "runtime/gui_runtime.h"
namespace crossdesk {
// SDL/ImGui application shell. Feature state and protocol behavior live in
// GuiRuntime and its composed controllers.
class GuiApplication final : private GuiRuntime {
public:
GuiApplication();
~GuiApplication();
int Run();
private:
// Native window integration.
static SDL_HitTestResult HitTestCallback(SDL_Window *window,
const SDL_Point *area, void *data);
// Application lifecycle.
void InitializeLogger();
void InitializeSettings();
void InitializeSDL();
void InitializeModules();
void InitializeMainWindow();
void MainLoop();
void HandleStreamWindow();
void HandleServerWindow();
void Cleanup();
// Window lifecycle and rendering.
int CreateMainWindow();
int DestroyMainWindow();
int CreateStreamWindow();
int DestroyStreamWindow();
int CreateServerWindow();
int DestroyServerWindow();
int SetupFontAndStyle(ImFont **system_chinese_font_out);
int DestroyMainWindowContext();
int DestroyStreamWindowContext();
int DestroyServerWindowContext();
int DrawMainWindow();
int DrawStreamWindow();
int DrawServerWindow();
// Views and dialogs.
int TitleBar(bool main_window);
int MainWindow();
int UpdateNotificationWindow();
int StreamWindow();
int ServerWindow();
int RemoteClientInfoWindow();
int LocalWindow();
int RemoteWindow();
int RecentConnectionsWindow();
int SettingWindow();
int SelfHostedServerWindow();
int ControlWindow(std::shared_ptr<RemoteSession> &props);
int ControlBar(std::shared_ptr<RemoteSession> &props);
int AboutWindow();
int StatusBar();
bool
ConnectionStatusWindow(std::shared_ptr<RemoteSession> &props);
int ShowRecentConnections();
int ConfirmDeleteConnection();
int EditRecentConnectionAliasWindow();
int OfflineWarningWindow();
int NetTrafficStats(std::shared_ptr<RemoteSession> &props);
int FileTransferWindow(std::shared_ptr<RemoteSession> &props);
void
DrawConnectionStatusText(std::shared_ptr<RemoteSession> &props);
void
DrawReceivingScreenText(std::shared_ptr<RemoteSession> &props);
bool OpenUrl(const std::string &url);
void Hyperlink(const std::string &label, const std::string &url,
float window_width);
std::string OpenFileDialog(std::string title);
bool MinimizeMainWindowToTray();
// SDL event dispatch and input translation.
void UpdateRenderRect();
void ProcessSdlEvent(const SDL_Event &event);
int ProcessKeyboardEvent(const SDL_Event &event);
int ProcessMouseEvent(const SDL_Event &event);
void CloseTab(decltype(remote_sessions_)::iterator &it);
#if _WIN32 && CROSSDESK_PORTABLE
void CheckPortableWindowsService();
int PortableServiceInstallWindow();
void StartPortableWindowsServiceInstall();
void JoinPortableWindowsServiceInstallThread();
#endif
#ifdef __APPLE__
int RequestPermissionWindow();
bool DrawToggleSwitch(const char *id, bool active, bool enabled);
#endif
};
} // namespace crossdesk
#endif // CROSSDESK_GUI_APPLICATION_H_
+320
View File
@@ -0,0 +1,320 @@
#include "application/gui_application.h"
#include <cstring>
#include <memory>
#include <mutex>
#include <string>
#include <vector>
#include "rd_log.h"
namespace crossdesk {
void GuiApplication::ProcessSdlEvent(const SDL_Event &event) {
if (main_ctx_) {
ImGui::SetCurrentContext(main_ctx_);
ImGui_ImplSDL3_ProcessEvent(&event);
} else {
LOG_ERROR("Main context is null");
return;
}
if (stream_window_inited_) {
if (stream_ctx_) {
ImGui::SetCurrentContext(stream_ctx_);
ImGui_ImplSDL3_ProcessEvent(&event);
} else {
LOG_ERROR("Stream context is null");
return;
}
}
if (server_window_inited_) {
if (server_ctx_) {
ImGui::SetCurrentContext(server_ctx_);
ImGui_ImplSDL3_ProcessEvent(&event);
} else {
LOG_ERROR("Server context is null");
return;
}
}
if (APP_EXIT_EVENT != 0 && event.type == APP_EXIT_EVENT) {
LOG_INFO("Quit program from system tray");
if (stream_window_) {
SDL_SetWindowMouseGrab(stream_window_, false);
}
#if defined(__linux__) && !defined(__APPLE__)
if (tray_) {
tray_->RemoveTrayIcon();
}
#endif
exit_ = true;
return;
}
if (clipboard_.event_type() != 0 && event.type == clipboard_.event_type()) {
clipboard_.ApplyPendingRemoteText();
return;
}
switch (event.type) {
case SDL_EVENT_QUIT:
if (stream_window_inited_) {
LOG_INFO("Destroy stream window");
SDL_SetWindowMouseGrab(stream_window_, false);
DestroyStreamWindow();
DestroyStreamWindowContext();
{
// std::shared_lock lock(remote_sessions_mutex_);
for (auto &[host_name, props] : remote_sessions_) {
std::shared_ptr<std::vector<unsigned char>> frame_snapshot;
int video_width = 0;
int video_height = 0;
{
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
frame_snapshot = props->front_frame_;
video_width = props->video_width_;
video_height = props->video_height_;
}
if (frame_snapshot && !frame_snapshot->empty() && video_width > 0 &&
video_height > 0) {
thumbnail_->SaveToThumbnail(
(char *)frame_snapshot->data(), video_width, video_height,
host_name, props->remote_host_name_,
props->remember_password_ ? props->remote_password_ : "");
}
if (props->peer_) {
std::string client_id = (host_name == client_id_)
? "C-" + std::string(client_id_)
: client_id_;
LOG_INFO("[{}] Leave connection [{}]", client_id, host_name);
LeaveConnection(props->peer_, host_name.c_str());
LOG_INFO("Destroy peer [{}]", client_id);
DestroyPeer(&props->peer_);
}
props->streaming_ = false;
props->remember_password_ = false;
props->connection_established_ = false;
props->audio_capture_button_pressed_ = false;
memset(&props->net_traffic_stats_, 0,
sizeof(props->net_traffic_stats_));
SDL_SetWindowFullscreen(main_window_, false);
SDL_FlushEvents(STREAM_REFRESH_EVENT, STREAM_REFRESH_EVENT);
memset(audio_buffer_, 0, 720);
}
}
{
// std::unique_lock lock(remote_sessions_mutex_);
remote_sessions_.clear();
}
rejoin_ = false;
is_client_mode_ = false;
reload_recent_connections_ = true;
fullscreen_button_pressed_ = false;
start_keyboard_capturer_ = false;
just_created_ = false;
recent_connection_image_save_time_ = SDL_GetTicks();
} else {
LOG_INFO("Quit program");
exit_ = true;
}
break;
case SDL_EVENT_WINDOW_CLOSE_REQUESTED:
if (stream_window_ &&
event.window.windowID == SDL_GetWindowID(stream_window_)) {
break;
}
if (main_window_ &&
event.window.windowID == SDL_GetWindowID(main_window_) &&
MinimizeMainWindowToTray()) {
break;
}
exit_ = true;
break;
case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED:
if (stream_window_created_ &&
event.window.windowID == SDL_GetWindowID(stream_window_)) {
UpdateRenderRect();
}
break;
case SDL_EVENT_WINDOW_FOCUS_GAINED:
if (stream_window_ &&
SDL_GetWindowID(stream_window_) == event.window.windowID) {
focus_on_stream_window_ = true;
} else if (main_window_ &&
SDL_GetWindowID(main_window_) == event.window.windowID) {
foucs_on_main_window_ = true;
}
break;
case SDL_EVENT_WINDOW_FOCUS_LOST:
if (stream_window_ &&
SDL_GetWindowID(stream_window_) == event.window.windowID) {
keyboard_.ForceReleasePressedKeys();
focus_on_stream_window_ = false;
} else if (main_window_ &&
SDL_GetWindowID(main_window_) == event.window.windowID) {
foucs_on_main_window_ = false;
}
break;
case SDL_EVENT_DROP_FILE:
transfers_.HandleDropEvent(event);
break;
case SDL_EVENT_CLIPBOARD_UPDATE:
clipboard_.HandleLocalUpdate();
break;
case SDL_EVENT_MOUSE_MOTION:
case SDL_EVENT_MOUSE_BUTTON_DOWN:
case SDL_EVENT_MOUSE_BUTTON_UP:
case SDL_EVENT_MOUSE_WHEEL: {
Uint32 mouse_window_id = 0;
if (event.type == SDL_EVENT_MOUSE_MOTION) {
mouse_window_id = event.motion.windowID;
} else if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN ||
event.type == SDL_EVENT_MOUSE_BUTTON_UP) {
mouse_window_id = event.button.windowID;
} else if (event.type == SDL_EVENT_MOUSE_WHEEL) {
mouse_window_id = event.wheel.windowID;
}
if (focus_on_stream_window_ && stream_window_ &&
SDL_GetWindowID(stream_window_) == mouse_window_id) {
ProcessMouseEvent(event);
}
break;
}
case SDL_EVENT_KEY_DOWN:
case SDL_EVENT_KEY_UP:
if (keyboard_capturer_is_started_ && keyboard_capturer_uses_sdl_events_ &&
focus_on_stream_window_ && stream_window_ &&
SDL_GetWindowID(stream_window_) == event.key.windowID) {
ProcessKeyboardEvent(event);
}
break;
default:
if (event.type == STREAM_REFRESH_EVENT) {
auto *props = static_cast<RemoteSession *>(event.user.data1);
if (!props) {
break;
}
std::shared_ptr<std::vector<unsigned char>> frame_snapshot;
int video_width = 0;
int video_height = 0;
bool render_rect_dirty = false;
bool cleanup_pending = false;
{
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
cleanup_pending = props->stream_cleanup_pending_;
if (!cleanup_pending) {
frame_snapshot = props->front_frame_;
video_width = props->video_width_;
video_height = props->video_height_;
}
render_rect_dirty = props->render_rect_dirty_;
}
if (cleanup_pending) {
if (props->stream_texture_) {
SDL_DestroyTexture(props->stream_texture_);
props->stream_texture_ = nullptr;
}
{
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
props->stream_cleanup_pending_ = false;
}
if (render_rect_dirty) {
UpdateRenderRect();
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
props->render_rect_dirty_ = false;
}
break;
}
if (video_width <= 0 || video_height <= 0) {
break;
}
if (!frame_snapshot || frame_snapshot->empty()) {
break;
}
if (props->stream_texture_) {
if (video_width != props->texture_width_ ||
video_height != props->texture_height_) {
props->texture_width_ = video_width;
props->texture_height_ = video_height;
SDL_DestroyTexture(props->stream_texture_);
// props->stream_texture_ = SDL_CreateTexture(
// stream_renderer_, stream_pixformat_,
// SDL_TEXTUREACCESS_STREAMING, props->texture_width_,
// props->texture_height_);
SDL_PropertiesID nvProps = SDL_CreateProperties();
SDL_SetNumberProperty(nvProps, SDL_PROP_TEXTURE_CREATE_WIDTH_NUMBER,
props->texture_width_);
SDL_SetNumberProperty(nvProps, SDL_PROP_TEXTURE_CREATE_HEIGHT_NUMBER,
props->texture_height_);
SDL_SetNumberProperty(nvProps, SDL_PROP_TEXTURE_CREATE_FORMAT_NUMBER,
SDL_PIXELFORMAT_NV12);
SDL_SetNumberProperty(nvProps,
SDL_PROP_TEXTURE_CREATE_COLORSPACE_NUMBER,
SDL_COLORSPACE_BT601_LIMITED);
props->stream_texture_ =
SDL_CreateTextureWithProperties(stream_renderer_, nvProps);
SDL_DestroyProperties(nvProps);
}
} else {
props->texture_width_ = video_width;
props->texture_height_ = video_height;
// props->stream_texture_ = SDL_CreateTexture(
// stream_renderer_, stream_pixformat_,
// SDL_TEXTUREACCESS_STREAMING, props->texture_width_,
// props->texture_height_);
SDL_PropertiesID nvProps = SDL_CreateProperties();
SDL_SetNumberProperty(nvProps, SDL_PROP_TEXTURE_CREATE_WIDTH_NUMBER,
props->texture_width_);
SDL_SetNumberProperty(nvProps, SDL_PROP_TEXTURE_CREATE_HEIGHT_NUMBER,
props->texture_height_);
SDL_SetNumberProperty(nvProps, SDL_PROP_TEXTURE_CREATE_FORMAT_NUMBER,
SDL_PIXELFORMAT_NV12);
SDL_SetNumberProperty(nvProps,
SDL_PROP_TEXTURE_CREATE_COLORSPACE_NUMBER,
SDL_COLORSPACE_BT601_LIMITED);
props->stream_texture_ =
SDL_CreateTextureWithProperties(stream_renderer_, nvProps);
SDL_DestroyProperties(nvProps);
}
SDL_UpdateTexture(props->stream_texture_, NULL, frame_snapshot->data(),
props->texture_width_);
if (render_rect_dirty) {
UpdateRenderRect();
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
props->render_rect_dirty_ = false;
}
}
break;
}
}
} // namespace crossdesk
+415
View File
@@ -0,0 +1,415 @@
#include "application/gui_application.h"
#include <algorithm>
#include <cmath>
#include <string>
#include "device_controller.h"
#include "rd_log.h"
namespace crossdesk {
namespace {
int TranslateSdlKeypadScancodeToVk(const SDL_KeyboardEvent &event) {
const bool numlock_enabled = (event.mod & SDL_KMOD_NUM) != 0;
switch (event.scancode) {
case SDL_SCANCODE_NUMLOCKCLEAR:
return 0x90;
case SDL_SCANCODE_KP_ENTER:
return 0x0D;
case SDL_SCANCODE_KP_0:
if (!numlock_enabled) {
return 0x2D;
}
return 0x60;
case SDL_SCANCODE_KP_1:
if (!numlock_enabled) {
return 0x23;
}
return 0x61;
case SDL_SCANCODE_KP_2:
if (!numlock_enabled) {
return 0x28;
}
return 0x62;
case SDL_SCANCODE_KP_3:
if (!numlock_enabled) {
return 0x22;
}
return 0x63;
case SDL_SCANCODE_KP_4:
if (!numlock_enabled) {
return 0x25;
}
return 0x64;
case SDL_SCANCODE_KP_5:
return 0x65;
case SDL_SCANCODE_KP_6:
if (!numlock_enabled) {
return 0x27;
}
return 0x66;
case SDL_SCANCODE_KP_7:
if (!numlock_enabled) {
return 0x24;
}
return 0x67;
case SDL_SCANCODE_KP_8:
if (!numlock_enabled) {
return 0x26;
}
return 0x68;
case SDL_SCANCODE_KP_9:
if (!numlock_enabled) {
return 0x21;
}
return 0x69;
case SDL_SCANCODE_KP_PERIOD:
case SDL_SCANCODE_KP_COMMA:
if (!numlock_enabled) {
return 0x2E;
}
return 0x6E;
case SDL_SCANCODE_KP_DIVIDE:
return 0x6F;
case SDL_SCANCODE_KP_MULTIPLY:
return 0x6A;
case SDL_SCANCODE_KP_MINUS:
return 0x6D;
case SDL_SCANCODE_KP_PLUS:
return 0x6B;
case SDL_SCANCODE_KP_EQUALS:
return 0xBB;
default:
return -1;
}
}
int TranslateSdlKeyboardEventToVk(const SDL_KeyboardEvent &event) {
const int keypad_key_code = TranslateSdlKeypadScancodeToVk(event);
if (keypad_key_code >= 0) {
return keypad_key_code;
}
const int key = static_cast<int>(event.key);
if (key >= 'a' && key <= 'z') {
return key - 'a' + 0x41;
}
if (key >= 'A' && key <= 'Z') {
return key;
}
if (key >= '0' && key <= '9') {
return key;
}
switch (key) {
case ';':
return 0xBA;
case '\'':
return 0xDE;
case '`':
return 0xC0;
case ',':
return 0xBC;
case '.':
return 0xBE;
case '/':
return 0xBF;
case '\\':
return 0xDC;
case '[':
return 0xDB;
case ']':
return 0xDD;
case '-':
return 0xBD;
case '=':
return 0xBB;
default:
break;
}
switch (event.scancode) {
case SDL_SCANCODE_ESCAPE:
return 0x1B;
case SDL_SCANCODE_RETURN:
return 0x0D;
case SDL_SCANCODE_SPACE:
return 0x20;
case SDL_SCANCODE_BACKSPACE:
return 0x08;
case SDL_SCANCODE_TAB:
return 0x09;
case SDL_SCANCODE_PRINTSCREEN:
return 0x2C;
case SDL_SCANCODE_SCROLLLOCK:
return 0x91;
case SDL_SCANCODE_PAUSE:
return 0x13;
case SDL_SCANCODE_INSERT:
return 0x2D;
case SDL_SCANCODE_DELETE:
return 0x2E;
case SDL_SCANCODE_HOME:
return 0x24;
case SDL_SCANCODE_END:
return 0x23;
case SDL_SCANCODE_PAGEUP:
return 0x21;
case SDL_SCANCODE_PAGEDOWN:
return 0x22;
case SDL_SCANCODE_LEFT:
return 0x25;
case SDL_SCANCODE_RIGHT:
return 0x27;
case SDL_SCANCODE_UP:
return 0x26;
case SDL_SCANCODE_DOWN:
return 0x28;
case SDL_SCANCODE_F1:
return 0x70;
case SDL_SCANCODE_F2:
return 0x71;
case SDL_SCANCODE_F3:
return 0x72;
case SDL_SCANCODE_F4:
return 0x73;
case SDL_SCANCODE_F5:
return 0x74;
case SDL_SCANCODE_F6:
return 0x75;
case SDL_SCANCODE_F7:
return 0x76;
case SDL_SCANCODE_F8:
return 0x77;
case SDL_SCANCODE_F9:
return 0x78;
case SDL_SCANCODE_F10:
return 0x79;
case SDL_SCANCODE_F11:
return 0x7A;
case SDL_SCANCODE_F12:
return 0x7B;
case SDL_SCANCODE_CAPSLOCK:
return 0x14;
case SDL_SCANCODE_LSHIFT:
return 0xA0;
case SDL_SCANCODE_RSHIFT:
return 0xA1;
case SDL_SCANCODE_LCTRL:
return 0xA2;
case SDL_SCANCODE_RCTRL:
return 0xA3;
case SDL_SCANCODE_LALT:
return 0xA4;
case SDL_SCANCODE_RALT:
return 0xA5;
case SDL_SCANCODE_LGUI:
return 0x5B;
case SDL_SCANCODE_RGUI:
return 0x5C;
default:
return -1;
}
}
} // namespace
int GuiApplication::ProcessKeyboardEvent(const SDL_Event &event) {
if (event.type != SDL_EVENT_KEY_DOWN && event.type != SDL_EVENT_KEY_UP) {
return -1;
}
if (event.type == SDL_EVENT_KEY_DOWN && event.key.repeat) {
return 0;
}
const int key_code = TranslateSdlKeyboardEventToVk(event.key);
if (key_code < 0) {
return 0;
}
return keyboard_.SendKeyCommand(key_code, event.type == SDL_EVENT_KEY_DOWN);
}
int GuiApplication::ProcessMouseEvent(const SDL_Event &event) {
controlled_remote_id_ = "";
RemoteAction remote_action{};
float cursor_x = last_mouse_event.motion.x;
float cursor_y = last_mouse_event.motion.y;
auto normalize_cursor_to_window_space = [&](float *x, float *y) {
if (!x || !y || !stream_window_) {
return;
}
int window_width = 0;
int window_height = 0;
int pixel_width = 0;
int pixel_height = 0;
SDL_GetWindowSize(stream_window_, &window_width, &window_height);
SDL_GetWindowSizeInPixels(stream_window_, &pixel_width, &pixel_height);
if (window_width <= 0 || window_height <= 0 || pixel_width <= 0 ||
pixel_height <= 0) {
return;
}
if ((window_width != pixel_width || window_height != pixel_height) &&
(*x > static_cast<float>(window_width) + 1.0f ||
*y > static_cast<float>(window_height) + 1.0f)) {
const float scale_x =
static_cast<float>(window_width) / static_cast<float>(pixel_width);
const float scale_y =
static_cast<float>(window_height) / static_cast<float>(pixel_height);
*x *= scale_x;
*y *= scale_y;
static bool logged_pixel_to_window_conversion = false;
if (!logged_pixel_to_window_conversion) {
LOG_INFO(
"Mouse coordinate space converted from pixels to window units: "
"window={}x{}, pixels={}x{}, scale=({:.4f},{:.4f})",
window_width, window_height, pixel_width, pixel_height, scale_x,
scale_y);
logged_pixel_to_window_conversion = true;
}
}
};
if (event.type == SDL_EVENT_MOUSE_MOTION) {
cursor_x = event.motion.x;
cursor_y = event.motion.y;
normalize_cursor_to_window_space(&cursor_x, &cursor_y);
} else if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN ||
event.type == SDL_EVENT_MOUSE_BUTTON_UP) {
cursor_x = event.button.x;
cursor_y = event.button.y;
normalize_cursor_to_window_space(&cursor_x, &cursor_y);
} else if (event.type == SDL_EVENT_MOUSE_WHEEL) {
cursor_x = last_mouse_event.motion.x;
cursor_y = last_mouse_event.motion.y;
}
const bool is_pointer_position_event =
(event.type == SDL_EVENT_MOUSE_MOTION ||
event.type == SDL_EVENT_MOUSE_BUTTON_DOWN ||
event.type == SDL_EVENT_MOUSE_BUTTON_UP);
// std::shared_lock lock(remote_sessions_mutex_);
for (auto &it : remote_sessions_) {
auto props = it.second;
if (!props->control_mouse_) {
continue;
}
const bool file_transfer_window_hovered =
props->file_transfer_.file_transfer_window_hovered_;
const bool overlay_hovered =
props->control_bar_hovered_ || props->display_selectable_hovered_ ||
props->shortcut_selectable_hovered_ || file_transfer_window_hovered;
const SDL_FRect render_rect = props->stream_render_rect_f_;
if (render_rect.w <= 1.0f || render_rect.h <= 1.0f) {
continue;
}
if (is_pointer_position_event && cursor_x >= render_rect.x &&
cursor_x <= render_rect.x + render_rect.w &&
cursor_y >= render_rect.y &&
cursor_y <= render_rect.y + render_rect.h) {
controlled_remote_id_ = it.first;
last_mouse_event.motion.x = cursor_x;
last_mouse_event.motion.y = cursor_y;
last_mouse_event.button.x = cursor_x;
last_mouse_event.button.y = cursor_y;
remote_action.m.x = (cursor_x - render_rect.x) / render_rect.w;
remote_action.m.y = (cursor_y - render_rect.y) / render_rect.h;
remote_action.m.x = std::clamp(remote_action.m.x, 0.0f, 1.0f);
remote_action.m.y = std::clamp(remote_action.m.y, 0.0f, 1.0f);
if (SDL_EVENT_MOUSE_BUTTON_DOWN == event.type) {
remote_action.type = ControlType::mouse;
if (SDL_BUTTON_LEFT == event.button.button) {
remote_action.m.flag = MouseFlag::left_down;
} else if (SDL_BUTTON_RIGHT == event.button.button) {
remote_action.m.flag = MouseFlag::right_down;
} else if (SDL_BUTTON_MIDDLE == event.button.button) {
remote_action.m.flag = MouseFlag::middle_down;
}
} else if (SDL_EVENT_MOUSE_BUTTON_UP == event.type) {
remote_action.type = ControlType::mouse;
if (SDL_BUTTON_LEFT == event.button.button) {
remote_action.m.flag = MouseFlag::left_up;
} else if (SDL_BUTTON_RIGHT == event.button.button) {
remote_action.m.flag = MouseFlag::right_up;
} else if (SDL_BUTTON_MIDDLE == event.button.button) {
remote_action.m.flag = MouseFlag::middle_up;
}
} else if (SDL_EVENT_MOUSE_MOTION == event.type) {
remote_action.type = ControlType::mouse;
remote_action.m.flag = MouseFlag::move;
}
if (overlay_hovered) {
break;
}
if (props->peer_) {
std::string msg = remote_action.to_json();
SendDataFrame(props->peer_, msg.c_str(), msg.size(),
props->mouse_label_.c_str());
}
} else if (SDL_EVENT_MOUSE_WHEEL == event.type &&
last_mouse_event.button.x >= render_rect.x &&
last_mouse_event.button.x <= render_rect.x + render_rect.w &&
last_mouse_event.button.y >= render_rect.y &&
last_mouse_event.button.y <= render_rect.y + render_rect.h) {
float scroll_x = event.wheel.x;
float scroll_y = event.wheel.y;
if (event.wheel.direction == SDL_MOUSEWHEEL_FLIPPED) {
scroll_x = -scroll_x;
scroll_y = -scroll_y;
}
remote_action.type = ControlType::mouse;
auto roundUp = [](float value) -> int {
if (value > 0) {
return static_cast<int>(std::ceil(value));
} else if (value < 0) {
return static_cast<int>(std::floor(value));
}
return 0;
};
if (std::abs(scroll_y) >= std::abs(scroll_x)) {
remote_action.m.flag = MouseFlag::wheel_vertical;
remote_action.m.s = roundUp(scroll_y);
} else {
remote_action.m.flag = MouseFlag::wheel_horizontal;
remote_action.m.s = roundUp(scroll_x);
}
remote_action.m.x = (last_mouse_event.button.x - render_rect.x) /
(std::max)(render_rect.w, 1.0f);
remote_action.m.y = (last_mouse_event.button.y - render_rect.y) /
(std::max)(render_rect.h, 1.0f);
remote_action.m.x = std::clamp(remote_action.m.x, 0.0f, 1.0f);
remote_action.m.y = std::clamp(remote_action.m.y, 0.0f, 1.0f);
if (overlay_hovered) {
continue;
}
if (props->peer_) {
std::string msg = remote_action.to_json();
SendDataFrame(props->peer_, msg.c_str(), msg.size(),
props->mouse_label_.c_str());
}
}
}
return 0;
}
} // namespace crossdesk
+687
View File
@@ -0,0 +1,687 @@
#include "application/gui_application.h"
#include <libyuv.h>
#if defined(__linux__) && !defined(__APPLE__)
#include <X11/Xatom.h>
#include <X11/Xlib.h>
#endif
#include <cmath>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <iostream>
#include <memory>
#include <mutex>
#include <shared_mutex>
#include <string>
#include <thread>
#include "fa_regular_400.h"
#include "fa_solid_900.h"
#include "layout_relative.h"
#include "localization.h"
#include "rd_log.h"
#include "version_checker.h"
#if defined(__APPLE__)
#include "window_util_mac.h"
#endif
namespace crossdesk {
namespace {
const ImWchar *GetMultilingualGlyphRanges() {
static std::vector<ImWchar> glyph_ranges;
if (glyph_ranges.empty()) {
ImGuiIO &io = ImGui::GetIO();
ImFontGlyphRangesBuilder builder;
builder.AddRanges(io.Fonts->GetGlyphRangesDefault());
builder.AddRanges(io.Fonts->GetGlyphRangesChineseFull());
builder.AddRanges(io.Fonts->GetGlyphRangesCyrillic());
ImVector<ImWchar> built_ranges;
builder.BuildRanges(&built_ranges);
glyph_ranges.assign(built_ranges.Data,
built_ranges.Data + built_ranges.Size);
}
return glyph_ranges.empty() ? nullptr : glyph_ranges.data();
}
bool CanReadFontFile(const char *font_path) {
if (!font_path) {
return false;
}
std::ifstream font_file(font_path, std::ios::binary);
return font_file.good();
}
#if _WIN32
HICON LoadTrayIcon() {
HMODULE module = GetModuleHandleW(nullptr);
HICON icon = reinterpret_cast<HICON>(
LoadImageW(module, L"IDI_ICON1", IMAGE_ICON, 0, 0, LR_DEFAULTSIZE));
if (icon) {
return icon;
}
return LoadIconW(nullptr, IDI_APPLICATION);
}
#endif
#if defined(__linux__) && !defined(__APPLE__)
inline bool X11GetDisplayAndWindow(SDL_Window *window, Display **display_out,
::Window *x11_window_out) {
if (!window || !display_out || !x11_window_out) {
return false;
}
#if !defined(SDL_PROP_WINDOW_X11_DISPLAY_POINTER) || \
!defined(SDL_PROP_WINDOW_X11_WINDOW_NUMBER)
// SDL build does not expose X11 window properties.
return false;
#else
SDL_PropertiesID props = SDL_GetWindowProperties(window);
Display *display = (Display *)SDL_GetPointerProperty(
props, SDL_PROP_WINDOW_X11_DISPLAY_POINTER, NULL);
const Sint64 x11_window_num =
SDL_GetNumberProperty(props, SDL_PROP_WINDOW_X11_WINDOW_NUMBER, 0);
const ::Window x11_window = (::Window)x11_window_num;
if (!display || !x11_window) {
return false;
}
*display_out = display;
*x11_window_out = x11_window;
return true;
#endif
}
inline void X11SendNetWmState(Display *display, ::Window x11_window,
long action, Atom state1, Atom state2 = 0) {
if (!display || !x11_window) {
return;
}
const Atom wm_state = XInternAtom(display, "_NET_WM_STATE", False);
XEvent event;
memset(&event, 0, sizeof(event));
event.xclient.type = ClientMessage;
event.xclient.serial = 0;
event.xclient.send_event = True;
event.xclient.message_type = wm_state;
event.xclient.window = x11_window;
event.xclient.format = 32;
event.xclient.data.l[0] = action;
event.xclient.data.l[1] = (long)state1;
event.xclient.data.l[2] = (long)state2;
event.xclient.data.l[3] = 1; // normal source indication
event.xclient.data.l[4] = 0;
XSendEvent(display, DefaultRootWindow(display), False,
SubstructureRedirectMask | SubstructureNotifyMask, &event);
}
inline void X11SetWindowTypeUtility(Display *display, ::Window x11_window) {
if (!display || !x11_window) {
return;
}
const Atom wm_window_type =
XInternAtom(display, "_NET_WM_WINDOW_TYPE", False);
const Atom wm_window_type_utility =
XInternAtom(display, "_NET_WM_WINDOW_TYPE_UTILITY", False);
XChangeProperty(display, x11_window, wm_window_type, XA_ATOM, 32,
PropModeReplace, (unsigned char *)&wm_window_type_utility, 1);
}
inline void X11SetWindowAlwaysOnTop(SDL_Window *window) {
Display *display = nullptr;
::Window x11_window = 0;
if (!X11GetDisplayAndWindow(window, &display, &x11_window)) {
return;
}
const Atom state_above = XInternAtom(display, "_NET_WM_STATE_ABOVE", False);
const Atom state_stays_on_top =
XInternAtom(display, "_NET_WM_STATE_STAYS_ON_TOP", False);
// Request _NET_WM_STATE_ADD for ABOVE + STAYS_ON_TOP.
X11SendNetWmState(display, x11_window, 1, state_above, state_stays_on_top);
XFlush(display);
}
inline void X11SetWindowSkipTaskbar(SDL_Window *window) {
Display *display = nullptr;
::Window x11_window = 0;
if (!X11GetDisplayAndWindow(window, &display, &x11_window)) {
return;
}
const Atom skip_taskbar =
XInternAtom(display, "_NET_WM_STATE_SKIP_TASKBAR", False);
const Atom skip_pager =
XInternAtom(display, "_NET_WM_STATE_SKIP_PAGER", False);
// Request _NET_WM_STATE_ADD for SKIP_TASKBAR + SKIP_PAGER.
X11SendNetWmState(display, x11_window, 1, skip_taskbar, skip_pager);
// Hint the WM that this is an auxiliary/utility window.
X11SetWindowTypeUtility(display, x11_window);
XFlush(display);
}
#endif
} // namespace
SDL_HitTestResult GuiApplication::HitTestCallback(SDL_Window *window,
const SDL_Point *area,
void *data) {
GuiApplication *application = (GuiApplication *)data;
if (!application) {
return SDL_HITTEST_NORMAL;
}
if (application->fullscreen_button_pressed_) {
return SDL_HITTEST_NORMAL;
}
// Server window: OS-level dragging for the title bar, but keep the left-side
// collapse/expand button clickable.
if (application->server_window_ && window == application->server_window_) {
const float title_h = application->server_window_title_bar_height_;
const float button_w = title_h;
if (area->y >= 0 && area->y < title_h) {
if (area->x >= 0 && area->x < button_w) {
return SDL_HITTEST_NORMAL;
}
return SDL_HITTEST_DRAGGABLE;
}
return SDL_HITTEST_NORMAL;
}
int window_width, window_height;
SDL_GetWindowSize(window, &window_width, &window_height);
// check if curosor is in tab bar
if (application->stream_window_inited_ && application->stream_window_created_ &&
!application->fullscreen_button_pressed_ && application->stream_ctx_) {
ImGuiContext *prev_ctx = ImGui::GetCurrentContext();
ImGui::SetCurrentContext(application->stream_ctx_);
ImGuiWindow *tab_bar_window = ImGui::FindWindowByName("TabBar");
if (tab_bar_window && tab_bar_window->Active) {
ImGuiIO &io = ImGui::GetIO();
float scale_x = io.DisplayFramebufferScale.x;
float scale_y = io.DisplayFramebufferScale.y;
float tab_bar_x = tab_bar_window->Pos.x * scale_x;
float tab_bar_y = tab_bar_window->Pos.y * scale_y;
float tab_bar_width = tab_bar_window->Size.x * scale_x;
float tab_bar_height = tab_bar_window->Size.y * scale_y;
ImGui::SetCurrentContext(prev_ctx);
if (area->x >= tab_bar_x && area->x <= tab_bar_x + tab_bar_width &&
area->y >= tab_bar_y && area->y <= tab_bar_y + tab_bar_height) {
return SDL_HITTEST_NORMAL;
}
} else {
ImGui::SetCurrentContext(prev_ctx);
}
}
float mouse_grab_padding = application->title_bar_button_width_ * 0.16f;
if (area->y < application->title_bar_button_width_ &&
area->y > mouse_grab_padding &&
area->x < window_width - application->title_bar_button_width_ * 3.0f &&
area->x > mouse_grab_padding) {
return SDL_HITTEST_DRAGGABLE;
}
// if (!application->streaming_) {
// return SDL_HITTEST_NORMAL;
// }
if (area->y < mouse_grab_padding) {
if (area->x < mouse_grab_padding) {
return SDL_HITTEST_RESIZE_TOPLEFT;
} else if (area->x > window_width - mouse_grab_padding) {
return SDL_HITTEST_RESIZE_TOPRIGHT;
} else {
return SDL_HITTEST_RESIZE_TOP;
}
} else if (area->y > window_height - mouse_grab_padding) {
if (area->x < mouse_grab_padding) {
return SDL_HITTEST_RESIZE_BOTTOMLEFT;
} else if (area->x > window_width - mouse_grab_padding) {
return SDL_HITTEST_RESIZE_BOTTOMRIGHT;
} else {
return SDL_HITTEST_RESIZE_BOTTOM;
}
} else if (area->x < mouse_grab_padding) {
return SDL_HITTEST_RESIZE_LEFT;
} else if (area->x > window_width - mouse_grab_padding) {
return SDL_HITTEST_RESIZE_RIGHT;
}
return SDL_HITTEST_NORMAL;
}
int GuiApplication::CreateMainWindow() {
main_ctx_ = ImGui::CreateContext();
if (!main_ctx_) {
LOG_ERROR("Main context is null");
return -1;
}
ImGui::SetCurrentContext(main_ctx_);
if (!SDL_CreateWindowAndRenderer(
"CrossDesk Main Window", (int)main_window_width_,
(int)main_window_height_,
SDL_WINDOW_HIGH_PIXEL_DENSITY | SDL_WINDOW_BORDERLESS |
SDL_WINDOW_HIDDEN | SDL_WINDOW_TRANSPARENT,
&main_window_, &main_renderer_)) {
LOG_ERROR("Error creating MainWindow and MainRenderer: {}", SDL_GetError());
return -1;
}
float dpi_scale = SDL_GetDisplayContentScale(SDL_GetPrimaryDisplay());
if (std::abs(dpi_scale_ - dpi_scale) > 0.01f) {
dpi_scale_ = dpi_scale;
main_window_width_ = (int)(main_window_width_default_ * dpi_scale_);
main_window_height_ = (int)(main_window_height_default_ * dpi_scale_);
stream_window_width_ = (int)(stream_window_width_default_ * dpi_scale_);
stream_window_height_ = (int)(stream_window_height_default_ * dpi_scale_);
server_window_width_ = (int)(server_window_width_default_ * dpi_scale_);
server_window_height_ = (int)(server_window_height_default_ * dpi_scale_);
server_window_normal_width_ =
(int)(server_window_width_default_ * dpi_scale_);
server_window_normal_height_ =
(int)(server_window_height_default_ * dpi_scale_);
window_rounding_ = window_rounding_default_ * dpi_scale_;
SDL_SetWindowSize(main_window_, (int)main_window_width_,
(int)main_window_height_);
}
SDL_SetWindowResizable(main_window_, false);
// for window region action
SDL_SetWindowHitTest(main_window_, HitTestCallback, this);
SDL_SetRenderDrawBlendMode(main_renderer_, SDL_BLENDMODE_BLEND);
SetupFontAndStyle(&main_windows_system_chinese_font_);
ImGuiStyle &style = ImGui::GetStyle();
style.ScaleAllSizes(dpi_scale_);
style.FontScaleDpi = dpi_scale_;
#if _WIN32
SDL_PropertiesID props = SDL_GetWindowProperties(main_window_);
HWND main_hwnd = (HWND)SDL_GetPointerProperty(
props, SDL_PROP_WINDOW_WIN32_HWND_POINTER, NULL);
HICON tray_icon = LoadTrayIcon();
tray_ = std::make_unique<WinTray>(main_hwnd, tray_icon, L"CrossDesk",
localization_language_index_);
#elif defined(__APPLE__)
tray_ = std::make_unique<MacTray>(main_window_, "CrossDesk",
localization_language_index_);
#elif defined(__linux__) && !defined(__APPLE__)
tray_ = std::make_unique<LinuxTray>(
main_window_, "CrossDesk", localization_language_index_, APP_EXIT_EVENT);
#endif
ImGui_ImplSDL3_InitForSDLRenderer(main_window_, main_renderer_);
ImGui_ImplSDLRenderer3_Init(main_renderer_);
return 0;
}
int GuiApplication::DestroyMainWindow() {
if (main_ctx_) {
ImGui::SetCurrentContext(main_ctx_);
}
if (main_renderer_) {
SDL_DestroyRenderer(main_renderer_);
}
if (main_window_) {
SDL_DestroyWindow(main_window_);
}
return 0;
}
int GuiApplication::CreateStreamWindow() {
if (stream_window_created_) {
return 0;
}
stream_window_width_ = (int)(stream_window_width_default_ * dpi_scale_);
stream_window_height_ = (int)(stream_window_height_default_ * dpi_scale_);
stream_ctx_ = ImGui::CreateContext();
if (!stream_ctx_) {
LOG_ERROR("Stream context is null");
return -1;
}
ImGui::SetCurrentContext(stream_ctx_);
if (!SDL_CreateWindowAndRenderer(
"CrossDesk Stream Window", (int)stream_window_width_,
(int)stream_window_height_,
SDL_WINDOW_HIGH_PIXEL_DENSITY | SDL_WINDOW_BORDERLESS |
SDL_WINDOW_TRANSPARENT,
&stream_window_, &stream_renderer_)) {
LOG_ERROR("Error creating stream_window_ and stream_renderer_: {}",
SDL_GetError());
return -1;
}
stream_pixformat_ = SDL_PIXELFORMAT_NV12;
SDL_SetWindowResizable(stream_window_, true);
// for window region action
SDL_SetWindowHitTest(stream_window_, HitTestCallback, this);
SDL_SetRenderDrawBlendMode(stream_renderer_, SDL_BLENDMODE_BLEND);
SetupFontAndStyle(&stream_windows_system_chinese_font_);
ImGuiStyle &style = ImGui::GetStyle();
style.ScaleAllSizes(dpi_scale_);
style.FontScaleDpi = dpi_scale_;
ImGui_ImplSDL3_InitForSDLRenderer(stream_window_, stream_renderer_);
ImGui_ImplSDLRenderer3_Init(stream_renderer_);
// change props->stream_render_rect_
SDL_Event event;
event.type = SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED;
event.window.windowID = SDL_GetWindowID(stream_window_);
SDL_PushEvent(&event);
stream_window_created_ = true;
just_created_ = true;
stream_window_inited_ = true;
LOG_INFO("Stream window inited");
return 0;
}
int GuiApplication::DestroyStreamWindow() {
stream_window_width_ = (float)stream_window_width_default_;
stream_window_height_ = (float)stream_window_height_default_;
if (stream_ctx_) {
ImGui::SetCurrentContext(stream_ctx_);
}
if (stream_renderer_) {
SDL_DestroyRenderer(stream_renderer_);
stream_renderer_ = nullptr;
}
if (stream_window_) {
SDL_DestroyWindow(stream_window_);
stream_window_ = nullptr;
}
stream_window_created_ = false;
focus_on_stream_window_ = false;
stream_window_grabbed_ = false;
control_mouse_ = false;
return 0;
}
int GuiApplication::CreateServerWindow() {
if (server_window_created_) {
return 0;
}
server_ctx_ = ImGui::CreateContext();
if (!server_ctx_) {
LOG_ERROR("Server context is null");
return -1;
}
ImGui::SetCurrentContext(server_ctx_);
if (!SDL_CreateWindowAndRenderer(
"CrossDesk Server Window", (int)server_window_width_,
(int)server_window_height_,
SDL_WINDOW_HIGH_PIXEL_DENSITY | SDL_WINDOW_BORDERLESS |
SDL_WINDOW_TRANSPARENT,
&server_window_, &server_renderer_)) {
LOG_ERROR("Error creating server_window_ and server_renderer_: {}",
SDL_GetError());
return -1;
}
#if _WIN32
// Hide server window from the taskbar by making it a tool window.
{
SDL_PropertiesID server_props = SDL_GetWindowProperties(server_window_);
HWND server_hwnd = (HWND)SDL_GetPointerProperty(
server_props, SDL_PROP_WINDOW_WIN32_HWND_POINTER, NULL);
if (server_hwnd) {
LONG_PTR ex_style = GetWindowLongPtr(server_hwnd, GWL_EXSTYLE);
ex_style |= WS_EX_TOOLWINDOW;
ex_style &= ~WS_EX_APPWINDOW;
SetWindowLongPtr(server_hwnd, GWL_EXSTYLE, ex_style);
// Keep the server window above normal windows.
SetWindowPos(server_hwnd, HWND_TOPMOST, 0, 0, 0, 0,
SWP_NOMOVE | SWP_NOSIZE | SWP_FRAMECHANGED | SWP_NOACTIVATE);
}
}
#endif
#if defined(__linux__) && !defined(__APPLE__)
// Best-effort keep above other windows on X11.
X11SetWindowAlwaysOnTop(server_window_);
// Best-effort hide from taskbar on X11.
X11SetWindowSkipTaskbar(server_window_);
#endif
#if defined(__APPLE__)
// Best-effort keep above other windows on macOS.
MacSetWindowAlwaysOnTop(server_window_, true);
// Best-effort exclude from Window menu / window cycling.
MacSetWindowExcludedFromWindowMenu(server_window_, true);
#endif
// Set window position to bottom-right corner
SDL_Rect display_bounds;
if (SDL_GetDisplayUsableBounds(SDL_GetDisplayForWindow(server_window_),
&display_bounds)) {
int window_x =
display_bounds.x + display_bounds.w - (int)server_window_width_;
int window_y =
display_bounds.y + display_bounds.h - (int)server_window_height_;
SDL_SetWindowPosition(server_window_, window_x, window_y);
}
SDL_SetWindowResizable(server_window_, false);
SDL_SetRenderDrawBlendMode(server_renderer_, SDL_BLENDMODE_BLEND);
// for window region action
SDL_SetWindowHitTest(server_window_, HitTestCallback, this);
SetupFontAndStyle(&server_windows_system_chinese_font_);
ImGuiStyle &style = ImGui::GetStyle();
style.ScaleAllSizes(dpi_scale_);
style.FontScaleDpi = dpi_scale_;
ImGui_ImplSDL3_InitForSDLRenderer(server_window_, server_renderer_);
ImGui_ImplSDLRenderer3_Init(server_renderer_);
server_window_created_ = true;
server_window_inited_ = true;
LOG_INFO("Server window inited");
return 0;
}
int GuiApplication::DestroyServerWindow() {
if (server_ctx_) {
ImGui::SetCurrentContext(server_ctx_);
}
if (server_renderer_) {
SDL_DestroyRenderer(server_renderer_);
server_renderer_ = nullptr;
}
if (server_window_) {
SDL_DestroyWindow(server_window_);
server_window_ = nullptr;
}
server_window_created_ = false;
server_window_inited_ = false;
return 0;
}
int GuiApplication::SetupFontAndStyle(ImFont **system_chinese_font_out) {
float font_size = 32.0f;
// Setup Dear ImGui style
ImGuiIO &io = ImGui::GetIO();
io.IniFilename = NULL; // disable imgui.ini
// Build one merged atlas: UI font + icon font + multilingual fallback fonts.
ImFontConfig config;
config.FontDataOwnedByAtlas = false;
config.MergeMode = false;
if (system_chinese_font_out) {
*system_chinese_font_out = nullptr;
}
ImFont *ui_font = nullptr;
const ImWchar *multilingual_ranges = GetMultilingualGlyphRanges();
#if defined(_WIN32)
const char *base_font_paths[] = {
"C:/Windows/Fonts/msyh.ttc", "C:/Windows/Fonts/msyhbd.ttc",
"C:/Windows/Fonts/segoeui.ttf", "C:/Windows/Fonts/arial.ttf",
"C:/Windows/Fonts/simsun.ttc", nullptr};
#elif defined(__APPLE__)
const char *base_font_paths[] = {
"/System/Library/Fonts/PingFang.ttc",
"/System/Library/Fonts/Supplemental/Arial Unicode.ttf",
"/System/Library/Fonts/Supplemental/Arial.ttf",
"/System/Library/Fonts/SFNS.ttf", nullptr};
#else
const char *base_font_paths[] = {
"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
"/usr/share/fonts/opentype/noto/NotoSans-Regular.ttf",
"/usr/share/fonts/truetype/wqy/wqy-microhei.ttc",
"/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc",
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
nullptr};
#endif
for (int i = 0; base_font_paths[i] != nullptr && ui_font == nullptr; ++i) {
if (!CanReadFontFile(base_font_paths[i])) {
continue;
}
ui_font = io.Fonts->AddFontFromFileTTF(base_font_paths[i], font_size,
&config, multilingual_ranges);
if (ui_font != nullptr) {
LOG_INFO("Loaded base UI font: {}", base_font_paths[i]);
}
}
if (!ui_font) {
ui_font = io.Fonts->AddFontDefault(&config);
}
if (!ui_font) {
LOG_WARN("Failed to initialize base UI font");
ImGui::StyleColorsLight();
return 0;
}
ImFontConfig icon_config = config;
icon_config.MergeMode = true;
static const ImWchar icon_ranges[] = {ICON_MIN_FA, ICON_MAX_FA, 0};
io.Fonts->AddFontFromMemoryTTF(fa_solid_900_ttf, fa_solid_900_ttf_len,
font_size, &icon_config, icon_ranges);
io.FontDefault = ui_font;
if (system_chinese_font_out) {
*system_chinese_font_out = ui_font;
}
ImGui::StyleColorsLight();
return 0;
}
int GuiApplication::DestroyMainWindowContext() {
if (!main_ctx_) {
return 0;
}
ImGui::SetCurrentContext(main_ctx_);
ImGui_ImplSDLRenderer3_Shutdown();
ImGui_ImplSDL3_Shutdown();
ImGui::DestroyContext(main_ctx_);
main_ctx_ = nullptr;
return 0;
}
int GuiApplication::DestroyStreamWindowContext() {
if (!stream_ctx_) {
stream_window_inited_ = false;
return 0;
}
stream_window_inited_ = false;
ImGui::SetCurrentContext(stream_ctx_);
ImGui_ImplSDLRenderer3_Shutdown();
ImGui_ImplSDL3_Shutdown();
ImGui::DestroyContext(stream_ctx_);
stream_ctx_ = nullptr;
return 0;
}
int GuiApplication::DestroyServerWindowContext() {
if (!server_ctx_) {
server_window_inited_ = false;
return 0;
}
server_window_inited_ = false;
ImGui::SetCurrentContext(server_ctx_);
ImGui_ImplSDLRenderer3_Shutdown();
ImGui_ImplSDL3_Shutdown();
ImGui::DestroyContext(server_ctx_);
server_ctx_ = nullptr;
return 0;
}
} // namespace crossdesk
+236
View File
@@ -0,0 +1,236 @@
#include "application/gui_application.h"
#include <cmath>
#include "rd_log.h"
namespace crossdesk {
int GuiApplication::DrawMainWindow() {
if (!main_ctx_) {
LOG_ERROR("Main context is null");
return -1;
}
ImGui::SetCurrentContext(main_ctx_);
ImGui_ImplSDLRenderer3_NewFrame();
ImGui_ImplSDL3_NewFrame();
ImGui::NewFrame();
ImGuiIO &io = ImGui::GetIO();
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always);
ImGui::SetNextWindowSize(ImVec2(io.DisplaySize.x, io.DisplaySize.y),
ImGuiCond_Always);
ImGui::Begin("MainRender", nullptr,
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoDecoration |
ImGuiWindowFlags_NoBringToFrontOnFocus |
ImGuiWindowFlags_NoDocking);
ImGui::PopStyleColor();
ImGui::PopStyleVar();
TitleBar(true);
MainWindow();
UpdateNotificationWindow();
#if _WIN32 && CROSSDESK_PORTABLE
PortableServiceInstallWindow();
#endif
#ifdef __APPLE__
if (show_request_permission_window_) {
RequestPermissionWindow();
}
#endif
ImGui::End();
// Rendering
(void)io;
ImGui::Render();
SDL_SetRenderScale(main_renderer_, io.DisplayFramebufferScale.x,
io.DisplayFramebufferScale.y);
SDL_SetRenderDrawColor(main_renderer_, 0, 0, 0, 0);
SDL_RenderClear(main_renderer_);
ImGui_ImplSDLRenderer3_RenderDrawData(ImGui::GetDrawData(), main_renderer_);
SDL_RenderPresent(main_renderer_);
return 0;
}
int GuiApplication::DrawStreamWindow() {
if (!stream_ctx_) {
LOG_ERROR("Stream context is null");
return -1;
}
ImGui::SetCurrentContext(stream_ctx_);
ImGui_ImplSDLRenderer3_NewFrame();
ImGui_ImplSDL3_NewFrame();
ImGui::NewFrame();
StreamWindow();
ImGuiIO &io = ImGui::GetIO();
float stream_title_window_height =
fullscreen_button_pressed_ ? 0 : title_bar_height_;
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
// Set minimum window size to 0 to allow exact height control
ImGui::PushStyleVar(ImGuiStyleVar_WindowMinSize, ImVec2(0, 0));
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always);
ImGui::SetNextWindowSize(ImVec2(io.DisplaySize.x, stream_title_window_height),
ImGuiCond_Always);
ImGui::Begin("StreamTitleWindow", nullptr,
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoDecoration |
ImGuiWindowFlags_NoBringToFrontOnFocus |
ImGuiWindowFlags_NoDocking);
ImGui::PopStyleVar(2);
ImGui::PopStyleColor();
if (!fullscreen_button_pressed_) {
TitleBar(false);
}
ImGui::End();
// Rendering
(void)io;
ImGui::Render();
SDL_SetRenderScale(stream_renderer_, io.DisplayFramebufferScale.x,
io.DisplayFramebufferScale.y);
SDL_SetRenderDrawColor(stream_renderer_, 0, 0, 0, 255);
SDL_RenderClear(stream_renderer_);
// std::shared_lock lock(remote_sessions_mutex_);
for (auto &it : remote_sessions_) {
auto props = it.second;
if (props->tab_selected_) {
SDL_FRect render_rect_f = {
props->stream_render_rect_f_.x, props->stream_render_rect_f_.y,
props->stream_render_rect_f_.w, props->stream_render_rect_f_.h};
SDL_RenderTexture(stream_renderer_, props->stream_texture_, NULL,
&render_rect_f);
}
}
ImGui_ImplSDLRenderer3_RenderDrawData(ImGui::GetDrawData(), stream_renderer_);
SDL_RenderPresent(stream_renderer_);
return 0;
}
int GuiApplication::DrawServerWindow() {
if (!server_ctx_) {
LOG_ERROR("Server context is null");
return -1;
}
ImGui::SetCurrentContext(server_ctx_);
ImGui_ImplSDLRenderer3_NewFrame();
ImGui_ImplSDL3_NewFrame();
ImGui::NewFrame();
ImGuiIO &io = ImGui::GetIO();
server_window_width_ = io.DisplaySize.x;
server_window_height_ = io.DisplaySize.y;
ServerWindow();
ImGui::Render();
SDL_SetRenderScale(server_renderer_, io.DisplayFramebufferScale.x,
io.DisplayFramebufferScale.y);
SDL_SetRenderDrawColor(server_renderer_, 0, 0, 0, 0);
SDL_RenderClear(server_renderer_);
ImGui_ImplSDLRenderer3_RenderDrawData(ImGui::GetDrawData(), server_renderer_);
SDL_RenderPresent(server_renderer_);
return 0;
}
bool GuiApplication::MinimizeMainWindowToTray() {
if (!enable_minimize_to_tray_ || !main_window_) {
return false;
}
#if defined(_WIN32) || defined(__APPLE__)
if (!tray_) {
return false;
}
tray_->MinimizeToTray();
return true;
#elif defined(__linux__) && !defined(__APPLE__)
if (!tray_) {
return false;
}
return tray_->MinimizeToTray();
#else
return false;
#endif
}
void GuiApplication::UpdateRenderRect() {
// std::shared_lock lock(remote_sessions_mutex_);
for (auto &[_, props] : remote_sessions_) {
if (!props->reset_control_bar_pos_) {
props->mouse_diff_control_bar_pos_x_ = 0;
props->mouse_diff_control_bar_pos_y_ = 0;
}
if (!just_created_) {
props->reset_control_bar_pos_ = true;
}
int stream_window_width, stream_window_height;
SDL_GetWindowSize(stream_window_, &stream_window_width,
&stream_window_height);
stream_window_width_ = (float)stream_window_width;
stream_window_height_ = (float)stream_window_height;
float video_ratio =
(float)props->video_width_ / (float)props->video_height_;
float video_ratio_reverse =
(float)props->video_height_ / (float)props->video_width_;
float render_area_width = props->render_window_width_;
float render_area_height = props->render_window_height_;
props->stream_render_rect_last_ = props->stream_render_rect_;
SDL_FRect rect_f{props->render_window_x_, props->render_window_y_,
render_area_width, render_area_height};
if (render_area_width < render_area_height * video_ratio) {
rect_f.x = props->render_window_x_;
rect_f.y = std::abs(render_area_height -
render_area_width * video_ratio_reverse) /
2.0f +
props->render_window_y_;
rect_f.w = render_area_width;
rect_f.h = render_area_width * video_ratio_reverse;
} else if (render_area_width > render_area_height * video_ratio) {
rect_f.x =
std::abs(render_area_width - render_area_height * video_ratio) /
2.0f +
props->render_window_x_;
rect_f.y = props->render_window_y_;
rect_f.w = render_area_height * video_ratio;
rect_f.h = render_area_height;
} else {
rect_f.x = props->render_window_x_;
rect_f.y = props->render_window_y_;
rect_f.w = render_area_width;
rect_f.h = render_area_height;
}
props->stream_render_rect_f_ = rect_f;
props->stream_render_rect_ = {static_cast<int>(std::lround(rect_f.x)),
static_cast<int>(std::lround(rect_f.y)),
static_cast<int>(std::lround(rect_f.w)),
static_cast<int>(std::lround(rect_f.h))};
}
}
} // namespace crossdesk
File diff suppressed because it is too large Load Diff
+67 -59
View File
@@ -7,71 +7,79 @@
#ifndef _LAYOUT_STYLE_H_
#define _LAYOUT_STYLE_H_
#define MENU_WINDOW_WIDTH_CN 300
#define MENU_WINDOW_HEIGHT_CN 280
#define LOCAL_WINDOW_WIDTH_CN 300
#define LOCAL_WINDOW_HEIGHT_CN 280
#define REMOTE_WINDOW_WIDTH_CN 300
#define REMOTE_WINDOW_HEIGHT_CN 280
#define MENU_WINDOW_WIDTH_EN 190
#define MENU_WINDOW_HEIGHT_EN 245
#define IPUT_WINDOW_WIDTH 160
#define INPUT_WINDOW_PADDING_CN 66
#define INPUT_WINDOW_PADDING_EN 96
#define SETTINGS_WINDOW_WIDTH_CN 202
#define SETTINGS_WINDOW_WIDTH_EN 248
#define MENU_WINDOW_WIDTH_CN 300 * dpi_scale_
#define MENU_WINDOW_HEIGHT_CN 280 * dpi_scale_
#define LOCAL_WINDOW_WIDTH_CN 300 * dpi_scale_
#define LOCAL_WINDOW_HEIGHT_CN 280 * dpi_scale_
#define REMOTE_WINDOW_WIDTH_CN 300 * dpi_scale_
#define REMOTE_WINDOW_HEIGHT_CN 280 * dpi_scale_
#define MENU_WINDOW_WIDTH_EN 190 * dpi_scale_
#define MENU_WINDOW_HEIGHT_EN 245 * dpi_scale_
#define IPUT_WINDOW_WIDTH 160 * dpi_scale_
#define INPUT_WINDOW_PADDING_CN 66 * dpi_scale_
#define INPUT_WINDOW_PADDING_EN 96 * dpi_scale_
#define SETTINGS_WINDOW_WIDTH_CN 202 * dpi_scale_
#define SETTINGS_WINDOW_WIDTH_EN 248 * dpi_scale_
#if USE_CUDA
#if _WIN32
#define SETTINGS_WINDOW_HEIGHT_CN 405
#define SETTINGS_WINDOW_HEIGHT_EN 405
#define SETTINGS_WINDOW_HEIGHT_CN 405 * dpi_scale_
#define SETTINGS_WINDOW_HEIGHT_EN 405 * dpi_scale_
#else
#define SETTINGS_WINDOW_HEIGHT_CN 375
#define SETTINGS_WINDOW_HEIGHT_EN 375
#define SETTINGS_WINDOW_HEIGHT_CN 375 * dpi_scale_
#define SETTINGS_WINDOW_HEIGHT_EN 375 * dpi_scale_
#endif
#else
#if _WIN32
#define SETTINGS_WINDOW_HEIGHT_CN 375
#define SETTINGS_WINDOW_HEIGHT_EN 375
#define SETTINGS_WINDOW_HEIGHT_CN 375 * dpi_scale_
#define SETTINGS_WINDOW_HEIGHT_EN 375 * dpi_scale_
#else
#define SETTINGS_WINDOW_HEIGHT_CN 345
#define SETTINGS_WINDOW_HEIGHT_EN 345
#define SETTINGS_WINDOW_HEIGHT_CN 345 * dpi_scale_
#define SETTINGS_WINDOW_HEIGHT_EN 345 * dpi_scale_
#endif
#endif
#define SELF_HOSTED_SERVER_CONFIG_WINDOW_WIDTH_CN 228
#define SELF_HOSTED_SERVER_CONFIG_WINDOW_WIDTH_EN 275
#define SELF_HOSTED_SERVER_CONFIG_WINDOW_HEIGHT_CN 195
#define SELF_HOSTED_SERVER_CONFIG_WINDOW_HEIGHT_EN 195
#define LANGUAGE_SELECT_WINDOW_PADDING_CN 120
#define LANGUAGE_SELECT_WINDOW_PADDING_EN 167
#define VIDEO_QUALITY_SELECT_WINDOW_PADDING_CN 120
#define VIDEO_QUALITY_SELECT_WINDOW_PADDING_EN 167
#define VIDEO_FRAME_RATE_SELECT_WINDOW_PADDING_CN 120
#define VIDEO_FRAME_RATE_SELECT_WINDOW_PADDING_EN 167
#define VIDEO_ENCODE_FORMAT_SELECT_WINDOW_PADDING_CN 120
#define VIDEO_ENCODE_FORMAT_SELECT_WINDOW_PADDING_EN 167
#define ENABLE_HARDWARE_VIDEO_CODEC_CHECKBOX_PADDING_CN 171
#define ENABLE_HARDWARE_VIDEO_CODEC_CHECKBOX_PADDING_EN 218
#define ENABLE_TURN_CHECKBOX_PADDING_CN 171
#define ENABLE_TURN_CHECKBOX_PADDING_EN 218
#define ENABLE_SRTP_CHECKBOX_PADDING_CN 171
#define ENABLE_SRTP_CHECKBOX_PADDING_EN 218
#define ENABLE_SELF_HOSTED_SERVER_CHECKBOX_PADDING_CN 171
#define ENABLE_SELF_HOSTED_SERVER_CHECKBOX_PADDING_EN 218
#define ENABLE_AUTOSTART_PADDING_CN 171
#define ENABLE_AUTOSTART_PADDING_EN 218
#define ENABLE_DAEMON_PADDING_CN 171
#define ENABLE_DAEMON_PADDING_EN 218
#define ENABLE_MINIZE_TO_TRAY_PADDING_CN 171
#define ENABLE_MINIZE_TO_TRAY_PADDING_EN 218
#define SELF_HOSTED_SERVER_HOST_INPUT_BOX_PADDING_CN 90
#define SELF_HOSTED_SERVER_HOST_INPUT_BOX_PADDING_EN 137
#define SELF_HOSTED_SERVER_PORT_INPUT_BOX_PADDING_CN 90
#define SELF_HOSTED_SERVER_PORT_INPUT_BOX_PADDING_EN 137
#define SETTINGS_SELECT_WINDOW_WIDTH 73
#define SELF_HOSTED_SERVER_INPUT_WINDOW_WIDTH 130
#define SETTINGS_OK_BUTTON_PADDING_CN 65
#define SETTINGS_OK_BUTTON_PADDING_EN 83
#define SELF_HOSTED_SERVER_CONFIG_OK_BUTTON_PADDING_CN 78
#define SELF_HOSTED_SERVER_CONFIG_OK_BUTTON_PADDING_EN 91
#endif
#define SELF_HOSTED_SERVER_CONFIG_WINDOW_WIDTH_CN 228 * dpi_scale_
#define SELF_HOSTED_SERVER_CONFIG_WINDOW_WIDTH_EN 275 * dpi_scale_
#define SELF_HOSTED_SERVER_CONFIG_WINDOW_HEIGHT_CN 195 * dpi_scale_
#define SELF_HOSTED_SERVER_CONFIG_WINDOW_HEIGHT_EN 195 * dpi_scale_
#define LANGUAGE_SELECT_WINDOW_PADDING_CN 120 * dpi_scale_
#define LANGUAGE_SELECT_WINDOW_PADDING_EN 167 * dpi_scale_
#define VIDEO_QUALITY_SELECT_WINDOW_PADDING_CN 120 * dpi_scale_
#define VIDEO_QUALITY_SELECT_WINDOW_PADDING_EN 167 * dpi_scale_
#define VIDEO_FRAME_RATE_SELECT_WINDOW_PADDING_CN 120 * dpi_scale_
#define VIDEO_FRAME_RATE_SELECT_WINDOW_PADDING_EN 167 * dpi_scale_
#define VIDEO_ENCODE_FORMAT_SELECT_WINDOW_PADDING_CN 120 * dpi_scale_
#define VIDEO_ENCODE_FORMAT_SELECT_WINDOW_PADDING_EN 167 * dpi_scale_
#define ENABLE_HARDWARE_VIDEO_CODEC_CHECKBOX_PADDING_CN 171 * dpi_scale_
#define ENABLE_HARDWARE_VIDEO_CODEC_CHECKBOX_PADDING_EN 218 * dpi_scale_
#define ENABLE_TURN_CHECKBOX_PADDING_CN 171 * dpi_scale_
#define ENABLE_TURN_CHECKBOX_PADDING_EN 218 * dpi_scale_
#define ENABLE_SRTP_CHECKBOX_PADDING_CN 171 * dpi_scale_
#define ENABLE_SRTP_CHECKBOX_PADDING_EN 218 * dpi_scale_
#define ENABLE_SELF_HOSTED_SERVER_CHECKBOX_PADDING_CN 171 * dpi_scale_
#define ENABLE_SELF_HOSTED_SERVER_CHECKBOX_PADDING_EN 218 * dpi_scale_
#define ENABLE_AUTOSTART_PADDING_CN 171 * dpi_scale_
#define ENABLE_AUTOSTART_PADDING_EN 218 * dpi_scale_
#define ENABLE_DAEMON_PADDING_CN 171 * dpi_scale_
#define ENABLE_DAEMON_PADDING_EN 218 * dpi_scale_
#define ENABLE_MINIZE_TO_TRAY_PADDING_CN 171 * dpi_scale_
#define ENABLE_MINIZE_TO_TRAY_PADDING_EN 218 * dpi_scale_
#define SELF_HOSTED_SERVER_HOST_INPUT_BOX_PADDING_CN 90 * dpi_scale_
#define SELF_HOSTED_SERVER_HOST_INPUT_BOX_PADDING_EN 137 * dpi_scale_
#define SELF_HOSTED_SERVER_PORT_INPUT_BOX_PADDING_CN 90 * dpi_scale_
#define SELF_HOSTED_SERVER_PORT_INPUT_BOX_PADDING_EN 137 * dpi_scale_
#define SETTINGS_SELECT_WINDOW_WIDTH 73 * dpi_scale_
#define SELF_HOSTED_SERVER_INPUT_WINDOW_WIDTH 130 * dpi_scale_
#define SETTINGS_OK_BUTTON_PADDING_CN 65 * dpi_scale_
#define SETTINGS_OK_BUTTON_PADDING_EN 83 * dpi_scale_
#define SELF_HOSTED_SERVER_CONFIG_OK_BUTTON_PADDING_CN 78 * dpi_scale_
#define SELF_HOSTED_SERVER_CONFIG_OK_BUTTON_PADDING_EN 91 * dpi_scale_
#define UPDATE_NOTIFICATION_OK_BUTTON_PADDING_CN 162 * dpi_scale_
#define UPDATE_NOTIFICATION_OK_BUTTON_PADDING_EN 146 * dpi_scale_
#define UPDATE_NOTIFICATION_RESERVED_HEIGHT 120 * dpi_scale_
#define REQUEST_PERMISSION_WINDOW_WIDTH_CN 130 * dpi_scale_
#define REQUEST_PERMISSION_WINDOW_HEIGHT_CN 125 * dpi_scale_
#define REQUEST_PERMISSION_WINDOW_WIDTH_EN 260 * dpi_scale_
#define REQUEST_PERMISSION_WINDOW_HEIGHT_EN 125 * dpi_scale_
#define REQUEST_PERMISSION_WINDOW_CHECKBOX_PADDING_CN 90 * dpi_scale_
#define REQUEST_PERMISSION_WINDOW_CHECKBOX_PADDING_EN 210 * dpi_scale_
#endif
+89
View File
@@ -0,0 +1,89 @@
/*
* @Author: DI JUNKUN
* @Date: 2024-06-14
* Copyright (c) 2024 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _LAYOUT_STYLE_H_
#define _LAYOUT_STYLE_H_
#define TITLE_BAR_HEIGHT 0.0625f
#define TITLE_BAR_BUTTON_WIDTH 0.0625f
#define TITLE_BAR_BUTTON_HEIGHT 0.0625f
#define STATUS_BAR_HEIGHT 0.05f
#define MENU_WINDOW_WIDTH_CN 300
#define MENU_WINDOW_HEIGHT_CN 280
#define LOCAL_WINDOW_WIDTH_CN 300
#define LOCAL_WINDOW_HEIGHT_CN 280
#define REMOTE_WINDOW_WIDTH_CN 300
#define REMOTE_WINDOW_HEIGHT_CN 280
#define MENU_WINDOW_WIDTH_EN 190
#define MENU_WINDOW_HEIGHT_EN 245
#define IPUT_WINDOW_WIDTH 160
#define INPUT_WINDOW_PADDING_CN 66
#define INPUT_WINDOW_PADDING_EN 96
#define SETTINGS_WINDOW_WIDTH_CN 202
#define SETTINGS_WINDOW_WIDTH_EN 248
#if USE_CUDA
#if _WIN32
#define SETTINGS_WINDOW_HEIGHT_CN 405
#define SETTINGS_WINDOW_HEIGHT_EN 405
#else
#define SETTINGS_WINDOW_HEIGHT_CN 375
#define SETTINGS_WINDOW_HEIGHT_EN 375
#endif
#else
#if _WIN32
#define SETTINGS_WINDOW_HEIGHT_CN 375
#define SETTINGS_WINDOW_HEIGHT_EN 375
#else
#define SETTINGS_WINDOW_HEIGHT_CN 345
#define SETTINGS_WINDOW_HEIGHT_EN 345
#endif
#endif
#define SELF_HOSTED_SERVER_CONFIG_WINDOW_WIDTH_CN 228
#define SELF_HOSTED_SERVER_CONFIG_WINDOW_WIDTH_EN 275
#define SELF_HOSTED_SERVER_CONFIG_WINDOW_HEIGHT_CN 195
#define SELF_HOSTED_SERVER_CONFIG_WINDOW_HEIGHT_EN 195
#define LANGUAGE_SELECT_WINDOW_PADDING_CN 120
#define LANGUAGE_SELECT_WINDOW_PADDING_EN 167
#define VIDEO_QUALITY_SELECT_WINDOW_PADDING_CN 120
#define VIDEO_QUALITY_SELECT_WINDOW_PADDING_EN 167
#define VIDEO_FRAME_RATE_SELECT_WINDOW_PADDING_CN 120
#define VIDEO_FRAME_RATE_SELECT_WINDOW_PADDING_EN 167
#define VIDEO_ENCODE_FORMAT_SELECT_WINDOW_PADDING_CN 120
#define VIDEO_ENCODE_FORMAT_SELECT_WINDOW_PADDING_EN 167
#define ENABLE_HARDWARE_VIDEO_CODEC_CHECKBOX_PADDING_CN 171
#define ENABLE_HARDWARE_VIDEO_CODEC_CHECKBOX_PADDING_EN 218
#define ENABLE_TURN_CHECKBOX_PADDING_CN 171
#define ENABLE_TURN_CHECKBOX_PADDING_EN 218
#define ENABLE_SRTP_CHECKBOX_PADDING_CN 171
#define ENABLE_SRTP_CHECKBOX_PADDING_EN 218
#define ENABLE_SELF_HOSTED_SERVER_CHECKBOX_PADDING_CN 171
#define ENABLE_SELF_HOSTED_SERVER_CHECKBOX_PADDING_EN 218
#define ENABLE_AUTOSTART_PADDING_CN 171
#define ENABLE_AUTOSTART_PADDING_EN 218
#define ENABLE_DAEMON_PADDING_CN 171
#define ENABLE_DAEMON_PADDING_EN 218
#define ENABLE_MINIZE_TO_TRAY_PADDING_CN 171
#define ENABLE_MINIZE_TO_TRAY_PADDING_EN 218
#define SELF_HOSTED_SERVER_HOST_INPUT_BOX_PADDING_CN 90
#define SELF_HOSTED_SERVER_HOST_INPUT_BOX_PADDING_EN 137
#define SELF_HOSTED_SERVER_PORT_INPUT_BOX_PADDING_CN 90
#define SELF_HOSTED_SERVER_PORT_INPUT_BOX_PADDING_EN 137
#define SETTINGS_SELECT_WINDOW_WIDTH 73
#define SELF_HOSTED_SERVER_INPUT_WINDOW_WIDTH 130
#define SETTINGS_OK_BUTTON_PADDING_CN 65
#define SETTINGS_OK_BUTTON_PADDING_EN 83
#define SELF_HOSTED_SERVER_CONFIG_OK_BUTTON_PADDING_CN 78
#define SELF_HOSTED_SERVER_CONFIG_OK_BUTTON_PADDING_EN 91
#define UPDATE_NOTIFICATION_OK_BUTTON_PADDING_CN 162
#define UPDATE_NOTIFICATION_OK_BUTTON_PADDING_EN 146
#define UPDATE_NOTIFICATION_RESERVED_HEIGHT 120
#define REQUEST_PERMISSION_WINDOW_WIDTH_CN 130
#define REQUEST_PERMISSION_WINDOW_HEIGHT_CN 125
#define REQUEST_PERMISSION_WINDOW_WIDTH_EN 260
#define REQUEST_PERMISSION_WINDOW_HEIGHT_EN 125
#define REQUEST_PERMISSION_WINDOW_CHECKBOX_PADDING_CN 90
#define REQUEST_PERMISSION_WINDOW_CHECKBOX_PADDING_EN 210
#endif
+144 -175
View File
@@ -1,187 +1,156 @@
/*
* @Author: DI JUNKUN
* @Date: 2024-05-29
* Copyright (c) 2024 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _LOCALIZATION_H_
#define _LOCALIZATION_H_
/*
* @Author: DI JUNKUN
* @Date: 2024-05-29
* Copyright (c) 2024 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _LOCALIZATION_H_
#define _LOCALIZATION_H_
#include <string>
#include <unordered_map>
#include <vector>
#include "localization_data.h"
#if _WIN32
#include <Windows.h>
#endif
namespace crossdesk {
namespace localization {
struct LanguageOption {
std::string code;
std::string display_name;
};
namespace crossdesk {
class LocalizedString {
public:
constexpr explicit LocalizedString(const char* key) : key_(key) {}
const std::string& operator[](int language_index) const;
private:
const char* key_;
};
inline const std::vector<LanguageOption>& GetSupportedLanguages() {
static const std::vector<LanguageOption> kSupportedLanguages = {
{"zh-CN", reinterpret_cast<const char*>(u8"中文")},
{"en-US", "English"},
{"ru-RU", reinterpret_cast<const char*>(u8"Русский")}};
return kSupportedLanguages;
}
namespace detail {
namespace localization {
inline int ClampLanguageIndex(int language_index) {
if (language_index >= 0 &&
language_index < static_cast<int>(GetSupportedLanguages().size())) {
return language_index;
}
return 0;
}
static std::vector<std::string> local_desktop = {
reinterpret_cast<const char*>(u8"本桌面"), "Local Desktop"};
static std::vector<std::string> local_id = {
reinterpret_cast<const char*>(u8"本机ID"), "Local ID"};
static std::vector<std::string> local_id_copied_to_clipboard = {
reinterpret_cast<const char*>(u8"已复制到剪贴板"), "Copied to clipboard"};
static std::vector<std::string> password = {
reinterpret_cast<const char*>(u8"密码"), "Password"};
static std::vector<std::string> max_password_len = {
reinterpret_cast<const char*>(u8"最大6个字符"), "Max 6 chars"};
using TranslationTable =
std::unordered_map<std::string,
std::unordered_map<std::string, std::string>>;
static std::vector<std::string> remote_desktop = {
reinterpret_cast<const char*>(u8"控制远程桌面"), "Control Remote Desktop"};
static std::vector<std::string> remote_id = {
reinterpret_cast<const char*>(u8"对端ID"), "Remote ID"};
static std::vector<std::string> connect = {
reinterpret_cast<const char*>(u8"连接"), "Connect"};
static std::vector<std::string> recent_connections = {
reinterpret_cast<const char*>(u8"近期连接"), "Recent Connections"};
static std::vector<std::string> disconnect = {
reinterpret_cast<const char*>(u8"断开连接"), "Disconnect"};
static std::vector<std::string> fullscreen = {
reinterpret_cast<const char*>(u8"全屏"), " Fullscreen"};
static std::vector<std::string> show_net_traffic_stats = {
reinterpret_cast<const char*>(u8"显示流量统计"), "Show Net Traffic Stats"};
static std::vector<std::string> hide_net_traffic_stats = {
reinterpret_cast<const char*>(u8"隐藏流量统计"), "Hide Net Traffic Stats"};
static std::vector<std::string> video = {
reinterpret_cast<const char*>(u8"视频"), "Video"};
static std::vector<std::string> audio = {
reinterpret_cast<const char*>(u8"音频"), "Audio"};
static std::vector<std::string> data = {reinterpret_cast<const char*>(u8"数据"),
"Data"};
static std::vector<std::string> total = {
reinterpret_cast<const char*>(u8"总计"), "Total"};
static std::vector<std::string> in = {reinterpret_cast<const char*>(u8"输入"),
"In"};
static std::vector<std::string> out = {reinterpret_cast<const char*>(u8"输出"),
"Out"};
static std::vector<std::string> loss_rate = {
reinterpret_cast<const char*>(u8"丢包率"), "Loss Rate"};
static std::vector<std::string> exit_fullscreen = {
reinterpret_cast<const char*>(u8"退出全屏"), "Exit fullscreen"};
static std::vector<std::string> control_mouse = {
reinterpret_cast<const char*>(u8"控制"), "Control"};
static std::vector<std::string> release_mouse = {
reinterpret_cast<const char*>(u8"释放"), "Release"};
static std::vector<std::string> audio_capture = {
reinterpret_cast<const char*>(u8"声音"), "Audio"};
static std::vector<std::string> mute = {
reinterpret_cast<const char*>(u8" 静音"), " Mute"};
static std::vector<std::string> settings = {
reinterpret_cast<const char*>(u8"设置"), "Settings"};
static std::vector<std::string> language = {
reinterpret_cast<const char*>(u8"语言:"), "Language:"};
static std::vector<std::string> language_zh = {
reinterpret_cast<const char*>(u8"中文"), "Chinese"};
static std::vector<std::string> language_en = {
reinterpret_cast<const char*>(u8"英文"), "English"};
static std::vector<std::string> video_quality = {
reinterpret_cast<const char*>(u8"视频质量:"), "Video Quality:"};
static std::vector<std::string> video_frame_rate = {
reinterpret_cast<const char*>(u8"画面采集帧率:"),
"Video Capture Frame Rate:"};
static std::vector<std::string> video_quality_high = {
reinterpret_cast<const char*>(u8""), "High"};
static std::vector<std::string> video_quality_medium = {
reinterpret_cast<const char*>(u8""), "Medium"};
static std::vector<std::string> video_quality_low = {
reinterpret_cast<const char*>(u8""), "Low"};
static std::vector<std::string> video_encode_format = {
reinterpret_cast<const char*>(u8"视频编码格式:"), "Video Encode Format:"};
static std::vector<std::string> av1 = {reinterpret_cast<const char*>(u8"AV1"),
"AV1"};
static std::vector<std::string> h264 = {
reinterpret_cast<const char*>(u8"H.264"), "H.264"};
static std::vector<std::string> enable_hardware_video_codec = {
reinterpret_cast<const char*>(u8"启用硬件编解码器:"),
"Enable Hardware Video Codec:"};
static std::vector<std::string> enable_turn = {
reinterpret_cast<const char*>(u8"启用中继服务:"), "Enable TURN Service:"};
static std::vector<std::string> enable_srtp = {
reinterpret_cast<const char*>(u8"启用SRTP:"), "Enable SRTP:"};
static std::vector<std::string> self_hosted_server_config = {
reinterpret_cast<const char*>(u8"自托管服务器配置"),
"Self-Hosted Server Config"};
static std::vector<std::string> self_hosted_server_settings = {
reinterpret_cast<const char*>(u8"自托管服务器设置"),
"Self-Hosted Server Settings"};
static std::vector<std::string> self_hosted_server_address = {
reinterpret_cast<const char*>(u8"服务器地址:"), "Server Address:"};
static std::vector<std::string> self_hosted_server_port = {
reinterpret_cast<const char*>(u8"信令服务端口:"), "Signal Service Port:"};
static std::vector<std::string> self_hosted_server_coturn_server_port = {
reinterpret_cast<const char*>(u8"中继服务端口:"), "Relay Service Port:"};
static std::vector<std::string> self_hosted_server_certificate_path = {
reinterpret_cast<const char*>(u8"证书文件路径:"), "Certificate File Path:"};
static std::vector<std::string> select_a_file = {
reinterpret_cast<const char*>(u8"请选择文件"), "Please select a file"};
static std::vector<std::string> ok = {reinterpret_cast<const char*>(u8"确认"),
"OK"};
static std::vector<std::string> cancel = {
reinterpret_cast<const char*>(u8"取消"), "Cancel"};
inline std::unordered_map<std::string, std::string> MakeLocalizedValues(
const TranslationRow& row) {
return {{"zh-CN", reinterpret_cast<const char*>(row.zh)},
{"en-US", row.en},
{"ru-RU", reinterpret_cast<const char*>(row.ru)}};
}
static std::vector<std::string> new_password = {
reinterpret_cast<const char*>(u8"请输入六位密码:"),
"Please input a six-char password:"};
inline TranslationTable BuildTranslationTable() {
TranslationTable table;
for (const auto& row : kTranslationRows) {
table[row.key] = MakeLocalizedValues(row);
}
static std::vector<std::string> input_password = {
reinterpret_cast<const char*>(u8"请输入密码:"), "Please input password:"};
static std::vector<std::string> validate_password = {
reinterpret_cast<const char*>(u8"验证密码中..."), "Validate password ..."};
static std::vector<std::string> reinput_password = {
reinterpret_cast<const char*>(u8"请重新输入密码"),
"Please input password again"};
static std::vector<std::string> remember_password = {
reinterpret_cast<const char*>(u8"记住密码"), "Remember password"};
static std::vector<std::string> signal_connected = {
reinterpret_cast<const char*>(u8"已连接服务器"), "Connected"};
static std::vector<std::string> signal_disconnected = {
reinterpret_cast<const char*>(u8"未连接服务器"), "Disconnected"};
static std::vector<std::string> p2p_connected = {
reinterpret_cast<const char*>(u8"对等连接已建立"), "P2P Connected"};
static std::vector<std::string> p2p_disconnected = {
reinterpret_cast<const char*>(u8"对等连接已断开"), "P2P Disconnected"};
static std::vector<std::string> p2p_connecting = {
reinterpret_cast<const char*>(u8"正在建立对等连接..."),
"P2P Connecting ..."};
static std::vector<std::string> p2p_failed = {
reinterpret_cast<const char*>(u8"对等连接失败"), "P2P Failed"};
static std::vector<std::string> p2p_closed = {
reinterpret_cast<const char*>(u8"对等连接已关闭"), "P2P closed"};
static std::vector<std::string> no_such_id = {
reinterpret_cast<const char*>(u8"无此ID"), "No such ID"};
static std::vector<std::string> about = {
reinterpret_cast<const char*>(u8"关于"), "About"};
static std::vector<std::string> new_version_available = {
reinterpret_cast<const char*>(u8"新版本可用"), "New Version Available"};
static std::vector<std::string> version = {
reinterpret_cast<const char*>(u8"版本"), "Version"};
static std::vector<std::string> access_website = {
reinterpret_cast<const char*>(u8"访问官网: "), "Access Website: "};
static std::vector<std::string> confirm_delete_connection = {
reinterpret_cast<const char*>(u8"确认删除此连接"),
"Confirm to delete this connection"};
static std::vector<std::string> enable_autostart = {
reinterpret_cast<const char*>(u8"开机自启:"), "Auto Start:"};
static std::vector<std::string> enable_daemon = {
reinterpret_cast<const char*>(u8"启用守护进程:"), "Enable Daemon:"};
static std::vector<std::string> takes_effect_after_restart = {
reinterpret_cast<const char*>(u8"重启后生效"),
"Takes effect after restart"};
#if _WIN32
static std::vector<std::string> minimize_to_tray = {
reinterpret_cast<const char*>(u8"退出时最小化到系统托盘:"),
"Minimize to system tray when exit:"};
static std::vector<LPCWSTR> exit_program = {L"退出", L"Exit"};
#endif
} // namespace localization
} // namespace crossdesk
#endif
return table;
}
inline const TranslationTable& GetTranslationTable() {
static const TranslationTable table = BuildTranslationTable();
return table;
}
inline const std::string& GetTranslatedText(const std::string& key,
int language_index) {
static const std::string kEmptyText = "";
const auto& table = GetTranslationTable();
const auto key_it = table.find(key);
if (key_it == table.end()) {
return kEmptyText;
}
const auto& localized_values = key_it->second;
const std::string& language_code =
GetSupportedLanguages()[ClampLanguageIndex(language_index)].code;
const auto exact_it = localized_values.find(language_code);
if (exact_it != localized_values.end()) {
return exact_it->second;
}
const auto english_it = localized_values.find("en-US");
if (english_it != localized_values.end()) {
return english_it->second;
}
const auto chinese_it = localized_values.find("zh-CN");
if (chinese_it != localized_values.end()) {
return chinese_it->second;
}
return kEmptyText;
}
} // namespace detail
inline const std::string& LocalizedString::operator[](
int language_index) const {
return detail::GetTranslatedText(key_, language_index);
}
#define CROSSDESK_DECLARE_LOCALIZED_STRING(name, zh, en, ru) \
inline const LocalizedString name(#name);
CROSSDESK_LOCALIZATION_ALL(CROSSDESK_DECLARE_LOCALIZED_STRING)
#undef CROSSDESK_DECLARE_LOCALIZED_STRING
#if _WIN32
inline const wchar_t* GetExitProgramLabel(int language_index) {
static std::vector<std::wstring> cache(GetSupportedLanguages().size());
const int normalized_index = detail::ClampLanguageIndex(language_index);
std::wstring& cached_text = cache[normalized_index];
if (!cached_text.empty()) {
return cached_text.c_str();
}
const std::string& utf8_text =
detail::GetTranslatedText("exit_program", normalized_index);
if (utf8_text.empty()) {
cached_text = L"Exit";
return cached_text.c_str();
}
int wide_length =
MultiByteToWideChar(CP_UTF8, 0, utf8_text.c_str(), -1, nullptr, 0);
if (wide_length <= 0) {
cached_text = L"Exit";
return cached_text.c_str();
}
cached_text.resize(static_cast<size_t>(wide_length - 1));
MultiByteToWideChar(CP_UTF8, 0, utf8_text.c_str(), -1, cached_text.data(),
wide_length);
return cached_text.c_str();
}
#endif
} // namespace localization
} // namespace crossdesk
#endif
@@ -0,0 +1,247 @@
/*
* @Author: DI JUNKUN
* @Date: 2024-05-29
* Copyright (c) 2024 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _LOCALIZATION_DATA_H_
#define _LOCALIZATION_DATA_H_
namespace crossdesk {
namespace localization {
namespace detail {
struct TranslationRow {
const char* key;
const char* zh;
const char* en;
const char* ru;
};
// Single source of truth for all UI strings.
#define CROSSDESK_LOCALIZATION_ALL(X) \
X(local_desktop, u8"本桌面", "Local Desktop", u8"Локальный рабочий стол") \
X(local_id, u8"本机ID", "Local ID", u8"Локальный ID") \
X(local_id_copied_to_clipboard, u8"已复制到剪贴板", "Copied to clipboard", \
u8"Скопировано в буфер обмена") \
X(password, u8"密码", "Password", u8"Пароль") \
X(max_password_len, u8"最大6个字符", "Max 6 chars", u8"Макс. 6 символов") \
X(remote_desktop, u8"远程桌面", "Remote Desktop", \
u8"Удаленный рабочий стол") \
X(device_name, u8"设备名称", "Device Name", u8"Имя устройства") \
X(remote_id, u8"对端ID", "Remote ID", u8"Удаленный ID") \
X(connect, u8"连接", "Connect", u8"Подключиться") \
X(recent_connections, u8"近期连接", "Recent Connections", \
u8"Недавние подключения") \
X(disconnect, u8"断开连接", "Disconnect", u8"Отключить") \
X(select_display, u8"选择显示器", "Select Display", u8"Выбрать дисплей") \
X(expand_control_bar, u8"展开控制栏", "Expand Control Bar", \
u8"Развернуть панель управления") \
X(collapse_control_bar, u8"收起控制栏", "Collapse Control Bar", \
u8"Свернуть панель управления") \
X(fullscreen, u8"全屏", " Fullscreen", u8"Полный экран") \
X(show_net_traffic_stats, u8"显示网络状态", "Show Net Traffic Stats", \
u8"Показать статистику трафика") \
X(hide_net_traffic_stats, u8"隐藏网络状态", "Hide Net Traffic Stats", \
u8"Скрыть статистику трафика") \
X(video, u8"视频", "Video", u8"Видео") \
X(audio, u8"音频", "Audio", u8"Аудио") \
X(data, u8"数据", "Data", u8"Данные") \
X(total, u8"总计", "Total", u8"Итого") \
X(in, u8"输入", "In", u8"Вход") \
X(out, u8"输出", "Out", u8"Выход") \
X(loss_rate, u8"丢包率", "Loss Rate", u8"Потери пакетов") \
X(exit_fullscreen, u8"退出全屏", "Exit fullscreen", \
u8"Выйти из полноэкранного режима") \
X(control_mouse, u8"控制鼠标", "Control Mouse", u8"Управление мышью") \
X(release_mouse, u8"释放鼠标", "Release Mouse", u8"Освободить мышь") \
X(audio_capture, u8"播放声音", "Audio Capture", u8"Воспроизведение звука") \
X(mute, u8" 静音", " Mute", u8"Без звука") \
X(send_shortcut, u8"发送组合键", "Send Shortcut", u8"Сочетания клавиш") \
X(send_sas, u8"发送SAS", "Send SAS", u8"Отправить SAS") \
X(lock_remote, u8"锁定远端", "Lock Remote", u8"Заблокировать") \
X(remote_password_box_visible, u8"远端密码框已出现", \
"Remote password box visible", u8"Окно ввода пароля видно") \
X(remote_lock_screen_hint, u8"远端处于锁屏封面,可发送SAS", \
"Remote lock screen visible, send SAS", \
u8"Видна блокировка, отправьте SAS") \
X(remote_secure_desktop_active, u8"远端已进入安全桌面", \
"Remote secure desktop active", u8"Активен защищенный рабочий стол") \
X(remote_service_unavailable, u8"远端Windows服务不可用", \
"Remote Windows service unavailable", \
u8"Служба Windows на удаленной стороне недоступна") \
X(windows_service_setup_title, u8"安装 CrossDesk Service", \
"Install CrossDesk Service", u8"Установить CrossDesk Service") \
X(windows_service_setup_message, \
u8"为支持该设备在锁屏状态下被远程控制,需要以管理员权限安装 CrossDesk " \
u8"Service。\n未安装该服务不影响 CrossDesk " \
u8"正常使用,仅无法在锁屏状态下控制本机。", \
"To support remote control of this device while it is locked, CrossDesk " \
"Service must be installed with administrator permission.\nWithout this " \
"service, CrossDesk still works normally; only lock-screen control of " \
"this computer is unavailable.", \
u8"Чтобы поддерживать удаленное управление этим устройством на экране " \
u8"блокировки, необходимо установить CrossDesk Service с правами " \
u8"администратора.\nБез этой службы CrossDesk продолжит работать " \
u8"нормально; будет недоступно только управление этим компьютером на " \
u8"экране блокировки.") \
X(install_windows_service, u8"安装", "Install", u8"Установить") \
X(windows_service_settings_label, u8"锁屏控制服务:", \
"Lock Screen Service:", u8"Служба блокировки экрана:") \
X(windows_service_installed, u8"已安装", "Installed", u8"Установлена") \
X(do_not_remind_again, u8"不再提醒", "Do not remind again", \
u8"Больше не напоминать") \
X(windows_service_prompt_suppressed_message, \
u8"已不再提醒。后续如需启用锁屏状态下被远程控制,可在设置中点击“安装”。", \
"You will not be reminded again. To enable remote control while locked " \
"later, click Install in Settings.", \
u8"Напоминание отключено. Чтобы позже включить удаленное управление на " \
u8"экране блокировки, нажмите «Установить» в настройках.") \
X(installing_windows_service, u8"正在安装服务...", "Installing service...", \
u8"Установка службы...") \
X(windows_service_install_success, u8"服务已安装并启动", \
"Service installed and started", u8"Служба установлена и запущена") \
X(windows_service_install_failed, \
u8"服务安装失败。请确认 " \
u8"CrossDesk.exe、crossdesk_service.exe、crossdesk_session_helper.exe " \
u8"位于同一便携目录中,并在系统弹窗中允许管理员权限。", \
"Service installation failed. Make sure CrossDesk.exe, " \
"crossdesk_service.exe, and crossdesk_session_helper.exe are in the same " \
"portable folder, then approve the administrator prompt.", \
u8"Не удалось установить службу. Убедитесь, что CrossDesk.exe, " \
u8"crossdesk_service.exe и crossdesk_session_helper.exe находятся в " \
u8"одной папке портативной версии, затем подтвердите запрос прав " \
u8"администратора.") \
X(remote_unlock_requires_secure_desktop, \
u8"当前仍需要安全桌面专用采集/输入", \
"Secure desktop capture/input is still required", \
u8"По-прежнему нужен отдельный захват/ввод для защищенного рабочего " \
u8"стола") \
X(settings, u8"设置", "Settings", u8"Настройки") \
X(language, u8"语言:", "Language:", u8"Язык:") \
X(video_quality, u8"视频质量:", "Video Quality:", u8"Качество видео:") \
X(video_frame_rate, u8"画面采集帧率:", \
"Video Capture Frame Rate:", u8"Частота захвата видео:") \
X(video_quality_high, u8"", "High", u8"Высокое") \
X(video_quality_medium, u8"", "Medium", u8"Среднее") \
X(video_quality_low, u8"", "Low", u8"Низкое") \
X(video_encode_format, u8"视频编码格式:", \
"Video Encode Format:", u8"Формат кодека видео:") \
X(av1, u8"AV1", "AV1", "AV1") \
X(h264, u8"H.264", "H.264", "H.264") \
X(enable_hardware_video_codec, u8"启用硬件编解码器:", \
"Enable Hardware Video Codec:", u8"Использовать аппаратный кодек:") \
X(enable_turn, u8"启用中继服务:", \
"Enable TURN Service:", u8"Включить TURN-сервис:") \
X(enable_srtp, u8"启用SRTP:", "Enable SRTP:", u8"Включить SRTP:") \
X(self_hosted_server_config, u8"自托管配置", "Self-Hosted Config", \
u8"Конфигурация self-hosted") \
X(self_hosted_server_settings, u8"自托管设置", "Self-Hosted Settings", \
u8"Настройки self-hosted") \
X(self_hosted_server_address, u8"服务器地址:", \
"Server Address:", u8"Адрес сервера:") \
X(self_hosted_server_port, u8"信令服务端口:", \
"Signal Service Port:", u8"Порт сигнального сервиса:") \
X(self_hosted_server_coturn_server_port, u8"中继服务端口:", \
"Relay Service Port:", u8"Порт реле-сервиса:") \
X(ok, u8"确认", "OK", u8"ОК") \
X(cancel, u8"取消", "Cancel", u8"Отмена") \
X(new_password, u8"请输入六位密码:", \
"Please input a six-char password:", u8"Введите шестизначный пароль:") \
X(input_password, u8"请输入密码:", \
"Please input password:", u8"Введите пароль:") \
X(validate_password, u8"验证密码中...", "Validate password ...", \
u8"Проверка пароля...") \
X(reinput_password, u8"请重新输入密码", "Please input password again", \
u8"Повторно введите пароль") \
X(remember_password, u8"记住密码", "Remember password", \
u8"Запомнить пароль") \
X(signal_connected, u8"已连接服务器", "Connected", u8"Подключено к серверу") \
X(signal_disconnected, u8"未连接服务器", "Disconnected", \
u8"Нет подключения к серверу") \
X(signal_tls_cert_error, u8"证书验证失败,请重新安装自托管根证书", \
"Certificate verification failed. Reinstall the self-hosted root " \
"certificate.", \
u8"Ошибка проверки сертификата. Переустановите корневой сертификат.") \
X(p2p_connected, u8"对等连接已建立", "P2P Connected", u8"P2P подключено") \
X(p2p_disconnected, u8"对等连接已断开", "P2P Disconnected", \
u8"P2P отключено") \
X(p2p_connecting, u8"正在建立对等连接...", "P2P Connecting ...", \
u8"Подключение P2P...") \
X(p2p_gathering, u8"正在收集候选地址...", "Gathering candidates ...", \
u8"Сбор кандидатов...") \
X(receiving_screen, u8"画面接收中...", "Receiving screen...", \
u8"Получение изображения...") \
X(p2p_failed, u8"对等连接失败", "P2P Failed", u8"Сбой P2P") \
X(p2p_closed, u8"对等连接已关闭", "P2P closed", u8"P2P закрыто") \
X(no_such_id, u8"无此ID", "No such ID", u8"ID не найден") \
X(about, u8"关于", "About", u8"О программе") \
X(notification, u8"通知", "Notification", u8"Уведомление") \
X(new_version_available, u8"新版本可用", "New Version Available", \
u8"Доступна новая версия") \
X(version, u8"版本", "Version", u8"Версия") \
X(release_date, u8"发布日期: ", "Release Date: ", u8"Дата релиза: ") \
X(access_website, u8"访问官网: ", \
"Access Website: ", u8"Официальный сайт: ") \
X(update, u8"更新", "Update", u8"Обновить") \
X(connection_alias, u8"修改名称", "Edit Alias", \
u8"Изменить имя подключения") \
X(delete_connection, u8"删除连接", "Delete Connection", \
u8"Удалить подключение") \
X(connect_to_this_connection, u8"发起连接", "Connect to this connection", \
u8"Подключиться") \
X(input_connection_alias, u8"请输入连接名称:", \
"Please input connection name:", u8"Введите имя подключения:") \
X(confirm_delete_connection, u8"确认删除此连接", \
"Confirm to delete this connection", u8"Удалить это подключение?") \
X(enable_autostart, u8"开机自启:", "Auto Start:", u8"Автозапуск:") \
X(enable_daemon, u8"启用守护进程:", "Enable Daemon:", u8"Включить демон:") \
X(takes_effect_after_restart, u8"重启后生效", "Takes effect after restart", \
u8"Вступит в силу после перезапуска") \
X(select_file, u8"选择文件发送", "Select File to Send", \
u8"Выбрать файл для отправки") \
X(file_transfer_progress, u8"文件传输进度", "File Transfer Progress", \
u8"Прогресс передачи файлов") \
X(queued, u8"队列中", "Queued", u8"В очереди") \
X(sending, u8"正在传输", "Sending", u8"Передача") \
X(completed, u8"已完成", "Completed", u8"Завершено") \
X(failed, u8"失败", "Failed", u8"Ошибка") \
X(controller, u8"控制端:", "Controller:", u8"Контроллер:") \
X(file_transfer, u8"文件传输:", "File Transfer:", u8"Передача файлов:") \
X(connection_status, u8"连接状态:", \
"Connection Status:", u8"Состояние соединения:") \
X(file_transfer_save_path, u8"文件接收保存路径:", \
"File Transfer Save Path:", u8"Путь сохранения файлов:") \
X(default_desktop, u8"桌面", "Desktop", u8"Рабочий стол") \
X(minimize_to_tray, u8"退出时最小化到系统托盘:", \
"Minimize on Exit:", u8"Сворачивать в трей при выходе:") \
X(resolution, u8"分辨率", "Res", u8"Разрешение") \
X(connection_mode, u8"连接模式", "Mode", u8"Режим") \
X(connection_mode_direct, u8"直连", "Direct", u8"Прямой") \
X(connection_mode_relay, u8"中继", "Relay", u8"Релейный") \
X(online, u8"在线", "Online", u8"Онлайн") \
X(offline, u8"离线", "Offline", u8"Офлайн") \
X(device_offline, u8"设备离线", "Device Offline", u8"Устройство офлайн") \
X(request_permissions, u8"权限请求", "Request Permissions", \
u8"Запрос разрешений") \
X(screen_recording_permission, u8"屏幕录制权限", \
"Screen Recording Permission", u8"Разрешение на запись экрана") \
X(accessibility_permission, u8"辅助功能权限", "Accessibility Permission", \
u8"Разрешение специальных возможностей") \
X(permission_required_message, u8"该应用需要授权以下权限:", \
"The application requires the following permissions:", \
u8"Для работы приложения требуются следующие разрешения:") \
X(exit_program, u8"退出", "Exit", u8"Выход")
inline constexpr TranslationRow kTranslationRows[] = {
#define CROSSDESK_DECLARE_TRANSLATION_ROW(name, zh, en, ru) {#name, zh, en, ru},
CROSSDESK_LOCALIZATION_ALL(CROSSDESK_DECLARE_TRANSLATION_ROW)
#undef CROSSDESK_DECLARE_TRANSLATION_ROW
};
} // namespace detail
} // namespace localization
} // namespace crossdesk
#endif
@@ -0,0 +1,177 @@
#include "features/clipboard/clipboard_controller.h"
#include <cstring>
#include <shared_mutex>
#include <utility>
#include "rd_log.h"
#include "runtime/gui_runtime.h"
namespace crossdesk {
namespace {
// Keep a reliable clipboard message within MiniRTC/KCP's single-message
// fragmentation window (MTU is configured to 1200 bytes).
constexpr size_t kMaxClipboardTextBytes = 128 * 1024;
} // namespace
ClipboardController::ClipboardController(GuiRuntime &owner) : owner_(owner) {}
void ClipboardController::SetEventType(uint32_t event_type) {
event_type_ = event_type;
}
uint32_t ClipboardController::event_type() const { return event_type_; }
void ClipboardController::Initialize() {
last_text_.clear();
char *clipboard_text = SDL_GetClipboardText();
if (clipboard_text) {
last_text_.assign(clipboard_text);
SDL_free(clipboard_text);
}
if (event_type_ == 0) {
LOG_ERROR("Clipboard synchronization disabled: SDL event is unavailable");
return;
}
events_enabled_.store(true, std::memory_order_release);
}
void ClipboardController::Shutdown() {
events_enabled_.store(false, std::memory_order_release);
std::lock_guard<std::mutex> lock(pending_mutex_);
pending_remote_text_.reset();
}
void ClipboardController::QueueRemoteText(const char *data, size_t size) {
if (!events_enabled_.load(std::memory_order_acquire)) {
return;
}
if (!data || size == 0) {
return;
}
if (size > kMaxClipboardTextBytes) {
LOG_WARN("Ignore oversized remote clipboard text: {} bytes", size);
return;
}
if (std::memchr(data, '\0', size) != nullptr) {
LOG_WARN("Ignore remote clipboard text containing an embedded NUL byte");
return;
}
{
std::lock_guard<std::mutex> lock(pending_mutex_);
if (!events_enabled_.load(std::memory_order_relaxed)) {
return;
}
// Only the newest clipboard value matters. Replacing it also bounds the
// amount of memory a busy or malicious peer can queue.
pending_remote_text_ = std::string(data, size);
}
SDL_Event event{};
event.type = event_type_;
if (!SDL_PushEvent(&event)) {
// MainLoop also drains the pending value after its wait timeout.
LOG_WARN("Failed to wake SDL loop for remote clipboard text: {}",
SDL_GetError());
}
}
void ClipboardController::ApplyPendingRemoteText() {
if (!events_enabled_.load(std::memory_order_acquire)) {
return;
}
std::optional<std::string> pending_text;
{
std::lock_guard<std::mutex> lock(pending_mutex_);
pending_text.swap(pending_remote_text_);
}
if (!pending_text || *pending_text == last_text_) {
return;
}
// SDL clipboard functions must run on the thread that initialized SDL.
if (!SDL_SetClipboardText(pending_text->c_str())) {
LOG_ERROR("Failed to set remote clipboard text: {}", SDL_GetError());
return;
}
// SDL will normally emit SDL_EVENT_CLIPBOARD_UPDATE for this write. Update
// the baseline first so that event cannot echo the text back to the sender.
last_text_ = std::move(*pending_text);
}
void ClipboardController::HandleLocalUpdate() {
if (!events_enabled_.load(std::memory_order_acquire)) {
return;
}
if (!SDL_HasClipboardText()) {
last_text_.clear();
return;
}
char *clipboard_text = SDL_GetClipboardText();
if (!clipboard_text) {
LOG_WARN("Failed to read local clipboard text: {}", SDL_GetError());
return;
}
std::string text(clipboard_text);
SDL_free(clipboard_text);
if (text == last_text_) {
return;
}
// Record the value before sending. Duplicate SDL notifications and a
// remote write of the same value must not create a clipboard feedback loop.
last_text_ = text;
if (text.empty()) {
return;
}
if (text.size() > kMaxClipboardTextBytes) {
LOG_WARN("Ignore oversized local clipboard text: {} bytes", text.size());
return;
}
SendToPeers(text);
}
int ClipboardController::SendToPeers(const std::string &text) {
std::shared_lock lock(owner_.remote_sessions_mutex_);
for (const auto &[remote_id, props] : owner_.remote_sessions_) {
if (!props || !props->peer_ || !props->connection_established_ ||
!props->enable_mouse_control_) {
continue;
}
const int ret =
SendReliableDataFrame(props->peer_, text.data(), text.size(),
props->clipboard_label_.c_str());
if (ret != 0) {
LOG_WARN("Failed to send clipboard data to peer [{}], ret={}",
remote_id.c_str(), ret);
return ret;
}
}
if (owner_.peer_) {
const int ret =
SendReliableDataFrame(owner_.peer_, text.data(), text.size(),
owner_.clipboard_label_.c_str());
if (ret != 0) {
LOG_WARN("Failed to send clipboard data to peer [{}], ret={}",
owner_.remote_id_display_, ret);
return ret;
}
}
return 0;
}
} // namespace crossdesk
@@ -0,0 +1,45 @@
#ifndef CROSSDESK_GUI_CLIPBOARD_CONTROLLER_H_
#define CROSSDESK_GUI_CLIPBOARD_CONTROLLER_H_
#include <SDL3/SDL.h>
#include <atomic>
#include <cstddef>
#include <cstdint>
#include <mutex>
#include <optional>
#include <string>
namespace crossdesk {
class GuiRuntime;
// Owns clipboard synchronization state and keeps feedback-loop prevention out
// of the main UI coordinator.
class ClipboardController {
public:
explicit ClipboardController(GuiRuntime &owner);
void SetEventType(uint32_t event_type);
uint32_t event_type() const;
void Initialize();
void Shutdown();
void QueueRemoteText(const char *data, size_t size);
void ApplyPendingRemoteText();
void HandleLocalUpdate();
private:
int SendToPeers(const std::string &text);
GuiRuntime &owner_;
uint32_t event_type_ = 0;
std::atomic<bool> events_enabled_{false};
std::mutex pending_mutex_;
std::optional<std::string> pending_remote_text_;
std::string last_text_;
};
} // namespace crossdesk
#endif // CROSSDESK_GUI_CLIPBOARD_CONTROLLER_H_
@@ -0,0 +1,484 @@
#include "features/devices/session_device_manager.h"
#include <chrono>
#include <cstdlib>
#include "platform.h"
#include "rd_log.h"
#include "runtime/gui_runtime.h"
namespace crossdesk {
namespace {
constexpr uint64_t kCaptureResumeKeyFrameGapMs = 500;
} // namespace
SessionDeviceManager::SessionDeviceManager(GuiRuntime &owner) : owner_(owner) {}
void SessionDeviceManager::Initialize() {
InitializeAudioOutput();
screen_capturer_factory_ = new ScreenCapturerFactory();
speaker_capturer_factory_ = new SpeakerCapturerFactory();
device_controller_factory_ = new DeviceControllerFactory();
keyboard_capturer_ =
static_cast<KeyboardCapturer *>(device_controller_factory_->Create(
DeviceControllerFactory::Device::Keyboard));
}
int SessionDeviceManager::InitializeScreenCapturer() {
#ifdef __APPLE__
if (!owner_.EnsureMacScreenRecordingPermission()) {
return -1;
}
#endif
if (!screen_capturer_) {
screen_capturer_ =
static_cast<ScreenCapturer *>(screen_capturer_factory_->Create());
}
last_frame_time_ = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch())
.count();
const int fps = owner_.config_center_->GetVideoFrameRate() ==
ConfigCenter::VIDEO_FRAME_RATE::FPS_30
? 30
: 60;
LOG_INFO("Init screen capturer with {} fps", fps);
const int init_ret = screen_capturer_->Init(
fps, [this, fps](unsigned char *data, int size, int width, int height,
const char *display_name) {
const auto now_time = static_cast<uint64_t>(
std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch())
.count());
const auto duration = now_time - last_frame_time_;
if (duration * fps < 1000) {
return;
}
const std::string stream_id = display_name ? display_name : "";
const bool resumed_after_gap =
last_frame_time_ != 0 && duration >= kCaptureResumeKeyFrameGapMs;
const bool stream_changed = !last_video_frame_stream_id_.empty() &&
last_video_frame_stream_id_ != stream_id;
if (resumed_after_gap || stream_changed) {
if (RequestVideoKeyFrame(owner_.peer_, stream_id.c_str()) == 0) {
LOG_INFO("Request video key frame before sending captured frame, "
"stream='{}', gap_ms={}, stream_changed={}",
stream_id, duration, stream_changed);
}
}
XVideoFrame frame{};
frame.data = reinterpret_cast<const char *>(data);
frame.size = size;
frame.width = width;
frame.height = height;
frame.captured_timestamp = GetSystemTimeMicros(owner_.peer_);
SendVideoFrame(owner_.peer_, &frame, stream_id.c_str());
last_video_frame_stream_id_ = stream_id;
last_frame_time_ = now_time;
});
if (init_ret == 0) {
LOG_INFO("Init screen capturer success");
const auto latest_display_info = screen_capturer_->GetDisplayInfoList();
if (!latest_display_info.empty()) {
display_info_list_ = latest_display_info;
}
return 0;
}
LOG_ERROR("Init screen capturer failed");
screen_capturer_->Destroy();
delete screen_capturer_;
screen_capturer_ = nullptr;
return -1;
}
int SessionDeviceManager::StartScreenCapturer() {
#ifdef __APPLE__
if (!owner_.EnsureMacScreenRecordingPermission()) {
return -1;
}
#endif
if (!screen_capturer_) {
LOG_INFO("Screen capturer instance missing, recreating before start");
if (InitializeScreenCapturer() != 0) {
LOG_ERROR("Recreate screen capturer failed");
return -1;
}
}
LOG_INFO("Start screen capturer, show cursor: {}", owner_.show_cursor_);
const int ret = screen_capturer_->Start(owner_.show_cursor_);
if (ret != 0) {
LOG_ERROR("Start screen capturer failed: {}", ret);
}
return ret;
}
int SessionDeviceManager::StopScreenCapturer() {
if (screen_capturer_) {
LOG_INFO("Stop screen capturer");
screen_capturer_->Stop();
}
return 0;
}
int SessionDeviceManager::StartSpeakerCapturer() {
if (!speaker_capturer_) {
speaker_capturer_ =
static_cast<SpeakerCapturer *>(speaker_capturer_factory_->Create());
const int init_ret = speaker_capturer_->Init(
[this](unsigned char *data, size_t size, const char *audio_name) {
SendAudioFrame(owner_.peer_, reinterpret_cast<const char *>(data),
size, owner_.audio_label_.c_str());
});
if (init_ret != 0) {
speaker_capturer_->Destroy();
delete speaker_capturer_;
speaker_capturer_ = nullptr;
}
}
if (!speaker_capturer_) {
return -1;
}
const int ret = speaker_capturer_->Start();
if (ret != 0) {
LOG_ERROR("Start speaker capturer failed: {}", ret);
return ret;
}
owner_.start_speaker_capturer_ = true;
return 0;
}
int SessionDeviceManager::StopSpeakerCapturer() {
if (speaker_capturer_) {
speaker_capturer_->Stop();
owner_.start_speaker_capturer_ = false;
}
return 0;
}
int SessionDeviceManager::StartMouseController() {
#ifdef __APPLE__
if (!owner_.EnsureMacAccessibilityPermission()) {
return -1;
}
#endif
if (!device_controller_factory_) {
LOG_INFO("Device controller factory is nullptr");
return -1;
}
#if defined(__linux__) && !defined(__APPLE__)
if (IsWaylandSession()) {
if (!screen_capturer_) {
return 1;
}
const auto latest_display_info = screen_capturer_->GetDisplayInfoList();
if (latest_display_info.empty() ||
latest_display_info[0].handle == nullptr) {
return 1;
}
}
if (screen_capturer_) {
const auto latest_display_info = screen_capturer_->GetDisplayInfoList();
if (!latest_display_info.empty()) {
display_info_list_ = latest_display_info;
}
}
#endif
mouse_controller_ =
static_cast<MouseController *>(device_controller_factory_->Create(
DeviceControllerFactory::Device::Mouse));
if (!mouse_controller_) {
LOG_ERROR("Create mouse controller failed");
return -1;
}
const int init_ret = mouse_controller_->Init(display_info_list_);
if (init_ret != 0) {
LOG_INFO("Destroy mouse controller");
mouse_controller_->Destroy();
delete mouse_controller_;
mouse_controller_ = nullptr;
}
return init_ret;
}
int SessionDeviceManager::StopMouseController() {
if (mouse_controller_) {
mouse_controller_->Destroy();
delete mouse_controller_;
mouse_controller_ = nullptr;
}
return 0;
}
int SessionDeviceManager::StartKeyboardCapturer() {
owner_.keyboard_capturer_uses_sdl_events_ = false;
#ifdef __APPLE__
if (!owner_.EnsureMacAccessibilityPermission()) {
owner_.keyboard_capturer_uses_sdl_events_ = true;
return 0;
}
#endif
#if defined(__linux__) && !defined(__APPLE__)
if (IsWaylandSession()) {
owner_.keyboard_capturer_uses_sdl_events_ = true;
LOG_INFO("Start keyboard capturer with SDL Wayland backend");
return 0;
}
#endif
if (!keyboard_capturer_) {
owner_.keyboard_capturer_uses_sdl_events_ = true;
LOG_WARN(
"keyboard capturer is nullptr, falling back to SDL keyboard events");
return 0;
}
const int hook_ret = keyboard_capturer_->Hook(
[](int key_code, bool is_down, uint32_t scan_code, bool extended,
void *user_ptr) {
if (user_ptr) {
auto *runtime = static_cast<GuiRuntime *>(user_ptr);
runtime->keyboard_.SendKeyCommand(key_code, is_down, scan_code,
extended);
}
},
&owner_);
if (hook_ret != 0) {
owner_.keyboard_capturer_uses_sdl_events_ = true;
LOG_WARN(
"Start keyboard capturer failed, falling back to SDL keyboard events");
} else {
LOG_INFO("Start keyboard capturer with native hook");
}
return 0;
}
int SessionDeviceManager::StopKeyboardCapturer() {
if (owner_.keyboard_capturer_uses_sdl_events_) {
owner_.keyboard_capturer_uses_sdl_events_ = false;
LOG_INFO("Stop keyboard capturer with SDL keyboard backend");
return 0;
}
if (keyboard_capturer_) {
keyboard_capturer_->Unhook();
LOG_INFO("Stop keyboard capturer");
}
return 0;
}
int SessionDeviceManager::InitializeAudioOutput() {
SDL_AudioSpec desired_out{};
desired_out.freq = 48000;
desired_out.format = SDL_AUDIO_S16;
desired_out.channels = 1;
auto open_stream = [&]() {
output_stream_ = SDL_OpenAudioDeviceStream(
SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &desired_out, nullptr, nullptr);
return output_stream_ != nullptr;
};
if (!open_stream()) {
#if defined(__linux__) && !defined(__APPLE__)
LOG_WARN("Failed to open output stream with driver [{}]: {}",
getenv("SDL_AUDIODRIVER") ? getenv("SDL_AUDIODRIVER")
: "(default)",
SDL_GetError());
setenv("SDL_AUDIODRIVER", "dummy", 1);
SDL_QuitSubSystem(SDL_INIT_AUDIO);
if (!SDL_InitSubSystem(SDL_INIT_AUDIO)) {
LOG_ERROR("Failed to reinitialize SDL audio with dummy driver: {}",
SDL_GetError());
return -1;
}
if (!open_stream()) {
LOG_ERROR("Failed to open output stream with dummy driver: {}",
SDL_GetError());
return -1;
}
LOG_WARN("Audio output disabled, using SDL dummy audio driver");
#else
LOG_ERROR("Failed to open output stream: {}", SDL_GetError());
return -1;
#endif
}
SDL_ResumeAudioDevice(SDL_GetAudioStreamDevice(output_stream_));
return 0;
}
int SessionDeviceManager::DestroyAudioOutput() {
if (output_stream_) {
SDL_CloseAudioDevice(SDL_GetAudioStreamDevice(output_stream_));
SDL_DestroyAudioStream(output_stream_);
output_stream_ = nullptr;
}
return 0;
}
void SessionDeviceManager::PushAudio(const char *data, size_t size) {
if (!output_stream_) {
return;
}
const int pushed = SDL_PutAudioStreamData(
output_stream_, reinterpret_cast<const Uint8 *>(data),
static_cast<int>(size));
if (pushed < 0) {
LOG_ERROR("Failed to push audio data: {}", SDL_GetError());
}
}
void SessionDeviceManager::UpdateInteractions() {
#if defined(__linux__) && !defined(__APPLE__)
const bool is_wayland_session = IsWaylandSession();
const bool stop_wayland_mouse_before_screen =
is_wayland_session && !owner_.start_screen_capturer_ &&
owner_.screen_capturer_is_started_ && !owner_.start_mouse_controller_ &&
owner_.mouse_controller_is_started_;
if (stop_wayland_mouse_before_screen) {
LOG_INFO("Stopping Wayland mouse controller before screen capturer to "
"cleanly release the shared portal session");
StopMouseController();
owner_.mouse_controller_is_started_ = false;
}
#endif
if (owner_.start_screen_capturer_ && !owner_.screen_capturer_is_started_) {
if (StartScreenCapturer() == 0) {
owner_.screen_capturer_is_started_ = true;
}
} else if (!owner_.start_screen_capturer_ &&
owner_.screen_capturer_is_started_) {
StopScreenCapturer();
owner_.screen_capturer_is_started_ = false;
}
if (owner_.start_speaker_capturer_ && !owner_.speaker_capturer_is_started_) {
if (StartSpeakerCapturer() == 0) {
owner_.speaker_capturer_is_started_ = true;
}
} else if (!owner_.start_speaker_capturer_ &&
owner_.speaker_capturer_is_started_) {
StopSpeakerCapturer();
owner_.speaker_capturer_is_started_ = false;
}
if (owner_.start_mouse_controller_ && !owner_.mouse_controller_is_started_) {
if (StartMouseController() == 0) {
owner_.mouse_controller_is_started_ = true;
}
} else if (!owner_.start_mouse_controller_ &&
owner_.mouse_controller_is_started_) {
StopMouseController();
owner_.mouse_controller_is_started_ = false;
}
#if defined(__linux__) && !defined(__APPLE__)
if (owner_.screen_capturer_is_started_ && screen_capturer_ &&
mouse_controller_) {
const auto latest_display_info = screen_capturer_->GetDisplayInfoList();
if (!latest_display_info.empty()) {
display_info_list_ = latest_display_info;
mouse_controller_->UpdateDisplayInfoList(display_info_list_);
}
}
#endif
if (owner_.start_keyboard_capturer_ && owner_.focus_on_stream_window_) {
if (!owner_.keyboard_capturer_is_started_ && StartKeyboardCapturer() == 0) {
owner_.keyboard_capturer_is_started_ = true;
}
if (owner_.keyboard_capturer_is_started_) {
owner_.keyboard_.SendHeartbeat(false);
}
} else if (owner_.keyboard_capturer_is_started_) {
owner_.keyboard_.ForceReleasePressedKeys();
StopKeyboardCapturer();
owner_.keyboard_capturer_is_started_ = false;
}
owner_.keyboard_.CheckRemoteTimeouts();
}
bool SessionDeviceManager::SendKeyboardCommand(int key_code, bool is_down,
uint32_t scan_code,
bool extended) {
return keyboard_capturer_ && keyboard_capturer_->SendKeyboardCommand(
key_code, is_down, scan_code, extended) == 0;
}
void SessionDeviceManager::SendMouseCommand(const RemoteAction &action,
int selected_display) {
if (mouse_controller_) {
mouse_controller_->SendMouseCommand(action, selected_display);
}
}
int SessionDeviceManager::SwitchDisplay(int display_id) {
return screen_capturer_ ? screen_capturer_->SwitchTo(display_id) : -1;
}
void SessionDeviceManager::ResetToInitialDisplay() {
if (screen_capturer_) {
screen_capturer_->ResetToInitialMonitor();
}
}
const std::vector<DisplayInfo> &
SessionDeviceManager::display_info_list() const {
return display_info_list_;
}
void SessionDeviceManager::DestroyDevices() {
if (mouse_controller_) {
mouse_controller_->Destroy();
delete mouse_controller_;
mouse_controller_ = nullptr;
}
if (screen_capturer_) {
screen_capturer_->Destroy();
delete screen_capturer_;
screen_capturer_ = nullptr;
}
if (speaker_capturer_) {
speaker_capturer_->Destroy();
delete speaker_capturer_;
speaker_capturer_ = nullptr;
}
if (keyboard_capturer_) {
delete keyboard_capturer_;
keyboard_capturer_ = nullptr;
}
}
void SessionDeviceManager::DestroyFactories() {
delete screen_capturer_factory_;
screen_capturer_factory_ = nullptr;
delete speaker_capturer_factory_;
speaker_capturer_factory_ = nullptr;
delete device_controller_factory_;
device_controller_factory_ = nullptr;
}
} // namespace crossdesk
@@ -0,0 +1,72 @@
#ifndef CROSSDESK_GUI_SESSION_DEVICE_MANAGER_H_
#define CROSSDESK_GUI_SESSION_DEVICE_MANAGER_H_
#include <SDL3/SDL.h>
#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>
#include "device_controller.h"
#include "device_controller_factory.h"
#include "display_info.h"
#include "screen_capturer_factory.h"
#include "speaker_capturer_factory.h"
namespace crossdesk {
class GuiRuntime;
// Owns media capture/playback and remote input devices for the active GUI
// session. GuiRuntime supplies application intent; this class handles device
// creation, updates and teardown.
class SessionDeviceManager {
public:
explicit SessionDeviceManager(GuiRuntime &owner);
void Initialize();
void UpdateInteractions();
void DestroyDevices();
void DestroyFactories();
int InitializeScreenCapturer();
int StartScreenCapturer();
int StopScreenCapturer();
int StartSpeakerCapturer();
int StopSpeakerCapturer();
int StartMouseController();
int StopMouseController();
int StartKeyboardCapturer();
int StopKeyboardCapturer();
int InitializeAudioOutput();
int DestroyAudioOutput();
void PushAudio(const char *data, size_t size);
bool SendKeyboardCommand(int key_code, bool is_down, uint32_t scan_code,
bool extended);
void SendMouseCommand(const RemoteAction &action, int selected_display);
int SwitchDisplay(int display_id);
void ResetToInitialDisplay();
const std::vector<DisplayInfo> &display_info_list() const;
private:
GuiRuntime &owner_;
SDL_AudioStream *output_stream_ = nullptr;
ScreenCapturerFactory *screen_capturer_factory_ = nullptr;
ScreenCapturer *screen_capturer_ = nullptr;
SpeakerCapturerFactory *speaker_capturer_factory_ = nullptr;
SpeakerCapturer *speaker_capturer_ = nullptr;
DeviceControllerFactory *device_controller_factory_ = nullptr;
MouseController *mouse_controller_ = nullptr;
KeyboardCapturer *keyboard_capturer_ = nullptr;
std::vector<DisplayInfo> display_info_list_;
uint64_t last_frame_time_ = 0;
std::string last_video_frame_stream_id_;
};
} // namespace crossdesk
#endif // CROSSDESK_GUI_SESSION_DEVICE_MANAGER_H_
@@ -0,0 +1,379 @@
#include "features/file_transfer/file_transfer_manager.h"
#include <algorithm>
#include <chrono>
#include <cstring>
#include <filesystem>
#include <limits>
#include <memory>
#include <mutex>
#include <shared_mutex>
#include <string>
#include <thread>
#include <utility>
#include "file_transfer.h"
#include "rd_log.h"
#include "runtime/gui_runtime.h"
namespace crossdesk {
FileTransferManager::FileTransferManager(GuiRuntime &owner) : owner_(owner) {}
FileTransferManager::FileTransferState &FileTransferManager::global_state() {
return global_state_;
}
FileTransferManager::FileTransferState &FileTransferManager::state_for(
const std::shared_ptr<RemoteSession> &props) {
return props ? props->file_transfer_ : global_state_;
}
void FileTransferManager::ProcessSelectedFile(
const std::string &path,
const std::shared_ptr<RemoteSession> &props,
const std::string &file_label, const std::string &remote_id) {
if (path.empty()) {
return;
}
FileTransferState &state = state_for(props);
LOG_INFO("Selected file: {}", path.c_str());
const std::filesystem::path file_path = std::filesystem::u8path(path);
std::error_code ec;
if (!std::filesystem::is_regular_file(file_path, ec)) {
LOG_ERROR("Selected path is not a regular file: {}", path);
return;
}
const uint64_t file_size = std::filesystem::file_size(file_path, ec);
if (ec) {
LOG_ERROR("Failed to get file size: {}", ec.message());
return;
}
{
std::lock_guard<std::mutex> lock(state.file_transfer_list_mutex_);
FileTransferState::FileTransferInfo info;
info.file_name = file_path.filename().u8string();
info.file_path = file_path;
info.file_size = file_size;
info.status = FileTransferState::FileTransferStatus::Queued;
state.file_transfer_list_.push_back(std::move(info));
}
state.file_transfer_window_visible_ = true;
auto enqueue = [&]() {
size_t queue_size = 0;
{
std::lock_guard<std::mutex> lock(state.file_queue_mutex_);
state.file_send_queue_.push(
FileTransferState::QueuedFile{file_path, file_label, remote_id});
queue_size = state.file_send_queue_.size();
}
LOG_INFO("File added to queue: {} ({} files in queue)",
file_path.filename().string().c_str(), queue_size);
};
if (state.file_sending_.load()) {
enqueue();
return;
}
Start(props, file_path, file_label, remote_id);
if (!state.file_sending_.load()) {
enqueue();
}
}
void FileTransferManager::Start(
std::shared_ptr<RemoteSession> props,
const std::filesystem::path &file_path, const std::string &file_label,
const std::string &remote_id) {
const bool is_global = !props;
PeerPtr *peer = is_global ? owner_.peer_ : props->peer_;
if (!peer) {
LOG_ERROR("StartFileTransfer: invalid peer");
return;
}
FileTransferState &initial_state = state_for(props);
bool expected = false;
if (!initial_state.file_sending_.compare_exchange_strong(expected, true)) {
LOG_WARN("StartFileTransfer called while another file is active: {}",
file_path.filename().string().c_str());
return;
}
const auto props_weak = std::weak_ptr<RemoteSession>(props);
std::thread([this, peer, file_path, file_label, props_weak, remote_id,
is_global]() {
auto props_locked = props_weak.lock();
FileTransferState *state = nullptr;
if (props_locked) {
state = &props_locked->file_transfer_;
} else if (is_global) {
state = &global_state_;
} else {
return;
}
std::error_code ec;
const uint64_t total_size = std::filesystem::file_size(file_path, ec);
if (ec) {
LOG_ERROR("Failed to get file size: {}", ec.message().c_str());
state->file_sending_ = false;
return;
}
state->file_sent_bytes_ = 0;
state->file_total_bytes_ = total_size;
state->file_send_rate_bps_ = 0;
state->file_transfer_window_visible_ = true;
{
std::lock_guard<std::mutex> lock(state->file_transfer_mutex_);
state->file_send_start_time_ = std::chrono::steady_clock::now();
state->file_send_last_update_time_ = state->file_send_start_time_;
state->file_send_last_bytes_ = 0;
}
FileSender sender;
const uint32_t file_id = FileSender::NextFileId();
if (props_locked) {
std::lock_guard<std::shared_mutex> lock(file_id_to_props_mutex_);
file_id_to_props_[file_id] = props_weak;
} else {
std::lock_guard<std::shared_mutex> lock(file_id_to_state_mutex_);
file_id_to_state_[file_id] = state;
}
state->current_file_id_ = file_id;
{
std::lock_guard<std::mutex> lock(state->file_transfer_list_mutex_);
for (auto &info : state->file_transfer_list_) {
if (info.file_path == file_path &&
info.status == FileTransferState::FileTransferStatus::Queued) {
info.status = FileTransferState::FileTransferStatus::Sending;
info.file_id = file_id;
info.file_size = total_size;
info.sent_bytes = 0;
break;
}
}
}
const int ret = sender.SendFile(
file_path, file_path.filename().string(),
[peer, file_label, remote_id](const char *buffer, size_t size) {
if (remote_id.empty()) {
return SendReliableDataFrame(peer, buffer, size,
file_label.c_str());
}
return SendReliableDataFrameToPeer(
peer, buffer, size, file_label.c_str(), remote_id.c_str(),
remote_id.size());
},
64 * 1024, file_id);
if (ret == 0) {
return;
}
state->file_sending_ = false;
state->file_transfer_window_visible_ = false;
state->file_sent_bytes_ = 0;
state->file_total_bytes_ = 0;
state->file_send_rate_bps_ = 0;
state->current_file_id_ = 0;
Unregister(file_id, props_locked != nullptr);
{
std::lock_guard<std::mutex> lock(state->file_transfer_list_mutex_);
for (auto &info : state->file_transfer_list_) {
if (info.file_id == file_id) {
info.status = FileTransferState::FileTransferStatus::Failed;
break;
}
}
}
LOG_ERROR("FileSender::SendFile failed for [{}], ret={}",
file_path.string().c_str(), ret);
ProcessQueue(props_locked);
}).detach();
}
void FileTransferManager::ProcessQueue(
std::shared_ptr<RemoteSession> props) {
FileTransferState &state = state_for(props);
if (state.file_sending_.load()) {
return;
}
FileTransferState::QueuedFile queued_file;
{
std::lock_guard<std::mutex> lock(state.file_queue_mutex_);
if (state.file_send_queue_.empty()) {
return;
}
queued_file = state.file_send_queue_.front();
state.file_send_queue_.pop();
}
Start(props, queued_file.file_path, queued_file.file_label,
queued_file.remote_id);
}
void FileTransferManager::Unregister(uint32_t file_id, bool per_peer) {
if (per_peer) {
std::lock_guard<std::shared_mutex> lock(file_id_to_props_mutex_);
file_id_to_props_.erase(file_id);
} else {
std::lock_guard<std::shared_mutex> lock(file_id_to_state_mutex_);
file_id_to_state_.erase(file_id);
}
}
void FileTransferManager::HandleAck(const char *data, size_t size) {
if (size < sizeof(FileTransferAck)) {
LOG_ERROR("FileTransferAck: buffer too small, size={}", size);
return;
}
FileTransferAck ack{};
std::memcpy(&ack, data, sizeof(ack));
if (ack.magic != kFileAckMagic) {
LOG_ERROR("FileTransferAck: invalid magic, got 0x{:08X}", ack.magic);
return;
}
std::shared_ptr<RemoteSession> props;
{
std::shared_lock lock(file_id_to_props_mutex_);
const auto it = file_id_to_props_.find(ack.file_id);
if (it != file_id_to_props_.end()) {
props = it->second.lock();
}
}
FileTransferState *state = props ? &props->file_transfer_ : nullptr;
if (!state) {
std::shared_lock lock(file_id_to_state_mutex_);
const auto it = file_id_to_state_.find(ack.file_id);
if (it != file_id_to_state_.end()) {
state = it->second;
}
}
if (!state) {
LOG_WARN("FileTransferAck: no state found for file_id={}", ack.file_id);
return;
}
state->file_sent_bytes_ = ack.acked_offset;
state->file_total_bytes_ = ack.total_size;
uint32_t rate_bps = 0;
if (props) {
const uint32_t bitrate =
props->net_traffic_stats_.data_outbound_stats.bitrate;
if (bitrate > 0 && state->file_sending_.load()) {
rate_bps = static_cast<uint32_t>(bitrate * 0.99f);
const uint32_t current_rate = state->file_send_rate_bps_.load();
if (current_rate > 0) {
rate_bps = static_cast<uint32_t>(current_rate * 0.7 + rate_bps * 0.3);
}
} else {
rate_bps = state->file_send_rate_bps_.load();
}
} else {
const uint32_t current_rate = state->file_send_rate_bps_.load();
uint32_t estimated_rate = 0;
const auto now = std::chrono::steady_clock::now();
uint64_t last_bytes = 0;
std::chrono::steady_clock::time_point last_time;
{
std::lock_guard<std::mutex> lock(state->file_transfer_mutex_);
last_bytes = state->file_send_last_bytes_;
last_time = state->file_send_last_update_time_;
}
if (state->file_sending_.load() && ack.acked_offset >= last_bytes) {
const auto delta_bytes = ack.acked_offset - last_bytes;
const double seconds =
std::chrono::duration<double>(now - last_time).count();
if (seconds > 0.0 && delta_bytes > 0) {
const double bits_per_second = delta_bytes * 8.0 / seconds;
estimated_rate = static_cast<uint32_t>((std::min)(
bits_per_second,
static_cast<double>((std::numeric_limits<uint32_t>::max)())));
}
}
rate_bps =
estimated_rate > 0 && current_rate > 0
? static_cast<uint32_t>(current_rate * 0.7 + estimated_rate * 0.3)
: estimated_rate > 0 ? estimated_rate
: current_rate;
}
state->file_send_rate_bps_ = rate_bps;
state->file_send_last_bytes_ = ack.acked_offset;
state->file_send_last_update_time_ = std::chrono::steady_clock::now();
{
std::lock_guard<std::mutex> lock(state->file_transfer_list_mutex_);
for (auto &info : state->file_transfer_list_) {
if (info.file_id == ack.file_id) {
info.sent_bytes = ack.acked_offset;
info.file_size = ack.total_size;
info.rate_bps = rate_bps;
if ((ack.flags & 0x01) != 0) {
info.status = FileTransferState::FileTransferStatus::Completed;
info.sent_bytes = ack.total_size;
}
break;
}
}
}
if ((ack.flags & 0x01) == 0) {
return;
}
LOG_INFO("File transfer completed: file_id={}, bytes={}", ack.file_id,
ack.total_size);
state->file_transfer_window_visible_ = true;
state->file_sending_ = false;
Unregister(ack.file_id, props != nullptr);
ProcessQueue(props);
}
void FileTransferManager::HandleDropEvent(const SDL_Event &event) {
if (event.type != SDL_EVENT_DROP_FILE ||
!((owner_.stream_window_ &&
SDL_GetWindowID(owner_.stream_window_) == event.window.windowID) ||
(owner_.server_window_ &&
SDL_GetWindowID(owner_.server_window_) == event.window.windowID))) {
return;
}
if (owner_.stream_window_ &&
SDL_GetWindowID(owner_.stream_window_) == event.window.windowID) {
if (!owner_.stream_window_inited_ || !event.drop.data) {
return;
}
std::shared_lock lock(owner_.remote_sessions_mutex_);
for (const auto &[remote_id, props] : owner_.remote_sessions_) {
if (props && props->tab_selected_ && props->peer_) {
ProcessSelectedFile(static_cast<const char *>(event.drop.data), props,
props->file_label_);
break;
}
}
return;
}
if (owner_.server_window_inited_ && event.drop.data) {
const auto file_path = std::filesystem::u8path(event.drop.data);
std::error_code ec;
if (std::filesystem::is_regular_file(file_path, ec)) {
LOG_INFO("Drop file [{}] on server window (size: {} bytes)",
event.drop.data, std::filesystem::file_size(file_path, ec));
}
}
}
} // namespace crossdesk
@@ -0,0 +1,57 @@
#ifndef CROSSDESK_GUI_FILE_TRANSFER_MANAGER_H_
#define CROSSDESK_GUI_FILE_TRANSFER_MANAGER_H_
#include <SDL3/SDL.h>
#include <cstddef>
#include <cstdint>
#include <filesystem>
#include <memory>
#include <shared_mutex>
#include <string>
#include <unordered_map>
#include "runtime/gui_state.h"
namespace crossdesk {
class GuiRuntime;
class FileTransferManager {
public:
using FileTransferState = gui_detail::FileTransferState;
using RemoteSession = gui_detail::RemoteSession;
explicit FileTransferManager(GuiRuntime &owner);
FileTransferState &global_state();
FileTransferState &
state_for(const std::shared_ptr<RemoteSession> &props);
void
ProcessSelectedFile(const std::string &path,
const std::shared_ptr<RemoteSession> &props,
const std::string &file_label,
const std::string &remote_id = "");
void HandleDropEvent(const SDL_Event &event);
void HandleAck(const char *data, size_t size);
private:
void Start(std::shared_ptr<RemoteSession> props,
const std::filesystem::path &file_path,
const std::string &file_label, const std::string &remote_id = "");
void ProcessQueue(std::shared_ptr<RemoteSession> props);
void Unregister(uint32_t file_id, bool per_peer);
GuiRuntime &owner_;
FileTransferState global_state_;
std::unordered_map<uint32_t, std::weak_ptr<RemoteSession>>
file_id_to_props_;
std::shared_mutex file_id_to_props_mutex_;
std::unordered_map<uint32_t, FileTransferState *> file_id_to_state_;
std::shared_mutex file_id_to_state_mutex_;
};
} // namespace crossdesk
#endif // CROSSDESK_GUI_FILE_TRANSFER_MANAGER_H_
@@ -0,0 +1,362 @@
#include "features/input/keyboard_controller.h"
#include <cstdint>
#include <mutex>
#include <string>
#include <unordered_map>
#include <vector>
#include <SDL3/SDL.h>
#include <nlohmann/json.hpp>
#include "minirtc.h"
#include "rd_log.h"
#include "runtime/gui_runtime.h"
#include "windows_key_metadata.h"
#if _WIN32
#include "interactive_state.h"
#include "service_host.h"
#endif
namespace crossdesk {
namespace {
constexpr uint32_t kHeartbeatIntervalMs = 500;
constexpr uint32_t kRemoteReleaseTimeoutMs = 2500;
int NormalizeWindowsModifierVk(int key_code, uint32_t scan_code,
bool extended) {
#if _WIN32
if (key_code != 0x10 && key_code != 0x11 && key_code != 0x12) {
return key_code;
}
UINT scan_code_with_prefix = static_cast<UINT>(scan_code & 0xFF);
if (extended) {
scan_code_with_prefix |= 0xE000;
}
const UINT normalized_vk =
MapVirtualKeyW(scan_code_with_prefix, MAPVK_VSC_TO_VK_EX);
return normalized_vk != 0 ? static_cast<int>(normalized_vk) : key_code;
#else
(void)scan_code;
(void)extended;
return key_code;
#endif
}
void PopulateWindowsKeyMetadataFromVk(int key_code, uint32_t *scan_code_out,
bool *extended_out) {
if (!scan_code_out || !extended_out) {
return;
}
#if _WIN32
const UINT scan_code =
MapVirtualKeyW(static_cast<UINT>(key_code), MAPVK_VK_TO_VSC_EX);
if (scan_code != 0) {
*scan_code_out = static_cast<uint32_t>(scan_code & 0xFF);
*extended_out = (scan_code & 0xFF00) != 0;
return;
}
#endif
LookupWindowsKeyMetadataFromVk(key_code, scan_code_out, extended_out);
}
#if _WIN32
constexpr uint32_t kSecureDesktopInputLogIntervalMs = 2000;
void LogSecureDesktopInputBlocked(uint32_t *last_tick, const char *stage) {
const uint32_t now = static_cast<uint32_t>(SDL_GetTicks());
if (*last_tick != 0 && now - *last_tick < kSecureDesktopInputLogIntervalMs) {
return;
}
*last_tick = now;
LOG_WARN(
"local secure-desktop input blocked, stage={}, normal SendInput path "
"cannot drive the Windows password UI",
stage ? stage : "");
}
bool IsTransientSecureDesktopInputFailure(const nlohmann::json &response,
const RemoteAction &action) {
return response.is_object() &&
response.value("error", std::string()) == "send_input_failed" &&
response.value("code", 0u) == ERROR_ACCESS_DENIED &&
action.type == ControlType::keyboard &&
action.k.flag == KeyFlag::key_up;
}
#endif
} // namespace
KeyboardController::KeyboardController(GuiRuntime &owner) : owner_(owner) {}
void KeyboardController::TrackPressedKey(int key_code, bool is_down,
uint32_t scan_code, bool extended) {
std::lock_guard<std::mutex> lock(pressed_keys_mutex_);
if (is_down) {
pressed_keys_[key_code] = PressedKey{key_code, scan_code, extended};
} else {
pressed_keys_.erase(key_code);
}
}
void KeyboardController::ForceReleasePressedKeys() {
std::vector<PressedKey> pressed_keys;
{
std::lock_guard<std::mutex> lock(pressed_keys_mutex_);
pressed_keys.reserve(pressed_keys_.size());
for (const auto &[_, key] : pressed_keys_) {
pressed_keys.push_back(key);
}
pressed_keys_.clear();
}
for (const PressedKey &key : pressed_keys) {
SendKeyCommand(key.key_code, false, key.scan_code, key.extended);
}
SendHeartbeat(true);
}
void KeyboardController::SendHeartbeat(bool force) {
const uint32_t now = static_cast<uint32_t>(SDL_GetTicks());
if (!force && now - last_heartbeat_tick_ < kHeartbeatIntervalMs) {
return;
}
RemoteAction action{};
action.type = ControlType::keyboard_state;
action.ks.seq = ++state_sequence_;
{
std::lock_guard<std::mutex> lock(pressed_keys_mutex_);
size_t index = 0;
for (const auto &[_, key] : pressed_keys_) {
if (index >= kMaxKeyboardStateKeys) {
LOG_WARN("Keyboard heartbeat truncated, pressed_keys={}",
pressed_keys_.size());
break;
}
action.ks.pressed_keys[index].key_value =
static_cast<size_t>(key.key_code);
action.ks.pressed_keys[index].scan_code = key.scan_code;
action.ks.pressed_keys[index].extended = key.extended;
++index;
}
action.ks.pressed_count = index;
}
const std::string target_id = owner_.controlled_remote_id_.empty()
? owner_.focused_remote_id_
: owner_.controlled_remote_id_;
const auto props_it = owner_.remote_sessions_.find(target_id);
if (target_id.empty() || props_it == owner_.remote_sessions_.end() ||
props_it->second->connection_status_.load() !=
ConnectionStatus::Connected ||
!props_it->second->peer_) {
last_heartbeat_tick_ = now;
return;
}
const std::string message = action.to_json();
const int result = SendReliableDataFrame(
props_it->second->peer_, message.c_str(), message.size(),
props_it->second->keyboard_label_.c_str());
if (result != 0) {
LOG_WARN("Send keyboard heartbeat failed, remote_id={}, ret={}", target_id,
result);
}
last_heartbeat_tick_ = now;
}
int KeyboardController::SendKeyCommand(int key_code, bool is_down,
uint32_t scan_code, bool extended) {
if (scan_code == 0) {
PopulateWindowsKeyMetadataFromVk(key_code, &scan_code, &extended);
}
#if _WIN32
key_code = NormalizeWindowsModifierVk(key_code, scan_code, extended);
#endif
RemoteAction action{};
action.type = ControlType::keyboard;
action.k.flag = is_down ? KeyFlag::key_down : KeyFlag::key_up;
action.k.key_value = key_code;
action.k.scan_code = scan_code;
action.k.extended = extended;
const std::string target_id = owner_.controlled_remote_id_.empty()
? owner_.focused_remote_id_
: owner_.controlled_remote_id_;
const auto props_it = owner_.remote_sessions_.find(target_id);
if (!target_id.empty() && props_it != owner_.remote_sessions_.end() &&
props_it->second->connection_status_.load() ==
ConnectionStatus::Connected &&
props_it->second->peer_) {
const std::string message = action.to_json();
const int result = SendReliableDataFrame(
props_it->second->peer_, message.c_str(), message.size(),
props_it->second->keyboard_label_.c_str());
if (result != 0) {
LOG_WARN("Send keyboard command failed, remote_id={}, ret={}", target_id,
result);
}
}
TrackPressedKey(key_code, is_down, scan_code, extended);
return 0;
}
bool KeyboardController::InjectRemoteKey(int key_code, bool is_down,
uint32_t scan_code, bool extended) {
#if _WIN32
if (owner_.local_service_status_received_ &&
IsSecureDesktopInteractionRequired(owner_.local_interactive_stage_)) {
const std::string response = SendCrossDeskSecureDesktopKeyInput(
key_code, is_down, scan_code, extended, 1000);
const auto json = nlohmann::json::parse(response, nullptr, false);
if (json.is_discarded() || !json.value("ok", false)) {
RemoteAction action{};
action.type = ControlType::keyboard;
action.k.key_value = static_cast<size_t>(key_code);
action.k.scan_code = scan_code;
action.k.extended = extended;
action.k.flag = is_down ? KeyFlag::key_down : KeyFlag::key_up;
if (!json.is_discarded() &&
IsTransientSecureDesktopInputFailure(json, action)) {
LOG_INFO("Secure desktop keyboard injection transient failure, "
"key_code={}, is_down={}, response={}",
key_code, is_down, response);
return true;
}
LogSecureDesktopInputBlocked(
&owner_.last_local_secure_input_block_log_tick_,
owner_.local_interactive_stage_.c_str());
LOG_WARN(
"Secure desktop keyboard injection failed, key_code={}, is_down={}, "
"response={}",
key_code, is_down, response);
return false;
}
return true;
}
#endif
return owner_.devices_.SendKeyboardCommand(key_code, is_down, scan_code,
extended);
}
void KeyboardController::ApplyRemoteEvent(const std::string &remote_id,
const RemoteAction &action) {
const int key_code = static_cast<int>(action.k.key_value);
const bool is_down = action.k.flag == KeyFlag::key_down;
const bool injected =
InjectRemoteKey(key_code, is_down, action.k.scan_code, action.k.extended);
std::lock_guard<std::mutex> lock(remote_states_mutex_);
auto &state = remote_states_[remote_id];
state.last_seen_tick = static_cast<uint32_t>(SDL_GetTicks());
if (is_down && injected) {
state.pressed_keys[key_code] =
PressedKey{key_code, action.k.scan_code, action.k.extended};
} else if (!is_down && injected) {
state.pressed_keys.erase(key_code);
}
}
void KeyboardController::ApplyRemoteState(const std::string &remote_id,
const RemoteAction &action) {
std::vector<PressedKey> keys_to_release;
std::vector<PressedKey> keys_to_press;
{
std::lock_guard<std::mutex> lock(remote_states_mutex_);
auto &state = remote_states_[remote_id];
if (action.ks.seq != 0 && state.last_seq != 0 &&
static_cast<int32_t>(action.ks.seq - state.last_seq) <= 0) {
return;
}
state.last_seq = action.ks.seq;
state.last_seen_tick = static_cast<uint32_t>(SDL_GetTicks());
state.keyboard_state_seen = true;
std::unordered_map<int, PressedKey> desired_keys;
const size_t count =
(std::min)(action.ks.pressed_count, kMaxKeyboardStateKeys);
for (size_t index = 0; index < count; ++index) {
const auto &key = action.ks.pressed_keys[index];
const int key_code = static_cast<int>(key.key_value);
desired_keys[key_code] =
PressedKey{key_code, key.scan_code, key.extended};
}
for (const auto &[key_code, key] : state.pressed_keys) {
if (desired_keys.find(key_code) == desired_keys.end()) {
keys_to_release.push_back(key);
}
}
for (const auto &[key_code, key] : desired_keys) {
if (state.pressed_keys.find(key_code) == state.pressed_keys.end()) {
keys_to_press.push_back(key);
}
}
}
for (const PressedKey &key : keys_to_release) {
if (InjectRemoteKey(key.key_code, false, key.scan_code, key.extended)) {
std::lock_guard<std::mutex> lock(remote_states_mutex_);
const auto state_it = remote_states_.find(remote_id);
if (state_it != remote_states_.end()) {
state_it->second.pressed_keys.erase(key.key_code);
}
}
}
for (const PressedKey &key : keys_to_press) {
if (InjectRemoteKey(key.key_code, true, key.scan_code, key.extended)) {
std::lock_guard<std::mutex> lock(remote_states_mutex_);
remote_states_[remote_id].pressed_keys[key.key_code] = key;
}
}
}
void KeyboardController::ReleaseRemotePressedKeys(const std::string &remote_id,
const char *reason) {
std::vector<PressedKey> keys_to_release;
{
std::lock_guard<std::mutex> lock(remote_states_mutex_);
const auto state_it = remote_states_.find(remote_id);
if (state_it == remote_states_.end()) {
return;
}
for (const auto &[_, key] : state_it->second.pressed_keys) {
keys_to_release.push_back(key);
}
remote_states_.erase(state_it);
}
if (!keys_to_release.empty()) {
LOG_WARN("Releasing {} remote keyboard keys for remote_id={}, reason={}",
keys_to_release.size(), remote_id, reason ? reason : "unknown");
}
for (const PressedKey &key : keys_to_release) {
InjectRemoteKey(key.key_code, false, key.scan_code, key.extended);
}
}
void KeyboardController::CheckRemoteTimeouts() {
const uint32_t now = static_cast<uint32_t>(SDL_GetTicks());
std::vector<std::string> timed_out_remotes;
{
std::lock_guard<std::mutex> lock(remote_states_mutex_);
for (const auto &[remote_id, state] : remote_states_) {
if (state.keyboard_state_seen && !state.pressed_keys.empty() &&
state.last_seen_tick != 0 &&
now - state.last_seen_tick > kRemoteReleaseTimeoutMs) {
timed_out_remotes.push_back(remote_id);
}
}
}
for (const std::string &remote_id : timed_out_remotes) {
ReleaseRemotePressedKeys(remote_id, "keyboard_heartbeat_timeout");
}
}
} // namespace crossdesk
@@ -0,0 +1,63 @@
#ifndef CROSSDESK_GUI_KEYBOARD_CONTROLLER_H_
#define CROSSDESK_GUI_KEYBOARD_CONTROLLER_H_
#include <cstdint>
#include <mutex>
#include <string>
#include <unordered_map>
#include "device_controller.h"
namespace crossdesk {
class GuiRuntime;
// Synchronizes local and remote keyboard state, including heartbeat recovery
// for keys whose key-up event was lost during a connection interruption.
class KeyboardController {
public:
explicit KeyboardController(GuiRuntime &owner);
int SendKeyCommand(int key_code, bool is_down, uint32_t scan_code = 0,
bool extended = false);
void ForceReleasePressedKeys();
void SendHeartbeat(bool force);
void ApplyRemoteEvent(const std::string &remote_id,
const RemoteAction &remote_action);
void ApplyRemoteState(const std::string &remote_id,
const RemoteAction &remote_action);
void ReleaseRemotePressedKeys(const std::string &remote_id,
const char *reason);
void CheckRemoteTimeouts();
private:
struct PressedKey {
int key_code = 0;
uint32_t scan_code = 0;
bool extended = false;
};
struct RemoteState {
std::unordered_map<int, PressedKey> pressed_keys;
uint32_t last_seq = 0;
uint32_t last_seen_tick = 0;
bool keyboard_state_seen = false;
};
void TrackPressedKey(int key_code, bool is_down, uint32_t scan_code,
bool extended);
bool InjectRemoteKey(int key_code, bool is_down, uint32_t scan_code,
bool extended);
GuiRuntime &owner_;
std::unordered_map<int, PressedKey> pressed_keys_;
std::mutex pressed_keys_mutex_;
uint32_t state_sequence_ = 0;
uint32_t last_heartbeat_tick_ = 0;
std::unordered_map<std::string, RemoteState> remote_states_;
std::mutex remote_states_mutex_;
};
} // namespace crossdesk
#endif // CROSSDESK_GUI_KEYBOARD_CONTROLLER_H_
@@ -0,0 +1,345 @@
#include "features/settings/settings_manager.h"
#include <cstring>
#include <exception>
#include <filesystem>
#include <fstream>
#include <memory>
#include <string>
#include "localization.h"
#include "rd_log.h"
#include "runtime/gui_runtime.h"
namespace crossdesk {
namespace {
template <size_t Size>
void CopyString(char (&destination)[Size], const char *source) {
static_assert(Size > 0);
std::memset(destination, 0, Size);
if (source) {
std::strncpy(destination, source, Size - 1);
}
}
} // namespace
SettingsManager::SettingsManager(GuiRuntime &owner) : owner_(owner) {}
int SettingsManager::Save() {
std::lock_guard<std::mutex> lock(cache_mutex_);
return SaveLocked();
}
int SettingsManager::SaveLocked() {
std::ofstream cache_v2_file(owner_.cache_path_ + "/secure_cache_v2.enc",
std::ios::binary);
if (!cache_v2_file) {
return -1;
}
CopyString(cache_v2_.client_id_with_password,
owner_.client_id_with_password_);
std::memcpy(cache_v2_.key, owner_.aes128_key_, sizeof(owner_.aes128_key_));
std::memcpy(cache_v2_.iv, owner_.aes128_iv_, sizeof(owner_.aes128_iv_));
CopyString(cache_v2_.self_hosted_id, owner_.self_hosted_id_);
cache_v2_file.write(reinterpret_cast<const char *>(&cache_v2_),
sizeof(cache_v2_));
// Keep writing the legacy cache while older installations may still read it.
std::ofstream cache_v1_file(owner_.cache_path_ + "/secure_cache.enc",
std::ios::binary);
if (cache_v1_file) {
CopyString(cache_v1_.client_id_with_password,
owner_.client_id_with_password_);
std::memcpy(cache_v1_.key, owner_.aes128_key_, sizeof(owner_.aes128_key_));
std::memcpy(cache_v1_.iv, owner_.aes128_iv_, sizeof(owner_.aes128_iv_));
cache_v1_file.write(reinterpret_cast<const char *>(&cache_v1_),
sizeof(cache_v1_));
}
return 0;
}
bool SettingsManager::ReadV2Locked() {
std::ifstream cache_v2_file(owner_.cache_path_ + "/secure_cache_v2.enc",
std::ios::binary);
if (!cache_v2_file) {
return false;
}
cache_v2_file.read(reinterpret_cast<char *>(&cache_v2_), sizeof(cache_v2_));
if (cache_v2_file.gcount() !=
static_cast<std::streamsize>(sizeof(cache_v2_))) {
return false;
}
cache_v2_
.client_id_with_password[sizeof(cache_v2_.client_id_with_password) - 1] =
'\0';
cache_v2_.self_hosted_id[sizeof(cache_v2_.self_hosted_id) - 1] = '\0';
return true;
}
int SettingsManager::Load() {
std::unique_lock<std::mutex> lock(cache_mutex_);
if (ReadV2Locked()) {
CopyString(owner_.client_id_with_password_,
cache_v2_.client_id_with_password);
CopyString(owner_.self_hosted_id_, cache_v2_.self_hosted_id);
std::memcpy(owner_.aes128_key_, cache_v2_.key, sizeof(cache_v2_.key));
std::memcpy(owner_.aes128_iv_, cache_v2_.iv, sizeof(cache_v2_.iv));
LOG_INFO("Load settings from v2 cache file");
} else {
std::ifstream cache_v1_file(owner_.cache_path_ + "/secure_cache.enc",
std::ios::binary);
if (!cache_v1_file) {
lock.unlock();
std::memset(owner_.password_saved_, 0, sizeof(owner_.password_saved_));
std::memset(owner_.aes128_key_, 0, sizeof(owner_.aes128_key_));
std::memset(owner_.aes128_iv_, 0, sizeof(owner_.aes128_iv_));
std::memset(owner_.self_hosted_id_, 0, sizeof(owner_.self_hosted_id_));
owner_.thumbnail_ =
std::make_shared<Thumbnail>(owner_.cache_path_ + "/thumbnails/");
owner_.thumbnail_->GetKeyAndIv(owner_.aes128_key_, owner_.aes128_iv_);
owner_.thumbnail_->DeleteAllFilesInDirectory();
Save();
return -1;
}
cache_v1_file.read(reinterpret_cast<char *>(&cache_v1_), sizeof(cache_v1_));
cache_v1_
.client_id_with_password[sizeof(cache_v1_.client_id_with_password) -
1] = '\0';
CopyString(cache_v2_.client_id_with_password,
cache_v1_.client_id_with_password);
std::memcpy(cache_v2_.key, cache_v1_.key, sizeof(cache_v1_.key));
std::memcpy(cache_v2_.iv, cache_v1_.iv, sizeof(cache_v1_.iv));
std::memset(cache_v2_.self_hosted_id, 0, sizeof(cache_v2_.self_hosted_id));
CopyString(owner_.client_id_with_password_,
cache_v1_.client_id_with_password);
std::memset(owner_.self_hosted_id_, 0, sizeof(owner_.self_hosted_id_));
std::memcpy(owner_.aes128_key_, cache_v1_.key, sizeof(cache_v1_.key));
std::memcpy(owner_.aes128_iv_, cache_v1_.iv, sizeof(cache_v1_.iv));
SaveLocked();
LOG_INFO("Migrated settings from v1 to v2 cache file");
}
lock.unlock();
const char *at_pos = std::strchr(owner_.client_id_with_password_, '@');
if (at_pos != nullptr) {
const std::string id(owner_.client_id_with_password_,
at_pos - owner_.client_id_with_password_);
const std::string password(at_pos + 1);
CopyString(owner_.client_id_, id.c_str());
CopyString(owner_.password_saved_, password.c_str());
}
owner_.thumbnail_ =
std::make_shared<Thumbnail>(owner_.cache_path_ + "/thumbnails/",
owner_.aes128_key_, owner_.aes128_iv_);
owner_.language_button_value_ = localization::detail::ClampLanguageIndex(
static_cast<int>(owner_.config_center_->GetLanguage()));
owner_.video_quality_button_value_ =
static_cast<int>(owner_.config_center_->GetVideoQuality());
owner_.video_frame_rate_button_value_ =
static_cast<int>(owner_.config_center_->GetVideoFrameRate());
owner_.video_encode_format_button_value_ =
static_cast<int>(owner_.config_center_->GetVideoEncodeFormat());
owner_.enable_hardware_video_codec_ =
owner_.config_center_->IsHardwareVideoCodec();
owner_.enable_turn_ = owner_.config_center_->IsEnableTurn();
owner_.enable_srtp_ = owner_.config_center_->IsEnableSrtp();
owner_.enable_self_hosted_ = owner_.config_center_->IsSelfHosted();
owner_.enable_autostart_ = owner_.config_center_->IsEnableAutostart();
owner_.enable_daemon_ = owner_.config_center_->IsEnableDaemon();
owner_.enable_minimize_to_tray_ = owner_.config_center_->IsMinimizeToTray();
#if _WIN32 && CROSSDESK_PORTABLE
owner_.portable_service_prompt_suppressed_ =
owner_.config_center_->IsPortableServicePromptSuppressed();
owner_.portable_service_do_not_remind_ =
owner_.portable_service_prompt_suppressed_;
#endif
const std::string saved_path =
owner_.config_center_->GetFileTransferSavePath();
CopyString(owner_.file_transfer_save_path_buf_, saved_path.c_str());
owner_.file_transfer_save_path_last_ = saved_path;
owner_.language_button_value_last_ = owner_.language_button_value_;
owner_.video_quality_button_value_last_ = owner_.video_quality_button_value_;
owner_.video_encode_format_button_value_last_ =
owner_.video_encode_format_button_value_;
owner_.enable_hardware_video_codec_last_ =
owner_.enable_hardware_video_codec_;
owner_.enable_turn_last_ = owner_.enable_turn_;
owner_.enable_srtp_last_ = owner_.enable_srtp_;
owner_.enable_self_hosted_last_ = owner_.enable_self_hosted_;
owner_.enable_autostart_last_ = owner_.enable_autostart_;
owner_.enable_minimize_to_tray_last_ = owner_.enable_minimize_to_tray_;
LOG_INFO("Load settings from cache file");
return 0;
}
bool SettingsManager::LoadCachedSelfHostedIdentity() {
std::lock_guard<std::mutex> lock(cache_mutex_);
if (!ReadV2Locked() || cache_v2_.self_hosted_id[0] == '\0') {
std::memset(owner_.self_hosted_id_, 0, sizeof(owner_.self_hosted_id_));
return false;
}
CopyString(owner_.self_hosted_id_, cache_v2_.self_hosted_id);
const char *at_pos = std::strchr(owner_.self_hosted_id_, '@');
if (at_pos == nullptr) {
CopyString(owner_.client_id_, owner_.self_hosted_id_);
std::memset(owner_.password_saved_, 0, sizeof(owner_.password_saved_));
} else {
const std::string id(owner_.self_hosted_id_,
at_pos - owner_.self_hosted_id_);
CopyString(owner_.client_id_, id.c_str());
CopyString(owner_.password_saved_, at_pos + 1);
}
return true;
}
void SettingsManager::PersistSelfHostedIdentity(const char *client_id) {
if (!client_id) {
return;
}
std::lock_guard<std::mutex> lock(cache_mutex_);
if (!ReadV2Locked()) {
cache_v2_ = {};
}
CopyString(cache_v2_.self_hosted_id, client_id);
CopyString(cache_v2_.client_id_with_password,
owner_.client_id_with_password_);
std::memcpy(cache_v2_.key, owner_.aes128_key_, sizeof(owner_.aes128_key_));
std::memcpy(cache_v2_.iv, owner_.aes128_iv_, sizeof(owner_.aes128_iv_));
std::ofstream cache_v2_file(owner_.cache_path_ + "/secure_cache_v2.enc",
std::ios::binary);
if (cache_v2_file) {
cache_v2_file.write(reinterpret_cast<const char *>(&cache_v2_),
sizeof(cache_v2_));
}
}
int SettingsManager::LoadRecentConnectionAliases() {
recent_connection_aliases_.clear();
std::ifstream alias_file(owner_.cache_path_ +
"/recent_connection_aliases.json");
if (!alias_file.good()) {
return 0;
}
try {
nlohmann::json alias_json;
alias_file >> alias_json;
const nlohmann::json *aliases = &alias_json;
if (alias_json.contains("aliases") && alias_json["aliases"].is_object()) {
aliases = &alias_json["aliases"];
}
if (!aliases->is_object()) {
LOG_WARN("Invalid recent connection alias file");
return -1;
}
for (auto it = aliases->begin(); it != aliases->end(); ++it) {
if (it.value().is_string()) {
const std::string remote_id = it.key();
const std::string alias = it.value().get<std::string>();
if (!remote_id.empty() && !alias.empty()) {
recent_connection_aliases_[remote_id] = alias;
}
}
}
} catch (const std::exception &e) {
LOG_WARN("Load recent connection aliases failed: {}", e.what());
return -1;
}
return 0;
}
int SettingsManager::SaveRecentConnectionAliases() const {
std::error_code ec;
std::filesystem::create_directories(owner_.cache_path_, ec);
if (ec) {
LOG_WARN("Create cache directory failed while saving aliases: {}",
ec.message());
return -1;
}
nlohmann::json alias_json;
alias_json["aliases"] = nlohmann::json::object();
for (const auto &[remote_id, alias] : recent_connection_aliases_) {
if (!remote_id.empty() && !alias.empty()) {
alias_json["aliases"][remote_id] = alias;
}
}
std::ofstream alias_file(
owner_.cache_path_ + "/recent_connection_aliases.json", std::ios::trunc);
if (!alias_file.good()) {
LOG_WARN("Open recent connection alias file failed");
return -1;
}
alias_file << alias_json.dump(2);
return 0;
}
std::string SettingsManager::RecentConnectionDisplayName(
const Thumbnail::RecentConnection &connection) const {
const auto alias_it = recent_connection_aliases_.find(connection.remote_id);
if (alias_it != recent_connection_aliases_.end() &&
!alias_it->second.empty()) {
return alias_it->second;
}
if (!connection.remote_host_name.empty() &&
connection.remote_host_name != "unknown") {
return connection.remote_host_name;
}
return connection.remote_id;
}
void SettingsManager::BeginEditRecentConnectionAlias(
const Thumbnail::RecentConnection &connection) {
owner_.edit_connection_alias_remote_id_ = connection.remote_id;
std::memset(owner_.edit_connection_alias_, 0,
sizeof(owner_.edit_connection_alias_));
const auto alias_it = recent_connection_aliases_.find(connection.remote_id);
const std::string alias = alias_it != recent_connection_aliases_.end()
? alias_it->second
: RecentConnectionDisplayName(connection);
CopyString(owner_.edit_connection_alias_, alias.c_str());
owner_.focus_on_input_widget_ = true;
owner_.show_edit_connection_alias_window_ = true;
}
void SettingsManager::SetRecentConnectionAlias(const std::string &remote_id,
const std::string &alias) {
if (!remote_id.empty()) {
recent_connection_aliases_[remote_id] = alias;
}
}
void SettingsManager::EraseRecentConnectionAlias(const std::string &remote_id) {
recent_connection_aliases_.erase(remote_id);
}
} // namespace crossdesk
@@ -0,0 +1,78 @@
#ifndef CROSSDESK_GUI_SETTINGS_MANAGER_H_
#define CROSSDESK_GUI_SETTINGS_MANAGER_H_
#include <mutex>
#include <string>
#include <unordered_map>
#include "thumbnail.h"
namespace crossdesk {
class GuiRuntime;
// Owns persistent GUI settings and recent-connection aliases. GuiRuntime keeps
// only the runtime/UI state that consumes these values.
class SettingsManager {
public:
explicit SettingsManager(GuiRuntime &owner);
int Save();
int Load();
int LoadRecentConnectionAliases();
int SaveRecentConnectionAliases() const;
std::string RecentConnectionDisplayName(
const Thumbnail::RecentConnection &connection) const;
void
BeginEditRecentConnectionAlias(const Thumbnail::RecentConnection &connection);
void SetRecentConnectionAlias(const std::string &remote_id,
const std::string &alias);
void EraseRecentConnectionAlias(const std::string &remote_id);
// Loads the cached self-hosted identity into the owner's active connection
// fields. Returns true only when a non-empty identity was restored.
bool LoadCachedSelfHostedIdentity();
void PersistSelfHostedIdentity(const char *client_id);
private:
struct CacheV1 {
char client_id_with_password[17];
int language;
int video_quality;
int video_frame_rate;
int video_encode_format;
bool enable_hardware_video_codec;
bool enable_turn;
bool enable_srtp;
unsigned char key[16];
unsigned char iv[16];
};
struct CacheV2 {
char client_id_with_password[17];
int language;
int video_quality;
int video_frame_rate;
int video_encode_format;
bool enable_hardware_video_codec;
bool enable_turn;
bool enable_srtp;
unsigned char key[16];
unsigned char iv[16];
char self_hosted_id[17];
};
int SaveLocked();
bool ReadV2Locked();
GuiRuntime &owner_;
CacheV1 cache_v1_{};
CacheV2 cache_v2_{};
mutable std::mutex cache_mutex_;
std::unordered_map<std::string, std::string> recent_connection_aliases_;
};
} // namespace crossdesk
#endif // CROSSDESK_GUI_SETTINGS_MANAGER_H_
-289
View File
@@ -1,289 +0,0 @@
#include "localization.h"
#include "rd_log.h"
#include "render.h"
namespace crossdesk {
int Render::RecentConnectionsWindow() {
ImGui::SetNextWindowPos(
ImVec2(0, title_bar_height_ + local_window_height_ - 1.0f),
ImGuiCond_Always);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::BeginChild(
"RecentConnectionsWindow",
ImVec2(main_window_width_default_,
main_window_height_default_ - title_bar_height_ -
local_window_height_ - status_bar_height_ + 1.0f),
ImGuiChildFlags_Border,
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar |
ImGuiWindowFlags_NoBringToFrontOnFocus);
ImGui::PopStyleVar();
ImGui::PopStyleColor();
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + main_window_text_y_padding_);
ImGui::Indent(main_child_window_x_padding_);
ImGui::TextColored(
ImVec4(0.0f, 0.0f, 0.0f, 0.5f), "%s",
localization::recent_connections[localization_language_index_].c_str());
ShowRecentConnections();
ImGui::EndChild();
return 0;
}
int Render::ShowRecentConnections() {
ImGui::SetCursorPosX(25.0f);
ImVec2 sub_window_pos = ImGui::GetCursorPos();
std::map<std::string, ImVec2> sub_containers_pos;
float recent_connection_sub_container_width =
recent_connection_image_width_ + 16.0f;
float recent_connection_sub_container_height =
recent_connection_image_height_ + 36.0f;
ImGui::PushStyleColor(ImGuiCol_ChildBg,
ImVec4(239.0f / 255, 240.0f / 255, 242.0f / 255, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 10.0f);
ImGui::BeginChild("RecentConnectionsContainer",
ImVec2(main_window_width_default_ - 50.0f, 145.0f),
ImGuiChildFlags_Border,
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar |
ImGuiWindowFlags_NoBringToFrontOnFocus |
ImGuiWindowFlags_AlwaysHorizontalScrollbar |
ImGuiWindowFlags_NoScrollbar |
ImGuiWindowFlags_NoScrollWithMouse);
ImGui::PopStyleVar();
ImGui::PopStyleColor();
size_t recent_connections_count = recent_connections_.size();
int count = 0;
float button_width = 22;
float button_height = 22;
for (auto& it : recent_connections_) {
sub_containers_pos[it.first] = ImGui::GetCursorPos();
std::string recent_connection_sub_window_name =
"RecentConnectionsSubContainer" + it.first;
// recent connections sub container
ImGui::BeginChild(recent_connection_sub_window_name.c_str(),
ImVec2(recent_connection_sub_container_width,
recent_connection_sub_container_height),
ImGuiChildFlags_None,
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoTitleBar |
ImGuiWindowFlags_NoBringToFrontOnFocus |
ImGuiWindowFlags_NoScrollbar);
std::string connection_info = it.first;
// remote id length is 9
// password length is 6
// connection_info -> remote_id + 'Y' + host_name + '@' + password
// -> remote_id + 'N' + host_name
if ('Y' == connection_info[9] && connection_info.size() >= 16) {
size_t pos_y = connection_info.find('Y');
size_t pos_at = connection_info.find('@');
if (pos_y == std::string::npos || pos_at == std::string::npos ||
pos_y >= pos_at) {
LOG_ERROR("Invalid filename");
continue;
}
it.second.remote_id = connection_info.substr(0, pos_y);
it.second.remote_host_name =
connection_info.substr(pos_y + 1, pos_at - pos_y - 1);
it.second.password = connection_info.substr(pos_at + 1);
it.second.remember_password = true;
} else if ('N' == connection_info[9] && connection_info.size() >= 10) {
size_t pos_n = connection_info.find('N');
size_t pos_at = connection_info.find('@');
if (pos_n == std::string::npos) {
LOG_ERROR("Invalid filename");
continue;
}
it.second.remote_id = connection_info.substr(0, pos_n);
it.second.remote_host_name = connection_info.substr(pos_n + 1);
it.second.password = "";
it.second.remember_password = false;
} else {
it.second.remote_host_name = "unknown";
}
ImVec2 image_screen_pos = ImVec2(ImGui::GetCursorScreenPos().x + 5.0f,
ImGui::GetCursorScreenPos().y + 5.0f);
ImVec2 image_pos =
ImVec2(ImGui::GetCursorPosX() + 5.0f, ImGui::GetCursorPosY() + 5.0f);
ImGui::SetCursorPos(image_pos);
ImGui::Image((ImTextureID)(intptr_t)it.second.texture,
ImVec2((float)recent_connection_image_width_,
(float)recent_connection_image_height_));
// remote id display button
{
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0.2f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0, 0, 0, 0.2f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0, 0, 0, 0.2f));
ImVec2 dummy_button_pos =
ImVec2(image_pos.x, image_pos.y + recent_connection_image_height_);
std::string dummy_button_name = "##DummyButton" + it.second.remote_id;
ImGui::SetCursorPos(dummy_button_pos);
ImGui::SetWindowFontScale(0.6f);
ImGui::Button(dummy_button_name.c_str(),
ImVec2(recent_connection_image_width_ - 2 * button_width,
button_height));
ImGui::SetWindowFontScale(1.0f);
ImGui::SetCursorPos(
ImVec2(dummy_button_pos.x + 2.0f, dummy_button_pos.y + 1.0f));
ImGui::SetWindowFontScale(0.65f);
ImGui::Text("%s", it.second.remote_id.c_str());
ImGui::SetWindowFontScale(1.0f);
ImGui::PopStyleColor(3);
if (ImGui::IsItemHovered()) {
ImGui::BeginTooltip();
ImGui::SetWindowFontScale(0.5f);
ImGui::Text("%s", it.second.remote_host_name.c_str());
ImGui::SetWindowFontScale(1.0f);
ImGui::EndTooltip();
}
}
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0.2f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
ImVec4(0.1f, 0.4f, 0.8f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive,
ImVec4(1.0f, 1.0f, 1.0f, 0.7f));
ImGui::SetWindowFontScale(0.5f);
// trash button
{
ImVec2 trash_can_button_pos = ImVec2(
image_pos.x + recent_connection_image_width_ - 2 * button_width,
image_pos.y + recent_connection_image_height_);
ImGui::SetCursorPos(trash_can_button_pos);
std::string trash_can = ICON_FA_TRASH_CAN;
std::string recent_connection_delete_button_name =
trash_can + "##RecentConnectionDelete" +
std::to_string(trash_can_button_pos.x);
if (ImGui::Button(recent_connection_delete_button_name.c_str(),
ImVec2(button_width, button_height))) {
show_confirm_delete_connection_ = true;
delete_connection_name_ = it.first;
}
if (delete_connection_ && delete_connection_name_ == it.first) {
if (!thumbnail_->DeleteThumbnail(it.first)) {
reload_recent_connections_ = true;
delete_connection_ = false;
}
}
}
// connect button
{
ImVec2 connect_button_pos =
ImVec2(image_pos.x + recent_connection_image_width_ - button_width,
image_pos.y + recent_connection_image_height_);
ImGui::SetCursorPos(connect_button_pos);
std::string connect = ICON_FA_ARROW_RIGHT_LONG;
std::string connect_to_this_connection_button_name =
connect + "##ConnectionTo" + it.first;
if (ImGui::Button(connect_to_this_connection_button_name.c_str(),
ImVec2(button_width, button_height))) {
ConnectTo(it.second.remote_id, it.second.password.c_str(),
it.second.remember_password);
}
}
ImGui::SetWindowFontScale(1.0f);
ImGui::PopStyleColor(3);
ImGui::EndChild();
if (count != recent_connections_count - 1) {
ImVec2 line_start =
ImVec2(image_screen_pos.x + recent_connection_image_width_ + 20.0f,
image_screen_pos.y);
ImVec2 line_end = ImVec2(
image_screen_pos.x + recent_connection_image_width_ + 20.0f,
image_screen_pos.y + recent_connection_image_height_ + button_height);
ImGui::GetWindowDrawList()->AddLine(line_start, line_end,
IM_COL32(0, 0, 0, 122), 1.0f);
}
count++;
ImGui::SameLine(0, count != recent_connections_count ? 26.0f : 0.0f);
}
ImGui::EndChild();
if (show_confirm_delete_connection_) {
ConfirmDeleteConnection();
}
return 0;
}
int Render::ConfirmDeleteConnection() {
const ImGuiViewport* viewport = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(ImVec2((viewport->WorkSize.x - viewport->WorkPos.x -
connection_status_window_width_) /
2,
(viewport->WorkSize.y - viewport->WorkPos.y -
connection_status_window_height_) /
2));
ImGui::SetNextWindowSize(ImVec2(connection_status_window_width_,
connection_status_window_height_));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0, 1.0, 1.0, 1.0));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 5.0f);
ImGui::Begin("ConfirmDeleteConnectionWindow", nullptr,
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar |
ImGuiWindowFlags_NoSavedSettings);
ImGui::PopStyleVar(2);
ImGui::PopStyleColor();
std::string text =
localization::confirm_delete_connection[localization_language_index_];
ImGui::SetCursorPosX(connection_status_window_width_ * 6 / 19);
ImGui::SetCursorPosY(connection_status_window_height_ * 2 / 3);
// ok
ImGui::SetWindowFontScale(0.5f);
if (ImGui::Button(localization::ok[localization_language_index_].c_str()) ||
ImGui::IsKeyPressed(ImGuiKey_Enter)) {
delete_connection_ = true;
show_confirm_delete_connection_ = false;
}
ImGui::SameLine();
// cancel
if (ImGui::Button(
localization::cancel[localization_language_index_].c_str()) ||
ImGui::IsKeyPressed(ImGuiKey_Escape)) {
delete_connection_ = false;
show_confirm_delete_connection_ = false;
}
auto window_width = ImGui::GetWindowSize().x;
auto window_height = ImGui::GetWindowSize().y;
auto text_width = ImGui::CalcTextSize(text.c_str()).x;
ImGui::SetCursorPosX((window_width - text_width) * 0.5f);
ImGui::SetCursorPosY(window_height * 0.2f);
ImGui::Text("%s", text.c_str());
ImGui::SetWindowFontScale(1.0f);
ImGui::End();
ImGui::PopStyleVar();
return 0;
}
} // namespace crossdesk
-188
View File
@@ -1,188 +0,0 @@
#include "layout.h"
#include "localization.h"
#include "rd_log.h"
#include "render.h"
namespace crossdesk {
static int InputTextCallback(ImGuiInputTextCallbackData* data);
int Render::RemoteWindow() {
ImGui::SetNextWindowPos(ImVec2(local_window_width_ + 1.0f, title_bar_height_),
ImGuiCond_Always);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f);
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::BeginChild("RemoteDesktopWindow",
ImVec2(remote_window_width_, remote_window_height_),
ImGuiChildFlags_None,
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar |
ImGuiWindowFlags_NoBringToFrontOnFocus);
ImGui::PopStyleColor();
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + main_window_text_y_padding_);
ImGui::Indent(main_child_window_x_padding_ - 1.0f);
ImGui::TextColored(
ImVec4(0.0f, 0.0f, 0.0f, 0.5f), "%s",
localization::remote_desktop[localization_language_index_].c_str());
ImGui::Spacing();
{
ImGui::SetNextWindowPos(
ImVec2(local_window_width_ + main_child_window_x_padding_ - 1.0f,
title_bar_height_ + main_child_window_y_padding_),
ImGuiCond_Always);
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(239.0f / 255, 240.0f / 255,
242.0f / 255, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 10.0f);
ImGui::BeginChild(
"RemoteDesktopWindow_1",
ImVec2(remote_child_window_width_, remote_child_window_height_),
ImGuiChildFlags_Border,
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar |
ImGuiWindowFlags_NoBringToFrontOnFocus);
ImGui::PopStyleVar();
ImGui::PopStyleColor();
{
ImGui::SetWindowFontScale(0.8f);
ImGui::Text(
"%s", localization::remote_id[localization_language_index_].c_str());
ImGui::Spacing();
ImGui::SetNextItemWidth(IPUT_WINDOW_WIDTH);
ImGui::SetWindowFontScale(1.0f);
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f);
if (re_enter_remote_id_) {
ImGui::SetKeyboardFocusHere();
re_enter_remote_id_ = false;
memset(remote_id_display_, 0, sizeof(remote_id_display_));
}
bool enter_pressed = ImGui::InputText(
"##remote_id_", remote_id_display_, IM_ARRAYSIZE(remote_id_display_),
ImGuiInputTextFlags_CharsDecimal |
ImGuiInputTextFlags_EnterReturnsTrue |
ImGuiInputTextFlags_CallbackEdit,
InputTextCallback);
ImGui::PopStyleVar();
ImGui::SameLine();
std::string remote_id = remote_id_display_;
remote_id.erase(remove_if(remote_id.begin(), remote_id.end(),
static_cast<int (*)(int)>(&isspace)),
remote_id.end());
if (ImGui::Button(ICON_FA_ARROW_RIGHT_LONG, ImVec2(55, 38)) ||
enter_pressed) {
connect_button_pressed_ = true;
bool found = false;
for (auto& [id, props] : recent_connections_) {
if (id.find(remote_id) != std::string::npos) {
found = true;
if (client_properties_.find(remote_id) !=
client_properties_.end()) {
if (!client_properties_[remote_id]->connection_established_) {
ConnectTo(props.remote_id, props.password.c_str(), false);
} else {
// todo: show warning message
LOG_INFO("Already connected to [{}]", remote_id);
}
} else {
ConnectTo(props.remote_id, props.password.c_str(), false);
}
}
}
if (!found) {
ConnectTo(remote_id, "", false);
}
}
if (need_to_rejoin_) {
need_to_rejoin_ = false;
for (const auto& [_, props] : client_properties_) {
if (props->rejoin_) {
ConnectTo(props->remote_id_, props->remote_password_,
props->remember_password_);
}
}
}
}
ImGui::EndChild();
}
ImGui::EndChild();
ImGui::PopStyleVar();
return 0;
}
static int InputTextCallback(ImGuiInputTextCallbackData* data) {
if (data->BufTextLen > 3 && data->Buf[3] != ' ') {
data->InsertChars(3, " ");
}
if (data->BufTextLen > 7 && data->Buf[7] != ' ') {
data->InsertChars(7, " ");
}
return 0;
}
int Render::ConnectTo(const std::string& remote_id, const char* password,
bool remember_password) {
LOG_INFO("Connect to [{}]", remote_id);
focused_remote_id_ = remote_id;
if (client_properties_.find(remote_id) == client_properties_.end()) {
client_properties_[remote_id] =
std::make_shared<SubStreamWindowProperties>();
auto props = client_properties_[remote_id];
props->local_id_ = "C-" + std::string(client_id_);
props->remote_id_ = remote_id;
memcpy(&props->params_, &params_, sizeof(Params));
props->params_.user_id = props->local_id_.c_str();
props->peer_ = CreatePeer(&props->params_);
for (auto& display_info : display_info_list_) {
AddVideoStream(peer_, display_info.name.c_str());
}
AddAudioStream(props->peer_, props->audio_label_.c_str());
AddDataStream(props->peer_, props->data_label_.c_str());
if (props->peer_) {
LOG_INFO("[{}] Create peer instance successful", props->local_id_);
Init(props->peer_);
LOG_INFO("[{}] Peer init finish", props->local_id_);
} else {
LOG_INFO("Create peer [{}] instance failed", props->local_id_);
}
props->connection_status_ = ConnectionStatus::Connecting;
}
int ret = -1;
auto props = client_properties_[remote_id];
if (!props->connection_established_) {
props->remember_password_ = remember_password;
if (strcmp(password, "") != 0 &&
strcmp(password, props->remote_password_) != 0) {
strncpy(props->remote_password_, password,
sizeof(props->remote_password_) - 1);
props->remote_password_[sizeof(props->remote_password_) - 1] = '\0';
}
std::string remote_id_with_pwd = remote_id + "@" + password;
ret = JoinConnection(props->peer_, remote_id_with_pwd.c_str());
if (0 == ret) {
props->rejoin_ = false;
} else {
props->rejoin_ = true;
need_to_rejoin_ = true;
}
}
return 0;
}
} // namespace crossdesk
File diff suppressed because it is too large Load Diff
+40
View File
@@ -0,0 +1,40 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-06-23
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _LINUX_TRAY_H_
#define _LINUX_TRAY_H_
#if defined(__linux__) && !defined(__APPLE__)
#include <cstdint>
#include <memory>
#include <string>
struct SDL_Window;
namespace crossdesk {
struct LinuxTrayImpl;
class LinuxTray {
public:
LinuxTray(::SDL_Window* app_window, const std::string& tooltip,
int language_index, uint32_t exit_event_type);
~LinuxTray();
bool MinimizeToTray();
void RemoveTrayIcon();
void ProcessEvents();
private:
std::unique_ptr<LinuxTrayImpl> impl_;
};
} // namespace crossdesk
#endif // defined(__linux__) && !defined(__APPLE__)
#endif // _LINUX_TRAY_H_
+34
View File
@@ -0,0 +1,34 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-06-23
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _MAC_TRAY_H_
#define _MAC_TRAY_H_
#include <memory>
#include <string>
struct SDL_Window;
namespace crossdesk {
struct MacTrayImpl;
class MacTray {
public:
MacTray(::SDL_Window* app_window, const std::string& tooltip,
int language_index);
~MacTray();
void MinimizeToTray();
void RemoveTrayIcon();
private:
std::unique_ptr<MacTrayImpl> impl_;
};
} // namespace crossdesk
#endif // _MAC_TRAY_H_
+249
View File
@@ -0,0 +1,249 @@
#include "mac_tray.h"
#if defined(__APPLE__)
#include <SDL3/SDL.h>
#import <Cocoa/Cocoa.h>
#include "localization.h"
#include <utility>
@interface CrossDeskMacTrayTarget : NSObject
- (instancetype)initWithOwner:(crossdesk::MacTrayImpl *)owner;
- (void)statusItemClicked:(id)sender;
- (void)exitApplication:(id)sender;
@end
namespace crossdesk {
struct MacTrayImpl {
explicit MacTrayImpl(::SDL_Window *window, std::string tray_tooltip,
int language_index_value)
: app_window(window),
tooltip(std::move(tray_tooltip)),
language_index(language_index_value),
target([[CrossDeskMacTrayTarget alloc] initWithOwner:this]) {}
~MacTrayImpl() {
RemoveTrayIcon();
target = nil;
}
void MinimizeToTray() {
EnsureStatusItem();
if (app_window) {
SDL_HideWindow(app_window);
}
}
void RemoveTrayIcon() {
if (!status_item) {
return;
}
[[NSStatusBar systemStatusBar] removeStatusItem:status_item];
status_item = nil;
}
void ShowWindow() {
if (!app_window) {
return;
}
SDL_ShowWindow(app_window);
SDL_RaiseWindow(app_window);
[NSApp activateIgnoringOtherApps:YES];
}
void ShowMenu() {
EnsureStatusItem();
if (!status_item) {
return;
}
NSMenu *menu = [[NSMenu alloc] initWithTitle:@"CrossDesk"];
NSString *exit_title =
NSStringFromUtf8(localization::exit_program
[localization::detail::ClampLanguageIndex(
language_index)]);
NSMenuItem *exit_item = [[NSMenuItem alloc] initWithTitle:exit_title
action:@selector(exitApplication:)
keyEquivalent:@""];
[exit_item setTarget:target];
[menu addItem:exit_item];
NSStatusBarButton *button = [status_item button];
if (!button) {
return;
}
const NSRect bounds = [button bounds];
[menu popUpMenuPositioningItem:nil
atLocation:NSMakePoint(NSMinX(bounds), NSMinY(bounds))
inView:button];
}
void RequestExit() {
SDL_Event event;
event.type = SDL_EVENT_QUIT;
SDL_PushEvent(&event);
}
private:
void EnsureStatusItem() {
if (status_item) {
return;
}
status_item = [[NSStatusBar systemStatusBar]
statusItemWithLength:NSSquareStatusItemLength];
NSStatusBarButton *button = [status_item button];
if (!button) {
return;
}
[button setToolTip:NSStringFromUtf8(tooltip)];
NSImage *crossdesk_icon = LoadCrossDeskIcon();
if (crossdesk_icon) {
NSImage *status_icon = [crossdesk_icon copy];
[status_icon setSize:NSMakeSize(18.0, 18.0)];
[status_icon setTemplate:NO];
[button setImage:status_icon];
[button setImagePosition:NSImageOnly];
} else {
[button setTitle:@"CD"];
}
[button setTarget:target];
[button setAction:@selector(statusItemClicked:)];
[button sendActionOn:NSEventMaskLeftMouseUp | NSEventMaskRightMouseUp];
}
NSString *NSStringFromUtf8(const std::string &text) {
return [NSString stringWithUTF8String:text.c_str()];
}
NSImage *LoadCrossDeskIcon() {
NSImage *icon = LoadIconFromBundleResource(@"crossdesk");
if (!icon) {
icon = LoadIconFromBundleResource(@"crossedesk");
}
if (!icon) {
icon = LoadIconFromDevelopmentPath();
}
if (!icon) {
icon = [NSApp applicationIconImage];
}
return icon;
}
NSImage *LoadIconFromBundleResource(NSString *resource_name) {
NSString *icon_path =
[[NSBundle mainBundle] pathForResource:resource_name ofType:@"icns"];
return LoadIconFromPath(icon_path);
}
NSImage *LoadIconFromDevelopmentPath() {
NSMutableArray<NSString *> *candidate_paths = [NSMutableArray array];
NSString *current_directory =
[[NSFileManager defaultManager] currentDirectoryPath];
[candidate_paths
addObject:[current_directory
stringByAppendingPathComponent:
@"icons/macos/crossdesk.icns"]];
const char *base_path = SDL_GetBasePath();
if (base_path && base_path[0] != '\0') {
NSString *base_directory = NSStringFromUtf8(base_path);
[candidate_paths
addObject:[base_directory
stringByAppendingPathComponent:
@"icons/macos/crossdesk.icns"]];
[candidate_paths
addObject:[base_directory
stringByAppendingPathComponent:
@"../../../../icons/macos/crossdesk.icns"]];
}
for (NSString *candidate_path in candidate_paths) {
NSImage *icon = LoadIconFromPath(
[candidate_path stringByStandardizingPath]);
if (icon) {
return icon;
}
}
return nil;
}
NSImage *LoadIconFromPath(NSString *icon_path) {
if (![icon_path length]) {
return nil;
}
if (![[NSFileManager defaultManager] fileExistsAtPath:icon_path]) {
return nil;
}
return [[NSImage alloc] initWithContentsOfFile:icon_path];
}
::SDL_Window *app_window = nullptr;
std::string tooltip;
int language_index = 0;
NSStatusItem *status_item = nil;
CrossDeskMacTrayTarget *target = nil;
};
MacTray::MacTray(::SDL_Window *app_window, const std::string &tooltip,
int language_index)
: impl_(
std::make_unique<MacTrayImpl>(app_window, tooltip, language_index)) {}
MacTray::~MacTray() = default;
void MacTray::MinimizeToTray() { impl_->MinimizeToTray(); }
void MacTray::RemoveTrayIcon() { impl_->RemoveTrayIcon(); }
} // namespace crossdesk
@implementation CrossDeskMacTrayTarget {
crossdesk::MacTrayImpl *owner_;
}
- (instancetype)initWithOwner:(crossdesk::MacTrayImpl *)owner {
self = [super init];
if (self) {
owner_ = owner;
}
return self;
}
- (void)statusItemClicked:(id)sender {
(void)sender;
if (!owner_) {
return;
}
NSEvent *event = [NSApp currentEvent];
if (event && [event type] == NSEventTypeRightMouseUp) {
owner_->ShowMenu();
return;
}
owner_->ShowWindow();
}
- (void)exitApplication:(id)sender {
(void)sender;
if (owner_) {
owner_->RequestExit();
}
}
@end
#endif // __APPLE__
@@ -89,7 +89,7 @@ bool WinTray::HandleTrayMessage(MSG* msg) {
GetCursorPos(&pt);
HMENU menu = CreatePopupMenu();
AppendMenuW(menu, MF_STRING, 1001,
localization::exit_program[language_index_]);
localization::GetExitProgramLabel(language_index_));
SetForegroundWindow(hwnd_message_only_);
int cmd =
@@ -112,4 +112,4 @@ bool WinTray::HandleTrayMessage(MSG* msg) {
}
return true;
}
} // namespace crossdesk
} // namespace crossdesk
+6 -1455
View File
File diff suppressed because it is too large Load Diff
+18 -482
View File
@@ -1,492 +1,28 @@
/*
* @Author: DI JUNKUN
* @Date: 2024-05-29
* Copyright (c) 2024 by DI JUNKUN, All Rights Reserved.
*/
#ifndef CROSSDESK_GUI_RENDER_H_
#define CROSSDESK_GUI_RENDER_H_
#ifndef _MAIN_WINDOW_H_
#define _MAIN_WINDOW_H_
#include <SDL3/SDL.h>
#include <atomic>
#include <chrono>
#include <fstream>
#include <mutex>
#include <optional>
#include <string>
#include <unordered_map>
#include "IconsFontAwesome6.h"
#include "config_center.h"
#include "device_controller_factory.h"
#include "imgui.h"
#include "imgui_impl_sdl3.h"
#include "imgui_impl_sdlrenderer3.h"
#include "imgui_internal.h"
#include "minirtc.h"
#include "path_manager.h"
#include "screen_capturer_factory.h"
#include "speaker_capturer_factory.h"
#include "thumbnail.h"
#if _WIN32
#include "win_tray.h"
#endif
#include <memory>
namespace crossdesk {
class Render {
public:
struct SubStreamWindowProperties {
Params params_;
PeerPtr* peer_ = nullptr;
std::string audio_label_ = "control_audio";
std::string data_label_ = "control_data";
std::string local_id_ = "";
std::string remote_id_ = "";
bool exit_ = false;
bool signal_connected_ = false;
SignalStatus signal_status_ = SignalStatus::SignalClosed;
bool connection_established_ = false;
bool rejoin_ = false;
bool net_traffic_stats_button_pressed_ = false;
bool mouse_control_button_pressed_ = true;
bool mouse_controller_is_started_ = false;
bool audio_capture_button_pressed_ = true;
bool control_mouse_ = true;
bool streaming_ = false;
bool is_control_bar_in_left_ = true;
bool control_bar_hovered_ = false;
bool display_selectable_hovered_ = false;
bool control_bar_expand_ = true;
bool reset_control_bar_pos_ = false;
bool control_window_width_is_changing_ = false;
bool control_window_height_is_changing_ = false;
bool p2p_mode_ = true;
bool remember_password_ = false;
char remote_password_[7] = "";
float sub_stream_window_width_ = 1280;
float sub_stream_window_height_ = 720;
float control_window_min_width_ = 20;
float control_window_max_width_ = 230;
float control_window_min_height_ = 40;
float control_window_max_height_ = 170;
float control_window_width_ = 230;
float control_window_height_ = 40;
float control_bar_pos_x_ = 0;
float control_bar_pos_y_ = 30;
float mouse_diff_control_bar_pos_x_ = 0;
float mouse_diff_control_bar_pos_y_ = 0;
double control_bar_button_pressed_time_ = 0;
double net_traffic_stats_button_pressed_time_ = 0;
unsigned char* dst_buffer_ = nullptr;
size_t dst_buffer_capacity_ = 0;
float mouse_pos_x_ = 0;
float mouse_pos_y_ = 0;
float mouse_pos_x_last_ = 0;
float mouse_pos_y_last_ = 0;
int texture_width_ = 1280;
int texture_height_ = 720;
int video_width_ = 0;
int video_height_ = 0;
int video_width_last_ = 0;
int video_height_last_ = 0;
int selected_display_ = 0;
size_t video_size_ = 0;
bool tab_selected_ = false;
bool tab_opened_ = true;
std::optional<float> pos_x_before_docked_;
std::optional<float> pos_y_before_docked_;
float render_window_x_ = 0;
float render_window_y_ = 0;
float render_window_width_ = 0;
float render_window_height_ = 0;
std::string fullscreen_button_label_ = "Fullscreen";
std::string net_traffic_stats_button_label_ = "Show Net Traffic Stats";
std::string mouse_control_button_label_ = "Mouse Control";
std::string audio_capture_button_label_ = "Audio Capture";
std::string remote_host_name_ = "";
std::vector<DisplayInfo> display_info_list_;
SDL_Texture* stream_texture_ = nullptr;
uint8_t* argb_buffer_ = nullptr;
int argb_buffer_size_ = 0;
SDL_Rect stream_render_rect_;
SDL_Rect stream_render_rect_last_;
ImVec2 control_window_pos_;
ConnectionStatus connection_status_ = ConnectionStatus::Closed;
TraversalMode traversal_mode_ = TraversalMode::UnknownMode;
int fps_ = 0;
int frame_count_ = 0;
std::chrono::steady_clock::time_point last_time_;
XNetTrafficStats net_traffic_stats_;
};
public:
class GuiApplication;
// Stable application-facing facade. The SDL application and feature
// controllers remain private implementation details.
class Render {
public:
Render();
~Render();
public:
Render(const Render &) = delete;
Render &operator=(const Render &) = delete;
int Run();
private:
void InitializeLogger();
void InitializeSettings();
void InitializeSDL();
void InitializeModules();
void InitializeMainWindow();
void MainLoop();
void UpdateLabels();
void UpdateInteractions();
void HandleRecentConnections();
void HandleStreamWindow();
void Cleanup();
void CleanupFactories();
void CleanupPeer(std::shared_ptr<SubStreamWindowProperties> props);
void CleanupPeers();
void CleanSubStreamWindowProperties(
std::shared_ptr<SubStreamWindowProperties> props);
void UpdateRenderRect();
void ProcessSdlEvent(const SDL_Event& event);
private:
int CreateStreamRenderWindow();
int TitleBar(bool main_window);
int MainWindow();
int StreamWindow();
int LocalWindow();
int RemoteWindow();
int RecentConnectionsWindow();
int SettingWindow();
int SelfHostedServerWindow();
int ShowSimpleFileBrowser();
int ControlWindow(std::shared_ptr<SubStreamWindowProperties>& props);
int ControlBar(std::shared_ptr<SubStreamWindowProperties>& props);
int AboutWindow();
int StatusBar();
bool ConnectionStatusWindow(
std::shared_ptr<SubStreamWindowProperties>& props);
int ShowRecentConnections();
private:
int ConnectTo(const std::string& remote_id, const char* password,
bool remember_password);
int CreateMainWindow();
int DestroyMainWindow();
int CreateStreamWindow();
int DestroyStreamWindow();
int SetupFontAndStyle();
int SetupMainWindow();
int DestroyMainWindowContext();
int SetupStreamWindow();
int DestroyStreamWindowContext();
int DrawMainWindow();
int DrawStreamWindow();
int ConfirmDeleteConnection();
int NetTrafficStats(std::shared_ptr<SubStreamWindowProperties>& props);
void DrawConnectionStatusText(
std::shared_ptr<SubStreamWindowProperties>& props);
public:
static void OnReceiveVideoBufferCb(const XVideoFrame* video_frame,
const char* user_id, size_t user_id_size,
void* user_data);
static void OnReceiveAudioBufferCb(const char* data, size_t size,
const char* user_id, size_t user_id_size,
void* user_data);
static void OnReceiveDataBufferCb(const char* data, size_t size,
const char* user_id, size_t user_id_size,
void* user_data);
static void OnSignalStatusCb(SignalStatus status, const char* user_id,
size_t user_id_size, void* user_data);
static void OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
size_t user_id_size, void* user_data);
static void NetStatusReport(const char* client_id, size_t client_id_size,
TraversalMode mode,
const XNetTrafficStats* net_traffic_stats,
const char* user_id, const size_t user_id_size,
void* user_data);
static SDL_HitTestResult HitTestCallback(SDL_Window* window,
const SDL_Point* area, void* data);
static std::vector<char> SerializeRemoteAction(const RemoteAction& action);
static bool DeserializeRemoteAction(const char* data, size_t size,
RemoteAction& out);
static void FreeRemoteAction(RemoteAction& action);
private:
int SendKeyCommand(int key_code, bool is_down);
int ProcessMouseEvent(const SDL_Event& event);
static void SdlCaptureAudioIn(void* userdata, Uint8* stream, int len);
static void SdlCaptureAudioOut(void* userdata, Uint8* stream, int len);
private:
int SaveSettingsIntoCacheFile();
int LoadSettingsFromCacheFile();
int ScreenCapturerInit();
int StartScreenCapturer();
int StopScreenCapturer();
int StartSpeakerCapturer();
int StopSpeakerCapturer();
int StartMouseController();
int StopMouseController();
int StartKeyboardCapturer();
int StopKeyboardCapturer();
int CreateConnectionPeer();
int AudioDeviceInit();
int AudioDeviceDestroy();
private:
struct CDCache {
char client_id_with_password[17];
int language;
int video_quality;
int video_frame_rate;
int video_encode_format;
bool enable_hardware_video_codec;
bool enable_turn;
bool enable_srtp;
unsigned char key[16];
unsigned char iv[16];
};
private:
CDCache cd_cache_;
std::mutex cd_cache_mutex_;
std::unique_ptr<ConfigCenter> config_center_;
ConfigCenter::LANGUAGE localization_language_ =
ConfigCenter::LANGUAGE::CHINESE;
std::unique_ptr<PathManager> path_manager_;
std::string cert_path_;
std::string exec_log_path_;
std::string dll_log_path_;
std::string cache_path_;
int localization_language_index_ = -1;
int localization_language_index_last_ = -1;
bool modules_inited_ = false;
/* ------ all windows property start ------ */
float title_bar_width_ = 640;
float title_bar_height_ = 30;
/* ------ all windows property end ------ */
/* ------ main window property start ------ */
// thumbnail
unsigned char aes128_key_[16];
unsigned char aes128_iv_[16];
std::unique_ptr<Thumbnail> thumbnail_;
// recent connections
std::vector<std::pair<std::string, Thumbnail::RecentConnection>>
recent_connections_;
int recent_connection_image_width_ = 160;
int recent_connection_image_height_ = 90;
uint32_t recent_connection_image_save_time_ = 0;
// main window render
SDL_Window* main_window_ = nullptr;
SDL_Renderer* main_renderer_ = nullptr;
ImGuiContext* main_ctx_ = nullptr;
bool exit_ = false;
const int sdl_refresh_ms_ = 16; // ~60 FPS
#if _WIN32
std::unique_ptr<WinTray> tray_;
#endif
// main window properties
std::string latest_version_ = "";
bool update_available_ = false;
bool start_mouse_controller_ = false;
bool mouse_controller_is_started_ = false;
bool start_screen_capturer_ = false;
bool screen_capturer_is_started_ = false;
bool start_speaker_capturer_ = false;
bool speaker_capturer_is_started_ = false;
bool start_keyboard_capturer_ = true;
bool show_cursor_ = false;
bool keyboard_capturer_is_started_ = false;
bool foucs_on_main_window_ = false;
bool foucs_on_stream_window_ = false;
bool audio_capture_ = false;
int main_window_width_real_ = 720;
int main_window_height_real_ = 540;
float main_window_dpi_scaling_w_ = 1.0f;
float main_window_dpi_scaling_h_ = 1.0f;
float main_window_width_default_ = 640;
float main_window_height_default_ = 480;
float main_window_width_ = 640;
float main_window_height_ = 480;
float main_window_width_last_ = 640;
float main_window_height_last_ = 480;
float local_window_width_ = 320;
float local_window_height_ = 235;
float remote_window_width_ = 320;
float remote_window_height_ = 235;
float local_child_window_width_ = 266;
float local_child_window_height_ = 180;
float remote_child_window_width_ = 266;
float remote_child_window_height_ = 180;
float main_window_text_y_padding_ = 10;
float main_child_window_x_padding_ = 27;
float main_child_window_y_padding_ = 45;
float status_bar_height_ = 22;
float connection_status_window_width_ = 200;
float connection_status_window_height_ = 150;
float notification_window_width_ = 200;
float notification_window_height_ = 80;
float about_window_width_ = 300;
float about_window_height_ = 170;
int screen_width_ = 1280;
int screen_height_ = 720;
int selected_display_ = 0;
std::string connect_button_label_ = "Connect";
char input_password_tmp_[7] = "";
char input_password_[7] = "";
std::string random_password_ = "";
char new_password_[7] = "";
char remote_id_display_[12] = "";
unsigned char audio_buffer_[720];
int audio_len_ = 0;
bool audio_buffer_fresh_ = false;
bool need_to_rejoin_ = false;
bool just_created_ = false;
std::string controlled_remote_id_ = "";
std::string focused_remote_id_ = "";
bool need_to_send_host_info_ = false;
SDL_Event last_mouse_event;
SDL_AudioStream* output_stream_;
uint32_t STREAM_REFRESH_EVENT = 0;
// stream window render
SDL_Window* stream_window_ = nullptr;
SDL_Renderer* stream_renderer_ = nullptr;
ImGuiContext* stream_ctx_ = nullptr;
// stream window properties
bool need_to_create_stream_window_ = false;
bool stream_window_created_ = false;
bool stream_window_inited_ = false;
bool window_maximized_ = false;
bool stream_window_grabbed_ = false;
bool control_mouse_ = false;
int stream_window_width_default_ = 1280;
int stream_window_height_default_ = 720;
float stream_window_width_ = 1280;
float stream_window_height_ = 720;
SDL_PixelFormat stream_pixformat_ = SDL_PIXELFORMAT_NV12;
int stream_window_width_real_ = 1280;
int stream_window_height_real_ = 720;
float stream_window_dpi_scaling_w_ = 1.0f;
float stream_window_dpi_scaling_h_ = 1.0f;
bool label_inited_ = false;
bool connect_button_pressed_ = false;
bool password_validating_ = false;
uint32_t password_validating_time_ = 0;
bool show_settings_window_ = false;
bool show_self_hosted_server_config_window_ = false;
bool rejoin_ = false;
bool local_id_copied_ = false;
bool show_password_ = true;
bool show_about_window_ = false;
bool show_connection_status_window_ = false;
bool show_reset_password_window_ = false;
bool fullscreen_button_pressed_ = false;
bool focus_on_input_widget_ = true;
bool is_client_mode_ = false;
bool reload_recent_connections_ = true;
bool show_confirm_delete_connection_ = false;
bool delete_connection_ = false;
bool is_tab_bar_hovered_ = false;
std::string delete_connection_name_ = "";
bool re_enter_remote_id_ = false;
double copy_start_time_ = 0;
SignalStatus signal_status_ = SignalStatus::SignalClosed;
std::string signal_status_str_ = "";
bool signal_connected_ = false;
PeerPtr* peer_ = nullptr;
PeerPtr* peer_reserved_ = nullptr;
std::string video_primary_label_ = "primary_display";
std::string video_secondary_label_ = "secondary_display";
std::string audio_label_ = "audio";
std::string data_label_ = "data";
Params params_;
SDL_AudioDeviceID input_dev_;
SDL_AudioDeviceID output_dev_;
ScreenCapturerFactory* screen_capturer_factory_ = nullptr;
ScreenCapturer* screen_capturer_ = nullptr;
SpeakerCapturerFactory* speaker_capturer_factory_ = nullptr;
SpeakerCapturer* speaker_capturer_ = nullptr;
DeviceControllerFactory* device_controller_factory_ = nullptr;
MouseController* mouse_controller_ = nullptr;
KeyboardCapturer* keyboard_capturer_ = nullptr;
std::vector<DisplayInfo> display_info_list_;
uint64_t last_frame_time_;
bool show_new_version_icon_ = false;
bool show_new_version_icon_in_menu_ = true;
uint64_t new_version_icon_last_trigger_time_ = 0;
uint64_t new_version_icon_render_start_time_ = 0;
char client_id_[10] = "";
char client_id_display_[12] = "";
char client_id_with_password_[17] = "";
char password_saved_[7] = "";
int language_button_value_ = 0;
int video_quality_button_value_ = 0;
int video_frame_rate_button_value_ = 1;
int video_encode_format_button_value_ = 0;
bool enable_hardware_video_codec_ = false;
bool enable_turn_ = true;
bool enable_srtp_ = false;
char signal_server_ip_[256] = "api.crossdesk.cn";
char signal_server_port_[6] = "9099";
char coturn_server_port_[6] = "3478";
char cert_file_path_[256] = "";
bool enable_self_hosted_ = false;
int language_button_value_last_ = 0;
int video_quality_button_value_last_ = 0;
int video_frame_rate_button_value_last_ = 0;
int video_encode_format_button_value_last_ = 0;
bool enable_hardware_video_codec_last_ = false;
bool enable_turn_last_ = true;
bool enable_srtp_last_ = false;
bool enable_self_hosted_last_ = false;
bool enable_autostart_ = false;
bool enable_autostart_last_ = false;
bool enable_daemon_ = false;
bool enable_daemon_last_ = false;
bool enable_minimize_to_tray_ = false;
bool enable_minimize_to_tray_last_ = false;
char signal_server_ip_self_[256] = "";
char signal_server_port_self_[6] = "";
char coturn_server_port_self_[6] = "";
std::string tls_cert_path_self_ = "";
bool settings_window_pos_reset_ = true;
bool self_hosted_server_config_window_pos_reset_ = true;
std::string selected_current_file_path_ = "";
bool show_file_browser_ = true;
/* ------ main window property end ------ */
/* ------ sub stream window property start ------ */
std::unordered_map<std::string, std::shared_ptr<SubStreamWindowProperties>>
client_properties_;
void CloseTab(decltype(client_properties_)::iterator& it);
/* ------ stream window property end ------ */
/* ------ server mode ------ */
std::unordered_map<std::string, ConnectionStatus> connection_status_;
private:
std::unique_ptr<GuiApplication> application_;
};
} // namespace crossdesk
#endif
} // namespace crossdesk
#endif // CROSSDESK_GUI_RENDER_H_
-584
View File
@@ -1,584 +0,0 @@
#include <cmath>
#include "device_controller.h"
#include "localization.h"
#include "platform.h"
#include "rd_log.h"
#include "render.h"
#define NV12_BUFFER_SIZE 1280 * 720 * 3 / 2
namespace crossdesk {
int Render::SendKeyCommand(int key_code, bool is_down) {
RemoteAction remote_action;
remote_action.type = ControlType::keyboard;
if (is_down) {
remote_action.k.flag = KeyFlag::key_down;
} else {
remote_action.k.flag = KeyFlag::key_up;
}
remote_action.k.key_value = key_code;
if (!controlled_remote_id_.empty()) {
if (client_properties_.find(controlled_remote_id_) !=
client_properties_.end()) {
auto props = client_properties_[controlled_remote_id_];
if (props->connection_status_ == ConnectionStatus::Connected) {
std::string msg = remote_action.to_json();
SendDataFrame(props->peer_, msg.c_str(), msg.size(),
props->data_label_.c_str());
}
}
}
return 0;
}
int Render::ProcessMouseEvent(const SDL_Event& event) {
controlled_remote_id_ = "";
int video_width, video_height = 0;
int render_width, render_height = 0;
float ratio_x, ratio_y = 0;
RemoteAction remote_action;
for (auto& it : client_properties_) {
auto props = it.second;
if (!props->control_mouse_) {
continue;
}
if (event.button.x >= props->stream_render_rect_.x &&
event.button.x <=
props->stream_render_rect_.x + props->stream_render_rect_.w &&
event.button.y >= props->stream_render_rect_.y &&
event.button.y <=
props->stream_render_rect_.y + props->stream_render_rect_.h) {
controlled_remote_id_ = it.first;
render_width = props->stream_render_rect_.w;
render_height = props->stream_render_rect_.h;
last_mouse_event.button.x = event.button.x;
last_mouse_event.button.y = event.button.y;
remote_action.m.x =
(float)(event.button.x - props->stream_render_rect_.x) / render_width;
remote_action.m.y =
(float)(event.button.y - props->stream_render_rect_.y) /
render_height;
if (SDL_EVENT_MOUSE_BUTTON_DOWN == event.type) {
remote_action.type = ControlType::mouse;
if (SDL_BUTTON_LEFT == event.button.button) {
remote_action.m.flag = MouseFlag::left_down;
} else if (SDL_BUTTON_RIGHT == event.button.button) {
remote_action.m.flag = MouseFlag::right_down;
} else if (SDL_BUTTON_MIDDLE == event.button.button) {
remote_action.m.flag = MouseFlag::middle_down;
}
} else if (SDL_EVENT_MOUSE_BUTTON_UP == event.type) {
remote_action.type = ControlType::mouse;
if (SDL_BUTTON_LEFT == event.button.button) {
remote_action.m.flag = MouseFlag::left_up;
} else if (SDL_BUTTON_RIGHT == event.button.button) {
remote_action.m.flag = MouseFlag::right_up;
} else if (SDL_BUTTON_MIDDLE == event.button.button) {
remote_action.m.flag = MouseFlag::middle_up;
}
} else if (SDL_EVENT_MOUSE_MOTION == event.type) {
remote_action.type = ControlType::mouse;
remote_action.m.flag = MouseFlag::move;
}
if (props->control_bar_hovered_ || props->display_selectable_hovered_) {
remote_action.m.flag = MouseFlag::move;
}
std::string msg = remote_action.to_json();
SendDataFrame(props->peer_, msg.c_str(), msg.size(),
props->data_label_.c_str());
} else if (SDL_EVENT_MOUSE_WHEEL == event.type &&
last_mouse_event.button.x >= props->stream_render_rect_.x &&
last_mouse_event.button.x <= props->stream_render_rect_.x +
props->stream_render_rect_.w &&
last_mouse_event.button.y >= props->stream_render_rect_.y &&
last_mouse_event.button.y <= props->stream_render_rect_.y +
props->stream_render_rect_.h) {
float scroll_x = event.wheel.x;
float scroll_y = event.wheel.y;
if (event.wheel.direction == SDL_MOUSEWHEEL_FLIPPED) {
scroll_x = -scroll_x;
scroll_y = -scroll_y;
}
remote_action.type = ControlType::mouse;
auto roundUp = [](float value) -> int {
if (value > 0) {
return static_cast<int>(std::ceil(value));
} else if (value < 0) {
return static_cast<int>(std::floor(value));
}
return 0;
};
if (std::abs(scroll_y) >= std::abs(scroll_x)) {
remote_action.m.flag = MouseFlag::wheel_vertical;
remote_action.m.s = roundUp(scroll_y);
} else {
remote_action.m.flag = MouseFlag::wheel_horizontal;
remote_action.m.s = roundUp(scroll_x);
}
render_width = props->stream_render_rect_.w;
render_height = props->stream_render_rect_.h;
remote_action.m.x =
(float)(last_mouse_event.button.x - props->stream_render_rect_.x) /
render_width;
remote_action.m.y =
(float)(last_mouse_event.button.y - props->stream_render_rect_.y) /
render_height;
std::string msg = remote_action.to_json();
SendDataFrame(props->peer_, msg.c_str(), msg.size(),
props->data_label_.c_str());
}
}
return 0;
}
void Render::SdlCaptureAudioIn(void* userdata, Uint8* stream, int len) {
Render* render = (Render*)userdata;
if (!render) {
return;
}
if (1) {
for (auto it : render->client_properties_) {
auto props = it.second;
if (props->connection_status_ == ConnectionStatus::Connected) {
SendAudioFrame(props->peer_, (const char*)stream, len,
render->audio_label_.c_str());
}
}
} else {
memcpy(render->audio_buffer_, stream, len);
render->audio_len_ = len;
SDL_Delay(10);
render->audio_buffer_fresh_ = true;
}
}
void Render::SdlCaptureAudioOut([[maybe_unused]] void* userdata,
[[maybe_unused]] Uint8* stream,
[[maybe_unused]] int len) {
// Render *render = (Render *)userdata;
// for (auto it : render->client_properties_) {
// auto props = it.second;
// if (props->connection_status_ == SignalStatus::SignalConnected) {
// SendAudioFrame(props->peer_, (const char *)stream, len);
// }
// }
// if (!render->audio_buffer_fresh_) {
// return;
// }
// SDL_memset(stream, 0, len);
// if (render->audio_len_ == 0) {
// return;
// } else {
// }
// len = (len > render->audio_len_ ? render->audio_len_ : len);
// SDL_MixAudioFormat(stream, render->audio_buffer_, AUDIO_S16LSB, len,
// SDL_MIX_MAXVOLUME);
// render->audio_buffer_fresh_ = false;
}
void Render::OnReceiveVideoBufferCb(const XVideoFrame* video_frame,
const char* user_id, size_t user_id_size,
void* user_data) {
Render* render = (Render*)user_data;
if (!render) {
return;
}
std::string remote_id(user_id, user_id_size);
if (render->client_properties_.find(remote_id) ==
render->client_properties_.end()) {
return;
}
SubStreamWindowProperties* props =
render->client_properties_.find(remote_id)->second.get();
if (props->connection_established_) {
if (!props->dst_buffer_) {
props->dst_buffer_capacity_ = video_frame->size;
props->dst_buffer_ = new unsigned char[video_frame->size];
}
if (props->dst_buffer_capacity_ < video_frame->size) {
delete props->dst_buffer_;
props->dst_buffer_capacity_ = video_frame->size;
props->dst_buffer_ = new unsigned char[video_frame->size];
}
memcpy(props->dst_buffer_, video_frame->data, video_frame->size);
bool need_to_update_render_rect = false;
if (props->video_width_ != props->video_width_last_ ||
props->video_height_ != props->video_height_last_) {
need_to_update_render_rect = true;
props->video_width_last_ = props->video_width_;
props->video_height_last_ = props->video_height_;
}
props->video_width_ = video_frame->width;
props->video_height_ = video_frame->height;
props->video_size_ = video_frame->size;
if (need_to_update_render_rect) {
render->UpdateRenderRect();
}
SDL_Event event;
event.type = render->STREAM_REFRESH_EVENT;
event.user.data1 = props;
SDL_PushEvent(&event);
props->streaming_ = true;
if (props->net_traffic_stats_button_pressed_) {
props->frame_count_++;
auto now = std::chrono::steady_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
now - props->last_time_)
.count();
if (elapsed >= 1000) {
props->fps_ = props->frame_count_ * 1000 / elapsed;
props->frame_count_ = 0;
props->last_time_ = now;
}
}
}
}
void Render::OnReceiveAudioBufferCb(const char* data, size_t size,
const char* user_id, size_t user_id_size,
void* user_data) {
Render* render = (Render*)user_data;
if (!render) {
return;
}
render->audio_buffer_fresh_ = true;
if (render->output_stream_) {
int pushed = SDL_PutAudioStreamData(
render->output_stream_, (const Uint8*)data, static_cast<int>(size));
if (pushed < 0) {
LOG_ERROR("Failed to push audio data: {}", SDL_GetError());
}
}
}
void Render::OnReceiveDataBufferCb(const char* data, size_t size,
const char* user_id, size_t user_id_size,
void* user_data) {
Render* render = (Render*)user_data;
if (!render) {
return;
}
std::string json_str(data, size);
RemoteAction remote_action;
try {
remote_action.from_json(json_str);
} catch (const std::exception& e) {
LOG_ERROR("Failed to parse RemoteAction JSON: {}", e.what());
return;
}
std::string remote_id(user_id, user_id_size);
if (render->client_properties_.find(remote_id) !=
render->client_properties_.end()) {
// local
auto props = render->client_properties_.find(remote_id)->second;
if (remote_action.type == ControlType::host_infomation &&
props->remote_host_name_.empty()) {
props->remote_host_name_ = std::string(remote_action.i.host_name,
remote_action.i.host_name_size);
LOG_INFO("Remote hostname: [{}]", props->remote_host_name_);
for (int i = 0; i < remote_action.i.display_num; i++) {
props->display_info_list_.push_back(
DisplayInfo(remote_action.i.display_list[i],
remote_action.i.left[i], remote_action.i.top[i],
remote_action.i.right[i], remote_action.i.bottom[i]));
}
}
FreeRemoteAction(remote_action);
} else {
// remote
if (remote_action.type == ControlType::mouse && render->mouse_controller_) {
render->mouse_controller_->SendMouseCommand(remote_action,
render->selected_display_);
} else if (remote_action.type == ControlType::audio_capture) {
if (remote_action.a && !render->start_speaker_capturer_)
render->StartSpeakerCapturer();
else if (!remote_action.a && render->start_speaker_capturer_)
render->StopSpeakerCapturer();
} else if (remote_action.type == ControlType::keyboard &&
render->keyboard_capturer_) {
render->keyboard_capturer_->SendKeyboardCommand(
(int)remote_action.k.key_value,
remote_action.k.flag == KeyFlag::key_down);
} else if (remote_action.type == ControlType::display_id &&
render->screen_capturer_) {
render->selected_display_ = remote_action.d;
render->screen_capturer_->SwitchTo(remote_action.d);
}
}
}
void Render::OnSignalStatusCb(SignalStatus status, const char* user_id,
size_t user_id_size, void* user_data) {
Render* render = (Render*)user_data;
if (!render) {
return;
}
std::string client_id(user_id, user_id_size);
if (client_id == render->client_id_) {
render->signal_status_ = status;
if (SignalStatus::SignalConnecting == status) {
render->signal_connected_ = false;
} else if (SignalStatus::SignalConnected == status) {
render->signal_connected_ = true;
LOG_INFO("[{}] connected to signal server", client_id);
} else if (SignalStatus::SignalFailed == status) {
render->signal_connected_ = false;
} else if (SignalStatus::SignalClosed == status) {
render->signal_connected_ = false;
} else if (SignalStatus::SignalReconnecting == status) {
render->signal_connected_ = false;
} else if (SignalStatus::SignalServerClosed == status) {
render->signal_connected_ = false;
}
} else {
if (client_id.rfind("C-", 0) != 0) {
return;
}
std::string remote_id(client_id.begin() + 2, client_id.end());
if (render->client_properties_.find(remote_id) ==
render->client_properties_.end()) {
return;
}
auto props = render->client_properties_.find(remote_id)->second;
props->signal_status_ = status;
if (SignalStatus::SignalConnecting == status) {
props->signal_connected_ = false;
} else if (SignalStatus::SignalConnected == status) {
props->signal_connected_ = true;
LOG_INFO("[{}] connected to signal server", remote_id);
} else if (SignalStatus::SignalFailed == status) {
props->signal_connected_ = false;
} else if (SignalStatus::SignalClosed == status) {
props->signal_connected_ = false;
} else if (SignalStatus::SignalReconnecting == status) {
props->signal_connected_ = false;
} else if (SignalStatus::SignalServerClosed == status) {
props->signal_connected_ = false;
}
}
}
void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
const size_t user_id_size, void* user_data) {
Render* render = (Render*)user_data;
if (!render) return;
std::string remote_id(user_id, user_id_size);
auto it = render->client_properties_.find(remote_id);
auto props = (it != render->client_properties_.end()) ? it->second : nullptr;
if (props) {
render->is_client_mode_ = true;
render->show_connection_status_window_ = true;
props->connection_status_ = status;
switch (status) {
case ConnectionStatus::Connected: {
if (!render->need_to_create_stream_window_ &&
!render->client_properties_.empty()) {
render->need_to_create_stream_window_ = true;
}
props->connection_established_ = true;
props->stream_render_rect_ = {
0, (int)render->title_bar_height_,
(int)render->stream_window_width_,
(int)(render->stream_window_height_ - render->title_bar_height_)};
break;
}
case ConnectionStatus::Disconnected:
case ConnectionStatus::Failed:
case ConnectionStatus::Closed: {
props->connection_established_ = false;
props->mouse_control_button_pressed_ = false;
if (props->dst_buffer_) {
memset(props->dst_buffer_, 0, props->dst_buffer_capacity_);
SDL_UpdateTexture(props->stream_texture_, NULL, props->dst_buffer_,
props->texture_width_);
}
render->CleanSubStreamWindowProperties(props);
break;
}
case ConnectionStatus::IncorrectPassword: {
render->password_validating_ = false;
render->password_validating_time_++;
if (render->connect_button_pressed_) {
render->connect_button_pressed_ = false;
props->connection_established_ = false;
render->connect_button_label_ =
localization::connect[render->localization_language_index_];
}
break;
}
case ConnectionStatus::NoSuchTransmissionId: {
if (render->connect_button_pressed_) {
props->connection_established_ = false;
render->connect_button_label_ =
localization::connect[render->localization_language_index_];
}
break;
}
default:
break;
}
} else {
render->is_client_mode_ = false;
render->show_connection_status_window_ = true;
render->connection_status_[remote_id] = status;
switch (status) {
case ConnectionStatus::Connected: {
render->need_to_send_host_info_ = true;
render->start_screen_capturer_ = true;
render->start_speaker_capturer_ = true;
#ifdef CROSSDESK_DEBUG
render->start_mouse_controller_ = false;
render->start_keyboard_capturer_ = false;
#else
render->start_mouse_controller_ = true;
#endif
if (std::all_of(render->connection_status_.begin(),
render->connection_status_.end(), [](const auto& kv) {
return kv.first.find("web") != std::string::npos;
})) {
render->show_cursor_ = true;
}
break;
}
case ConnectionStatus::Closed: {
if (std::all_of(render->connection_status_.begin(),
render->connection_status_.end(), [](const auto& kv) {
return kv.second == ConnectionStatus::Closed ||
kv.second == ConnectionStatus::Failed ||
kv.second == ConnectionStatus::Disconnected;
})) {
render->start_screen_capturer_ = false;
render->start_speaker_capturer_ = false;
render->start_mouse_controller_ = false;
render->start_keyboard_capturer_ = false;
render->need_to_send_host_info_ = false;
if (props) props->connection_established_ = false;
if (render->audio_capture_) {
render->StopSpeakerCapturer();
render->audio_capture_ = false;
}
render->connection_status_.erase(remote_id);
}
if (std::all_of(render->connection_status_.begin(),
render->connection_status_.end(), [](const auto& kv) {
return kv.first.find("web") == std::string::npos;
})) {
render->show_cursor_ = false;
}
break;
}
default:
break;
}
}
}
void Render::NetStatusReport(const char* client_id, size_t client_id_size,
TraversalMode mode,
const XNetTrafficStats* net_traffic_stats,
const char* user_id, const size_t user_id_size,
void* user_data) {
Render* render = (Render*)user_data;
if (!render) {
return;
}
if (strchr(client_id, '@') != nullptr && strchr(user_id, '-') == nullptr) {
std::string id, password;
const char* at_pos = strchr(client_id, '@');
if (at_pos == nullptr) {
id = client_id;
password.clear();
} else {
id.assign(client_id, at_pos - client_id);
password = at_pos + 1;
}
memset(&render->client_id_, 0, sizeof(render->client_id_));
strncpy(render->client_id_, id.c_str(), sizeof(render->client_id_) - 1);
render->client_id_[sizeof(render->client_id_) - 1] = '\0';
memset(&render->password_saved_, 0, sizeof(render->password_saved_));
strncpy(render->password_saved_, password.c_str(),
sizeof(render->password_saved_) - 1);
render->password_saved_[sizeof(render->password_saved_) - 1] = '\0';
memset(&render->client_id_with_password_, 0,
sizeof(render->client_id_with_password_));
strncpy(render->client_id_with_password_, client_id,
sizeof(render->client_id_with_password_) - 1);
render->client_id_with_password_[sizeof(render->client_id_with_password_) -
1] = '\0';
LOG_INFO("Use client id [{}] and save id into cache file", id);
render->SaveSettingsIntoCacheFile();
}
std::string remote_id(user_id, user_id_size);
if (render->client_properties_.find(remote_id) ==
render->client_properties_.end()) {
return;
}
auto props = render->client_properties_.find(remote_id)->second;
if (props->traversal_mode_ != mode) {
props->traversal_mode_ = mode;
LOG_INFO("Net mode: [{}]", int(props->traversal_mode_));
}
if (!net_traffic_stats) {
return;
}
// only display client side net status if connected to itself
if (!(render->peer_reserved_ && !strstr(client_id, "C-"))) {
props->net_traffic_stats_ = *net_traffic_stats;
}
}
} // namespace crossdesk
+439
View File
@@ -0,0 +1,439 @@
#include "runtime/gui_runtime.h"
#include <algorithm>
#include <chrono>
#include <cstring>
#include <memory>
#include <mutex>
#include <shared_mutex>
#include <string>
#include <thread>
#include <vector>
#include "localization.h"
#include "rd_log.h"
namespace crossdesk {
namespace {
constexpr auto kPresenceProbeTimeout = std::chrono::seconds(5);
constexpr auto kConnectionAttemptTimeout = std::chrono::seconds(20);
bool IsConnectionAttemptPending(ConnectionStatus status) {
return status == ConnectionStatus::Connecting ||
status == ConnectionStatus::Gathering;
}
} // namespace
void GuiRuntime::HandleConnectionStatusChange() {
if (signal_connected_ && peer_ && need_to_send_recent_connections_) {
if (!recent_connection_ids_.empty()) {
nlohmann::json j;
j["type"] = "recent_connections_presence";
j["user_id"] = client_id_;
j["devices"] = nlohmann::json::array();
for (const auto &id : recent_connection_ids_) {
std::string pure_id = id;
size_t pos_y = pure_id.find('Y');
size_t pos_n = pure_id.find('N');
size_t pos = std::string::npos;
if (pos_y != std::string::npos &&
(pos_n == std::string::npos || pos_y < pos_n)) {
pos = pos_y;
} else if (pos_n != std::string::npos) {
pos = pos_n;
}
if (pos != std::string::npos) {
pure_id = pure_id.substr(0, pos);
}
j["devices"].push_back(pure_id);
}
auto s = j.dump();
SendSignalMessage(peer_, s.data(), s.size());
}
}
need_to_send_recent_connections_ = false;
}
void GuiRuntime::HandlePendingPresenceProbe() {
bool has_action = false;
bool should_connect = false;
bool remember_password = false;
std::string remote_id;
std::string password;
{
std::lock_guard<std::mutex> lock(pending_presence_probe_mutex_);
if (!pending_presence_probe_ || !pending_presence_result_ready_) {
return;
}
has_action = true;
should_connect = pending_presence_online_;
remote_id = pending_presence_remote_id_;
password = pending_presence_password_;
remember_password = pending_presence_remember_password_;
pending_presence_probe_ = false;
pending_presence_result_ready_ = false;
pending_presence_online_ = false;
pending_presence_remote_id_.clear();
pending_presence_password_.clear();
pending_presence_remember_password_ = false;
}
if (!has_action) {
return;
}
if (should_connect) {
ConnectTo(remote_id, password.c_str(), remember_password, true);
return;
}
offline_warning_text_ =
localization::device_offline[localization_language_index_];
show_offline_warning_window_ = true;
}
void GuiRuntime::HandleConnectionTimeouts() {
const auto now = std::chrono::steady_clock::now();
bool presence_probe_timed_out = false;
std::string presence_remote_id;
{
std::lock_guard<std::mutex> lock(pending_presence_probe_mutex_);
if (pending_presence_probe_ && !pending_presence_result_ready_ &&
now - pending_presence_probe_started_at_ >= kPresenceProbeTimeout) {
presence_probe_timed_out = true;
presence_remote_id = pending_presence_remote_id_;
pending_presence_probe_ = false;
pending_presence_result_ready_ = false;
pending_presence_online_ = false;
pending_presence_remote_id_.clear();
pending_presence_password_.clear();
pending_presence_remember_password_ = false;
}
}
if (presence_probe_timed_out) {
offline_warning_text_ =
localization::device_offline[localization_language_index_];
show_offline_warning_window_ = true;
LOG_WARN("Presence probe timed out for [{}]", presence_remote_id);
}
bool rejoin_state_changed = false;
for (auto &[_, props] : remote_sessions_) {
if (!props || !props->connection_attempt_active_.load()) {
continue;
}
const ConnectionStatus status = props->connection_status_.load();
if (!IsConnectionAttemptPending(status)) {
props->connection_attempt_active_.store(false);
continue;
}
if (now - props->connection_attempt_started_at_ <
kConnectionAttemptTimeout) {
continue;
}
LOG_WARN("Connection to [{}] timed out, status={}", props->remote_id_,
static_cast<int>(status));
props->connection_attempt_active_.store(false);
props->connection_established_ = false;
props->rejoin_ = false;
props->connection_status_.store(ConnectionStatus::Failed);
focused_remote_id_ = props->remote_id_;
show_connection_status_window_ = true;
rejoin_state_changed = true;
}
if (rejoin_state_changed) {
need_to_rejoin_ = false;
for (const auto &[_, props] : remote_sessions_) {
if (props && props->rejoin_) {
need_to_rejoin_ = true;
break;
}
}
}
}
int GuiRuntime::RequestSingleDevicePresence(const std::string &remote_id,
const char *password,
bool remember_password) {
if (!signal_connected_ || !peer_) {
return -1;
}
{
std::lock_guard<std::mutex> lock(pending_presence_probe_mutex_);
pending_presence_probe_ = true;
pending_presence_result_ready_ = false;
pending_presence_online_ = false;
pending_presence_probe_started_at_ = std::chrono::steady_clock::now();
pending_presence_remote_id_ = remote_id;
pending_presence_password_ = password ? password : "";
pending_presence_remember_password_ = remember_password;
}
nlohmann::json j;
j["type"] = "recent_connections_presence";
j["user_id"] = client_id_;
j["devices"] = nlohmann::json::array({remote_id});
auto s = j.dump();
int ret = SendSignalMessage(peer_, s.data(), s.size());
if (ret != 0) {
std::lock_guard<std::mutex> lock(pending_presence_probe_mutex_);
pending_presence_probe_ = false;
pending_presence_result_ready_ = false;
pending_presence_online_ = false;
pending_presence_remote_id_.clear();
pending_presence_password_.clear();
pending_presence_remember_password_ = false;
}
return ret;
}
void GuiRuntime::CloseRemoteSession(std::shared_ptr<RemoteSession> props) {
SDL_FlushEvent(STREAM_REFRESH_EVENT);
std::shared_ptr<std::vector<unsigned char>> frame_snapshot;
int video_width = 0;
int video_height = 0;
{
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
frame_snapshot = props->front_frame_;
video_width = props->video_width_;
video_height = props->video_height_;
}
if (frame_snapshot && !frame_snapshot->empty() && video_width > 0 &&
video_height > 0) {
std::vector<unsigned char> buffer_copy(*frame_snapshot);
std::string remote_id = props->remote_id_;
std::string remote_host_name = props->remote_host_name_;
std::string password =
props->remember_password_ ? props->remote_password_ : "";
std::thread save_thread([buffer_copy, video_width, video_height, remote_id,
remote_host_name, password,
thumbnail = thumbnail_]() {
thumbnail->SaveToThumbnail((char *)buffer_copy.data(), video_width,
video_height, remote_id, remote_host_name,
password);
});
{
std::lock_guard<std::mutex> lock(thumbnail_save_threads_mutex_);
thumbnail_save_threads_.emplace_back(std::move(save_thread));
}
}
if (props->peer_) {
LOG_INFO("[{}] Leave connection [{}]", props->local_id_, props->remote_id_);
LeaveConnection(props->peer_, props->remote_id_.c_str());
LOG_INFO("Destroy peer [{}]", props->local_id_);
DestroyPeer(&props->peer_);
}
}
void GuiRuntime::CloseAllRemoteSessions() {
if (peer_) {
LOG_INFO("[{}] Leave connection [{}]", client_id_, client_id_);
LeaveConnection(peer_, client_id_);
is_client_mode_ = false;
devices_.StopMouseController();
devices_.StopScreenCapturer();
devices_.StopSpeakerCapturer();
devices_.StopKeyboardCapturer();
LOG_INFO("Destroy peer [{}]", client_id_);
DestroyPeer(&peer_);
}
{
// std::shared_lock lock(remote_sessions_mutex_);
for (auto &it : remote_sessions_) {
auto props = it.second;
CloseRemoteSession(props);
}
}
{
// std::unique_lock lock(remote_sessions_mutex_);
remote_sessions_.clear();
}
}
void GuiRuntime::WaitForThumbnailSaveTasks() {
std::vector<std::thread> threads_to_join;
{
std::lock_guard<std::mutex> lock(thumbnail_save_threads_mutex_);
threads_to_join.swap(thumbnail_save_threads_);
}
if (threads_to_join.empty()) {
return;
}
for (auto &thread : threads_to_join) {
if (thread.joinable()) {
thread.join();
}
}
}
void GuiRuntime::ResetRemoteSessionResources(
std::shared_ptr<RemoteSession> props) {
if (props->stream_texture_) {
SDL_DestroyTexture(props->stream_texture_);
props->stream_texture_ = nullptr;
}
{
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
props->front_frame_.reset();
props->back_frame_.reset();
props->video_width_ = 0;
props->video_height_ = 0;
props->video_size_ = 0;
props->render_rect_dirty_ = true;
props->stream_cleanup_pending_ = false;
}
}
std::shared_ptr<GuiRuntime::RemoteSession>
GuiRuntime::FindRemoteSession(
const std::string &remote_id) {
if (remote_id.empty()) {
return nullptr;
}
std::shared_lock lock(remote_sessions_mutex_);
auto it = remote_sessions_.find(remote_id);
if (it == remote_sessions_.end()) {
return nullptr;
}
return it->second;
}
int GuiRuntime::ConnectTo(const std::string &remote_id, const char *password,
bool remember_password, bool bypass_presence_check) {
if (!bypass_presence_check && !device_presence_cache_.IsOnline(remote_id)) {
int ret =
RequestSingleDevicePresence(remote_id, password, remember_password);
if (ret != 0) {
offline_warning_text_ =
localization::device_offline[localization_language_index_];
show_offline_warning_window_ = true;
LOG_WARN("Presence probe failed for [{}], ret={}", remote_id, ret);
} else {
LOG_INFO("Presence probe requested for [{}] before connect", remote_id);
}
return -1;
}
LOG_INFO("Connect to [{}]", remote_id);
focused_remote_id_ = remote_id;
// std::shared_lock shared_lock(remote_sessions_mutex_);
bool exists =
(remote_sessions_.find(remote_id) != remote_sessions_.end());
// shared_lock.unlock();
if (!exists) {
PeerPtr *peer_to_init = nullptr;
std::string local_id;
{
// std::unique_lock unique_lock(remote_sessions_mutex_);
if (remote_sessions_.find(remote_id) == remote_sessions_.end()) {
remote_sessions_[remote_id] =
std::make_shared<RemoteSession>();
auto props = remote_sessions_[remote_id];
props->local_id_ = "C-" + std::string(client_id_);
props->remote_id_ = remote_id;
memcpy(&props->params_, &params_, sizeof(Params));
props->params_.user_id = props->local_id_.c_str();
props->peer_ = CreatePeer(&props->params_);
props->control_window_width_ = title_bar_height_ * 10.0f;
props->control_window_height_ = title_bar_height_ * 1.3f;
props->control_window_min_width_ = title_bar_height_ * 0.65f;
props->control_window_min_height_ = title_bar_height_ * 1.3f;
props->control_window_max_width_ = title_bar_height_ * 10.0f;
props->control_window_max_height_ = title_bar_height_ * 7.0f;
props->connection_status_.store(ConnectionStatus::Connecting);
show_connection_status_window_ = true;
if (!props->peer_) {
LOG_INFO("Create peer [{}] instance failed", props->local_id_);
return -1;
}
for (const auto &display_info : devices_.display_info_list()) {
AddVideoStream(props->peer_, display_info.name.c_str());
}
AddAudioStream(props->peer_, props->audio_label_.c_str());
AddDataStream(props->peer_, props->data_label_.c_str(), false);
AddDataStream(props->peer_, props->mouse_label_.c_str(), false);
AddDataStream(props->peer_, props->keyboard_label_.c_str(), true);
AddDataStream(props->peer_, props->control_data_label_.c_str(), true);
AddDataStream(props->peer_, props->file_label_.c_str(), true);
AddDataStream(props->peer_, props->file_feedback_label_.c_str(), true);
AddDataStream(props->peer_, props->clipboard_label_.c_str(), true);
props->connection_status_.store(ConnectionStatus::Connecting);
peer_to_init = props->peer_;
local_id = props->local_id_;
}
}
if (peer_to_init) {
LOG_INFO("[{}] Create peer instance successful", local_id);
Init(peer_to_init);
LOG_INFO("[{}] Peer init finish", local_id);
}
}
int ret = -1;
// std::shared_lock read_lock(remote_sessions_mutex_);
auto props = remote_sessions_[remote_id];
if (!props->connection_established_) {
props->connection_status_.store(ConnectionStatus::Connecting);
props->connection_attempt_active_.store(true);
props->connection_attempt_started_at_ = std::chrono::steady_clock::now();
show_connection_status_window_ = true;
props->remember_password_ = remember_password;
if (strcmp(password, "") != 0 &&
strcmp(password, props->remote_password_) != 0) {
strncpy(props->remote_password_, password,
sizeof(props->remote_password_) - 1);
props->remote_password_[sizeof(props->remote_password_) - 1] = '\0';
}
std::string remote_id_with_pwd = remote_id + "@" + password;
if (props->peer_) {
ret = JoinConnection(props->peer_, remote_id_with_pwd.c_str());
if (0 == ret) {
props->rejoin_ = false;
} else {
props->rejoin_ = true;
need_to_rejoin_ = true;
}
}
}
// read_lock.unlock();
return 0;
}
} // namespace crossdesk
+41
View File
@@ -0,0 +1,41 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-02-28
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef CROSSDESK_GUI_DEVICE_PRESENCE_CACHE_H_
#define CROSSDESK_GUI_DEVICE_PRESENCE_CACHE_H_
#include <mutex>
#include <string>
#include <unordered_map>
namespace crossdesk {
class DevicePresenceCache {
public:
void SetOnline(const std::string &device_id, bool online) {
std::lock_guard<std::mutex> lock(mutex_);
cache_[device_id] = online;
}
bool IsOnline(const std::string &device_id) const {
std::lock_guard<std::mutex> lock(mutex_);
const auto it = cache_.find(device_id);
return it != cache_.end() && it->second;
}
void Clear() {
std::lock_guard<std::mutex> lock(mutex_);
cache_.clear();
}
private:
std::unordered_map<std::string, bool> cache_;
mutable std::mutex mutex_;
};
} // namespace crossdesk
#endif // CROSSDESK_GUI_DEVICE_PRESENCE_CACHE_H_
+246
View File
@@ -0,0 +1,246 @@
#include "runtime/gui_runtime.h"
#include <algorithm>
#include <chrono>
#include <cstring>
#include <memory>
#include <mutex>
#include <shared_mutex>
#include <string>
#include <thread>
#include <vector>
#include "localization.h"
#include "rd_log.h"
namespace crossdesk {
GuiRuntime::GuiRuntime()
: clipboard_(*this), devices_(*this), transfers_(*this), settings_(*this),
keyboard_(*this), peer_events_(*this) {}
GuiRuntime::~GuiRuntime() = default;
int GuiRuntime::CreateConnectionPeer() {
params_.use_cfg_file = false;
std::string signal_server_ip;
int signal_server_port;
int coturn_server_port;
if (config_center_->IsSelfHosted()) {
signal_server_ip = config_center_->GetSignalServerHost();
signal_server_port = config_center_->GetSignalServerPort();
coturn_server_port = config_center_->GetCoturnServerPort();
std::string current_self_hosted_ip = config_center_->GetSignalServerHost();
const bool use_cached_id = settings_.LoadCachedSelfHostedIdentity();
if (!use_cached_id) {
LOG_INFO(
"secure_cache_v2.enc not found, will use empty id to get new id from "
"server");
}
if (use_cached_id && strlen(self_hosted_id_) > 0) {
memset(&self_hosted_user_id_, 0, sizeof(self_hosted_user_id_));
strncpy(self_hosted_user_id_, self_hosted_id_,
sizeof(self_hosted_user_id_) - 1);
self_hosted_user_id_[sizeof(self_hosted_user_id_) - 1] = '\0';
params_.user_id = self_hosted_user_id_;
} else {
memset(&self_hosted_user_id_, 0, sizeof(self_hosted_user_id_));
params_.user_id = self_hosted_user_id_;
LOG_INFO(
"Using empty id for self-hosted server, server will assign new id");
}
} else {
signal_server_ip = config_center_->GetDefaultServerHost();
signal_server_port = config_center_->GetDefaultSignalServerPort();
coturn_server_port = config_center_->GetDefaultCoturnServerPort();
params_.user_id = client_id_with_password_;
}
// self hosted server config
strncpy(signal_server_ip_self_, config_center_->GetSignalServerHost().c_str(),
sizeof(signal_server_ip_self_) - 1);
signal_server_ip_self_[sizeof(signal_server_ip_self_) - 1] = '\0';
int signal_port = config_center_->GetSignalServerPort();
if (signal_port > 0) {
strncpy(signal_server_port_self_, std::to_string(signal_port).c_str(),
sizeof(signal_server_port_self_) - 1);
signal_server_port_self_[sizeof(signal_server_port_self_) - 1] = '\0';
} else {
signal_server_port_self_[0] = '\0';
}
int coturn_port = config_center_->GetCoturnServerPort();
if (coturn_port > 0) {
strncpy(coturn_server_port_self_, std::to_string(coturn_port).c_str(),
sizeof(coturn_server_port_self_) - 1);
coturn_server_port_self_[sizeof(coturn_server_port_self_) - 1] = '\0';
} else {
coturn_server_port_self_[0] = '\0';
}
// peer config
strncpy((char *)params_.signal_server_ip, signal_server_ip.c_str(),
sizeof(params_.signal_server_ip) - 1);
params_.signal_server_ip[sizeof(params_.signal_server_ip) - 1] = '\0';
params_.signal_server_port = signal_server_port;
strncpy((char *)params_.stun_server_ip, signal_server_ip.c_str(),
sizeof(params_.stun_server_ip) - 1);
params_.stun_server_ip[sizeof(params_.stun_server_ip) - 1] = '\0';
params_.stun_server_port = coturn_server_port;
strncpy((char *)params_.turn_server_ip, signal_server_ip.c_str(),
sizeof(params_.turn_server_ip) - 1);
params_.turn_server_ip[sizeof(params_.turn_server_ip) - 1] = '\0';
params_.turn_server_port = coturn_server_port;
strncpy((char *)params_.turn_server_username, "crossdesk",
sizeof(params_.turn_server_username) - 1);
params_.turn_server_username[sizeof(params_.turn_server_username) - 1] = '\0';
strncpy((char *)params_.turn_server_password, "crossdeskpw",
sizeof(params_.turn_server_password) - 1);
params_.turn_server_password[sizeof(params_.turn_server_password) - 1] = '\0';
strncpy(params_.log_path, dll_log_path_.c_str(),
sizeof(params_.log_path) - 1);
params_.log_path[sizeof(params_.log_path) - 1] = '\0';
params_.hardware_acceleration = config_center_->IsHardwareVideoCodec();
params_.av1_encoding = config_center_->GetVideoEncodeFormat() ==
ConfigCenter::VIDEO_ENCODE_FORMAT::AV1
? true
: false;
params_.turn_mode = static_cast<TurnMode>(config_center_->GetTurnMode());
params_.enable_srtp = config_center_->IsEnableSrtp();
params_.video_quality =
static_cast<VideoQuality>(config_center_->GetVideoQuality());
params_.on_receive_video_buffer = nullptr;
params_.on_receive_audio_buffer = PeerEventHandler::OnReceiveAudioBuffer;
params_.on_receive_data_buffer = PeerEventHandler::OnReceiveDataBuffer;
params_.on_receive_video_frame = PeerEventHandler::OnReceiveVideoBuffer;
params_.on_signal_status = PeerEventHandler::OnSignalStatus;
params_.on_signal_message = PeerEventHandler::OnSignalMessage;
params_.on_connection_status = PeerEventHandler::OnConnectionStatus;
params_.on_net_status_report = PeerEventHandler::OnNetStatusReport;
params_.user_data = &peer_events_;
peer_ = CreatePeer(&params_);
if (peer_) {
LOG_INFO("Create peer instance [{}] successful", client_id_);
Init(peer_);
LOG_INFO("Peer [{}] init finish", client_id_);
} else {
LOG_INFO("Create peer [{}] instance failed", client_id_);
}
if (0 == devices_.InitializeScreenCapturer()) {
for (const auto &display_info : devices_.display_info_list()) {
AddVideoStream(peer_, display_info.name.c_str());
}
AddAudioStream(peer_, audio_label_.c_str());
AddDataStream(peer_, data_label_.c_str(), false);
AddDataStream(peer_, mouse_label_.c_str(), false);
AddDataStream(peer_, keyboard_label_.c_str(), true);
AddDataStream(peer_, control_data_label_.c_str(), true);
AddDataStream(peer_, file_label_.c_str(), true);
AddDataStream(peer_, file_feedback_label_.c_str(), true);
AddDataStream(peer_, clipboard_label_.c_str(), true);
return 0;
} else {
return -1;
}
}
void GuiRuntime::UpdateLabels() {
if (!label_inited_ ||
localization_language_index_last_ != localization_language_index_) {
connect_button_label_ =
connect_button_pressed_
? localization::disconnect[localization_language_index_]
: localization::connect[localization_language_index_];
label_inited_ = true;
localization_language_index_last_ = localization_language_index_;
}
}
void GuiRuntime::HandleRecentConnections() {
if (reload_recent_connections_ && main_renderer_) {
uint32_t now_time = SDL_GetTicks();
if (now_time - recent_connection_image_save_time_ >= 50) {
int ret = thumbnail_->LoadThumbnail(main_renderer_, recent_connections_,
&recent_connection_image_width_,
&recent_connection_image_height_);
if (!ret) {
LOG_INFO("Load recent connection thumbnails");
}
reload_recent_connections_ = false;
recent_connection_ids_.clear();
for (const auto &conn : recent_connections_) {
recent_connection_ids_.push_back(conn.first);
}
need_to_send_recent_connections_ = true;
}
}
}
void GuiRuntime::SdlCaptureAudioIn(void *userdata, Uint8 *stream, int len) {
GuiRuntime *runtime = static_cast<GuiRuntime *>(userdata);
if (!runtime) {
return;
}
if (1) {
std::shared_lock lock(runtime->remote_sessions_mutex_);
for (const auto &it : runtime->remote_sessions_) {
auto props = it.second;
if (props->connection_status_.load() == ConnectionStatus::Connected) {
if (props->peer_) {
SendAudioFrame(props->peer_, (const char *)stream, len,
runtime->audio_label_.c_str());
}
}
}
} else {
memcpy(runtime->audio_buffer_, stream, len);
runtime->audio_len_ = len;
SDL_Delay(10);
runtime->audio_buffer_fresh_ = true;
}
}
void GuiRuntime::SdlCaptureAudioOut([[maybe_unused]] void *userdata,
[[maybe_unused]] Uint8 *stream,
[[maybe_unused]] int len) {
// GuiApplication *runtime = (GuiApplication *)userdata;
// for (auto it : runtime->remote_sessions_) {
// auto props = it.second;
// if (props->connection_status_ == SignalStatus::SignalConnected) {
// SendAudioFrame(props->peer_, (const char *)stream, len);
// }
// }
// if (!runtime->audio_buffer_fresh_) {
// return;
// }
// SDL_memset(stream, 0, len);
// if (runtime->audio_len_ == 0) {
// return;
// } else {
// }
// len = (len > runtime->audio_len_ ? runtime->audio_len_ : len);
// SDL_MixAudioFormat(stream, runtime->audio_buffer_, AUDIO_S16LSB, len,
// SDL_MIX_MAXVOLUME);
// runtime->audio_buffer_fresh_ = false;
}
} // namespace crossdesk
+96
View File
@@ -0,0 +1,96 @@
#ifndef CROSSDESK_GUI_RUNTIME_H_
#define CROSSDESK_GUI_RUNTIME_H_
#include <memory>
#include <string>
#include "features/clipboard/clipboard_controller.h"
#include "features/devices/session_device_manager.h"
#include "features/file_transfer/file_transfer_manager.h"
#include "features/input/keyboard_controller.h"
#include "runtime/gui_state.h"
#include "runtime/peer_event_handler.h"
#include "features/settings/settings_manager.h"
namespace crossdesk {
// Shared GUI runtime. It owns subsystem controllers and cross-cutting session
// state, but no window lifecycle, ImGui view, or transport callback methods.
class GuiRuntime : protected gui_detail::GuiState {
protected:
using FileTransferState = gui_detail::FileTransferState;
using RemoteSession = gui_detail::RemoteSession;
enum class RemoteUnlockState {
none,
service_unavailable,
lock_screen,
credential_ui,
secure_desktop,
};
GuiRuntime();
~GuiRuntime();
static void SdlCaptureAudioIn(void *userdata, Uint8 *stream, int len);
static void SdlCaptureAudioOut(void *userdata, Uint8 *stream, int len);
int CreateConnectionPeer();
int ConnectTo(const std::string &remote_id, const char *password,
bool remember_password, bool bypass_presence_check = false);
int RequestSingleDevicePresence(const std::string &remote_id,
const char *password, bool remember_password);
void UpdateLabels();
void HandleRecentConnections();
void HandleConnectionStatusChange();
void HandlePendingPresenceProbe();
void HandleConnectionTimeouts();
void HandleWindowsServiceIntegration();
void CloseRemoteSession(std::shared_ptr<RemoteSession> props);
void CloseAllRemoteSessions();
void ResetRemoteSessionResources(
std::shared_ptr<RemoteSession> props);
void WaitForThumbnailSaveTasks();
std::shared_ptr<RemoteSession>
FindRemoteSession(const std::string &remote_id);
void ResetRemoteServiceStatus(RemoteSession &props);
void ApplyRemoteServiceStatus(RemoteSession &props,
const ServiceStatus &status);
RemoteUnlockState
GetRemoteUnlockState(const RemoteSession &props) const;
#if _WIN32
void ResetLocalWindowsServiceState(bool clear_pending_sas);
#endif
#ifdef __APPLE__
bool CheckScreenRecordingPermission();
bool CheckAccessibilityPermission();
void OpenScreenRecordingPreferences();
void OpenAccessibilityPreferences();
void RefreshMacPermissionStatus(bool force);
bool EnsureMacScreenRecordingPermission();
bool EnsureMacAccessibilityPermission();
#endif
ClipboardController clipboard_;
SessionDeviceManager devices_;
FileTransferManager transfers_;
SettingsManager settings_;
KeyboardController keyboard_;
PeerEventHandler peer_events_;
private:
friend class ClipboardController;
friend class SessionDeviceManager;
friend class FileTransferManager;
friend class SettingsManager;
friend class KeyboardController;
friend class PeerEventHandler;
};
} // namespace crossdesk
#endif // CROSSDESK_GUI_RUNTIME_H_
+21
View File
@@ -0,0 +1,21 @@
/*
* Aggregate state visible to GuiRuntime and GuiApplication.
*
* Definitions live beside their owners: SDL/window state in application and
* connection/session state in runtime. This header is intentionally only the
* composition point.
*/
#ifndef CROSSDESK_GUI_STATE_H_
#define CROSSDESK_GUI_STATE_H_
#include "application/application_state.h"
#include "runtime/runtime_state.h"
namespace crossdesk::gui_detail {
struct GuiState : ApplicationState, RuntimeState {};
} // namespace crossdesk::gui_detail
#endif // CROSSDESK_GUI_STATE_H_
+113
View File
@@ -0,0 +1,113 @@
#include "runtime/gui_runtime.h"
#include "rd_log.h"
#include <ApplicationServices/ApplicationServices.h>
#include <CoreGraphics/CoreGraphics.h>
#import <Foundation/Foundation.h>
#include <cstdlib>
#include <string>
namespace crossdesk {
namespace {
constexpr uint32_t kPermissionRefreshIntervalVisibleMs = 500;
void OpenPrivacyPreferences(const char *pane) {
if (pane == nullptr || pane[0] == '\0') {
return;
}
std::string command =
"open \"x-apple.systempreferences:com.apple.preference.security?";
command += pane;
command += "\"";
system(command.c_str());
}
} // namespace
bool GuiRuntime::CheckScreenRecordingPermission() {
// CGPreflightScreenCaptureAccess is available on macOS 10.15+
if (@available(macOS 10.15, *)) {
bool granted = CGPreflightScreenCaptureAccess();
return granted;
}
// for older macOS versions, assume permission is granted
return true;
}
bool GuiRuntime::CheckAccessibilityPermission() {
NSDictionary *options = @{(__bridge id)kAXTrustedCheckOptionPrompt : @NO};
bool trusted =
AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)options);
return trusted;
}
void GuiRuntime::OpenAccessibilityPreferences() {
if (!mac_accessibility_permission_requested_) {
NSDictionary *options = @{(__bridge id)kAXTrustedCheckOptionPrompt : @YES};
AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)options);
} else {
OpenPrivacyPreferences("Privacy_Accessibility");
}
}
void GuiRuntime::OpenScreenRecordingPreferences() {
if (@available(macOS 10.15, *)) {
if (!mac_screen_recording_permission_requested_) {
CGRequestScreenCaptureAccess();
} else {
OpenPrivacyPreferences("Privacy_ScreenCapture");
}
} else {
OpenPrivacyPreferences("Privacy_ScreenCapture");
}
}
void GuiRuntime::RefreshMacPermissionStatus(bool force) {
const uint32_t now = static_cast<uint32_t>(SDL_GetTicks());
if (!force && mac_permission_status_initialized_ &&
now - mac_permission_last_check_tick_ <
kPermissionRefreshIntervalVisibleMs) {
return;
}
const bool old_screen_recording_granted =
mac_screen_recording_permission_granted_;
const bool old_accessibility_granted = mac_accessibility_permission_granted_;
mac_screen_recording_permission_granted_ = CheckScreenRecordingPermission();
mac_accessibility_permission_granted_ = CheckAccessibilityPermission();
mac_permission_last_check_tick_ = now;
mac_permission_status_initialized_ = true;
if (old_screen_recording_granted !=
mac_screen_recording_permission_granted_ ||
old_accessibility_granted != mac_accessibility_permission_granted_) {
LOG_INFO("macOS permission status: screen_recording={}, accessibility={}",
mac_screen_recording_permission_granted_,
mac_accessibility_permission_granted_);
}
}
bool GuiRuntime::EnsureMacScreenRecordingPermission() {
RefreshMacPermissionStatus(false);
if (mac_screen_recording_permission_granted_) {
return true;
}
show_request_permission_window_ = true;
return false;
}
bool GuiRuntime::EnsureMacAccessibilityPermission() {
RefreshMacPermissionStatus(false);
if (mac_accessibility_permission_granted_) {
return true;
}
show_request_permission_window_ = true;
return false;
}
} // namespace crossdesk
+267
View File
@@ -0,0 +1,267 @@
#include "runtime/peer_event_handler.h"
#include <algorithm>
#include <chrono>
#include <cstring>
#include <filesystem>
#include <memory>
#include <mutex>
#include <shared_mutex>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include "device_controller.h"
#include "file_transfer.h"
#include "localization.h"
#include "platform.h"
#include "rd_log.h"
#include "runtime/gui_runtime.h"
#include "runtime/remote_action_codec.h"
#if _WIN32
#include "interactive_state.h"
#include "service_host.h"
#endif
namespace crossdesk {
namespace {
#if _WIN32
constexpr uint32_t kSecureDesktopInputLogIntervalMs = 2000;
bool BuildAbsoluteMousePosition(const std::vector<DisplayInfo> &displays,
int display_index, float normalized_x,
float normalized_y, int *absolute_x_out,
int *absolute_y_out) {
if (absolute_x_out == nullptr || absolute_y_out == nullptr ||
display_index < 0 || display_index >= static_cast<int>(displays.size())) {
return false;
}
const DisplayInfo &display = displays[display_index];
if (display.width <= 0 || display.height <= 0) {
return false;
}
const float clamped_x = std::clamp(normalized_x, 0.0f, 1.0f);
const float clamped_y = std::clamp(normalized_y, 0.0f, 1.0f);
*absolute_x_out = static_cast<int>(clamped_x * display.width) + display.left;
*absolute_y_out = static_cast<int>(clamped_y * display.height) + display.top;
return true;
}
void LogSecureDesktopInputBlocked(uint32_t *last_tick, const char *side,
const char *stage) {
if (last_tick == nullptr) {
return;
}
const uint32_t now = static_cast<uint32_t>(SDL_GetTicks());
if (*last_tick != 0 && now - *last_tick < kSecureDesktopInputLogIntervalMs) {
return;
}
*last_tick = now;
LOG_WARN("{} secure-desktop input blocked, stage={}, normal SendInput path "
"cannot drive the Windows password UI",
side != nullptr ? side : "unknown", stage != nullptr ? stage : "");
}
#endif
} // namespace
void PeerEventHandler::OnReceiveDataBuffer(
const char *data, size_t size, const char *user_id, size_t user_id_size,
const char *src_id, size_t src_id_size, void *user_data) {
auto *handler = static_cast<PeerEventHandler *>(user_data);
GuiRuntime *runtime = handler ? &handler->owner_ : nullptr;
if (!runtime) {
return;
}
std::string source_id = std::string(src_id, src_id_size);
if (source_id == runtime->file_label_) {
std::string remote_user_id = std::string(user_id, user_id_size);
static FileReceiver receiver;
// Update output directory from config
std::string configured_path =
runtime->config_center_->GetFileTransferSavePath();
if (!configured_path.empty()) {
receiver.SetOutputDir(std::filesystem::u8path(configured_path));
} else if (receiver.OutputDir().empty()) {
receiver = FileReceiver(); // re-init with default desktop path
}
receiver.SetOnSendAck([runtime,
remote_user_id](const FileTransferAck &ack) -> int {
bool is_server_sending = remote_user_id.rfind("C-", 0) != 0;
if (is_server_sending) {
auto props =
runtime->FindRemoteSession(remote_user_id);
if (props) {
PeerPtr *peer = props->peer_;
return SendReliableDataFrame(
peer, reinterpret_cast<const char *>(&ack),
sizeof(FileTransferAck), runtime->file_feedback_label_.c_str());
}
}
return SendReliableDataFrame(
runtime->peer_, reinterpret_cast<const char *>(&ack),
sizeof(FileTransferAck), runtime->file_feedback_label_.c_str());
});
receiver.OnData(data, size);
return;
} else if (source_id == runtime->clipboard_label_) {
if (size > 0) {
std::string remote_user_id(user_id, user_id_size);
auto props =
runtime->FindRemoteSession(remote_user_id);
if (props && !props->enable_mouse_control_) {
return;
}
runtime->clipboard_.QueueRemoteText(data, size);
}
return;
} else if (source_id == runtime->file_feedback_label_) {
runtime->transfers_.HandleAck(data, size);
return;
}
std::string json_str(data, size);
RemoteAction remote_action{};
if (!remote_action.from_json(json_str)) {
LOG_ERROR("Failed to parse RemoteAction JSON payload");
return;
}
std::string remote_id(user_id, user_id_size);
if (remote_action.type == ControlType::service_status) {
auto props_it = runtime->remote_sessions_.find(remote_id);
if (props_it != runtime->remote_sessions_.end()) {
runtime->ApplyRemoteServiceStatus(*props_it->second, remote_action.ss);
}
return;
}
if (remote_action.type == ControlType::service_command) {
#if _WIN32
if (remote_action.c.flag == ServiceCommandFlag::send_sas) {
runtime->pending_windows_service_sas_.store(true,
std::memory_order_relaxed);
} else if (remote_action.c.flag == ServiceCommandFlag::lock_workstation) {
if (!LockWorkStation()) {
LOG_WARN("Remote lock workstation request failed, error={}",
GetLastError());
}
}
#endif
return;
}
if (remote_action.type == ControlType::host_infomation) {
bool is_client_mode = false;
std::shared_ptr<GuiRuntime::RemoteSession> props;
{
std::shared_lock lock(runtime->remote_sessions_mutex_);
auto props_it = runtime->remote_sessions_.find(remote_id);
if (props_it != runtime->remote_sessions_.end()) {
is_client_mode = true;
props = props_it->second;
}
}
if (is_client_mode) {
// client mode
if (props && props->remote_host_name_.empty()) {
props->remote_host_name_ = std::string(remote_action.i.host_name,
remote_action.i.host_name_size);
LOG_INFO("Remote hostname: [{}]", props->remote_host_name_);
for (int i = 0; i < remote_action.i.display_num; i++) {
props->display_info_list_.push_back(
DisplayInfo(remote_action.i.display_list[i],
remote_action.i.left[i], remote_action.i.top[i],
remote_action.i.right[i], remote_action.i.bottom[i]));
}
}
remote_action_codec::Free(remote_action);
} else {
// server mode
std::string host_name(remote_action.i.host_name,
remote_action.i.host_name_size);
{
std::unique_lock lock(runtime->connection_status_mutex_);
runtime->connection_host_names_[remote_id] = host_name;
}
LOG_INFO("Remote hostname: [{}]", host_name);
remote_action_codec::Free(remote_action);
}
} else {
// remote
#if _WIN32
if (runtime->local_service_status_received_ &&
IsSecureDesktopInteractionRequired(runtime->local_interactive_stage_) &&
remote_action.type != ControlType::keyboard &&
remote_action.type != ControlType::keyboard_state) {
if (remote_action.type == ControlType::mouse) {
int absolute_x = 0;
int absolute_y = 0;
if (!BuildAbsoluteMousePosition(runtime->devices_.display_info_list(),
runtime->selected_display_,
remote_action.m.x, remote_action.m.y,
&absolute_x, &absolute_y)) {
LOG_WARN("Secure desktop mouse injection skipped, invalid display "
"mapping: display_index={}, x={}, y={}",
runtime->selected_display_, remote_action.m.x,
remote_action.m.y);
return;
}
const std::string response = SendCrossDeskSecureDesktopMouseInput(
absolute_x, absolute_y, remote_action.m.s,
static_cast<int>(remote_action.m.flag), 1000);
auto json = nlohmann::json::parse(response, nullptr, false);
if (json.is_discarded() || !json.value("ok", false)) {
LogSecureDesktopInputBlocked(
&runtime->last_local_secure_input_block_log_tick_, "local",
runtime->local_interactive_stage_.c_str());
LOG_WARN(
"Secure desktop mouse injection failed, x={}, y={}, wheel={}, "
"flag={}, response={}",
absolute_x, absolute_y, remote_action.m.s,
static_cast<int>(remote_action.m.flag), response);
}
return;
}
}
#endif
if (remote_action.type == ControlType::mouse) {
runtime->devices_.SendMouseCommand(remote_action,
runtime->selected_display_);
} else if (remote_action.type == ControlType::audio_capture) {
if (remote_action.a && !runtime->start_speaker_capturer_)
runtime->devices_.StartSpeakerCapturer();
else if (!remote_action.a && runtime->start_speaker_capturer_)
runtime->devices_.StopSpeakerCapturer();
} else if (remote_action.type == ControlType::keyboard) {
runtime->keyboard_.ApplyRemoteEvent(remote_id, remote_action);
} else if (remote_action.type == ControlType::keyboard_state) {
runtime->keyboard_.ApplyRemoteState(remote_id, remote_action);
} else if (remote_action.type == ControlType::display_id) {
const int ret = runtime->devices_.SwitchDisplay(remote_action.d);
if (ret == 0) {
runtime->selected_display_ = remote_action.d;
} else {
LOG_WARN("Display switch skipped, invalid display_id={}",
remote_action.d);
}
}
}
}
} // namespace crossdesk
+524
View File
@@ -0,0 +1,524 @@
#include "runtime/peer_event_handler.h"
#include <algorithm>
#include <chrono>
#include <cstring>
#include <filesystem>
#include <memory>
#include <mutex>
#include <shared_mutex>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include "device_controller.h"
#include "file_transfer.h"
#include "localization.h"
#include "platform.h"
#include "rd_log.h"
#include "runtime/gui_runtime.h"
#include "runtime/remote_action_codec.h"
#if _WIN32
#include "interactive_state.h"
#include "service_host.h"
#endif
namespace crossdesk {
PeerEventHandler::PeerEventHandler(GuiRuntime &owner) : owner_(owner) {}
void PeerEventHandler::OnSignalMessage(const char *message, size_t size,
void *user_data) {
auto *handler = static_cast<PeerEventHandler *>(user_data);
GuiRuntime *runtime = handler ? &handler->owner_ : nullptr;
if (!runtime || !message || size == 0) {
return;
}
std::string s(message, size);
auto j = nlohmann::json::parse(s, nullptr, false);
if (j.is_discarded() || !j.contains("type") || !j["type"].is_string()) {
return;
}
std::string type = j["type"].get<std::string>();
if (type == "presence") {
if (j.contains("devices") && j["devices"].is_array()) {
for (auto &dev : j["devices"]) {
if (!dev.is_object()) {
continue;
}
if (!dev.contains("id") || !dev["id"].is_string()) {
continue;
}
if (!dev.contains("online") || !dev["online"].is_boolean()) {
continue;
}
std::string id = dev["id"].get<std::string>();
bool online = dev["online"].get<bool>();
runtime->device_presence_cache_.SetOnline(id, online);
{
std::lock_guard<std::mutex> lock(
runtime->pending_presence_probe_mutex_);
if (runtime->pending_presence_probe_ &&
runtime->pending_presence_remote_id_ == id) {
runtime->pending_presence_result_ready_ = true;
runtime->pending_presence_online_ = online;
}
}
}
}
} else if (type == "presence_update") {
if (j.contains("id") && j["id"].is_string() && j.contains("online") &&
j["online"].is_boolean()) {
std::string id = j["id"].get<std::string>();
bool online = j["online"].get<bool>();
if (!id.empty()) {
runtime->device_presence_cache_.SetOnline(id, online);
{
std::lock_guard<std::mutex> lock(
runtime->pending_presence_probe_mutex_);
if (runtime->pending_presence_probe_ &&
runtime->pending_presence_remote_id_ == id) {
runtime->pending_presence_result_ready_ = true;
runtime->pending_presence_online_ = online;
}
}
}
}
}
}
void PeerEventHandler::OnSignalStatus(SignalStatus status, const char *user_id,
size_t user_id_size, void *user_data) {
auto *handler = static_cast<PeerEventHandler *>(user_data);
GuiRuntime *runtime = handler ? &handler->owner_ : nullptr;
if (!runtime) {
return;
}
std::string client_id(user_id, user_id_size);
if (client_id == runtime->client_id_) {
runtime->signal_status_ = status;
if (SignalStatus::SignalConnecting == status) {
runtime->signal_connected_ = false;
} else if (SignalStatus::SignalConnected == status) {
runtime->signal_connected_ = true;
runtime->need_to_send_recent_connections_ = true;
LOG_INFO("[{}] connected to signal server", client_id);
} else if (SignalStatus::SignalFailed == status) {
runtime->signal_connected_ = false;
} else if (SignalStatus::SignalClosed == status) {
runtime->signal_connected_ = false;
} else if (SignalStatus::SignalReconnecting == status) {
runtime->signal_connected_ = false;
} else if (SignalStatus::SignalServerClosed == status) {
runtime->signal_connected_ = false;
} else if (SignalStatus::SignalTlsCertError == status) {
runtime->signal_connected_ = false;
}
} else {
if (client_id.rfind("C-", 0) != 0) {
return;
}
std::string remote_id(client_id.begin() + 2, client_id.end());
// std::shared_lock lock(runtime->remote_sessions_mutex_);
if (runtime->remote_sessions_.find(remote_id) ==
runtime->remote_sessions_.end()) {
return;
}
auto props = runtime->remote_sessions_.find(remote_id)->second;
props->signal_status_ = status;
if (SignalStatus::SignalConnecting == status) {
props->signal_connected_ = false;
} else if (SignalStatus::SignalConnected == status) {
props->signal_connected_ = true;
LOG_INFO("[{}] connected to signal server", remote_id);
} else if (SignalStatus::SignalFailed == status) {
props->signal_connected_ = false;
} else if (SignalStatus::SignalClosed == status) {
props->signal_connected_ = false;
} else if (SignalStatus::SignalReconnecting == status) {
props->signal_connected_ = false;
} else if (SignalStatus::SignalServerClosed == status) {
props->signal_connected_ = false;
} else if (SignalStatus::SignalTlsCertError == status) {
props->signal_connected_ = false;
}
}
}
void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
const char *user_id,
const size_t user_id_size,
void *user_data) {
auto *handler = static_cast<PeerEventHandler *>(user_data);
GuiRuntime *runtime = handler ? &handler->owner_ : nullptr;
if (!runtime)
return;
std::string remote_id(user_id, user_id_size);
std::shared_ptr<GuiRuntime::RemoteSession> props;
{
std::shared_lock lock(runtime->remote_sessions_mutex_);
auto it = runtime->remote_sessions_.find(remote_id);
if (it != runtime->remote_sessions_.end()) {
props = it->second;
}
}
if (props) {
runtime->is_client_mode_ = true;
runtime->show_connection_status_window_ = true;
props->connection_status_.store(status);
if (status != ConnectionStatus::Connecting &&
status != ConnectionStatus::Gathering) {
props->connection_attempt_active_.store(false);
}
switch (status) {
case ConnectionStatus::Connected: {
runtime->ResetRemoteServiceStatus(*props);
{
RemoteAction remote_action;
remote_action.i.display_num =
runtime->devices_.display_info_list().size();
remote_action.i.display_list =
(char **)malloc(remote_action.i.display_num * sizeof(char *));
remote_action.i.left =
(int *)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.top =
(int *)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.right =
(int *)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.bottom =
(int *)malloc(remote_action.i.display_num * sizeof(int));
for (int i = 0; i < remote_action.i.display_num; i++) {
LOG_INFO("Local display [{}:{}]", i + 1,
runtime->devices_.display_info_list()[i].name);
remote_action.i.display_list[i] = (char *)malloc(
runtime->devices_.display_info_list()[i].name.length() + 1);
strncpy(remote_action.i.display_list[i],
runtime->devices_.display_info_list()[i].name.c_str(),
runtime->devices_.display_info_list()[i].name.length());
remote_action.i.display_list
[i][runtime->devices_.display_info_list()[i].name.length()] = '\0';
remote_action.i.left[i] =
runtime->devices_.display_info_list()[i].left;
remote_action.i.top[i] = runtime->devices_.display_info_list()[i].top;
remote_action.i.right[i] =
runtime->devices_.display_info_list()[i].right;
remote_action.i.bottom[i] =
runtime->devices_.display_info_list()[i].bottom;
}
std::string host_name = GetHostName();
remote_action.type = ControlType::host_infomation;
memcpy(&remote_action.i.host_name, host_name.data(), host_name.size());
remote_action.i.host_name[host_name.size()] = '\0';
remote_action.i.host_name_size = host_name.size();
std::string msg = remote_action.to_json();
int ret = SendReliableDataFrame(props->peer_, msg.data(), msg.size(),
runtime->control_data_label_.c_str());
remote_action_codec::Free(remote_action);
}
if (!runtime->need_to_create_stream_window_ &&
!runtime->remote_sessions_.empty()) {
runtime->need_to_create_stream_window_ = true;
}
props->connection_established_ = true;
props->stream_render_rect_ = {
0, (int)runtime->title_bar_height_, (int)runtime->stream_window_width_,
(int)(runtime->stream_window_height_ - runtime->title_bar_height_)};
props->stream_render_rect_f_ = {
0.0f, runtime->title_bar_height_, runtime->stream_window_width_,
runtime->stream_window_height_ - runtime->title_bar_height_};
runtime->start_keyboard_capturer_ = true;
break;
}
case ConnectionStatus::Disconnected:
case ConnectionStatus::Failed:
case ConnectionStatus::Closed: {
runtime->keyboard_.ReleaseRemotePressedKeys(remote_id,
"connection_closed");
props->connection_established_ = false;
props->enable_mouse_control_ = false;
runtime->ResetRemoteServiceStatus(*props);
{
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
props->front_frame_.reset();
props->back_frame_.reset();
props->video_width_ = 0;
props->video_height_ = 0;
props->video_size_ = 0;
props->render_rect_dirty_ = true;
props->stream_cleanup_pending_ = true;
}
SDL_Event event;
event.type = runtime->STREAM_REFRESH_EVENT;
event.user.data1 = props.get();
SDL_PushEvent(&event);
runtime->focus_on_stream_window_ = false;
break;
}
case ConnectionStatus::IncorrectPassword: {
runtime->password_validating_ = false;
runtime->password_validating_time_++;
if (runtime->connect_button_pressed_) {
runtime->connect_button_pressed_ = false;
props->connection_established_ = false;
runtime->connect_button_label_ =
localization::connect[runtime->localization_language_index_];
}
break;
}
case ConnectionStatus::NoSuchTransmissionId:
case ConnectionStatus::RemoteUnavailable: {
if (runtime->connect_button_pressed_) {
props->connection_established_ = false;
runtime->connect_button_label_ =
localization::connect[runtime->localization_language_index_];
}
break;
}
default:
break;
}
} else {
runtime->is_client_mode_ = false;
runtime->show_connection_status_window_ = true;
{
std::unique_lock lock(runtime->connection_status_mutex_);
runtime->connection_status_[remote_id] = status;
}
switch (status) {
case ConnectionStatus::Connected: {
#if _WIN32
runtime->last_windows_service_status_tick_ = 0;
#endif
{
RemoteAction remote_action;
remote_action.i.display_num =
runtime->devices_.display_info_list().size();
remote_action.i.display_list =
(char **)malloc(remote_action.i.display_num * sizeof(char *));
remote_action.i.left =
(int *)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.top =
(int *)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.right =
(int *)malloc(remote_action.i.display_num * sizeof(int));
remote_action.i.bottom =
(int *)malloc(remote_action.i.display_num * sizeof(int));
for (int i = 0; i < remote_action.i.display_num; i++) {
LOG_INFO("Local display [{}:{}]", i + 1,
runtime->devices_.display_info_list()[i].name);
remote_action.i.display_list[i] = (char *)malloc(
runtime->devices_.display_info_list()[i].name.length() + 1);
strncpy(remote_action.i.display_list[i],
runtime->devices_.display_info_list()[i].name.c_str(),
runtime->devices_.display_info_list()[i].name.length());
remote_action.i.display_list
[i][runtime->devices_.display_info_list()[i].name.length()] = '\0';
remote_action.i.left[i] =
runtime->devices_.display_info_list()[i].left;
remote_action.i.top[i] = runtime->devices_.display_info_list()[i].top;
remote_action.i.right[i] =
runtime->devices_.display_info_list()[i].right;
remote_action.i.bottom[i] =
runtime->devices_.display_info_list()[i].bottom;
}
std::string host_name = GetHostName();
remote_action.type = ControlType::host_infomation;
memcpy(&remote_action.i.host_name, host_name.data(), host_name.size());
remote_action.i.host_name[host_name.size()] = '\0';
remote_action.i.host_name_size = host_name.size();
std::string msg = remote_action.to_json();
int ret = SendReliableDataFrame(runtime->peer_, msg.data(), msg.size(),
runtime->control_data_label_.c_str());
remote_action_codec::Free(remote_action);
}
runtime->need_to_create_server_window_ = true;
runtime->is_server_mode_ = true;
runtime->start_screen_capturer_ = true;
runtime->start_speaker_capturer_ = true;
runtime->remote_client_id_ = remote_id;
runtime->start_mouse_controller_ = true;
{
std::shared_lock lock(runtime->connection_status_mutex_);
if (std::all_of(runtime->connection_status_.begin(),
runtime->connection_status_.end(), [](const auto &kv) {
return kv.first.find("web") != std::string::npos;
})) {
runtime->show_cursor_ = true;
}
}
break;
}
case ConnectionStatus::Disconnected:
case ConnectionStatus::Failed:
case ConnectionStatus::Closed: {
runtime->keyboard_.ReleaseRemotePressedKeys(remote_id,
"connection_closed");
bool all_disconnected = false;
{
std::shared_lock lock(runtime->connection_status_mutex_);
all_disconnected =
std::all_of(runtime->connection_status_.begin(),
runtime->connection_status_.end(), [](const auto &kv) {
return kv.second == ConnectionStatus::Closed ||
kv.second == ConnectionStatus::Failed ||
kv.second == ConnectionStatus::Disconnected;
});
}
if (all_disconnected) {
runtime->need_to_destroy_server_window_ = true;
runtime->is_server_mode_ = false;
#if defined(__linux__) && !defined(__APPLE__)
if (IsWaylandSession()) {
// Keep Wayland capture session warm to avoid black screen on
// subsequent reconnects.
runtime->start_screen_capturer_ = true;
LOG_INFO("Keeping Wayland screen capturer running after "
"disconnect to preserve reconnect stability");
} else {
runtime->start_screen_capturer_ = false;
}
#else
runtime->start_screen_capturer_ = false;
#endif
runtime->start_speaker_capturer_ = false;
runtime->start_mouse_controller_ = false;
runtime->start_keyboard_capturer_ = false;
runtime->remote_client_id_ = "";
if (props)
props->connection_established_ = false;
if (runtime->audio_capture_) {
runtime->devices_.StopSpeakerCapturer();
runtime->audio_capture_ = false;
}
{
std::unique_lock lock(runtime->connection_status_mutex_);
runtime->connection_status_.erase(remote_id);
runtime->connection_host_names_.erase(remote_id);
}
runtime->devices_.ResetToInitialDisplay();
}
{
std::shared_lock lock(runtime->connection_status_mutex_);
if (std::all_of(runtime->connection_status_.begin(),
runtime->connection_status_.end(), [](const auto &kv) {
return kv.first.find("web") == std::string::npos;
})) {
runtime->show_cursor_ = false;
}
}
break;
}
default:
break;
}
}
}
void PeerEventHandler::OnNetStatusReport(
const char *client_id, size_t client_id_size, TraversalMode mode,
const XNetTrafficStats *net_traffic_stats, const char *user_id,
const size_t user_id_size, void *user_data) {
auto *handler = static_cast<PeerEventHandler *>(user_data);
GuiRuntime *runtime = handler ? &handler->owner_ : nullptr;
if (!runtime) {
return;
}
if (strchr(client_id, '@') != nullptr && strchr(user_id, '-') == nullptr) {
std::string id, password;
const char *at_pos = strchr(client_id, '@');
if (at_pos == nullptr) {
id = client_id;
password.clear();
} else {
id.assign(client_id, at_pos - client_id);
password = at_pos + 1;
}
bool is_self_hosted = runtime->config_center_->IsSelfHosted();
if (is_self_hosted) {
memset(&runtime->client_id_, 0, sizeof(runtime->client_id_));
strncpy(runtime->client_id_, id.c_str(), sizeof(runtime->client_id_) - 1);
runtime->client_id_[sizeof(runtime->client_id_) - 1] = '\0';
memset(&runtime->password_saved_, 0, sizeof(runtime->password_saved_));
strncpy(runtime->password_saved_, password.c_str(),
sizeof(runtime->password_saved_) - 1);
runtime->password_saved_[sizeof(runtime->password_saved_) - 1] = '\0';
memset(&runtime->self_hosted_id_, 0, sizeof(runtime->self_hosted_id_));
strncpy(runtime->self_hosted_id_, client_id,
sizeof(runtime->self_hosted_id_) - 1);
runtime->self_hosted_id_[sizeof(runtime->self_hosted_id_) - 1] = '\0';
LOG_INFO("Use self-hosted client id [{}] and save to cache file", id);
runtime->settings_.PersistSelfHostedIdentity(client_id);
} else {
memset(&runtime->client_id_, 0, sizeof(runtime->client_id_));
strncpy(runtime->client_id_, id.c_str(), sizeof(runtime->client_id_) - 1);
runtime->client_id_[sizeof(runtime->client_id_) - 1] = '\0';
memset(&runtime->password_saved_, 0, sizeof(runtime->password_saved_));
strncpy(runtime->password_saved_, password.c_str(),
sizeof(runtime->password_saved_) - 1);
runtime->password_saved_[sizeof(runtime->password_saved_) - 1] = '\0';
memset(&runtime->client_id_with_password_, 0,
sizeof(runtime->client_id_with_password_));
strncpy(runtime->client_id_with_password_, client_id,
sizeof(runtime->client_id_with_password_) - 1);
runtime
->client_id_with_password_[sizeof(runtime->client_id_with_password_) -
1] = '\0';
LOG_INFO("Use client id [{}] and save id into cache file", id);
runtime->settings_.Save();
}
}
std::string remote_id(user_id, user_id_size);
// std::shared_lock lock(runtime->remote_sessions_mutex_);
if (runtime->remote_sessions_.find(remote_id) ==
runtime->remote_sessions_.end()) {
return;
}
auto props = runtime->remote_sessions_.find(remote_id)->second;
if (props->traversal_mode_ != mode) {
props->traversal_mode_ = mode;
LOG_INFO("Net mode: [{}]", int(props->traversal_mode_));
}
if (!net_traffic_stats) {
return;
}
// only display client side net status if connected to itself
if (!(runtime->peer_reserved_ && !strstr(client_id, "C-"))) {
props->net_traffic_stats_ = *net_traffic_stats;
}
}
} // namespace crossdesk
+47
View File
@@ -0,0 +1,47 @@
#ifndef CROSSDESK_GUI_PEER_EVENT_HANDLER_H_
#define CROSSDESK_GUI_PEER_EVENT_HANDLER_H_
#include <cstddef>
#include "minirtc.h"
namespace crossdesk {
class GuiRuntime;
// Adapts MiniRTC C callbacks to the GUI runtime.
class PeerEventHandler {
public:
explicit PeerEventHandler(GuiRuntime &owner);
static void OnReceiveVideoBuffer(const XVideoFrame *video_frame,
const char *user_id, size_t user_id_size,
const char *src_id, size_t src_id_size,
void *user_data);
static void OnReceiveAudioBuffer(const char *data, size_t size,
const char *user_id, size_t user_id_size,
const char *src_id, size_t src_id_size,
void *user_data);
static void OnReceiveDataBuffer(const char *data, size_t size,
const char *user_id, size_t user_id_size,
const char *src_id, size_t src_id_size,
void *user_data);
static void OnSignalStatus(SignalStatus status, const char *user_id,
size_t user_id_size, void *user_data);
static void OnSignalMessage(const char *message, size_t size,
void *user_data);
static void OnConnectionStatus(ConnectionStatus status, const char *user_id,
size_t user_id_size, void *user_data);
static void OnNetStatusReport(const char *client_id, size_t client_id_size,
TraversalMode mode,
const XNetTrafficStats *net_traffic_stats,
const char *user_id, size_t user_id_size,
void *user_data);
private:
GuiRuntime &owner_;
};
} // namespace crossdesk
#endif // CROSSDESK_GUI_PEER_EVENT_HANDLER_H_
+111
View File
@@ -0,0 +1,111 @@
#include "runtime/peer_event_handler.h"
#include <algorithm>
#include <chrono>
#include <cstring>
#include <filesystem>
#include <memory>
#include <mutex>
#include <shared_mutex>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include "device_controller.h"
#include "file_transfer.h"
#include "localization.h"
#include "platform.h"
#include "rd_log.h"
#include "runtime/gui_runtime.h"
#include "runtime/remote_action_codec.h"
#if _WIN32
#include "interactive_state.h"
#include "service_host.h"
#endif
namespace crossdesk {
void PeerEventHandler::OnReceiveVideoBuffer(
const XVideoFrame *video_frame, const char *user_id, size_t user_id_size,
const char *src_id, size_t src_id_size, void *user_data) {
auto *handler = static_cast<PeerEventHandler *>(user_data);
GuiRuntime *runtime = handler ? &handler->owner_ : nullptr;
if (!runtime) {
return;
}
std::string remote_id(user_id, user_id_size);
// std::shared_lock lock(runtime->remote_sessions_mutex_);
if (runtime->remote_sessions_.find(remote_id) ==
runtime->remote_sessions_.end()) {
return;
}
GuiRuntime::RemoteSession *props =
runtime->remote_sessions_.find(remote_id)->second.get();
if (props->connection_established_) {
{
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
if (!props->back_frame_) {
props->back_frame_ =
std::make_shared<std::vector<unsigned char>>(video_frame->size);
}
if (props->back_frame_->size() != video_frame->size) {
props->back_frame_->resize(video_frame->size);
}
std::memcpy(props->back_frame_->data(), video_frame->data,
video_frame->size);
const bool size_changed = (props->video_width_ != video_frame->width) ||
(props->video_height_ != video_frame->height);
if (size_changed) {
props->render_rect_dirty_ = true;
}
props->video_width_ = video_frame->width;
props->video_height_ = video_frame->height;
props->video_size_ = video_frame->size;
props->front_frame_.swap(props->back_frame_);
}
SDL_Event event;
event.type = runtime->STREAM_REFRESH_EVENT;
event.user.data1 = props;
SDL_PushEvent(&event);
props->streaming_ = true;
if (props->net_traffic_stats_button_pressed_) {
props->frame_count_++;
auto now = std::chrono::steady_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
now - props->last_time_)
.count();
if (elapsed >= 1000) {
props->fps_ = props->frame_count_ * 1000 / elapsed;
props->frame_count_ = 0;
props->last_time_ = now;
}
}
}
}
void PeerEventHandler::OnReceiveAudioBuffer(
const char *data, size_t size, const char *user_id, size_t user_id_size,
const char *src_id, size_t src_id_size, void *user_data) {
auto *handler = static_cast<PeerEventHandler *>(user_data);
GuiRuntime *runtime = handler ? &handler->owner_ : nullptr;
if (!runtime) {
return;
}
runtime->audio_buffer_fresh_ = true;
runtime->devices_.PushAudio(data, size);
}
} // namespace crossdesk
+117
View File
@@ -0,0 +1,117 @@
#include "runtime/remote_action_codec.h"
#include <cstdlib>
#include <cstring>
namespace crossdesk::remote_action_codec {
std::vector<char> Serialize(const RemoteAction &action) {
std::vector<char> buffer;
buffer.push_back(static_cast<char>(action.type));
auto insert_bytes = [&](const void *ptr, size_t len) {
buffer.insert(buffer.end(), static_cast<const char *>(ptr),
static_cast<const char *>(ptr) + len);
};
if (action.type == ControlType::host_infomation) {
insert_bytes(&action.i.host_name_size, sizeof(size_t));
insert_bytes(action.i.host_name, action.i.host_name_size);
size_t num = action.i.display_num;
insert_bytes(&num, sizeof(size_t));
for (size_t i = 0; i < num; ++i) {
const size_t len = std::strlen(action.i.display_list[i]);
insert_bytes(&len, sizeof(size_t));
insert_bytes(action.i.display_list[i], len);
}
insert_bytes(action.i.left, sizeof(int) * num);
insert_bytes(action.i.top, sizeof(int) * num);
insert_bytes(action.i.right, sizeof(int) * num);
insert_bytes(action.i.bottom, sizeof(int) * num);
}
return buffer;
}
bool Deserialize(const char *data, size_t size, RemoteAction &out) {
size_t offset = 0;
auto read = [&](void *dst, size_t len) -> bool {
if (offset + len > size) {
return false;
}
std::memcpy(dst, data + offset, len);
offset += len;
return true;
};
if (size < 1) {
return false;
}
out.type = static_cast<ControlType>(data[offset++]);
if (out.type == ControlType::host_infomation) {
size_t name_len;
if (!read(&name_len, sizeof(size_t)) ||
name_len >= sizeof(out.i.host_name)) {
return false;
}
if (!read(out.i.host_name, name_len)) {
return false;
}
out.i.host_name[name_len] = '\0';
out.i.host_name_size = name_len;
size_t num;
if (!read(&num, sizeof(size_t))) {
return false;
}
out.i.display_num = num;
out.i.display_list =
static_cast<char **>(std::malloc(num * sizeof(char *)));
for (size_t i = 0; i < num; ++i) {
size_t len;
if (!read(&len, sizeof(size_t)) || offset + len > size) {
return false;
}
out.i.display_list[i] = static_cast<char *>(std::malloc(len + 1));
std::memcpy(out.i.display_list[i], data + offset, len);
out.i.display_list[i][len] = '\0';
offset += len;
}
auto alloc_int_array = [&](int *&array) {
array = static_cast<int *>(std::malloc(num * sizeof(int)));
return read(array, num * sizeof(int));
};
return alloc_int_array(out.i.left) && alloc_int_array(out.i.top) &&
alloc_int_array(out.i.right) && alloc_int_array(out.i.bottom);
}
return true;
}
void Free(RemoteAction &action) {
if (action.type != ControlType::host_infomation) {
return;
}
for (size_t i = 0; i < action.i.display_num; ++i) {
std::free(action.i.display_list[i]);
}
std::free(action.i.display_list);
std::free(action.i.left);
std::free(action.i.top);
std::free(action.i.right);
std::free(action.i.bottom);
action.i.display_list = nullptr;
action.i.left = action.i.top = action.i.right = action.i.bottom = nullptr;
action.i.display_num = 0;
}
} // namespace crossdesk::remote_action_codec
+17
View File
@@ -0,0 +1,17 @@
#ifndef CROSSDESK_GUI_REMOTE_ACTION_CODEC_H_
#define CROSSDESK_GUI_REMOTE_ACTION_CODEC_H_
#include <cstddef>
#include <vector>
#include "device_controller.h"
namespace crossdesk::remote_action_codec {
std::vector<char> Serialize(const RemoteAction &action);
bool Deserialize(const char *data, size_t size, RemoteAction &out);
void Free(RemoteAction &action);
} // namespace crossdesk::remote_action_codec
#endif // CROSSDESK_GUI_REMOTE_ACTION_CODEC_H_
+175
View File
@@ -0,0 +1,175 @@
#ifndef CROSSDESK_GUI_REMOTE_SESSION_H_
#define CROSSDESK_GUI_REMOTE_SESSION_H_
#include <SDL3/SDL.h>
#include <atomic>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <filesystem>
#include <memory>
#include <mutex>
#include <optional>
#include <queue>
#include <string>
#include <vector>
#include "display_info.h"
#include "imgui.h"
#include "minirtc.h"
namespace crossdesk::gui_detail {
struct FileTransferState {
std::atomic<bool> file_sending_ = false;
std::atomic<uint64_t> file_sent_bytes_ = 0;
std::atomic<uint64_t> file_total_bytes_ = 0;
std::atomic<uint32_t> file_send_rate_bps_ = 0;
std::mutex file_transfer_mutex_;
std::chrono::steady_clock::time_point file_send_start_time_;
std::chrono::steady_clock::time_point file_send_last_update_time_;
uint64_t file_send_last_bytes_ = 0;
bool file_transfer_window_visible_ = false;
bool file_transfer_window_hovered_ = false;
std::atomic<uint32_t> current_file_id_{0};
struct QueuedFile {
std::filesystem::path file_path;
std::string file_label;
std::string remote_id;
};
std::queue<QueuedFile> file_send_queue_;
std::mutex file_queue_mutex_;
enum class FileTransferStatus { Queued, Sending, Completed, Failed };
struct FileTransferInfo {
std::string file_name;
std::filesystem::path file_path;
uint64_t file_size = 0;
FileTransferStatus status = FileTransferStatus::Queued;
uint64_t sent_bytes = 0;
uint32_t file_id = 0;
uint32_t rate_bps = 0;
};
std::vector<FileTransferInfo> file_transfer_list_;
std::mutex file_transfer_list_mutex_;
};
// Runtime state for one connected or connecting remote endpoint. It groups the
// connection, media, input, window and transfer data that share one lifetime.
struct RemoteSession {
Params params_;
PeerPtr *peer_ = nullptr;
std::string audio_label_ = "control_audio";
std::string data_label_ = "data";
std::string mouse_label_ = "mouse";
std::string keyboard_label_ = "keyboard";
std::string file_label_ = "file";
std::string control_data_label_ = "control_data";
std::string file_feedback_label_ = "file_feedback";
std::string clipboard_label_ = "clipboard";
std::string local_id_;
std::string remote_id_;
bool exit_ = false;
bool signal_connected_ = false;
SignalStatus signal_status_ = SignalStatus::SignalClosed;
bool connection_established_ = false;
bool rejoin_ = false;
std::atomic<bool> connection_attempt_active_ = false;
std::chrono::steady_clock::time_point connection_attempt_started_at_;
bool net_traffic_stats_button_pressed_ = false;
bool enable_mouse_control_ = true;
bool mouse_controller_is_started_ = false;
bool audio_capture_button_pressed_ = true;
bool control_mouse_ = true;
bool streaming_ = false;
bool is_control_bar_in_left_ = true;
bool control_bar_hovered_ = false;
bool display_selectable_hovered_ = false;
bool shortcut_selectable_hovered_ = false;
bool control_bar_expand_ = true;
bool reset_control_bar_pos_ = false;
bool control_window_width_is_changing_ = false;
bool control_window_height_is_changing_ = false;
bool p2p_mode_ = true;
bool remember_password_ = false;
char remote_password_[7] = "";
float sub_stream_window_width_ = 1280;
float sub_stream_window_height_ = 720;
float control_window_min_width_ = 20;
float control_window_max_width_ = 300;
float control_window_min_height_ = 38;
float control_window_max_height_ = 180;
float control_window_width_ = 300;
float control_window_height_ = 38;
float control_bar_pos_x_ = 0;
float control_bar_pos_y_ = 30;
float mouse_diff_control_bar_pos_x_ = 0;
float mouse_diff_control_bar_pos_y_ = 0;
double control_bar_button_pressed_time_ = 0;
double net_traffic_stats_button_pressed_time_ = 0;
// Written by the decode callback thread and consumed by the SDL thread.
std::mutex video_frame_mutex_;
std::shared_ptr<std::vector<unsigned char>> front_frame_;
std::shared_ptr<std::vector<unsigned char>> back_frame_;
bool render_rect_dirty_ = false;
bool stream_cleanup_pending_ = false;
float mouse_pos_x_ = 0;
float mouse_pos_y_ = 0;
float mouse_pos_x_last_ = 0;
float mouse_pos_y_last_ = 0;
int texture_width_ = 1280;
int texture_height_ = 720;
int video_width_ = 0;
int video_height_ = 0;
int video_width_last_ = 0;
int video_height_last_ = 0;
int selected_display_ = 0;
size_t video_size_ = 0;
bool tab_selected_ = false;
bool tab_opened_ = true;
std::optional<float> pos_x_before_docked_;
std::optional<float> pos_y_before_docked_;
float render_window_x_ = 0;
float render_window_y_ = 0;
float render_window_width_ = 0;
float render_window_height_ = 0;
std::string fullscreen_button_label_ = "Fullscreen";
std::string net_traffic_stats_button_label_ = "Show Net Traffic Stats";
std::string mouse_control_button_label_ = "Mouse Control";
std::string audio_capture_button_label_ = "Audio Capture";
std::string remote_host_name_;
bool remote_service_status_received_ = false;
bool remote_service_available_ = false;
std::string remote_interactive_stage_;
std::vector<DisplayInfo> display_info_list_;
SDL_Texture *stream_texture_ = nullptr;
uint8_t *argb_buffer_ = nullptr;
int argb_buffer_size_ = 0;
SDL_FRect stream_render_rect_f_ = {0.0f, 0.0f, 0.0f, 0.0f};
SDL_Rect stream_render_rect_{};
SDL_Rect stream_render_rect_last_{};
ImVec2 control_window_pos_{};
// Shared by minirtc callbacks, SDL rendering and SDL audio callbacks.
std::atomic<ConnectionStatus> connection_status_ = ConnectionStatus::Closed;
TraversalMode traversal_mode_ = TraversalMode::UnknownMode;
int fps_ = 0;
int frame_count_ = 0;
std::chrono::steady_clock::time_point last_time_;
XNetTrafficStats net_traffic_stats_{};
using QueuedFile = FileTransferState::QueuedFile;
using FileTransferStatus = FileTransferState::FileTransferStatus;
using FileTransferInfo = FileTransferState::FileTransferInfo;
FileTransferState file_transfer_;
};
using RemoteSessionPtr = std::shared_ptr<RemoteSession>;
} // namespace crossdesk::gui_detail
#endif // CROSSDESK_GUI_REMOTE_SESSION_H_
+186
View File
@@ -0,0 +1,186 @@
#ifndef CROSSDESK_GUI_RUNTIME_STATE_H_
#define CROSSDESK_GUI_RUNTIME_STATE_H_
#include <atomic>
#include <chrono>
#include <cstdint>
#include <memory>
#include <mutex>
#include <shared_mutex>
#include <string>
#include <thread>
#include <unordered_map>
#include <utility>
#include <vector>
#include "config_center.h"
#include "path_manager.h"
#include "runtime/device_presence_cache.h"
#include "runtime/remote_session.h"
#include "thumbnail.h"
namespace crossdesk::gui_detail {
struct InfrastructureState {
std::unique_ptr<ConfigCenter> config_center_;
ConfigCenter::LANGUAGE localization_language_ =
ConfigCenter::LANGUAGE::CHINESE;
std::unique_ptr<PathManager> path_manager_;
std::string exec_log_path_;
std::string dll_log_path_;
std::string cache_path_;
int localization_language_index_ = -1;
int localization_language_index_last_ = -1;
bool modules_inited_ = false;
unsigned char aes128_key_[16]{};
unsigned char aes128_iv_[16]{};
};
struct RecentConnectionsState {
std::shared_ptr<Thumbnail> thumbnail_;
std::vector<std::pair<std::string, Thumbnail::RecentConnection>>
recent_connections_;
std::vector<std::string> recent_connection_ids_;
int recent_connection_image_width_ = 160;
int recent_connection_image_height_ = 90;
uint32_t recent_connection_image_save_time_ = 0;
DevicePresenceCache device_presence_cache_;
bool need_to_send_recent_connections_ = true;
};
struct PeerState {
SignalStatus signal_status_ = SignalStatus::SignalClosed;
std::string signal_status_str_;
bool signal_connected_ = false;
PeerPtr *peer_ = nullptr;
PeerPtr *peer_reserved_ = nullptr;
std::string video_primary_label_ = "primary_display";
std::string video_secondary_label_ = "secondary_display";
std::string audio_label_ = "audio";
std::string data_label_ = "data";
std::string mouse_label_ = "mouse";
std::string keyboard_label_ = "keyboard";
std::string info_label_ = "info";
std::string control_data_label_ = "control_data";
std::string file_label_ = "file";
std::string file_feedback_label_ = "file_feedback";
std::string clipboard_label_ = "clipboard";
Params params_;
};
// OS-specific service and permission state is kept separate from transport
// state so platform code does not appear to be part of the peer protocol.
struct PlatformIntegrationState {
#if _WIN32
std::atomic<bool> pending_windows_service_sas_{false};
bool local_service_status_received_ = false;
bool local_service_available_ = false;
std::string local_interactive_stage_;
uint32_t last_local_secure_input_block_log_tick_ = 0;
uint32_t last_windows_service_status_tick_ = 0;
uint32_t optimistic_windows_secure_desktop_until_tick_ = 0;
#if CROSSDESK_PORTABLE
enum class PortableServiceInstallState {
idle,
installing,
succeeded,
failed,
};
bool portable_service_prompt_checked_ = false;
bool show_portable_service_install_window_ = false;
bool show_portable_service_prompt_suppressed_window_ = false;
bool portable_service_do_not_remind_ = false;
bool portable_service_prompt_suppressed_ = false;
std::atomic<PortableServiceInstallState> portable_service_install_state_{
PortableServiceInstallState::idle};
std::thread portable_service_install_thread_;
#endif
#endif
#ifdef __APPLE__
bool show_request_permission_window_ = true;
bool mac_permission_status_initialized_ = false;
uint32_t mac_permission_last_check_tick_ = 0;
bool mac_screen_recording_permission_granted_ = false;
bool mac_accessibility_permission_granted_ = false;
bool mac_screen_recording_permission_requested_ = false;
bool mac_accessibility_permission_requested_ = false;
#endif
};
struct UserSettingsState {
char client_id_[10] = "";
char client_id_display_[12] = "";
char client_id_with_password_[17] = "";
char password_saved_[7] = "";
char self_hosted_id_[17] = "";
char self_hosted_user_id_[17] = "";
int language_button_value_ = 0;
int video_quality_button_value_ = 2;
int video_frame_rate_button_value_ = 1;
int video_encode_format_button_value_ = 0;
bool enable_hardware_video_codec_ = true;
bool enable_turn_ = true;
bool enable_srtp_ = false;
char signal_server_ip_[256] = "api.crossdesk.cn";
char signal_server_port_[6] = "9099";
char coturn_server_port_[6] = "3478";
bool enable_self_hosted_ = false;
int language_button_value_last_ = 0;
int video_quality_button_value_last_ = 0;
int video_frame_rate_button_value_last_ = 0;
int video_encode_format_button_value_last_ = 0;
bool enable_hardware_video_codec_last_ = false;
bool enable_turn_last_ = true;
bool enable_srtp_last_ = false;
bool enable_self_hosted_last_ = false;
bool enable_autostart_ = false;
bool enable_autostart_last_ = false;
bool enable_daemon_ = false;
bool enable_daemon_last_ = false;
bool enable_minimize_to_tray_ = false;
bool enable_minimize_to_tray_last_ = false;
char file_transfer_save_path_buf_[512] = "";
std::string file_transfer_save_path_last_;
char signal_server_ip_self_[256] = "";
char signal_server_port_self_[6] = "";
char coturn_server_port_self_[6] = "";
bool settings_window_pos_reset_ = true;
bool self_hosted_server_config_window_pos_reset_ = true;
std::string selected_current_file_path_;
bool show_file_browser_ = true;
};
struct ConnectionState {
using RemoteSessionMap =
std::unordered_map<std::string, RemoteSessionPtr>;
RemoteSessionMap remote_sessions_;
std::shared_mutex remote_sessions_mutex_;
std::vector<std::thread> thumbnail_save_threads_;
std::mutex thumbnail_save_threads_mutex_;
std::shared_mutex connection_status_mutex_;
std::unordered_map<std::string, ConnectionStatus> connection_status_;
std::unordered_map<std::string, std::string> connection_host_names_;
std::string selected_server_remote_id_;
std::string selected_server_remote_hostname_;
std::mutex pending_presence_probe_mutex_;
bool pending_presence_probe_ = false;
bool pending_presence_result_ready_ = false;
bool pending_presence_online_ = false;
std::chrono::steady_clock::time_point pending_presence_probe_started_at_;
std::string pending_presence_remote_id_;
std::string pending_presence_password_;
bool pending_presence_remember_password_ = false;
};
struct RuntimeState : InfrastructureState,
RecentConnectionsState,
PeerState,
PlatformIntegrationState,
UserSettingsState,
ConnectionState {};
} // namespace crossdesk::gui_detail
#endif // CROSSDESK_GUI_RUNTIME_STATE_H_
+262
View File
@@ -0,0 +1,262 @@
#include "runtime/gui_runtime.h"
#include <algorithm>
#include <cstring>
#include <shared_mutex>
#include <string>
#include "rd_log.h"
#if _WIN32
#include "interactive_state.h"
#include "service_host.h"
#endif
namespace crossdesk {
namespace {
#if _WIN32
struct WindowsServiceInteractiveStatus {
bool available = false;
bool sas_secure_desktop_grace_active = false;
unsigned int error_code = 0;
std::string interactive_stage;
std::string error;
};
constexpr uint32_t kWindowsServiceStatusIntervalMs = 1000;
constexpr uint32_t kWindowsServiceSasSecureDesktopGraceMs = 2000;
constexpr DWORD kWindowsServiceQueryTimeoutMs = 500;
constexpr DWORD kWindowsServiceSasTimeoutMs = 500;
bool IsTransientWindowsServiceStatusError(const std::string &error) {
return error == "pipe_unavailable" || error == "pipe_connect_failed" ||
error == "pipe_read_failed";
}
RemoteAction
BuildWindowsServiceStatusAction(const WindowsServiceInteractiveStatus &status) {
RemoteAction action{};
action.type = ControlType::service_status;
action.ss.available = status.available;
std::strncpy(action.ss.interactive_stage, status.interactive_stage.c_str(),
sizeof(action.ss.interactive_stage) - 1);
action.ss.interactive_stage[sizeof(action.ss.interactive_stage) - 1] = '\0';
return action;
}
bool QueryWindowsServiceInteractiveStatus(
WindowsServiceInteractiveStatus *status) {
if (status == nullptr) {
return false;
}
*status = WindowsServiceInteractiveStatus{};
const std::string response =
QueryCrossDeskService("status", kWindowsServiceQueryTimeoutMs);
auto json = nlohmann::json::parse(response, nullptr, false);
if (json.is_discarded() || !json.is_object()) {
status->error = "invalid_service_status_json";
return false;
}
status->available = json.value("ok", false);
if (!status->available) {
status->error = json.value("error", std::string("service_unavailable"));
status->error_code = json.value("code", 0u);
return true;
}
status->interactive_stage = json.value("interactive_stage", std::string());
status->sas_secure_desktop_grace_active =
json.value("sas_secure_desktop_grace_active", false);
if (ShouldNormalizeUnlockToUserDesktop(
json.value("interactive_lock_screen_visible", false),
status->interactive_stage, json.value("session_locked", false),
json.value("interactive_logon_ui_visible", false),
json.value("interactive_secure_desktop_active",
json.value("secure_desktop_active", false)),
json.value("credential_ui_visible", false),
json.value("password_box_visible", false),
json.value("unlock_ui_visible", false),
json.value("last_session_event", std::string()))) {
status->interactive_stage = "user-desktop";
}
return true;
}
#endif
} // namespace
void GuiRuntime::ResetRemoteServiceStatus(RemoteSession &props) {
props.remote_service_status_received_ = false;
props.remote_service_available_ = false;
props.remote_interactive_stage_.clear();
}
void GuiRuntime::ApplyRemoteServiceStatus(RemoteSession &props,
const ServiceStatus &status) {
props.remote_service_status_received_ = true;
props.remote_service_available_ = status.available;
props.remote_interactive_stage_ = status.interactive_stage;
}
GuiRuntime::RemoteUnlockState
GuiRuntime::GetRemoteUnlockState(const RemoteSession &props) const {
if (!props.remote_service_status_received_) {
return RemoteUnlockState::none;
}
if (!props.remote_service_available_) {
return RemoteUnlockState::service_unavailable;
}
if (props.remote_interactive_stage_ == "credential-ui") {
return RemoteUnlockState::credential_ui;
}
if (props.remote_interactive_stage_ == "lock-screen") {
return RemoteUnlockState::lock_screen;
}
if (props.remote_interactive_stage_ == "secure-desktop") {
return RemoteUnlockState::secure_desktop;
}
return RemoteUnlockState::none;
}
void GuiRuntime::HandleWindowsServiceIntegration() {
#if _WIN32
static bool last_logged_service_available = true;
static unsigned int last_logged_service_error_code = 0;
static std::string last_logged_service_error;
if (!is_server_mode_ || peer_ == nullptr) {
ResetLocalWindowsServiceState(true);
return;
}
const bool has_connected_remote = [&] {
std::shared_lock lock(connection_status_mutex_);
return std::any_of(connection_status_.begin(), connection_status_.end(),
[](const auto &entry) {
return entry.second == ConnectionStatus::Connected;
});
}();
if (!has_connected_remote) {
ResetLocalWindowsServiceState(false);
return;
}
bool force_broadcast = false;
if (pending_windows_service_sas_.exchange(false, std::memory_order_relaxed)) {
const std::string response =
QueryCrossDeskService("sas", kWindowsServiceSasTimeoutMs);
auto json = nlohmann::json::parse(response, nullptr, false);
if (json.is_discarded() || !json.value("ok", false)) {
LOG_WARN("Remote SAS request failed: {}", response);
} else {
LOG_INFO("Remote SAS request forwarded to local Windows service");
optimistic_windows_secure_desktop_until_tick_ =
static_cast<uint32_t>(SDL_GetTicks()) +
kWindowsServiceSasSecureDesktopGraceMs;
local_service_status_received_ = true;
local_service_available_ = true;
local_interactive_stage_ = "secure-desktop";
}
last_windows_service_status_tick_ = 0;
force_broadcast = true;
}
const uint32_t now = static_cast<uint32_t>(SDL_GetTicks());
if (!force_broadcast && last_windows_service_status_tick_ != 0 &&
now - last_windows_service_status_tick_ <
kWindowsServiceStatusIntervalMs) {
return;
}
last_windows_service_status_tick_ = now;
WindowsServiceInteractiveStatus status;
const bool status_ok = QueryWindowsServiceInteractiveStatus(&status);
WindowsServiceInteractiveStatus broadcast_status = status;
const bool previous_secure_desktop_interaction =
IsSecureDesktopInteractionRequired(local_interactive_stage_);
const bool optimistic_secure_desktop_active =
optimistic_windows_secure_desktop_until_tick_ != 0 &&
static_cast<int32_t>(optimistic_windows_secure_desktop_until_tick_ -
now) > 0;
const bool keep_optimistic_secure_desktop =
status_ok && status.available && optimistic_secure_desktop_active &&
status.sas_secure_desktop_grace_active &&
status.interactive_stage == "user-desktop";
local_service_status_received_ =
status_ok || previous_secure_desktop_interaction;
local_service_available_ = status.available;
if (status.available) {
if (keep_optimistic_secure_desktop) {
local_interactive_stage_ = "secure-desktop";
broadcast_status.interactive_stage = local_interactive_stage_;
} else {
local_interactive_stage_ = status.interactive_stage;
optimistic_windows_secure_desktop_until_tick_ = 0;
}
} else if (!previous_secure_desktop_interaction) {
local_interactive_stage_.clear();
optimistic_windows_secure_desktop_until_tick_ = 0;
}
if (status_ok) {
const bool availability_changed =
status.available != last_logged_service_available;
const bool error_changed =
!status.available &&
(status.error != last_logged_service_error ||
status.error_code != last_logged_service_error_code);
if (availability_changed || error_changed) {
if (status.available) {
LOG_INFO(
"Local Windows service available for secure desktop integration");
} else if (IsTransientWindowsServiceStatusError(status.error)) {
LOG_INFO("Local Windows service temporarily unavailable, keeping last "
"secure desktop state: error={}, code={}",
status.error, status.error_code);
} else {
LOG_WARN(
"Local Windows service unavailable, secure desktop integration "
"disabled: error={}, code={}",
status.error, status.error_code);
}
last_logged_service_available = status.available;
last_logged_service_error = status.error;
last_logged_service_error_code = status.error_code;
}
} else if (last_logged_service_available ||
last_logged_service_error != "invalid_service_status_json") {
LOG_WARN(
"Local Windows service status query failed, secure desktop integration "
"disabled");
last_logged_service_available = false;
last_logged_service_error = "invalid_service_status_json";
last_logged_service_error_code = 0;
}
RemoteAction remote_action =
BuildWindowsServiceStatusAction(broadcast_status);
std::string msg = remote_action.to_json();
int ret = SendReliableDataFrame(peer_, msg.data(), msg.size(),
control_data_label_.c_str());
if (ret != 0) {
LOG_WARN("Broadcast Windows service status failed, ret={}", ret);
}
#endif
}
#if _WIN32
void GuiRuntime::ResetLocalWindowsServiceState(bool clear_pending_sas) {
last_windows_service_status_tick_ = 0;
if (clear_pending_sas) {
pending_windows_service_sas_.store(false, std::memory_order_relaxed);
}
local_service_status_received_ = false;
local_service_available_ = false;
local_interactive_stage_.clear();
optimistic_windows_secure_desktop_until_tick_ = 0;
}
#endif
} // namespace crossdesk

Some files were not shown because too many files have changed in this diff Show More