mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-11-01 19:47:44 +00:00
* fix web break introduced in 38f130071 fix(linux): enable mouse side buttons in remote sessions (#14848)
* feat(shortcuts): user-configurable keyboard shortcuts for session actions
Adds a keyboard shortcut feature (Rust matcher + Dart UI + cross-language
parity tests) that lets users bind combinations like Ctrl+Alt+Shift+P to
session actions. Bindings are stored in LocalConfig under
`keyboard-shortcuts`; the matcher gates dispatch on `enabled` and
`pass_through` flags so flipping the master switch off is a hard stop.
Wire-up summary:
- src/keyboard/shortcuts.rs: matcher, default bindings, parity test against
flutter/test/fixtures/default_keyboard_shortcuts.json
- src/keyboard.rs: shortcut intercept in process_event{,_with_session},
feature-gated to `flutter`; runs before key swapping so users bind to
physical keys
- src/flutter_ffi.rs: main_reload_keyboard_shortcuts +
main_get_default_keyboard_shortcuts; reload_from_config seeded in main_init
- flutter/lib/common/widgets/keyboard_shortcuts/: shared config page body,
recording dialog, shortcut display formatter, action group registry
- flutter/lib/desktop/pages/desktop_keyboard_shortcuts_page.dart and
flutter/lib/mobile/pages/mobile_keyboard_shortcuts_page.dart: platform
shells around the shared body
- flutter/lib/models/shortcut_model.dart: per-session ShortcutModel +
registerSessionShortcutActions for actions with no toolbar TToggleMenu /
TRadioMenu (fullscreen, switch display/tab, close tab, voice call, etc.)
- flutter/lib/common/widgets/toolbar.dart: optional `actionId` field on
TToggleMenu / TRadioMenu, plus per-helper auto-register pass that wires
tagged entries' existing onChanged into the ShortcutModel
- flutter/test/keyboard_shortcuts_test.dart + fixtures: cross-language
parity (default bindings, supported key vocabulary)
Design principles applied during review:
1. Additions are fine; modifications to original logic must be deliberate.
Tagging an existing TToggleMenu entry with `actionId:` is an addition.
Rewriting its onChanged to satisfy a new contract is a modification —
and was reverted for every case where the original click behavior was
working. Four closures were touched and then reverted (mobile View
Mode, Privacy mode multi-impl, Relative mouse mode, Reverse mouse
wheel); their shortcuts are wired via standalone closures in
shortcut_model.dart instead.
2. Toolbar auto-register is reserved for entries whose onChanged is
inherently self-flipping — typically `sessionToggleOption(name)` where
the named option is flipped in place and the input bool is unused. The
register pass passes `!menu.value` from registration time, which is
harmless under self-flipping but wrong for closures that consume the
input bool directly. Tagging a non-self-flipping entry forces a closure
rewrite; choose non-toolbar registration in that case.
3. When shortcuts are disabled, toolbar behavior must be bit-for-bit
unchanged. The matcher's `enabled`-gate already guarantees no
dispatch; the auto-register pass is left unconditional (its only effect
is HashMap operations on a separate ShortcutModel) so mid-session
enable works without a reconnect. The trade-off is intentional and
documented at the top of toolbarControls.
4. Comments stay terse. Rationale lives in one place — the doc comment of
the helper or registration site, not duplicated at every call site.
5. Where an existing helper needs a new optional behavior (e.g.
`_OptionCheckBox` gaining a tooltip slot), the new branch must reduce
to byte-identical output for existing callers (`trailing == null`
case → original `Expanded(Text)` layout). Verified.
6. Action IDs and labels stay consistent. Renamed `reset_cursor` →
`reset_canvas` so the action ID matches its user-facing label
("Reset canvas") and capability flag.
Out-of-scope but included:
- AGENTS.md: documents flutter_rust_bridge no-codegen workflow and the
Web target's hand-written TS client, since both are load-bearing for
any new FFI work.
- remote_toolbar.dart: i18n fix for the per-monitor tooltip ("All
monitors" / "Monitor #N"), unrelated to shortcuts but kept here.
* langs
* fix(keyboard): shortcuts, harden config and callback lifecycle
Signed-off-by: fufesou <linlong1266@gmail.com>
* feat(keyboard): shortcuts, color of "Reset to defaults"
Signed-off-by: fufesou <linlong1266@gmail.com>
* feat(keyboard): shortcuts, release keys before shortcut callback
Signed-off-by: fufesou <linlong1266@gmail.com>
* feat(keyboard): shortcuts, debug web
Signed-off-by: fufesou <linlong1266@gmail.com>
* fix(keyboard): iPad, icon
Signed-off-by: fufesou <linlong1266@gmail.com>
* langs: add shortcut keys to az, gl, ur
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ra4my61t8q1FN5n59wB16D
* fix(keyboard): shortcuts, keep local modifiers and release keys on the right session
When a chord fired, the shortcut path reused release_remote_keys(), which
replays synthetic releases through process_event(). That cleared the local
MODIFIERS_STATE while the user still held the modifiers, so the next key of
a held chord no longer matched and leaked to the remote, and it sent the
releases to the globally current session instead of the one that typed the
chord.
The shortcut path now releases the remote keys itself: it sends through the
caller's session, and restores the modifiers it released on the remote as
still held locally. The press that fired is remembered until its release,
so its auto repeats and its release are consumed instead of reaching the
remote.
Also state the contract for unavailable actions: a bound chord always
belongs to RustDesk and is a no-op when the session has no handler.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ra4my61t8q1FN5n59wB16D
* fix(keyboard): shortcuts, record the physical key
The recording dialog and the Web Flutter matcher named keys by
LogicalKeyboardKey, while the native matcher sees the physical key through
its USB HID usage and the Web JS matcher through KeyboardEvent.code. On
AZERTY, QWERTZ, Dvorak and similar layouts a recorded binding then did not
fire, or fired from another key.
Name keys by their physical position everywhere: physicalKeyName maps the
USB HID usage to the stored name. A new fixture pins every name to its USB
HID usage, checked on the Dart side and against rdev::usb_hid_key_from_code
on the Rust side, and a test covers AZERTY and QWERTZ events.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ra4my61t8q1FN5n59wB16D
* fix(keyboard): shortcuts, keep an empty binding list on re-enable
setEnabled seeded the defaults whenever the binding list was empty, so a
user who cleared every binding got all defaults back after disable and
enable. Seed only when the config has never held a bindings list.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ra4my61t8q1FN5n59wB16D
* fix(keyboard): shortcuts, track every fired key until its release
FIRED_KEY held a single key, so overlapping shortcut keys broke it: with the
chord held, P then C overwrote P, P's release reached the remote, and a P
repeat could fire the screenshot again. A repeat after the modifiers were
released was also treated as a new press and leaked to the remote.
Keep a set of fired keys instead. Once a press fires, every event of that
key belongs to RustDesk until its release: repeats are consumed without
dispatching, whatever the modifiers do, and the release removes the key.
Entries whose release never arrives are dropped when a session view is
entered or a session is closed, not by guessing from the modifiers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ra4my61t8q1FN5n59wB16D
* fix(keyboard): shortcuts, keep fired keys per session
FIRED_KEYS was process-global while dispatch is per session. Closing or
entering any session cleared the fired keys of all of them, so a key still
held in another session leaked its release to that remote, and a key fired
in one session made the same key in another session look like a repeat.
Key the fired set by session: try_dispatch resolves the session first and
only looks at that session's keys, and session_close clears only the
closing session. Entering a session view no longer clears anything: it is
a pointer enter, and a fired key may still be physically held across it.
A key whose release is missed otherwise heals itself: its next press is
consumed as a repeat and that release removes it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ra4my61t8q1FN5n59wB16D
* fix(shortcuts): preserve input state and refresh action availability
* fix(keyboard): shortcuts, keep a fired key owned across session close
Close tab is itself a shortcut, so the session that consumed the key press
is gone before the key is released. Both matchers dropped the key with the
session: the Flutter dispatcher removed its own entries in clear(), and the
Rust set was keyed by session and cleared in session_close. The release,
and the repeats while the key stayed held, then reached the next session's
remote as ordinary input.
Ownership of a fired key is physical, so it is now one set shared by every
session on each side, and closing a session no longer touches it: clear()
only drops the pending action and modifier replay, and the Rust session
state only tracks released modifiers. A key whose release is missed heals
itself on its next press. Entering view-only with the Flutter input source
still clears the Rust set, since that source stops routing key events to it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ra4my61t8q1FN5n59wB16D
* fix(keyboard): shortcuts, drop the Toggle Input Source action
The action switches the key capture backend between key down and key up.
Fired-key ownership lives in whichever matcher saw the press, the Flutter
dispatcher or the Rust set, and there is no handoff between them: after the
switch the repeats and the release of the held key reach the other backend,
which forwards them to the remote and, while the chord is held, matches the
action again and toggles the source back. Remove the shortcut instead of
building a handoff for one action. The toolbar radio menu is unchanged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ra4my61t8q1FN5n59wB16D
* perf(keyboard): shortcuts, parse the config once per stored value
The Flutter matcher read the local config three times and parsed its JSON
three times on every key press in legacy mode, on Web and in view-only.
Keep one parsed snapshot keyed by the raw stored string, so a key press
costs one config read and no parsing while the config is unchanged. The
existing readers are thin views of the same snapshot.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ra4my61t8q1FN5n59wB16D
* fix(keyboard): shortcuts, run focus-changing actions on key release
Close tab and tab switching move keyboard focus to another session before
the key that fired them is released. On Linux the Flutter input source
routes map-mode sessions through the Rust matcher and legacy-mode sessions
through the Dart matcher, and neither knows the other's fired keys, so
after such an action the repeats and the release of the held key reached
the next session's remote, and the key stayed recorded as fired in the
matcher that saw the press.
Routing Linux legacy mode through the Rust FFI path instead is not a drop-in
change: the raw key handler builds its event with no unicode, so Rust's
legacy mode would fall back to US key names and lose the layout-aware
characters the Dart path sends today, and it would take modifiers from the
X11 key state instead of Flutter's.
Run these three actions when the fired key is released. The matcher that
consumed the press then also sees the repeats and the release, the remote
modifiers are still released on the press, and a session that closes drops
the action it owed. Both matchers keep the same list of actions.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ra4my61t8q1FN5n59wB16D
* test(keyboard): shortcuts, pin the key-up action list on both sides
kShortcutActionsRunOnKeyUp and runs_on_release are two hand-kept lists of
the actions that run on key release. Add a shared fixture and check each
side against it, the way the key vocabulary and the default bindings are
already checked, so adding a focus-changing action on one side breaks a
test until the other side follows. The Dart test also checks that every
listed id is a configurable action.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ra4my61t8q1FN5n59wB16D
* fix: prevent lost input after remote session shortcuts
Avoid synthetic Alt presses that activate the Windows system menu
and swallow the next character.
Trigger restart, switch-sides, and chat actions on key release so
focus changes cannot leave subsequent shortcut presses ignored.
Signed-off-by: fufesou <linlong1266@gmail.com>
* fix(mobile): correct Chat keyboard and input helper behavior
- Keep soft keyboard and helper hidden when Chat opens via shortcut
- Restore soft input on touch and remote focus after Chat closes
- Handle modifier releases while Chat owns focus
- Clear hidden virtual modifiers while preserving held keys and Pin behavior
Signed-off-by: fufesou <linlong1266@gmail.com>
* fix(shortcuts): prevent recording non-simultaneous key combinations
Build new captures from currently held keys instead of reusing released
letters. Preserve the last valid chord after release so it can be saved.
Signed-off-by: fufesou <linlong1266@gmail.com>
* fix(shortcuts): isolate window actions and prevent duplicate Alt presses
- Dispatch shortcut actions only to the originating session window.
- Clear local modifier state on focus loss without resending modifiers
already released to Windows peers, preventing lost following input.
- Strengthen the regression assertion against extra Alt keydown events
Signed-off-by: fufesou <linlong1266@gmail.com>
* refact(shortcuts): empty default shortcuts for Cmd+Option+Shift+Del on macOS and iOS
Because Del may be ambiguous for new macOS users.
Cmd+Option+Shift+Backspace is "Empty Trash without confirmation" shortcut on macOS.
Signed-off-by: fufesou <linlong1266@gmail.com>
* fix: use session peer platform for Ctrl+Alt+Del events
Fix Android-to-Linux Ctrl+Alt+Del delivery by using the session's peer
platform instead of the desktop global session, which defaults to Windows
Signed-off-by: fufesou <linlong1266@gmail.com>
* fix: hide relative mouse mode shortcuts on Wayland
Separate relative mouse mode availability from screenshot support.
Filter unsupported actions and defaults while preserving saved bindings
Signed-off-by: fufesou <linlong1266@gmail.com>
* fix: align shortcut availability with platform support
- Hide unsupported mobile and web shortcut actions
- Enable mobile scaling and Android voice call shortcuts
- Use openMonitorInTheSameTab() for display switching
- Preserve cursor position when switching displays on desktop and web
Signed-off-by: fufesou <linlong1266@gmail.com>
* fix: prevent held-key shortcut recording bypass
Signed-off-by: fufesou <linlong1266@gmail.com>
* fix: preserve keyboard input after shortcut dialogs
Run native clipboard shortcuts on key release and wait for remote modifier releases before dispatching.
Release remote modifiers when mobile local UI takes focus, and document the tested iPad Bluetooth Mute shortcut limitation.
Signed-off-by: fufesou <linlong1266@gmail.com>
* fix: preserve remote key state during local shortcuts
Keep unrelated held keys pressed for session actions that retain input. Preserve key release when focus or input handling changes, and avoid synthetic Alt taps during shortcut dispatch.
Signed-off-by: fufesou <linlong1266@gmail.com>
* fix: suspend saved relative mouse shortcuts on Wayland
Filter the native active binding cache without changing stored configuration, so the binding becomes available again under X11.
Signed-off-by: fufesou <linlong1266@gmail.com>
* fix: exclude unsupported Wayland shortcuts from Flutter matching
Keep saved bindings intact while filtering the active config used by the Flutter matcher. Cover stored bindings and enable/pass-through flags in the existing shortcut tests.
Signed-off-by: fufesou <linlong1266@gmail.com>
* fix: retain forwarded key ownership across shortcut repeats
Ignore repeat presses for keys already forwarded to the peer, and release ordinary held keys before Show My Cursor enables view-only. Cover both paths with converter-level regressions.
Signed-off-by: fufesou <linlong1266@gmail.com>
* fix: release Flutter-held keys before view-only shortcuts
Track ordinary keys forwarded by the Dart matcher paths and await their releases before View Only or Show My Cursor disables input. Keep Mute key ownership and existing empty-cleanup timing unchanged.
Signed-off-by: fufesou <linlong1266@gmail.com>
* fix: filter active Flutter shortcuts by platform capabilities
Reuse the Settings action filter for saved bindings while preserving storage, default-seeding behavior and configurable Apple Ctrl+Alt+Del bindings.
Signed-off-by: fufesou <linlong1266@gmail.com>
* fix: filter native shortcut caches for mobile controllers
Suspend saved desktop-only actions on Android and iOS and hide unavailable iOS actions without changing persisted bindings. Keep native cache filtering aligned with Flutter platform capabilities.
Signed-off-by: fufesou <linlong1266@gmail.com>
* fix: share filtered shortcut bindings with the Web matcher
Pass the active platform config to JavaScript at startup and after settings changes, preserving unsupported saved bindings in local storage.
Signed-off-by: fufesou <linlong1266@gmail.com>
* fix: release Flutter-held keys before focus-changing shortcuts
Drain ordinary keys owned by the original session before run-on-key-up
actions and native clipboard callbacks. Preserve Mute holds, pending-action
cancellation and Web clipboard key-down activation.
Document the required Web matcher counterpart in the bridge:
rustdesk/rustdesk-web-js@94abe431d6
Validation: 61 native Flutter tests, 32 Chrome/HTML tests, targeted analysis
and Windows debug build.
Signed-off-by: fufesou <linlong1266@gmail.com>
* refact: remove the useless comments
Signed-off-by: fufesou <linlong1266@gmail.com>
* fix: prevent Windows menu activation from Alt-only shortcuts
Neutralize the forwarded Alt with a balanced temporary Ctrl press when a consumed local shortcut releases its only modifier in Windows Map mode. Keep physical modifier tracking and normal Alt pass-through unchanged.
Add a regression covering both Alt keys, trigger repeats and the eventual physical release. Verified with 36 native shortcut tests and real Windows menu/input checks.
Signed-off-by: fufesou <linlong1266@gmail.com>
* fix: prevent Alt menu activation in all keyboard modes
Extend Windows Alt-only shortcut neutralization to Translate and Legacy. Keep the temporary Ctrl packets in Map mode so Legacy modifier synchronization cannot release Alt before Ctrl.
Extend the existing regression across keyboard modes and decode scan, virtual and control-key events. Verify 36 native tests and 34 Windows menu replay cases.
Signed-off-by: fufesou <linlong1266@gmail.com>
* fix: initialize outgoing chat before shortcut dispatch
Signed-off-by: fufesou <linlong1266@gmail.com>
* fix: guard Windows Alt release in Flutter legacy shortcuts
Signed-off-by: fufesou <linlong1266@gmail.com>
* fix: coordinate Web shortcut dispatch and Alt modifier cleanup
Signed-off-by: fufesou <linlong1266@gmail.com>
* fix: distinguish AltGraph from ordinary Alt shortcuts
Signed-off-by: fufesou <linlong1266@gmail.com>
* fix: dispatch macOS Web shortcuts without waiting for keyup
Signed-off-by: fufesou <linlong1266@gmail.com>
* fix: support legacy peers in privacy mode shortcut
Signed-off-by: fufesou <linlong1266@gmail.com>
* fix: exclude AltGraph from Flutter shortcut matching
Signed-off-by: fufesou <linlong1266@gmail.com>
* fix: exclude GTK AltGr input from raw shortcut matching
Signed-off-by: fufesou <linlong1266@gmail.com>
* test: trim redundant keyboard shortcut coverage
Signed-off-by: fufesou <linlong1266@gmail.com>
* fix(keyboard): prevent Cmd shortcuts from opening remote Start
Signed-off-by: fufesou <linlong1266@gmail.com>
* fix(shortcuts): guard Windows menu keys after Ctrl/Cmd swapping
Signed-off-by: fufesou <linlong1266@gmail.com>
* fix(shortcuts): align recording and display routing
Reject non-Apple AltGraph captures to match runtime filtering. Honor individual-display windows for next, previous, and all-display shortcuts.
Signed-off-by: fufesou <linlong1266@gmail.com>
* fix(shortcuts): reject GTK AltGr chords in recorder
Signed-off-by: fufesou <linlong1266@gmail.com>
* fix(android): restore keyboard focus for modifier-first input
Focus the existing FlutterView when a modifier is pressed without native view focus. This restores shortcut-first input after returning to RustDesk while preserving focused inputs and normal key dispatch.
Signed-off-by: fufesou <linlong1266@gmail.com>
* fix(shortcuts): release held keys before keyboard mode changes
Drain Flutter-forwarded keys before keyboard-mode shortcut actions so releases use the original encoding. This prevents held keys from remaining down after a Web Input source 2 mode change.
Signed-off-by: fufesou <linlong1266@gmail.com>
* fix(i18n): use sentence case for shortcut labels
Signed-off-by: fufesou <linlong1266@gmail.com>
* fix(mobile): preserve touch modifiers in the input helper
Keep touch-selected modifiers visible after hiding the keyboard. Clear physical modifier releases locally in view-only mode so they do not reopen the helper.
Signed-off-by: fufesou <linlong1266@gmail.com>
* fix(web): preserve modifiers across Legacy-to-Map shortcuts
Signed-off-by: fufesou <linlong1266@gmail.com>
* fix(shortcuts): check support before registering all displays view
Signed-off-by: fufesou <linlong1266@gmail.com>
---------
Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: fufesou <linlong1266@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2112 lines
70 KiB
Rust
2112 lines
70 KiB
Rust
use crate::{
|
|
common::{get_supported_keyboard_modes, is_keyboard_mode_supported},
|
|
input::{
|
|
MOUSE_BUTTON_LEFT, MOUSE_BUTTON_RIGHT, MOUSE_TYPE_DOWN, MOUSE_TYPE_MASK,
|
|
MOUSE_TYPE_TRACKPAD, MOUSE_TYPE_UP, MOUSE_TYPE_WHEEL,
|
|
},
|
|
ui_interface::use_texture_render,
|
|
};
|
|
use async_trait::async_trait;
|
|
#[cfg(all(target_os = "windows", not(feature = "flutter")))]
|
|
use base::config::keys;
|
|
#[cfg(not(feature = "flutter"))]
|
|
use base::fs;
|
|
use base::message_proto::*;
|
|
use bytes::Bytes;
|
|
use hbb_common::{
|
|
allow_err,
|
|
config::{Config, LocalConfig, PeerConfig},
|
|
get_version_number, log,
|
|
rendezvous_proto::ConnType,
|
|
tokio::{
|
|
self,
|
|
sync::mpsc,
|
|
time::{Duration as TokioDuration, Instant},
|
|
},
|
|
whoami, SessionID, Stream,
|
|
};
|
|
use rdev::{Event, EventType::*, KeyCode};
|
|
#[cfg(all(feature = "vram", feature = "flutter"))]
|
|
use std::ffi::c_void;
|
|
use std::{
|
|
collections::HashMap,
|
|
ops::{Deref, DerefMut},
|
|
str::FromStr,
|
|
sync::{
|
|
atomic::{AtomicUsize, Ordering},
|
|
Arc, Mutex, RwLock,
|
|
},
|
|
time::SystemTime,
|
|
};
|
|
use uuid::Uuid;
|
|
|
|
use crate::client::io_loop::Remote;
|
|
use crate::client::{
|
|
check_if_retry, handle_hash, handle_login_error, handle_login_from_ui, handle_test_delay,
|
|
input_os_password, send_mouse, send_pointer_device_event, FileManager, Key, LoginConfigHandler,
|
|
QualityStatus, KEY_MAP,
|
|
};
|
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
|
use crate::common::GrabState;
|
|
use crate::keyboard;
|
|
use crate::{client::Data, client::Interface};
|
|
|
|
#[cfg(any(target_os = "android", target_os = "ios", test))]
|
|
#[path = "platform/mobile_wheel.rs"]
|
|
mod mobile_wheel;
|
|
|
|
const CHANGE_RESOLUTION_VALID_TIMEOUT_SECS: u64 = 15;
|
|
|
|
#[derive(Clone, Default)]
|
|
pub struct Session<T: InvokeUiSession> {
|
|
pub password: String,
|
|
pub args: Vec<String>,
|
|
pub lc: Arc<RwLock<LoginConfigHandler>>,
|
|
pub sender: Arc<RwLock<Option<mpsc::UnboundedSender<Data>>>>,
|
|
pub thread: Arc<Mutex<Option<std::thread::JoinHandle<()>>>>,
|
|
pub ui_handler: T,
|
|
pub server_keyboard_enabled: Arc<RwLock<bool>>,
|
|
pub server_file_transfer_enabled: Arc<RwLock<bool>>,
|
|
pub server_clipboard_enabled: Arc<RwLock<bool>>,
|
|
pub last_change_display: Arc<Mutex<ChangeDisplayRecord>>,
|
|
pub connection_round_state: Arc<Mutex<ConnectionRoundState>>,
|
|
pub printer_names: Arc<RwLock<HashMap<i32, String>>>,
|
|
// Indicate whether the session is reconnected.
|
|
// Used to auto start file transfer after reconnection.
|
|
pub reconnect_count: Arc<AtomicUsize>,
|
|
pub last_audit_note: Arc<Mutex<String>>,
|
|
pub audit_guid: Arc<Mutex<String>>,
|
|
// Every cursor shape as the peer sent it, compressed, by the id the UI knows it by. The
|
|
// peer sends a shape once, and a window asks for it whenever it has to draw it again.
|
|
#[cfg(feature = "flutter")]
|
|
pub cursor_shapes: Arc<RwLock<HashMap<u64, CursorData>>>,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct SessionPermissionConfig {
|
|
pub lc: Arc<RwLock<LoginConfigHandler>>,
|
|
pub server_keyboard_enabled: Arc<RwLock<bool>>,
|
|
pub server_file_transfer_enabled: Arc<RwLock<bool>>,
|
|
pub server_clipboard_enabled: Arc<RwLock<bool>>,
|
|
}
|
|
|
|
pub struct ChangeDisplayRecord {
|
|
time: Instant,
|
|
display: i32,
|
|
width: i32,
|
|
height: i32,
|
|
}
|
|
|
|
enum ConnectionState {
|
|
Connecting,
|
|
Connected,
|
|
Disconnected,
|
|
}
|
|
|
|
/// ConnectionRoundState is used to control the reconnecting logic.
|
|
pub struct ConnectionRoundState {
|
|
round: u32,
|
|
state: ConnectionState,
|
|
}
|
|
|
|
impl ConnectionRoundState {
|
|
pub fn new_round(&mut self) -> u32 {
|
|
self.round += 1;
|
|
self.state = ConnectionState::Connecting;
|
|
self.round
|
|
}
|
|
|
|
pub fn set_connected(&mut self) {
|
|
self.state = ConnectionState::Connected;
|
|
}
|
|
|
|
pub fn is_round_gt(&self, round: u32) -> bool {
|
|
if round == u32::MAX && self.round == 0 {
|
|
true
|
|
} else {
|
|
round < self.round
|
|
}
|
|
}
|
|
|
|
pub fn set_disconnected(&mut self, round: u32) -> bool {
|
|
if self.is_round_gt(round) {
|
|
false
|
|
} else {
|
|
self.state = ConnectionState::Disconnected;
|
|
true
|
|
}
|
|
}
|
|
|
|
pub fn is_connected(&self) -> bool {
|
|
matches!(self.state, ConnectionState::Connected)
|
|
}
|
|
}
|
|
|
|
impl Default for ConnectionRoundState {
|
|
fn default() -> Self {
|
|
Self {
|
|
round: 0,
|
|
state: ConnectionState::Connecting,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for ChangeDisplayRecord {
|
|
fn default() -> Self {
|
|
Self {
|
|
time: Instant::now()
|
|
- TokioDuration::from_secs(CHANGE_RESOLUTION_VALID_TIMEOUT_SECS + 1),
|
|
display: 0,
|
|
width: 0,
|
|
height: 0,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ChangeDisplayRecord {
|
|
fn new(display: i32, width: i32, height: i32) -> Self {
|
|
Self {
|
|
time: Instant::now(),
|
|
display,
|
|
width,
|
|
height,
|
|
}
|
|
}
|
|
|
|
pub fn is_the_same_record(&self, display: i32, width: i32, height: i32) -> bool {
|
|
self.time.elapsed().as_secs() < CHANGE_RESOLUTION_VALID_TIMEOUT_SECS
|
|
&& self.display == display
|
|
&& self.width == width
|
|
&& self.height == height
|
|
}
|
|
}
|
|
|
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
|
impl SessionPermissionConfig {
|
|
pub fn is_text_clipboard_required(&self) -> bool {
|
|
*self.server_clipboard_enabled.read().unwrap()
|
|
&& *self.server_keyboard_enabled.read().unwrap()
|
|
&& !self.lc.read().unwrap().disable_clipboard.v
|
|
&& !self.lc.read().unwrap().view_only.v
|
|
}
|
|
|
|
#[cfg(feature = "unix-file-copy-paste")]
|
|
pub fn is_file_clipboard_required(&self) -> bool {
|
|
let lc = self.lc.read().unwrap();
|
|
*self.server_keyboard_enabled.read().unwrap()
|
|
&& *self.server_file_transfer_enabled.read().unwrap()
|
|
&& lc.enable_file_copy_paste.v
|
|
&& !lc.view_only.v
|
|
}
|
|
}
|
|
|
|
impl<T: InvokeUiSession> Session<T> {
|
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
|
pub fn get_permission_config(&self) -> SessionPermissionConfig {
|
|
SessionPermissionConfig {
|
|
lc: self.lc.clone(),
|
|
server_keyboard_enabled: self.server_keyboard_enabled.clone(),
|
|
server_file_transfer_enabled: self.server_file_transfer_enabled.clone(),
|
|
server_clipboard_enabled: self.server_clipboard_enabled.clone(),
|
|
}
|
|
}
|
|
|
|
pub fn is_file_transfer(&self) -> bool {
|
|
self.lc
|
|
.read()
|
|
.unwrap()
|
|
.conn_type
|
|
.eq(&ConnType::FILE_TRANSFER)
|
|
}
|
|
|
|
pub fn is_default(&self) -> bool {
|
|
self.lc
|
|
.read()
|
|
.unwrap()
|
|
.conn_type
|
|
.eq(&ConnType::DEFAULT_CONN)
|
|
}
|
|
|
|
pub fn is_view_camera(&self) -> bool {
|
|
self.lc.read().unwrap().conn_type.eq(&ConnType::VIEW_CAMERA)
|
|
}
|
|
|
|
pub fn is_terminal(&self) -> bool {
|
|
self.lc.read().unwrap().conn_type.eq(&ConnType::TERMINAL)
|
|
}
|
|
|
|
pub fn is_port_forward(&self) -> bool {
|
|
let conn_type = self.lc.read().unwrap().conn_type;
|
|
conn_type == ConnType::PORT_FORWARD || conn_type == ConnType::RDP
|
|
}
|
|
|
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
|
pub fn is_rdp(&self) -> bool {
|
|
self.lc.read().unwrap().conn_type.eq(&ConnType::RDP)
|
|
}
|
|
|
|
#[cfg(feature = "flutter")]
|
|
pub fn is_multi_ui_session(&self) -> bool {
|
|
self.ui_handler.is_multi_ui_session()
|
|
}
|
|
|
|
pub fn get_view_style(&self) -> String {
|
|
self.lc.read().unwrap().view_style.clone()
|
|
}
|
|
|
|
pub fn get_scroll_style(&self) -> String {
|
|
self.lc.read().unwrap().scroll_style.clone()
|
|
}
|
|
|
|
pub fn get_edge_scroll_edge_thickness(&self) -> i32 {
|
|
self.lc.read().unwrap().edge_scroll_edge_thickness
|
|
}
|
|
|
|
pub fn get_image_quality(&self) -> String {
|
|
self.lc.read().unwrap().image_quality.clone()
|
|
}
|
|
|
|
pub fn get_custom_image_quality(&self) -> Vec<i32> {
|
|
self.lc.read().unwrap().custom_image_quality.clone()
|
|
}
|
|
|
|
pub fn get_peer_version(&self) -> i64 {
|
|
self.lc.read().unwrap().version.clone()
|
|
}
|
|
|
|
pub fn get_trackpad_speed(&self) -> i32 {
|
|
self.lc.read().unwrap().trackpad_speed
|
|
}
|
|
|
|
pub fn fallback_keyboard_mode(&self) -> String {
|
|
let peer_version = self.get_peer_version();
|
|
let platform = self.peer_platform();
|
|
|
|
let supported_modes = get_supported_keyboard_modes(peer_version, &platform);
|
|
if let Some(mode) = supported_modes.first() {
|
|
return mode.to_string();
|
|
} else {
|
|
if self.get_peer_version() >= get_version_number("1.2.0") {
|
|
return KeyboardMode::Map.to_string();
|
|
} else {
|
|
return KeyboardMode::Legacy.to_string();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Caution: This function must be called after peer info is received.
|
|
pub fn get_keyboard_mode(&self) -> String {
|
|
let mode = self.lc.read().unwrap().keyboard_mode.clone();
|
|
let keyboard_mode = KeyboardMode::from_str(&mode);
|
|
|
|
// Note: peer_version is 0 before peer info is received.
|
|
let peer_version = self.get_peer_version();
|
|
let platform = self.peer_platform();
|
|
|
|
// Saved keyboard mode still exists in this version.
|
|
if let Ok(mode) = keyboard_mode {
|
|
if is_keyboard_mode_supported(&mode, peer_version, &platform) {
|
|
return mode.to_string();
|
|
}
|
|
}
|
|
self.fallback_keyboard_mode()
|
|
}
|
|
|
|
pub fn is_keyboard_mode_supported(&self, mode: String) -> bool {
|
|
if let Ok(mode) = KeyboardMode::from_str(&mode[..]) {
|
|
crate::common::is_keyboard_mode_supported(
|
|
&mode,
|
|
self.get_peer_version(),
|
|
&self.peer_platform(),
|
|
)
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
pub fn save_keyboard_mode(&self, value: String) {
|
|
self.lc.write().unwrap().save_keyboard_mode(value);
|
|
}
|
|
|
|
pub fn get_reverse_mouse_wheel(&self) -> String {
|
|
self.lc.read().unwrap().reverse_mouse_wheel.clone()
|
|
}
|
|
|
|
pub fn get_displays_as_individual_windows(&self) -> String {
|
|
self.lc
|
|
.read()
|
|
.unwrap()
|
|
.displays_as_individual_windows
|
|
.clone()
|
|
}
|
|
|
|
pub fn get_use_all_my_displays_for_the_remote_session(&self) -> String {
|
|
self.lc
|
|
.read()
|
|
.unwrap()
|
|
.use_all_my_displays_for_the_remote_session
|
|
.clone()
|
|
}
|
|
|
|
pub fn save_reverse_mouse_wheel(&self, value: String) {
|
|
self.lc.write().unwrap().save_reverse_mouse_wheel(value);
|
|
}
|
|
|
|
pub fn save_displays_as_individual_windows(&self, value: String) {
|
|
self.lc
|
|
.write()
|
|
.unwrap()
|
|
.save_displays_as_individual_windows(value);
|
|
}
|
|
|
|
pub fn save_use_all_my_displays_for_the_remote_session(&self, value: String) {
|
|
self.lc
|
|
.write()
|
|
.unwrap()
|
|
.save_use_all_my_displays_for_the_remote_session(value);
|
|
}
|
|
|
|
pub fn save_view_style(&self, value: String) {
|
|
self.lc.write().unwrap().save_view_style(value);
|
|
}
|
|
|
|
pub fn save_scroll_style(&self, value: String) {
|
|
self.lc.write().unwrap().save_scroll_style(value);
|
|
}
|
|
|
|
pub fn save_edge_scroll_edge_thickness(&self, value: i32) {
|
|
self.lc
|
|
.write()
|
|
.unwrap()
|
|
.save_edge_scroll_edge_thickness(value);
|
|
}
|
|
|
|
pub fn save_flutter_option(&self, k: String, v: String) {
|
|
self.lc.write().unwrap().save_ui_flutter(k, v);
|
|
}
|
|
|
|
pub fn get_flutter_option(&self, k: String) -> String {
|
|
self.lc.read().unwrap().get_ui_flutter(&k)
|
|
}
|
|
|
|
pub fn toggle_option(&self, name: String) {
|
|
let msg = self.lc.write().unwrap().toggle_option(name.clone());
|
|
#[cfg(all(target_os = "windows", not(feature = "flutter")))]
|
|
if name == keys::OPTION_ENABLE_FILE_COPY_PASTE {
|
|
self.send(Data::ToggleClipboardFile);
|
|
}
|
|
if let Some(msg) = msg {
|
|
self.send(Data::Message(msg));
|
|
}
|
|
}
|
|
|
|
pub fn toggle_privacy_mode(&self, impl_key: String, on: bool) {
|
|
let mut misc = Misc::new();
|
|
misc.set_toggle_privacy_mode(TogglePrivacyMode {
|
|
impl_key,
|
|
on,
|
|
..Default::default()
|
|
});
|
|
let mut msg_out = Message::new();
|
|
msg_out.set_misc(misc);
|
|
self.send(Data::Message(msg_out));
|
|
}
|
|
|
|
pub fn get_toggle_option(&self, name: String) -> bool {
|
|
self.lc.read().unwrap().get_toggle_option(&name)
|
|
}
|
|
|
|
#[cfg(not(feature = "flutter"))]
|
|
pub fn is_privacy_mode_supported(&self) -> bool {
|
|
self.lc.read().unwrap().is_privacy_mode_supported()
|
|
}
|
|
|
|
#[cfg(not(target_os = "ios"))]
|
|
pub fn is_text_clipboard_required(&self) -> bool {
|
|
*self.server_clipboard_enabled.read().unwrap()
|
|
&& *self.server_keyboard_enabled.read().unwrap()
|
|
&& !self.lc.read().unwrap().disable_clipboard.v
|
|
&& !self.lc.read().unwrap().view_only.v
|
|
}
|
|
|
|
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))]
|
|
pub fn is_file_clipboard_required(&self) -> bool {
|
|
let lc = self.lc.read().unwrap();
|
|
*self.server_keyboard_enabled.read().unwrap()
|
|
&& *self.server_file_transfer_enabled.read().unwrap()
|
|
&& lc.enable_file_copy_paste.v
|
|
&& !lc.view_only.v
|
|
}
|
|
|
|
#[cfg(feature = "flutter")]
|
|
pub fn refresh_video(&self, display: i32) {
|
|
if crate::common::is_support_multi_ui_session_num(self.lc.read().unwrap().version) {
|
|
self.send(Data::Message(LoginConfigHandler::refresh_display(
|
|
display as _,
|
|
)));
|
|
} else {
|
|
self.send(Data::Message(LoginConfigHandler::refresh()));
|
|
}
|
|
}
|
|
|
|
pub fn toggle_virtual_display(&self, index: i32, on: bool) {
|
|
let mut misc = Misc::new();
|
|
misc.set_toggle_virtual_display(ToggleVirtualDisplay {
|
|
display: index,
|
|
on,
|
|
..Default::default()
|
|
});
|
|
let mut msg_out = Message::new();
|
|
msg_out.set_misc(misc);
|
|
self.send(Data::Message(msg_out));
|
|
}
|
|
|
|
#[cfg(not(feature = "flutter"))]
|
|
pub fn refresh_video(&self, _display: i32) {
|
|
self.send(Data::Message(LoginConfigHandler::refresh()));
|
|
}
|
|
|
|
pub fn record_screen(&self, start: bool) {
|
|
self.send(Data::RecordScreen(start));
|
|
}
|
|
|
|
pub fn is_screenshot_supported(&self) -> bool {
|
|
crate::common::is_support_screenshot_num(self.lc.read().unwrap().version)
|
|
}
|
|
|
|
pub fn take_screenshot(&self, display: i32, sid: String) {
|
|
self.send(Data::TakeScreenshot((display, sid)));
|
|
}
|
|
|
|
pub fn is_recording(&self) -> bool {
|
|
self.lc.read().unwrap().record_state
|
|
}
|
|
|
|
pub fn save_custom_image_quality(&self, custom_image_quality: i32) {
|
|
let msg = self
|
|
.lc
|
|
.write()
|
|
.unwrap()
|
|
.save_custom_image_quality(custom_image_quality);
|
|
self.send(Data::Message(msg));
|
|
}
|
|
|
|
pub fn save_image_quality(&self, value: String) {
|
|
let msg = self.lc.write().unwrap().save_image_quality(value.clone());
|
|
if let Some(msg) = msg {
|
|
self.send(Data::Message(msg));
|
|
}
|
|
if value != "custom" {
|
|
let last_auto_fps = self.lc.read().unwrap().last_auto_fps;
|
|
if last_auto_fps.unwrap_or(usize::MAX) >= 30 {
|
|
// non custom quality use 30 fps
|
|
let msg = self.lc.write().unwrap().set_custom_fps(30, false);
|
|
self.send(Data::Message(msg));
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn save_trackpad_speed(&self, trackpad_speed: i32) {
|
|
self.lc.write().unwrap().save_trackpad_speed(trackpad_speed);
|
|
}
|
|
|
|
pub fn set_custom_fps(&self, custom_fps: i32) {
|
|
let msg = self.lc.write().unwrap().set_custom_fps(custom_fps, true);
|
|
self.send(Data::Message(msg));
|
|
}
|
|
|
|
pub fn get_remember(&self) -> bool {
|
|
self.lc.read().unwrap().remember
|
|
}
|
|
|
|
#[cfg(not(feature = "flutter"))]
|
|
pub fn set_write_override(
|
|
&mut self,
|
|
job_id: i32,
|
|
file_num: i32,
|
|
is_override: bool,
|
|
remember: bool,
|
|
is_upload: bool,
|
|
) -> bool {
|
|
self.send(Data::SetConfirmOverrideFile((
|
|
job_id,
|
|
file_num,
|
|
is_override,
|
|
remember,
|
|
is_upload,
|
|
)));
|
|
true
|
|
}
|
|
|
|
pub fn alternative_codecs(&self) -> (bool, bool, bool, bool) {
|
|
let luid = self.lc.read().unwrap().adapter_luid;
|
|
let mark_unsupported = self.lc.read().unwrap().mark_unsupported.clone();
|
|
let decoder = scrap::codec::Decoder::supported_decodings(
|
|
None,
|
|
use_texture_render(),
|
|
luid,
|
|
&mark_unsupported,
|
|
);
|
|
let mut vp8 = decoder.ability_vp8 > 0;
|
|
let mut av1 = decoder.ability_av1 > 0;
|
|
let mut h264 = decoder.ability_h264 > 0;
|
|
let mut h265 = decoder.ability_h265 > 0;
|
|
let enc = &self.lc.read().unwrap().supported_encoding;
|
|
vp8 = vp8 && enc.vp8;
|
|
av1 = av1 && enc.av1;
|
|
h264 = h264 && enc.h264;
|
|
h265 = h265 && enc.h265;
|
|
(vp8, av1, h264, h265)
|
|
}
|
|
|
|
pub fn update_supported_decodings(&self) {
|
|
let msg = self.lc.write().unwrap().update_supported_decodings();
|
|
self.send(Data::Message(msg));
|
|
}
|
|
|
|
pub fn use_texture_render_changed(&self) {
|
|
self.send(Data::ResetDecoder(None));
|
|
self.update_supported_decodings();
|
|
self.send(Data::Message(LoginConfigHandler::refresh()));
|
|
}
|
|
|
|
pub fn restart_remote_device(&self) {
|
|
let mut lc = self.lc.write().unwrap();
|
|
lc.mark_restarting_remote_device();
|
|
let msg = lc.restart_remote_device();
|
|
self.send(Data::Message(msg));
|
|
}
|
|
|
|
pub fn get_audit_server(&self, typ: String) -> String {
|
|
if LocalConfig::get_option("access_token").is_empty() {
|
|
return "".to_owned();
|
|
}
|
|
crate::get_audit_server(
|
|
Config::get_option("api-server"),
|
|
Config::get_option("custom-rendezvous-server"),
|
|
typ,
|
|
)
|
|
}
|
|
|
|
pub fn send_note(&self, note: String) {
|
|
let url = self.get_audit_server("conn".to_string());
|
|
let id = self.get_id();
|
|
let session_id = self.lc.read().unwrap().session_id;
|
|
*self.last_audit_note.lock().unwrap() = note.clone();
|
|
std::thread::spawn(move || {
|
|
send_note(url, id, session_id, note);
|
|
});
|
|
}
|
|
|
|
#[cfg(not(feature = "flutter"))]
|
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
|
pub fn is_xfce(&self) -> bool {
|
|
crate::platform::is_xfce()
|
|
}
|
|
|
|
pub fn remove_port_forward(&self, port: i32) {
|
|
let mut config = self.load_config();
|
|
config.port_forwards = config
|
|
.port_forwards
|
|
.drain(..)
|
|
.filter(|x| x.0 != port)
|
|
.collect();
|
|
self.save_config(config);
|
|
self.send(Data::RemovePortForward(port));
|
|
}
|
|
|
|
pub fn add_port_forward(&self, port: i32, remote_host: String, remote_port: i32) {
|
|
let mut config = self.load_config();
|
|
if config
|
|
.port_forwards
|
|
.iter()
|
|
.filter(|x| x.0 == port)
|
|
.next()
|
|
.is_some()
|
|
{
|
|
return;
|
|
}
|
|
let pf = (port, remote_host, remote_port);
|
|
config.port_forwards.push(pf.clone());
|
|
self.save_config(config);
|
|
self.send(Data::AddPortForward(pf));
|
|
}
|
|
|
|
pub fn get_option(&self, k: String) -> String {
|
|
if k.eq("remote_dir") {
|
|
return self.lc.read().unwrap().get_remote_dir();
|
|
}
|
|
self.lc.read().unwrap().get_option(&k)
|
|
}
|
|
|
|
pub fn set_option(&self, k: String, mut v: String) {
|
|
let mut lc = self.lc.write().unwrap();
|
|
if k.eq("remote_dir") {
|
|
v = lc.get_all_remote_dir(v);
|
|
}
|
|
lc.set_option(k, v);
|
|
}
|
|
|
|
#[inline]
|
|
pub fn load_config(&self) -> PeerConfig {
|
|
self.lc.read().unwrap().load_config()
|
|
}
|
|
|
|
#[inline]
|
|
pub(super) fn save_config(&self, config: PeerConfig) {
|
|
self.lc.write().unwrap().save_config(config);
|
|
}
|
|
|
|
pub fn is_restarting_remote_device(&self) -> bool {
|
|
self.lc.read().unwrap().is_restarting_remote_device()
|
|
}
|
|
|
|
#[inline]
|
|
pub fn peer_platform(&self) -> String {
|
|
self.lc.read().unwrap().info.platform.clone()
|
|
}
|
|
|
|
pub fn get_platform(&self, is_remote: bool) -> String {
|
|
if is_remote {
|
|
self.peer_platform()
|
|
} else {
|
|
whoami::platform().to_string()
|
|
}
|
|
}
|
|
|
|
pub fn get_path_sep(&self, is_remote: bool) -> &'static str {
|
|
let p = self.get_platform(is_remote);
|
|
if &p == crate::PLATFORM_WINDOWS {
|
|
return "\\";
|
|
} else {
|
|
return "/";
|
|
}
|
|
}
|
|
|
|
pub fn input_os_password(&self, pass: String, activate: bool) {
|
|
input_os_password(pass, activate, self.clone());
|
|
}
|
|
|
|
#[cfg(not(feature = "flutter"))]
|
|
pub fn get_chatbox(&self) -> String {
|
|
#[cfg(feature = "inline")]
|
|
return crate::ui::inline::get_chatbox();
|
|
#[cfg(not(feature = "inline"))]
|
|
return "".to_owned();
|
|
}
|
|
|
|
pub fn swap_modifier_key(&self, msg: &mut KeyEvent) {
|
|
let allow_swap_key = self.get_toggle_option("allow_swap_key".to_string());
|
|
if allow_swap_key {
|
|
if let Some(key_event::Union::ControlKey(ck)) = msg.union {
|
|
let ck = ck.enum_value_or_default();
|
|
let ck = match ck {
|
|
ControlKey::Control => ControlKey::Meta,
|
|
ControlKey::Meta => ControlKey::Control,
|
|
ControlKey::RControl => ControlKey::Meta,
|
|
ControlKey::RWin => ControlKey::Control,
|
|
_ => ck,
|
|
};
|
|
msg.set_control_key(ck);
|
|
}
|
|
msg.modifiers = msg
|
|
.modifiers
|
|
.iter()
|
|
.map(|ck| {
|
|
let ck = ck.enum_value_or_default();
|
|
let ck = match ck {
|
|
ControlKey::Control => ControlKey::Meta,
|
|
ControlKey::Meta => ControlKey::Control,
|
|
ControlKey::RControl => ControlKey::Meta,
|
|
ControlKey::RWin => ControlKey::Control,
|
|
_ => ck,
|
|
};
|
|
hbb_common::protobuf::EnumOrUnknown::new(ck)
|
|
})
|
|
.collect();
|
|
|
|
let code = msg.chr();
|
|
if code != 0 {
|
|
let mut peer = self.peer_platform().to_lowercase();
|
|
peer.retain(|c| !c.is_whitespace());
|
|
|
|
let key = match peer.as_str() {
|
|
"windows" => {
|
|
let key = rdev::win_key_from_scancode(code);
|
|
let key = match key {
|
|
rdev::Key::ControlLeft => rdev::Key::MetaLeft,
|
|
rdev::Key::MetaLeft => rdev::Key::ControlLeft,
|
|
rdev::Key::ControlRight => rdev::Key::MetaLeft,
|
|
rdev::Key::MetaRight => rdev::Key::ControlLeft,
|
|
_ => key,
|
|
};
|
|
rdev::win_scancode_from_key(key).unwrap_or_default()
|
|
}
|
|
"macos" => {
|
|
let key = rdev::macos_key_from_code(code as _);
|
|
let key = match key {
|
|
rdev::Key::ControlLeft => rdev::Key::MetaLeft,
|
|
rdev::Key::MetaLeft => rdev::Key::ControlLeft,
|
|
rdev::Key::ControlRight => rdev::Key::MetaLeft,
|
|
rdev::Key::MetaRight => rdev::Key::ControlLeft,
|
|
_ => key,
|
|
};
|
|
rdev::macos_keycode_from_key(key).unwrap_or_default() as _
|
|
}
|
|
_ => {
|
|
let key = rdev::linux_key_from_code(code);
|
|
let key = match key {
|
|
rdev::Key::ControlLeft => rdev::Key::MetaLeft,
|
|
rdev::Key::MetaLeft => rdev::Key::ControlLeft,
|
|
rdev::Key::ControlRight => rdev::Key::MetaLeft,
|
|
rdev::Key::MetaRight => rdev::Key::ControlLeft,
|
|
_ => key,
|
|
};
|
|
rdev::linux_keycode_from_key(key).unwrap_or_default()
|
|
}
|
|
};
|
|
msg.set_chr(key);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn send_key_event(&self, evt: &KeyEvent) {
|
|
// mode: legacy(0), map(1), translate(2), auto(3)
|
|
|
|
let mut msg = evt.clone();
|
|
self.swap_modifier_key(&mut msg);
|
|
let mut msg_out = Message::new();
|
|
msg_out.set_key_event(msg);
|
|
self.send(Data::Message(msg_out));
|
|
}
|
|
|
|
pub fn send_chat(&self, text: String) {
|
|
let mut misc = Misc::new();
|
|
misc.set_chat_message(ChatMessage {
|
|
text,
|
|
..Default::default()
|
|
});
|
|
let mut msg_out = Message::new();
|
|
msg_out.set_misc(misc);
|
|
self.send(Data::Message(msg_out));
|
|
}
|
|
|
|
// Terminal methods
|
|
pub fn open_terminal(&self, terminal_id: i32, rows: u32, cols: u32) {
|
|
let mut action = TerminalAction::new();
|
|
action.set_open(OpenTerminal {
|
|
terminal_id,
|
|
rows,
|
|
cols,
|
|
..Default::default()
|
|
});
|
|
let mut msg_out = Message::new();
|
|
msg_out.set_terminal_action(action);
|
|
self.send(Data::Message(msg_out));
|
|
}
|
|
|
|
pub fn send_terminal_input(&self, terminal_id: i32, data: String) {
|
|
let mut action = TerminalAction::new();
|
|
action.set_data(TerminalData {
|
|
terminal_id,
|
|
data: bytes::Bytes::from(data.into_bytes()),
|
|
..Default::default()
|
|
});
|
|
let mut msg_out = Message::new();
|
|
msg_out.set_terminal_action(action);
|
|
self.send(Data::Message(msg_out));
|
|
}
|
|
|
|
pub fn resize_terminal(&self, terminal_id: i32, rows: u32, cols: u32) {
|
|
let mut action = TerminalAction::new();
|
|
action.set_resize(ResizeTerminal {
|
|
terminal_id,
|
|
rows,
|
|
cols,
|
|
..Default::default()
|
|
});
|
|
let mut msg_out = Message::new();
|
|
msg_out.set_terminal_action(action);
|
|
self.send(Data::Message(msg_out));
|
|
}
|
|
|
|
pub fn close_terminal(&self, terminal_id: i32) {
|
|
let mut action = TerminalAction::new();
|
|
action.set_close(CloseTerminal {
|
|
terminal_id,
|
|
..Default::default()
|
|
});
|
|
let mut msg_out = Message::new();
|
|
msg_out.set_terminal_action(action);
|
|
self.send(Data::Message(msg_out));
|
|
}
|
|
|
|
pub fn capture_displays(&self, add: Vec<i32>, sub: Vec<i32>, set: Vec<i32>) {
|
|
let mut misc = Misc::new();
|
|
misc.set_capture_displays(CaptureDisplays {
|
|
add,
|
|
sub,
|
|
set,
|
|
..Default::default()
|
|
});
|
|
let mut msg_out = Message::new();
|
|
msg_out.set_misc(misc);
|
|
self.send(Data::Message(msg_out));
|
|
}
|
|
|
|
pub fn switch_display(&self, display: i32) {
|
|
let (w, h) = match self.lc.read().unwrap().get_custom_resolution(display) {
|
|
Some((w, h)) => (w, h),
|
|
None => (0, 0),
|
|
};
|
|
|
|
let mut misc = Misc::new();
|
|
misc.set_switch_display(SwitchDisplay {
|
|
display,
|
|
width: w,
|
|
height: h,
|
|
..Default::default()
|
|
});
|
|
let mut msg_out = Message::new();
|
|
msg_out.set_misc(misc);
|
|
self.send(Data::Message(msg_out));
|
|
|
|
if !use_texture_render() {
|
|
self.capture_displays(vec![], vec![], vec![display]);
|
|
}
|
|
}
|
|
|
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
|
pub fn enter(&self, keyboard_mode: String) {
|
|
let session_id = self.lc.read().unwrap().session_id as u128;
|
|
keyboard::client::change_grab_status(GrabState::Run, &keyboard_mode, session_id);
|
|
}
|
|
|
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
|
pub fn leave(&self, keyboard_mode: String) {
|
|
let session_id = self.lc.read().unwrap().session_id as u128;
|
|
keyboard::client::change_grab_status(GrabState::Wait, &keyboard_mode, session_id);
|
|
}
|
|
|
|
// flutter only TODO new input
|
|
pub fn input_key(
|
|
&self,
|
|
name: &str,
|
|
down: bool,
|
|
press: bool,
|
|
alt: bool,
|
|
ctrl: bool,
|
|
shift: bool,
|
|
command: bool,
|
|
) {
|
|
let chars: Vec<char> = name.chars().collect();
|
|
if chars.len() == 1 {
|
|
let key = Key::_Raw(chars[0] as _);
|
|
self._input_key(key, down, press, alt, ctrl, shift, command);
|
|
} else {
|
|
if let Some(key) = KEY_MAP.get(name) {
|
|
self._input_key(key.clone(), down, press, alt, ctrl, shift, command);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn input_string(&self, value: &str) {
|
|
let mut key_event = KeyEvent::new();
|
|
key_event.set_seq(value.to_owned());
|
|
let mut msg_out = Message::new();
|
|
msg_out.set_key_event(key_event);
|
|
self.send(Data::Message(msg_out));
|
|
}
|
|
|
|
#[cfg(any(target_os = "ios"))]
|
|
pub fn handle_flutter_raw_key_event(
|
|
&self,
|
|
_session_id: SessionID,
|
|
_keyboard_mode: &str,
|
|
_name: &str,
|
|
_platform_code: i32,
|
|
_position_code: i32,
|
|
_lock_modes: i32,
|
|
_down_or_up: bool,
|
|
) {
|
|
}
|
|
|
|
#[cfg(not(any(target_os = "ios")))]
|
|
pub fn handle_flutter_raw_key_event(
|
|
&self,
|
|
session_id: SessionID,
|
|
keyboard_mode: &str,
|
|
name: &str,
|
|
platform_code: i32,
|
|
position_code: i32,
|
|
lock_modes: i32,
|
|
down_or_up: bool,
|
|
) {
|
|
if name == "flutter_key" {
|
|
self._handle_key_flutter_simulation(keyboard_mode, platform_code, down_or_up);
|
|
} else {
|
|
self._handle_raw_key_non_flutter_simulation(
|
|
session_id,
|
|
keyboard_mode,
|
|
platform_code,
|
|
position_code,
|
|
lock_modes,
|
|
down_or_up,
|
|
);
|
|
}
|
|
}
|
|
|
|
#[cfg(not(any(target_os = "ios")))]
|
|
fn _handle_raw_key_non_flutter_simulation(
|
|
&self,
|
|
session_id: SessionID,
|
|
keyboard_mode: &str,
|
|
platform_code: i32,
|
|
position_code: i32,
|
|
lock_modes: i32,
|
|
down_or_up: bool,
|
|
) {
|
|
if position_code < 0 || platform_code < 0 {
|
|
return;
|
|
}
|
|
let platform_code: u32 = platform_code as _;
|
|
let position_code: KeyCode = position_code as _;
|
|
|
|
#[cfg(not(target_os = "windows"))]
|
|
let key = rdev::key_from_code(position_code) as rdev::Key;
|
|
// Windows requires special handling
|
|
#[cfg(target_os = "windows")]
|
|
let key = rdev::get_win_key(platform_code, position_code);
|
|
|
|
let event_type = if down_or_up {
|
|
KeyPress(key)
|
|
} else {
|
|
KeyRelease(key)
|
|
};
|
|
let event = Event {
|
|
time: SystemTime::now(),
|
|
unicode: None,
|
|
platform_code,
|
|
position_code: position_code as _,
|
|
event_type,
|
|
usb_hid: 0,
|
|
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
|
extra_data: 0,
|
|
};
|
|
keyboard::client::process_event_with_session(
|
|
keyboard_mode,
|
|
&event,
|
|
Some(lock_modes),
|
|
self,
|
|
session_id,
|
|
);
|
|
}
|
|
|
|
pub fn handle_flutter_key_event(
|
|
&self,
|
|
session_id: SessionID,
|
|
keyboard_mode: &str,
|
|
character: &str,
|
|
usb_hid: i32,
|
|
lock_modes: i32,
|
|
down_or_up: bool,
|
|
) {
|
|
if character == "flutter_key" {
|
|
self._handle_key_flutter_simulation(keyboard_mode, usb_hid, down_or_up);
|
|
} else {
|
|
self._handle_key_non_flutter_simulation(
|
|
session_id,
|
|
keyboard_mode,
|
|
character,
|
|
usb_hid,
|
|
lock_modes,
|
|
down_or_up,
|
|
);
|
|
}
|
|
}
|
|
|
|
fn _handle_key_flutter_simulation(
|
|
&self,
|
|
_keyboard_mode: &str,
|
|
platform_code: i32,
|
|
down_or_up: bool,
|
|
) {
|
|
// https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/services/keyboard_key.g.dart#L4356
|
|
let ctrl_key = match platform_code {
|
|
0x007f => Some(ControlKey::VolumeMute),
|
|
0x0080 => Some(ControlKey::VolumeUp),
|
|
0x0081 => Some(ControlKey::VolumeDown),
|
|
0x0066 => Some(ControlKey::Power),
|
|
_ => None,
|
|
};
|
|
let Some(ctrl_key) = ctrl_key else { return };
|
|
let mut key_event = KeyEvent {
|
|
mode: KeyboardMode::Translate.into(),
|
|
down: down_or_up,
|
|
..Default::default()
|
|
};
|
|
key_event.set_control_key(ctrl_key);
|
|
self.send_key_event(&key_event);
|
|
}
|
|
|
|
fn _handle_key_non_flutter_simulation(
|
|
&self,
|
|
session_id: SessionID,
|
|
keyboard_mode: &str,
|
|
character: &str,
|
|
usb_hid: i32,
|
|
lock_modes: i32,
|
|
down_or_up: bool,
|
|
) {
|
|
let key = rdev::usb_hid_key_from_code(usb_hid as _);
|
|
|
|
#[cfg(any(target_os = "android", target_os = "ios"))]
|
|
let position_code: KeyCode = 0;
|
|
#[cfg(any(target_os = "android", target_os = "ios"))]
|
|
let platform_code: KeyCode = 0;
|
|
|
|
#[cfg(target_os = "windows")]
|
|
let platform_code: u32 = rdev::win_code_from_key(key).unwrap_or(0);
|
|
#[cfg(target_os = "windows")]
|
|
let position_code: KeyCode = rdev::win_scancode_from_key(key).unwrap_or(0) as _;
|
|
|
|
#[cfg(not(any(target_os = "windows", target_os = "android", target_os = "ios")))]
|
|
let position_code: KeyCode = rdev::code_from_key(key).unwrap_or(0) as _;
|
|
#[cfg(not(any(
|
|
target_os = "windows",
|
|
target_os = "android",
|
|
target_os = "ios",
|
|
target_os = "linux"
|
|
)))]
|
|
let platform_code: u32 = position_code as _;
|
|
// For translate mode.
|
|
// We need to set the platform code (keysym) if is AltGr.
|
|
// https://github.com/rustdesk/rustdesk/blob/07cf1b4db5ef2f925efd3b16b87c33ce03c94809/src/keyboard.rs#L1029
|
|
// https://github.com/flutter/flutter/issues/153811
|
|
#[cfg(target_os = "linux")]
|
|
let platform_code: u32 = position_code as _;
|
|
|
|
let event_type = if down_or_up {
|
|
KeyPress(key)
|
|
} else {
|
|
KeyRelease(key)
|
|
};
|
|
let event = Event {
|
|
time: SystemTime::now(),
|
|
unicode: if character.is_empty() {
|
|
None
|
|
} else {
|
|
Some(rdev::UnicodeInfo {
|
|
name: Some(character.to_string()),
|
|
unicode: character.encode_utf16().collect(),
|
|
// is_dead: is not correct here, because flutter cannot detect deadcode for now.
|
|
is_dead: false,
|
|
})
|
|
},
|
|
platform_code,
|
|
position_code: position_code as _,
|
|
event_type,
|
|
#[cfg(any(target_os = "android", target_os = "ios"))]
|
|
usb_hid: usb_hid as _,
|
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
|
usb_hid: 0,
|
|
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
|
extra_data: 0,
|
|
};
|
|
keyboard::client::process_event_with_session(
|
|
keyboard_mode,
|
|
&event,
|
|
Some(lock_modes),
|
|
self,
|
|
session_id,
|
|
);
|
|
}
|
|
|
|
// flutter only TODO new input
|
|
fn _input_key(
|
|
&self,
|
|
key: Key,
|
|
down: bool,
|
|
press: bool,
|
|
alt: bool,
|
|
ctrl: bool,
|
|
shift: bool,
|
|
command: bool,
|
|
) {
|
|
let v = if press {
|
|
3
|
|
} else if down {
|
|
1
|
|
} else {
|
|
0
|
|
};
|
|
let mut key_event = KeyEvent::new();
|
|
match key {
|
|
Key::Chr(chr) => {
|
|
key_event.set_chr(chr);
|
|
}
|
|
Key::ControlKey(key) => {
|
|
key_event.set_control_key(key.clone());
|
|
}
|
|
Key::_Raw(raw) => {
|
|
key_event.set_chr(raw);
|
|
}
|
|
}
|
|
|
|
if v == 1 {
|
|
key_event.down = true;
|
|
} else if v == 3 {
|
|
key_event.press = true;
|
|
}
|
|
keyboard::client::legacy_modifiers(&mut key_event, alt, ctrl, shift, command);
|
|
key_event.mode = KeyboardMode::Legacy.into();
|
|
|
|
self.send_key_event(&key_event);
|
|
}
|
|
|
|
pub fn send_touch_scale(&self, scale: i32, alt: bool, ctrl: bool, shift: bool, command: bool) {
|
|
let scale_evt = TouchScaleUpdate {
|
|
scale,
|
|
..Default::default()
|
|
};
|
|
let mut touch_evt = TouchEvent::new();
|
|
touch_evt.set_scale_update(scale_evt);
|
|
let mut evt = PointerDeviceEvent::new();
|
|
evt.set_touch_event(touch_evt);
|
|
send_pointer_device_event(evt, alt, ctrl, shift, command, self);
|
|
}
|
|
|
|
pub fn send_touch_pan_event(
|
|
&self,
|
|
event: &str,
|
|
x: i32,
|
|
y: i32,
|
|
alt: bool,
|
|
ctrl: bool,
|
|
shift: bool,
|
|
command: bool,
|
|
) {
|
|
let mut touch_evt = TouchEvent::new();
|
|
match event {
|
|
"pan_start" => {
|
|
touch_evt.set_pan_start(TouchPanStart {
|
|
x,
|
|
y,
|
|
..Default::default()
|
|
});
|
|
}
|
|
"pan_update" => {
|
|
let (x, y) = self.get_scroll_xy((x, y));
|
|
touch_evt.set_pan_update(TouchPanUpdate {
|
|
x,
|
|
y,
|
|
..Default::default()
|
|
});
|
|
}
|
|
"pan_end" => {
|
|
touch_evt.set_pan_end(TouchPanEnd {
|
|
x,
|
|
y,
|
|
..Default::default()
|
|
});
|
|
}
|
|
_ => {
|
|
log::warn!("unknown touch pan event: {}", event);
|
|
return;
|
|
}
|
|
};
|
|
let mut evt = PointerDeviceEvent::new();
|
|
evt.set_touch_event(touch_evt);
|
|
send_pointer_device_event(evt, alt, ctrl, shift, command, self);
|
|
}
|
|
|
|
#[inline]
|
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
|
fn is_scroll_reverse_mode(&self) -> bool {
|
|
self.lc.read().unwrap().reverse_mouse_wheel.eq("Y")
|
|
}
|
|
|
|
#[inline]
|
|
fn get_scroll_xy(&self, xy: (i32, i32)) -> (i32, i32) {
|
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
|
if self.is_scroll_reverse_mode() {
|
|
return (-xy.0, -xy.1);
|
|
}
|
|
xy
|
|
}
|
|
|
|
pub fn send_mouse(
|
|
&self,
|
|
mut mask: i32,
|
|
x: i32,
|
|
y: i32,
|
|
alt: bool,
|
|
ctrl: bool,
|
|
shift: bool,
|
|
command: bool,
|
|
) {
|
|
#[allow(unused_mut)]
|
|
let mut command = command;
|
|
#[cfg(windows)]
|
|
{
|
|
if !command && crate::platform::windows::get_win_key_state() {
|
|
command = true;
|
|
}
|
|
}
|
|
|
|
// Compute event type once using MOUSE_TYPE_MASK for reuse
|
|
let event_type = mask & MOUSE_TYPE_MASK;
|
|
let (x, y) = if event_type == MOUSE_TYPE_WHEEL || event_type == MOUSE_TYPE_TRACKPAD {
|
|
self.get_scroll_xy((x, y))
|
|
} else {
|
|
(x, y)
|
|
};
|
|
#[cfg(any(target_os = "android", target_os = "ios"))]
|
|
let (x, y) = if event_type == MOUSE_TYPE_WHEEL || event_type == MOUSE_TYPE_TRACKPAD {
|
|
mobile_wheel::mobile_wheel_delta((x, y), &self.lc.read().unwrap().reverse_mouse_wheel)
|
|
} else {
|
|
(x, y)
|
|
};
|
|
|
|
// #[cfg(not(any(target_os = "android", target_os = "ios")))]
|
|
let (alt, ctrl, shift, command) =
|
|
keyboard::client::get_modifiers_state(alt, ctrl, shift, command);
|
|
let is_left = (mask & (MOUSE_BUTTON_LEFT << 3)) > 0;
|
|
let is_right = (mask & (MOUSE_BUTTON_RIGHT << 3)) > 0;
|
|
if is_left ^ is_right {
|
|
let swap_lr = self.get_toggle_option("swap-left-right-mouse".to_string());
|
|
if swap_lr {
|
|
if is_left {
|
|
mask = (mask & (!(MOUSE_BUTTON_LEFT << 3))) | (MOUSE_BUTTON_RIGHT << 3);
|
|
} else {
|
|
mask = (mask & (!(MOUSE_BUTTON_RIGHT << 3))) | (MOUSE_BUTTON_LEFT << 3);
|
|
}
|
|
}
|
|
}
|
|
|
|
send_mouse(mask, x, y, alt, ctrl, shift, command, self);
|
|
// on macos, ctrl + left button down = right button down, up won't emit, so we need to
|
|
// emit up myself if peer is not macos
|
|
// to-do: how about ctrl + left from win to macos
|
|
if cfg!(target_os = "macos") {
|
|
let buttons = mask >> 3;
|
|
if buttons == MOUSE_BUTTON_LEFT
|
|
&& event_type == MOUSE_TYPE_DOWN
|
|
&& ctrl
|
|
&& self.peer_platform() != "Mac OS"
|
|
{
|
|
self.send_mouse(
|
|
(MOUSE_BUTTON_LEFT << 3 | MOUSE_TYPE_UP) as _,
|
|
x,
|
|
y,
|
|
alt,
|
|
ctrl,
|
|
shift,
|
|
command,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn reconnect(&self, force_relay: bool) {
|
|
// 1. If current session is connecting, do not reconnect.
|
|
// 2. If the connection is established, send `Data::Close`.
|
|
// 3. If the connection is disconnected, do nothing.
|
|
let mut connection_round_state_lock = self.connection_round_state.lock().unwrap();
|
|
if self.thread.lock().unwrap().is_some() {
|
|
match connection_round_state_lock.state {
|
|
ConnectionState::Connecting => return,
|
|
ConnectionState::Connected => self.send(Data::Close),
|
|
ConnectionState::Disconnected => {}
|
|
}
|
|
}
|
|
let round = connection_round_state_lock.new_round();
|
|
drop(connection_round_state_lock);
|
|
|
|
let cloned = self.clone();
|
|
|
|
// override only if true
|
|
if true == force_relay {
|
|
let mut lc = self.lc.write().unwrap();
|
|
lc.force_relay = true;
|
|
// An explicit retry-via-relay is a decision about this peer, not transport
|
|
// necessity: Relay-only ICE for this round like any force-always-relay session,
|
|
// and it is the one kind of relay that belongs in the peer's saved config.
|
|
lc.policy_relay = true;
|
|
lc.peer_relay = true;
|
|
}
|
|
self.lc.write().unwrap().peer_info = None;
|
|
self.reconnect_count.fetch_add(1, Ordering::SeqCst);
|
|
let mut lock = self.thread.lock().unwrap();
|
|
// No need to join the previous thread, because it will exit automatically.
|
|
// And the previous thread will not change important states.
|
|
*lock = Some(std::thread::spawn(move || {
|
|
io_loop(cloned, round);
|
|
}));
|
|
}
|
|
|
|
#[cfg(not(feature = "flutter"))]
|
|
pub fn get_icon_path(&self, file_type: i32, ext: String) -> String {
|
|
let mut path = Config::icon_path();
|
|
if file_type == FileType::DirLink as i32 {
|
|
let new_path = path.join("dir_link");
|
|
if !std::fs::metadata(&new_path).is_ok() {
|
|
#[cfg(windows)]
|
|
allow_err!(std::os::windows::fs::symlink_file(&path, &new_path));
|
|
#[cfg(not(windows))]
|
|
allow_err!(std::os::unix::fs::symlink(&path, &new_path));
|
|
}
|
|
path = new_path;
|
|
} else if file_type == FileType::File as i32 {
|
|
if !ext.is_empty() {
|
|
path = path.join(format!("file.{}", ext));
|
|
} else {
|
|
path = path.join("file");
|
|
}
|
|
if !std::fs::metadata(&path).is_ok() {
|
|
allow_err!(std::fs::File::create(&path));
|
|
}
|
|
} else if file_type == FileType::FileLink as i32 {
|
|
let new_path = path.join("file_link");
|
|
if !std::fs::metadata(&new_path).is_ok() {
|
|
path = path.join("file");
|
|
if !std::fs::metadata(&path).is_ok() {
|
|
allow_err!(std::fs::File::create(&path));
|
|
}
|
|
#[cfg(windows)]
|
|
allow_err!(std::os::windows::fs::symlink_file(&path, &new_path));
|
|
#[cfg(not(windows))]
|
|
allow_err!(std::os::unix::fs::symlink(&path, &new_path));
|
|
}
|
|
path = new_path;
|
|
} else if file_type == FileType::DirDrive as i32 {
|
|
if cfg!(windows) {
|
|
path = fs::get_path("C:");
|
|
} else if cfg!(target_os = "macos") {
|
|
if let Ok(entries) = fs::get_path("/Volumes/").read_dir() {
|
|
for entry in entries {
|
|
if let Ok(entry) = entry {
|
|
path = entry.path();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
fs::get_string(&path)
|
|
}
|
|
|
|
pub fn login(
|
|
&self,
|
|
os_username: String,
|
|
os_password: String,
|
|
password: String,
|
|
remember: bool,
|
|
) {
|
|
self.send(Data::Login((os_username, os_password, password, remember)));
|
|
}
|
|
|
|
pub fn send2fa(&self, code: String, trust_this_device: bool) {
|
|
let mut msg_out = Message::new();
|
|
let hwid = if trust_this_device {
|
|
crate::get_hwid()
|
|
} else {
|
|
Bytes::new()
|
|
};
|
|
self.lc.write().unwrap().set_option(
|
|
"trust-this-device".to_string(),
|
|
if trust_this_device { "Y" } else { "" }.to_string(),
|
|
);
|
|
msg_out.set_auth_2fa(Auth2FA {
|
|
code,
|
|
hwid,
|
|
..Default::default()
|
|
});
|
|
self.send(Data::Message(msg_out));
|
|
}
|
|
|
|
pub fn get_enable_trusted_devices(&self) -> bool {
|
|
self.lc.read().unwrap().enable_trusted_devices
|
|
}
|
|
|
|
pub fn new_rdp(&self) {
|
|
self.send(Data::NewRDP);
|
|
}
|
|
|
|
pub fn close(&self) {
|
|
self.send(Data::Close);
|
|
}
|
|
|
|
pub fn continue_insecure_connection(&self, continue_insecure: bool) {
|
|
let data = if continue_insecure {
|
|
Data::ContinueInsecureConnection
|
|
} else {
|
|
Data::RejectInsecureConnection
|
|
};
|
|
self.send(data);
|
|
}
|
|
|
|
fn try_auto_start_job_str(is_reconnected: bool, job_str: &str) -> Option<String> {
|
|
if is_reconnected {
|
|
let job_str = job_str.trim();
|
|
if let Some(stripped) = job_str.strip_suffix('}') {
|
|
format!(r#"{},"auto_start": true}}"#, stripped).into()
|
|
} else {
|
|
// unreachable in normal cases
|
|
log::warn!(
|
|
"The last character is not '}}': {}, auto start is ignored on flutter",
|
|
job_str
|
|
);
|
|
Some(job_str.to_owned())
|
|
}
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
pub fn load_last_jobs(&self) {
|
|
self.clear_all_jobs();
|
|
let pc = self.load_config();
|
|
if pc.transfer.write_jobs.is_empty() && pc.transfer.read_jobs.is_empty() {
|
|
// no last jobs
|
|
return;
|
|
}
|
|
let reconnect_count_thr = if cfg!(feature = "flutter") { 0 } else { 1 };
|
|
let is_reconnected = self.reconnect_count.load(Ordering::SeqCst) > reconnect_count_thr;
|
|
// TODO: can add a confirm dialog
|
|
let mut cnt = 1;
|
|
for job_str in pc.transfer.read_jobs.iter() {
|
|
if !job_str.is_empty() {
|
|
self.load_last_job(
|
|
cnt,
|
|
Self::try_auto_start_job_str(is_reconnected, job_str)
|
|
.as_deref()
|
|
.unwrap_or(job_str),
|
|
is_reconnected,
|
|
);
|
|
cnt += 1;
|
|
log::info!("restore read_job: {:?}", job_str);
|
|
}
|
|
}
|
|
for job_str in pc.transfer.write_jobs.iter() {
|
|
if !job_str.is_empty() {
|
|
self.load_last_job(
|
|
cnt,
|
|
Self::try_auto_start_job_str(is_reconnected, job_str)
|
|
.as_deref()
|
|
.unwrap_or(job_str),
|
|
is_reconnected,
|
|
);
|
|
cnt += 1;
|
|
log::info!("restore write_job: {:?}", job_str);
|
|
}
|
|
}
|
|
self.update_transfer_list();
|
|
}
|
|
|
|
pub fn elevate_direct(&self) {
|
|
self.send(Data::ElevateDirect);
|
|
}
|
|
|
|
pub fn elevate_with_logon(&self, username: String, password: String) {
|
|
self.send(Data::ElevateWithLogon(username, password));
|
|
}
|
|
|
|
#[cfg(any(target_os = "android", target_os = "ios", not(feature = "flutter")))]
|
|
pub fn switch_sides(&self) {}
|
|
|
|
#[cfg(feature = "flutter")]
|
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
|
#[tokio::main(flavor = "current_thread")]
|
|
pub async fn switch_sides(&self) {
|
|
match crate::ipc::connect(1000, "").await {
|
|
Ok(mut conn) => {
|
|
if conn
|
|
.send(&crate::ipc::Data::SwitchSidesRequest(self.get_id()))
|
|
.await
|
|
.is_ok()
|
|
{
|
|
if let Ok(Some(data)) = conn.next_timeout(1000).await {
|
|
match data {
|
|
crate::ipc::Data::SwitchSidesRequest(str_uuid) => {
|
|
if let Ok(uuid) = Uuid::from_str(&str_uuid) {
|
|
let mut misc = Misc::new();
|
|
misc.set_switch_sides_request(SwitchSidesRequest {
|
|
uuid: Bytes::from(uuid.as_bytes().to_vec()),
|
|
..Default::default()
|
|
});
|
|
let mut msg_out = Message::new();
|
|
msg_out.set_misc(misc);
|
|
self.send(Data::Message(msg_out));
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Err(err) => {
|
|
log::info!("server not started (will try to start): {}", err);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn set_custom_resolution(&self, display: &SwitchDisplay) {
|
|
if display.width == display.original_resolution.width
|
|
&& display.height == display.original_resolution.height
|
|
{
|
|
self.lc
|
|
.write()
|
|
.unwrap()
|
|
.set_custom_resolution(display.display, None);
|
|
} else {
|
|
let last_change_display = self.last_change_display.lock().unwrap();
|
|
if last_change_display.display == display.display {
|
|
let wh = if last_change_display.is_the_same_record(
|
|
display.display,
|
|
display.width,
|
|
display.height,
|
|
) {
|
|
Some((display.width, display.height))
|
|
} else {
|
|
// display origin is changed, or some other events.
|
|
None
|
|
};
|
|
self.lc
|
|
.write()
|
|
.unwrap()
|
|
.set_custom_resolution(display.display, wh);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
pub fn handle_peer_switch_display(&self, display: &SwitchDisplay) {
|
|
self.ui_handler.switch_display(display);
|
|
self.set_custom_resolution(display);
|
|
}
|
|
|
|
#[inline]
|
|
pub fn change_resolution(&self, display: i32, width: i32, height: i32) {
|
|
*self.last_change_display.lock().unwrap() =
|
|
ChangeDisplayRecord::new(display, width, height);
|
|
self.do_change_resolution(display, width, height);
|
|
}
|
|
|
|
#[inline]
|
|
fn try_change_init_resolution(&self, display: i32) {
|
|
let Some((w, h)) = self.lc.read().unwrap().get_custom_resolution(display) else {
|
|
return;
|
|
};
|
|
self.change_resolution(display, w, h);
|
|
}
|
|
|
|
fn do_change_resolution(&self, display: i32, width: i32, height: i32) {
|
|
let mut misc = Misc::new();
|
|
let resolution = Resolution {
|
|
width,
|
|
height,
|
|
..Default::default()
|
|
};
|
|
if crate::common::is_support_multi_ui_session_num(self.lc.read().unwrap().version) {
|
|
misc.set_change_display_resolution(DisplayResolution {
|
|
display,
|
|
resolution: Some(resolution).into(),
|
|
..Default::default()
|
|
});
|
|
} else {
|
|
misc.set_change_resolution(resolution);
|
|
}
|
|
let mut msg = Message::new();
|
|
msg.set_misc(misc);
|
|
self.send(Data::Message(msg));
|
|
}
|
|
|
|
#[inline]
|
|
pub fn request_voice_call(&self) {
|
|
#[cfg(target_os = "linux")]
|
|
std::thread::spawn(crate::ipc::start_pa);
|
|
self.send(Data::NewVoiceCall);
|
|
}
|
|
|
|
#[inline]
|
|
pub fn close_voice_call(&self) {
|
|
self.send(Data::CloseVoiceCall);
|
|
}
|
|
|
|
pub fn send_selected_session_id(&self, sid: String) {
|
|
if let Ok(sid) = sid.parse::<u32>() {
|
|
self.lc.write().unwrap().selected_windows_session_id = Some(sid);
|
|
let mut misc = Misc::new();
|
|
misc.set_selected_sid(sid);
|
|
let mut msg = Message::new();
|
|
msg.set_misc(misc);
|
|
self.send(Data::Message(msg));
|
|
let pi = self.lc.read().unwrap().peer_info.clone();
|
|
if let Some(pi) = pi {
|
|
if pi.windows_sessions.current_sid == sid {
|
|
if self.is_file_transfer() {
|
|
if pi.username.is_empty() {
|
|
self.on_error(
|
|
"No active console user logged on, please connect and logon first.",
|
|
);
|
|
} else {
|
|
#[cfg(not(feature = "flutter"))]
|
|
{
|
|
let remote_dir = self.get_option("remote_dir".to_string());
|
|
let show_hidden =
|
|
!self.get_option("remote_show_hidden".to_string()).is_empty();
|
|
self.read_remote_dir(remote_dir, show_hidden);
|
|
}
|
|
}
|
|
} else if !self.is_terminal() {
|
|
self.msgbox(
|
|
"success",
|
|
"Successful",
|
|
"Connected, waiting for image...",
|
|
"",
|
|
);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
log::error!("selected invalid sid: {}", sid);
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
pub fn request_init_msgs(&self, display: usize) {
|
|
self.send_message_query(display);
|
|
}
|
|
|
|
fn send_message_query(&self, display: usize) {
|
|
let mut misc = Misc::new();
|
|
misc.set_message_query(MessageQuery {
|
|
switch_display: display as _,
|
|
..Default::default()
|
|
});
|
|
let mut msg = Message::new();
|
|
msg.set_misc(misc);
|
|
self.send(Data::Message(msg));
|
|
}
|
|
|
|
pub fn get_conn_token(&self) -> Option<String> {
|
|
self.lc.read().unwrap().get_conn_token()
|
|
}
|
|
|
|
pub fn printer_response(&self, id: i32, path: String, printer_name: String) {
|
|
self.printer_names.write().unwrap().insert(id, printer_name);
|
|
let to = std::env::temp_dir().join(format!("rustdesk_printer_{id}"));
|
|
self.send(Data::SendFiles((
|
|
id,
|
|
base::fs::JobType::Printer,
|
|
path,
|
|
to.to_string_lossy().to_string(),
|
|
0,
|
|
false,
|
|
true,
|
|
)));
|
|
}
|
|
}
|
|
|
|
pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default {
|
|
fn set_cursor_data(&self, cd: CursorData);
|
|
fn set_cursor_id(&self, id: String);
|
|
fn set_cursor_position(&self, cp: CursorPosition);
|
|
fn set_display(&self, x: i32, y: i32, w: i32, h: i32, cursor_embedded: bool, scale: f64);
|
|
fn switch_display(&self, display: &SwitchDisplay);
|
|
fn set_peer_info(&self, peer_info: &PeerInfo); // flutter
|
|
fn set_displays(&self, displays: &Vec<DisplayInfo>);
|
|
fn set_platform_additions(&self, data: &str);
|
|
fn on_connected(&self, conn_type: ConnType);
|
|
fn update_privacy_mode(&self);
|
|
fn set_permission(&self, name: &str, value: bool);
|
|
fn close_success(&self);
|
|
fn update_quality_status(&self, qs: QualityStatus);
|
|
fn set_connection_type(&self, is_secured: bool, direct: bool, stream_type: &str);
|
|
fn set_fingerprint(&self, fingerprint: String);
|
|
fn job_error(&self, id: i32, err: String, file_num: i32);
|
|
fn job_done(&self, id: i32, file_num: i32);
|
|
fn clear_all_jobs(&self);
|
|
fn new_message(&self, msg: String);
|
|
fn update_transfer_list(&self);
|
|
fn load_last_job(&self, cnt: i32, job_json: &str, auto_start: bool);
|
|
fn update_folder_files(
|
|
&self,
|
|
id: i32,
|
|
entries: &Vec<FileEntry>,
|
|
path: String,
|
|
is_local: bool,
|
|
only_count: bool,
|
|
);
|
|
fn confirm_delete_files(&self, id: i32, i: i32, name: String);
|
|
fn override_file_confirm(
|
|
&self,
|
|
id: i32,
|
|
file_num: i32,
|
|
to: String,
|
|
is_upload: bool,
|
|
is_identical: bool,
|
|
);
|
|
fn update_block_input_state(&self, on: bool);
|
|
fn job_progress(&self, id: i32, file_num: i32, speed: f64, finished_size: f64);
|
|
fn adapt_size(&self);
|
|
fn on_rgba(&self, display: usize, rgba: &mut scrap::ImageRgb);
|
|
fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str, retry: bool);
|
|
#[cfg(any(target_os = "android", target_os = "ios"))]
|
|
fn clipboard(&self, content: String);
|
|
fn cancel_msgbox(&self, tag: &str);
|
|
fn switch_back(&self, id: &str);
|
|
fn portable_service_running(&self, running: bool);
|
|
fn on_voice_call_started(&self);
|
|
fn on_voice_call_closed(&self, reason: &str);
|
|
fn on_voice_call_waiting(&self);
|
|
fn on_voice_call_incoming(&self);
|
|
fn get_rgba(&self, display: usize) -> *const u8;
|
|
fn next_rgba(&self, display: usize);
|
|
#[cfg(all(feature = "vram", feature = "flutter"))]
|
|
fn on_texture(&self, display: usize, texture: *mut c_void);
|
|
fn set_multiple_windows_session(&self, sessions: Vec<WindowsSession>);
|
|
fn set_current_display(&self, disp_idx: i32);
|
|
#[cfg(feature = "flutter")]
|
|
fn is_multi_ui_session(&self) -> bool;
|
|
fn update_record_status(&self, start: bool);
|
|
fn update_empty_dirs(&self, _res: ReadEmptyDirsResponse) {}
|
|
fn printer_request(&self, id: i32, path: String);
|
|
fn handle_screenshot_resp(&self, sid: String, msg: String);
|
|
fn handle_terminal_response(&self, response: TerminalResponse);
|
|
}
|
|
|
|
impl<T: InvokeUiSession> Deref for Session<T> {
|
|
type Target = T;
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
&self.ui_handler
|
|
}
|
|
}
|
|
|
|
impl<T: InvokeUiSession> DerefMut for Session<T> {
|
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
&mut self.ui_handler
|
|
}
|
|
}
|
|
|
|
impl<T: InvokeUiSession> FileManager for Session<T> {}
|
|
|
|
#[async_trait]
|
|
impl<T: InvokeUiSession> Interface for Session<T> {
|
|
fn get_lch(&self) -> Arc<RwLock<LoginConfigHandler>> {
|
|
return self.lc.clone();
|
|
}
|
|
|
|
fn send(&self, data: Data) {
|
|
if let Some(sender) = self.sender.read().unwrap().as_ref() {
|
|
sender.send(data).ok();
|
|
}
|
|
}
|
|
|
|
fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str) {
|
|
let direct = self.lc.read().unwrap().direct;
|
|
let received = self.lc.read().unwrap().received;
|
|
let retry_for_relay = direct == Some(true) && !received;
|
|
let retry = check_if_retry(msgtype, title, text, retry_for_relay);
|
|
self.ui_handler.msgbox(msgtype, title, text, link, retry);
|
|
}
|
|
|
|
fn handle_login_error(&self, err: &str) -> bool {
|
|
handle_login_error(self.lc.clone(), err, self)
|
|
}
|
|
|
|
fn set_multiple_windows_session(&self, sessions: Vec<WindowsSession>) {
|
|
self.ui_handler.set_multiple_windows_session(sessions);
|
|
}
|
|
|
|
fn handle_peer_info(&self, mut pi: PeerInfo) {
|
|
log::debug!("handle_peer_info :{:?}", pi);
|
|
self.lc.write().unwrap().peer_info = Some(pi.clone());
|
|
if pi.current_display as usize >= pi.displays.len() {
|
|
pi.current_display = 0;
|
|
}
|
|
if get_version_number(&pi.version) < get_version_number("1.1.10") {
|
|
self.set_permission("restart", false);
|
|
}
|
|
if self.is_file_transfer() {
|
|
if pi.username.is_empty() && pi.windows_sessions.sessions.is_empty() {
|
|
self.on_error("No active console user logged on, please connect and logon first.");
|
|
return;
|
|
}
|
|
} else if !self.is_port_forward() && !self.is_terminal() {
|
|
if pi.displays.is_empty() {
|
|
self.lc.write().unwrap().handle_peer_info(&pi);
|
|
self.update_privacy_mode();
|
|
let msg = if self.is_view_camera() {
|
|
"No cameras"
|
|
} else {
|
|
"No displays"
|
|
};
|
|
self.msgbox("error", "Error", msg, "");
|
|
return;
|
|
}
|
|
if !self.is_view_camera() {
|
|
self.try_change_init_resolution(pi.current_display);
|
|
let p = self.lc.read().unwrap().should_auto_login();
|
|
if !p.is_empty() {
|
|
crate::client::auto_login(p, &pi, self.clone());
|
|
}
|
|
}
|
|
let current = &pi.displays[pi.current_display as usize];
|
|
self.set_display(
|
|
current.x,
|
|
current.y,
|
|
current.width,
|
|
current.height,
|
|
current.cursor_embedded,
|
|
current.scale,
|
|
);
|
|
}
|
|
self.update_privacy_mode();
|
|
// Clear audit_guid when connection is established successfully
|
|
*self.audit_guid.lock().unwrap() = String::new();
|
|
*self.last_audit_note.lock().unwrap() = String::new();
|
|
// Save recent peers, then push event to flutter. So flutter can refresh peer page.
|
|
self.lc.write().unwrap().handle_peer_info(&pi);
|
|
self.set_peer_info(&pi);
|
|
if self.is_file_transfer() {
|
|
self.close_success();
|
|
} else if !self.is_port_forward() && !self.is_terminal() {
|
|
self.msgbox(
|
|
"success",
|
|
"Successful",
|
|
"Connected, waiting for image...",
|
|
"",
|
|
);
|
|
}
|
|
self.on_connected(self.lc.read().unwrap().conn_type);
|
|
#[cfg(windows)]
|
|
{
|
|
let mut path = std::env::temp_dir();
|
|
path.push(self.get_id());
|
|
let path = path.with_extension(crate::get_app_name().to_lowercase());
|
|
std::fs::File::create(&path).ok();
|
|
if let Some(path) = path.to_str() {
|
|
crate::platform::windows::add_recent_document(&path);
|
|
}
|
|
}
|
|
if !pi.windows_sessions.sessions.is_empty() {
|
|
let selected = self
|
|
.lc
|
|
.read()
|
|
.unwrap()
|
|
.selected_windows_session_id
|
|
.to_owned();
|
|
if selected == Some(pi.windows_sessions.current_sid) {
|
|
self.send_selected_session_id(pi.windows_sessions.current_sid.to_string());
|
|
} else {
|
|
self.set_multiple_windows_session(pi.windows_sessions.sessions.clone());
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn handle_hash(&self, pass: &str, hash: Hash, peer: &mut Stream) -> bool {
|
|
handle_hash(self.lc.clone(), pass, hash, self, peer).await
|
|
}
|
|
|
|
async fn handle_login_from_ui(
|
|
&self,
|
|
os_username: String,
|
|
os_password: String,
|
|
password: String,
|
|
remember: bool,
|
|
peer: &mut Stream,
|
|
) {
|
|
handle_login_from_ui(
|
|
self.lc.clone(),
|
|
os_username,
|
|
os_password,
|
|
password,
|
|
remember,
|
|
peer,
|
|
)
|
|
.await;
|
|
}
|
|
|
|
async fn handle_test_delay(&self, t: TestDelay, peer: &mut Stream) {
|
|
if !t.from_client {
|
|
self.update_quality_status(QualityStatus {
|
|
delay: Some(t.last_delay as _),
|
|
target_bitrate: Some(t.target_bitrate as _),
|
|
..Default::default()
|
|
});
|
|
handle_test_delay(t, peer).await;
|
|
}
|
|
}
|
|
|
|
fn swap_modifier_mouse(&self, msg: &mut base::protos::message::MouseEvent) {
|
|
let allow_swap_key = self.get_toggle_option("allow_swap_key".to_string());
|
|
if allow_swap_key {
|
|
msg.modifiers = msg
|
|
.modifiers
|
|
.iter()
|
|
.map(|ck| {
|
|
let ck = ck.enum_value_or_default();
|
|
let ck = match ck {
|
|
ControlKey::Control => ControlKey::Meta,
|
|
ControlKey::Meta => ControlKey::Control,
|
|
ControlKey::RControl => ControlKey::Meta,
|
|
ControlKey::RWin => ControlKey::Control,
|
|
_ => ck,
|
|
};
|
|
hbb_common::protobuf::EnumOrUnknown::new(ck)
|
|
})
|
|
.collect();
|
|
};
|
|
}
|
|
}
|
|
|
|
impl<T: InvokeUiSession> Session<T> {
|
|
pub fn lock_screen(&self) {
|
|
self.send_key_event(&crate::keyboard::client::event_lock_screen());
|
|
}
|
|
pub fn ctrl_alt_del(&self) {
|
|
self.send_key_event(&crate::keyboard::client::event_ctrl_alt_del(
|
|
&self.peer_platform(),
|
|
));
|
|
}
|
|
}
|
|
|
|
#[tokio::main(flavor = "current_thread")]
|
|
pub async fn io_loop<T: InvokeUiSession>(handler: Session<T>, round: u32) {
|
|
#[cfg(any(target_os = "android", target_os = "ios"))]
|
|
let (sender, receiver) = mpsc::unbounded_channel::<Data>();
|
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
|
let (sender, mut receiver) = mpsc::unbounded_channel::<Data>();
|
|
*handler.sender.write().unwrap() = Some(sender.clone());
|
|
let token = LocalConfig::get_option("access_token");
|
|
let key = crate::get_key(false).await;
|
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
|
if handler.is_port_forward() {
|
|
handler.lc.write().unwrap().port_forward_mux = crate::port_forward::mux_enabled();
|
|
if handler.is_rdp() {
|
|
let port = handler
|
|
.get_option("rdp_port".to_owned())
|
|
.parse::<i32>()
|
|
.unwrap_or(3389);
|
|
std::env::set_var(
|
|
"rdp_username",
|
|
handler.get_option("rdp_username".to_owned()),
|
|
);
|
|
std::env::set_var(
|
|
"rdp_password",
|
|
handler.get_option("rdp_password".to_owned()),
|
|
);
|
|
log::info!("Remote rdp port: {}", port);
|
|
start_one_port_forward(handler, 0, "".to_owned(), port, receiver, &key, &token).await;
|
|
} else if handler.args.len() == 0 {
|
|
let pfs = handler.lc.read().unwrap().port_forwards.clone();
|
|
let mut queues = HashMap::<i32, mpsc::UnboundedSender<Data>>::new();
|
|
for d in pfs {
|
|
sender.send(Data::AddPortForward(d)).ok();
|
|
}
|
|
loop {
|
|
match receiver.recv().await {
|
|
Some(Data::AddPortForward((port, remote_host, remote_port))) => {
|
|
if port <= 0 || remote_port <= 0 {
|
|
continue;
|
|
}
|
|
let (sender, receiver) = mpsc::unbounded_channel::<Data>();
|
|
queues.insert(port, sender);
|
|
let handler = handler.clone();
|
|
let key = key.clone();
|
|
let token = token.clone();
|
|
tokio::spawn(async move {
|
|
start_one_port_forward(
|
|
handler,
|
|
port,
|
|
remote_host,
|
|
remote_port,
|
|
receiver,
|
|
&key,
|
|
&token,
|
|
)
|
|
.await;
|
|
});
|
|
}
|
|
Some(Data::RemovePortForward(port)) => {
|
|
if let Some(s) = queues.remove(&port) {
|
|
s.send(Data::Close).ok();
|
|
}
|
|
}
|
|
Some(Data::Close) => {
|
|
break;
|
|
}
|
|
Some(d) => {
|
|
for (_, s) in queues.iter() {
|
|
s.send(d.clone()).ok();
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
} else {
|
|
let port = handler.args[0].parse::<i32>().unwrap_or(0);
|
|
if handler.args.len() != 3
|
|
|| handler.args[2].parse::<i32>().unwrap_or(0) <= 0
|
|
|| port <= 0
|
|
{
|
|
handler.on_error("Invalid arguments, usage:<br><br> rustdesk --port-forward remote-id listen-port remote-host remote-port");
|
|
}
|
|
let remote_host = handler.args[1].clone();
|
|
let remote_port = handler.args[2].parse::<i32>().unwrap_or(0);
|
|
start_one_port_forward(
|
|
handler,
|
|
port,
|
|
remote_host,
|
|
remote_port,
|
|
receiver,
|
|
&key,
|
|
&token,
|
|
)
|
|
.await;
|
|
}
|
|
return;
|
|
}
|
|
let mut remote = Remote::new(handler, receiver, sender);
|
|
remote.io_loop(&key, &token, round).await;
|
|
let _ = remote.sync_jobs_status_to_local().await;
|
|
}
|
|
|
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
|
async fn start_one_port_forward<T: InvokeUiSession>(
|
|
handler: Session<T>,
|
|
port: i32,
|
|
remote_host: String,
|
|
remote_port: i32,
|
|
receiver: mpsc::UnboundedReceiver<Data>,
|
|
key: &str,
|
|
token: &str,
|
|
) {
|
|
if let Err(err) = crate::port_forward::listen(
|
|
handler.get_id(),
|
|
handler.password.clone(),
|
|
port,
|
|
handler.clone(),
|
|
receiver,
|
|
key,
|
|
token,
|
|
handler.lc.clone(),
|
|
remote_host,
|
|
remote_port,
|
|
)
|
|
.await
|
|
{
|
|
handler.on_error(&format!("Failed to listen on {}: {}", port, err));
|
|
}
|
|
log::info!("port forward (:{}) exit", port);
|
|
}
|
|
|
|
#[tokio::main(flavor = "current_thread")]
|
|
async fn send_note(url: String, id: String, sid: u64, note: String) {
|
|
let body = serde_json::json!({ "id": id, "session_id": sid, "note": note });
|
|
allow_err!(crate::post_request(url, body.to_string(), "").await);
|
|
}
|