diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 97a4073af..bf73bdaa1 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -402,7 +402,7 @@ jobs: - name: Install dependencies run: | sudo apt update - sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev pkg-config tree g++ libc6-dev gcc-multilib g++-multilib openjdk-11-jdk-headless + sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libpam0g-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev pkg-config tree g++ libc6-dev gcc-multilib g++-multilib openjdk-11-jdk-headless - name: Checkout source code uses: actions/checkout@v3 - name: Install flutter @@ -654,7 +654,7 @@ jobs: install: | apt update -y echo -e "installing deps" - apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev pkg-config tree g++ gcc libvpx-dev tree > /dev/null + apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libpam0g-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev pkg-config tree g++ gcc libvpx-dev tree > /dev/null # we have libopus compiled by us. apt remove -y libopus-dev || true # output devs @@ -687,7 +687,7 @@ jobs: x86_64) # no need mock on x86_64 export VCPKG_ROOT=/opt/artifacts/vcpkg - cargo build --lib --features hwcodec,flutter,flutter_texture_render,${{ matrix.job.extra-build-features }} --release + cargo build --lib --features hwcodec,flutter,flutter_texture_render,linux_headless,${{ matrix.job.extra-build-features }} --release ;; esac @@ -816,7 +816,7 @@ jobs: install: | apt update -y echo -e "installing deps" - apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev pkg-config tree g++ gcc libvpx-dev tree > /dev/null + apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libpam0g-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev pkg-config tree g++ gcc libvpx-dev tree > /dev/null # we have libopus compiled by us. apt remove -y libopus-dev || true # output devs @@ -947,7 +947,7 @@ jobs: apt update -y apt-get -qq install -y git cmake g++ gcc build-essential nasm yasm curl unzip xz-utils python3 wget pkg-config ninja-build pkg-config libgtk-3-dev liblzma-dev clang libappindicator3-dev rpm libclang-dev apt-get -qq install -y libdbus-1-dev pkg-config nasm yasm libglib2.0-dev libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev - apt-get -qq install -y libpulse-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvpx-dev libvdpau-dev libva-dev + apt-get -qq install -y libpulse-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvpx-dev libvdpau-dev libva-dev libpam0g-dev run: | # disable git safe.directory git config --global --add safe.directory "*" diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index ab625e431..c812f43fa 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -11,4 +11,3 @@ jobs: uses: ./.github/workflows/flutter-build.yml with: upload-artifact: true - \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 5a4cad9f8..9d2984abe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4159,6 +4159,38 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "pam" +version = "0.7.0" +source = "git+https://github.com/fufesou/pam#10da2cbbabe32cbc9de22a66abe44738e7ec0ea0" +dependencies = [ + "libc", + "pam-macros", + "pam-sys", + "users 0.10.0", +] + +[[package]] +name = "pam-macros" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c94f3b9b97df3c6d4e51a14916639b24e02c7d15d1dba686ce9b1118277cb811" +dependencies = [ + "proc-macro2 1.0.54", + "quote 1.0.26", + "syn 1.0.109", +] + +[[package]] +name = "pam-sys" +version = "1.0.0-alpha4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e9dfd42858f6a6bb1081079fd9dc259ca3e2aaece6cb689fd36b1058046c969" +dependencies = [ + "bindgen 0.59.2", + "libc", +] + [[package]] name = "pango" version = "0.16.5" @@ -5124,6 +5156,7 @@ dependencies = [ "objc", "objc_id", "os-version", + "pam", "parity-tokio-ipc", "rdev", "repng", @@ -5149,6 +5182,7 @@ dependencies = [ "tray-icon", "trayicon", "url", + "users 0.11.0", "uuid", "virtual_display", "whoami", @@ -6443,6 +6477,26 @@ dependencies = [ "serde 1.0.159", ] +[[package]] +name = "users" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa4227e95324a443c9fcb06e03d4d85e91aabe9a5a02aa818688b6918b6af486" +dependencies = [ + "libc", + "log", +] + +[[package]] +name = "users" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24cc0f6d6f267b73e5a2cadf007ba8f9bc39c6a6f9666f8cf25ea809a153b032" +dependencies = [ + "libc", + "log", +] + [[package]] name = "utf8parse" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index a01415837..48bd16045 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ flutter = ["flutter_rust_bridge"] default = ["use_dasp"] hwcodec = ["scrap/hwcodec"] mediacodec = ["scrap/mediacodec"] +linux_headless = ["pam", "users"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -122,6 +123,8 @@ mouce = { git="https://github.com/fufesou/mouce.git" } evdev = { git="https://github.com/fufesou/evdev" } dbus = "0.9" dbus-crossroads = "0.5" +pam = { git="https://github.com/fufesou/pam", optional = true } +users = { version = "0.11.0", optional = true } [target.'cfg(target_os = "android")'.dependencies] android_logger = "0.11" diff --git a/build.py b/build.py index 7acf97a7d..3a06c4161 100755 --- a/build.py +++ b/build.py @@ -287,6 +287,8 @@ def build_flutter_deb(version, features): system2('flutter build linux --release') system2('mkdir -p tmpdeb/usr/bin/') system2('mkdir -p tmpdeb/usr/lib/rustdesk') + system2('mkdir -p tmpdeb/etc/rustdesk/') + system2('mkdir -p tmpdeb/etc/pam.d/') system2('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/') system2('mkdir -p tmpdeb/usr/share/applications/') system2('mkdir -p tmpdeb/usr/share/polkit-1/actions') @@ -303,6 +305,12 @@ def build_flutter_deb(version, features): 'cp ../res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop') system2( 'cp ../res/com.rustdesk.RustDesk.policy tmpdeb/usr/share/polkit-1/actions/') + system2( + 'cp ../res/startwm.sh tmpdeb/etc/rustdesk/') + system2( + 'cp ../res/xorg.conf tmpdeb/etc/rustdesk/') + system2( + 'cp ../res/pam.d/rustdesk.debian tmpdeb/etc/pam.d/rustdesk') system2( "echo \"#!/bin/sh\" >> tmpdeb/usr/share/rustdesk/files/polkit && chmod a+x tmpdeb/usr/share/rustdesk/files/polkit") @@ -553,12 +561,21 @@ def main(): 'cp res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop') system2( 'cp res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop') - system2('cp -a res/DEBIAN/* tmpdeb/DEBIAN/') + os.system('mkdir -p tmpdeb/etc/rustdesk/') + os.system('cp -a res/startwm.sh tmpdeb/etc/rustdesk/') + os.system('mkdir -p tmpdeb/etc/X11/rustdesk/') + os.system('cp res/xorg.conf tmpdeb/etc/X11/rustdesk/') + os.system('cp -a DEBIAN/* tmpdeb/DEBIAN/') + os.system('mkdir -p tmpdeb/etc/pam.d/') + os.system('cp pam.d/rustdesk.debian tmpdeb/etc/pam.d/rustdesk') system2('strip tmpdeb/usr/bin/rustdesk') system2('mkdir -p tmpdeb/usr/lib/rustdesk') system2('mv tmpdeb/usr/bin/rustdesk tmpdeb/usr/lib/rustdesk/') system2('cp libsciter-gtk.so tmpdeb/usr/lib/rustdesk/') md5_file('usr/share/rustdesk/files/systemd/rustdesk.service') + md5_file('etc/rustdesk/startwm.sh') + md5_file('etc/X11/rustdesk/xorg.conf') + md5_file('etc/pam.d/rustdesk') md5_file('usr/lib/rustdesk/libsciter-gtk.so') system2('dpkg-deb -b tmpdeb rustdesk.deb; /bin/rm -rf tmpdeb/') os.rename('rustdesk.deb', 'rustdesk-%s.deb' % version) diff --git a/flutter/lib/common/widgets/dialog.dart b/flutter/lib/common/widgets/dialog.dart index cc815c5a3..cf1ced92f 100644 --- a/flutter/lib/common/widgets/dialog.dart +++ b/flutter/lib/common/widgets/dialog.dart @@ -456,7 +456,7 @@ void enterPasswordDialog(String id, OverlayDialogManager dialogManager) async { await _connectDialog( id, dialogManager, - peerPasswordController: TextEditingController(), + passwordController: TextEditingController(), ); } @@ -464,8 +464,8 @@ void enterUserLoginDialog(String id, OverlayDialogManager dialogManager) async { await _connectDialog( id, dialogManager, - usernameController: TextEditingController(), - passwordController: TextEditingController(), + osUsernameController: TextEditingController(), + osPasswordController: TextEditingController(), ); } @@ -474,20 +474,27 @@ void enterUserLoginAndPasswordDialog( await _connectDialog( id, dialogManager, - usernameController: TextEditingController(), + osUsernameController: TextEditingController(), + osPasswordController: TextEditingController(), passwordController: TextEditingController(), - peerPasswordController: TextEditingController(), ); } _connectDialog( String id, OverlayDialogManager dialogManager, { - TextEditingController? usernameController, + TextEditingController? osUsernameController, + TextEditingController? osPasswordController, TextEditingController? passwordController, - TextEditingController? peerPasswordController, }) async { - var remember = await bind.sessionGetRemember(id: id) ?? false; + var rememberPassword = false; + if (passwordController != null) { + rememberPassword = await bind.sessionGetRemember(id: id) ?? false; + } + var rememberAccount = false; + if (osUsernameController != null) { + rememberAccount = await bind.sessionGetRemember(id: id) ?? false; + } dialogManager.dismissAll(); dialogManager.show((setState, close) { cancel() { @@ -496,82 +503,129 @@ _connectDialog( } submit() { - // to-do: - // username and password are about remote OS account. - // If the remote side is headless. - // The client side should login to remote OS account, to enable X desktop session. - // `username` and `password` will be used in the near future. - final username = usernameController?.text.trim() ?? ''; + final osUsername = osUsernameController?.text.trim() ?? ''; + final osPassword = osPasswordController?.text.trim() ?? ''; final password = passwordController?.text.trim() ?? ''; - final peerPassword = peerPasswordController?.text.trim() ?? ''; - if (peerPasswordController != null && peerPassword.isEmpty) return; + if (passwordController != null && password.isEmpty) return; + if (rememberAccount) { + bind.sessionPeerOption(id: id, name: 'os-username', value: osUsername); + bind.sessionPeerOption(id: id, name: 'os-password', value: osPassword); + } gFFI.login( - // username, - // password, + osUsername, + osPassword, id, - peerPassword, - remember, + password, + rememberPassword, ); close(); dialogManager.showLoading(translate('Logging in...'), onCancel: closeConnection); } + descWidget(String text) { + return Column( + children: [ + Align( + alignment: Alignment.centerLeft, + child: Text( + text, + maxLines: 3, + softWrap: true, + overflow: TextOverflow.ellipsis, + style: TextStyle(fontSize: 16), + ), + ), + Container( + height: 8, + ), + ], + ); + } + + rememberWidget( + String desc, + bool remember, + ValueChanged? onChanged, + ) { + return CheckboxListTile( + contentPadding: const EdgeInsets.all(0), + dense: true, + controlAffinity: ListTileControlAffinity.leading, + title: Text(desc), + value: remember, + onChanged: onChanged, + ); + } + + osAccountWidget() { + if (osUsernameController == null || osPasswordController == null) { + return Offstage(); + } + return Column( + children: [ + descWidget(translate('login_linux_tip')), + DialogTextField( + title: translate(DialogTextField.kUsernameTitle), + controller: osUsernameController, + prefixIcon: DialogTextField.kUsernameIcon, + errorText: null, + ), + PasswordWidget( + controller: osPasswordController, + autoFocus: false, + ), + rememberWidget( + translate('remember_account_tip'), + rememberAccount, + (v) { + if (v != null) { + setState(() => rememberAccount = v); + } + }, + ), + ], + ); + } + + passwdWidget() { + if (passwordController == null) { + return Offstage(); + } + return Column( + children: [ + descWidget(translate('verify_rustdesk_password_tip')), + PasswordWidget( + controller: passwordController, + autoFocus: osUsernameController == null, + ), + rememberWidget( + translate('Remember password'), + rememberPassword, + (v) { + if (v != null) { + setState(() => rememberPassword = v); + } + }, + ), + ], + ); + } + return CustomAlertDialog( title: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.password_rounded, color: MyTheme.accent), - (usernameController == null - ? Text(translate('Password Required')) - : Tooltip( - message: translate('login_linux_tooltip_tip'), - child: Text(translate('login_linux_tip')), - )) - .paddingOnly(left: 10), + Text(translate('Password Required')).paddingOnly(left: 10), ], ), content: Column(mainAxisSize: MainAxisSize.min, children: [ - usernameController == null + osAccountWidget(), + osUsernameController == null || passwordController == null ? Offstage() - : DialogTextField( - title: translate(DialogTextField.kUsernameTitle), - controller: usernameController, - prefixIcon: DialogTextField.kUsernameIcon, - errorText: null, - ), - passwordController == null - ? Offstage() - : PasswordWidget( - controller: passwordController, - autoFocus: false, - ), - usernameController == null || peerPasswordController == null - ? Offstage() - : const Divider(), - peerPasswordController == null - ? Offstage() - : PasswordWidget( - controller: peerPasswordController, - autoFocus: usernameController == null, - hintText: 'enter_rustdesk_passwd_tip', - ), - peerPasswordController == null - ? Offstage() - : CheckboxListTile( - contentPadding: const EdgeInsets.all(0), - dense: true, - controlAffinity: ListTileControlAffinity.leading, - title: Text( - translate('remember_rustdesk_passwd_tip'), - ), - value: remember, - onChanged: (v) { - if (v != null) { - setState(() => remember = v); - } - }, - ), + : Container(height: 12), + passwdWidget(), ]), actions: [ dialogButton( @@ -847,3 +901,149 @@ void showRestartRemoteDevice( )); if (res == true) bind.sessionRestartRemoteDevice(id: id); } + +showSetOSPassword( + String id, + bool login, + OverlayDialogManager dialogManager, +) async { + final controller = TextEditingController(); + var password = await bind.sessionGetOption(id: id, arg: 'os-password') ?? ''; + var autoLogin = await bind.sessionGetOption(id: id, arg: 'auto-login') != ''; + controller.text = password; + dialogManager.show((setState, close) { + submit() { + var text = controller.text.trim(); + bind.sessionPeerOption(id: id, name: 'os-password', value: text); + bind.sessionPeerOption( + id: id, name: 'auto-login', value: autoLogin ? 'Y' : ''); + if (text != '' && login) { + bind.sessionInputOsPassword(id: id, value: text); + } + close(); + } + + return CustomAlertDialog( + title: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.password_rounded, color: MyTheme.accent), + Text(translate('OS Password')).paddingOnly(left: 10), + ], + ), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + PasswordWidget(controller: controller), + CheckboxListTile( + contentPadding: const EdgeInsets.all(0), + dense: true, + controlAffinity: ListTileControlAffinity.leading, + title: Text( + translate('Auto Login'), + ), + value: autoLogin, + onChanged: (v) { + if (v == null) return; + setState(() => autoLogin = v); + }, + ), + ], + ), + actions: [ + dialogButton( + "Cancel", + icon: Icon(Icons.close_rounded), + onPressed: close, + isOutline: true, + ), + dialogButton( + "OK", + icon: Icon(Icons.done_rounded), + onPressed: submit, + ), + ], + onSubmit: submit, + onCancel: close, + ); + }); +} + +showSetOSAccount( + String id, + OverlayDialogManager dialogManager, +) async { + final usernameController = TextEditingController(); + final passwdController = TextEditingController(); + var username = await bind.sessionGetOption(id: id, arg: 'os-username') ?? ''; + var password = await bind.sessionGetOption(id: id, arg: 'os-password') ?? ''; + usernameController.text = username; + passwdController.text = password; + dialogManager.show((setState, close) { + submit() { + final username = usernameController.text.trim(); + final password = usernameController.text.trim(); + bind.sessionPeerOption(id: id, name: 'os-username', value: username); + bind.sessionPeerOption(id: id, name: 'os-password', value: password); + close(); + } + + descWidget(String text) { + return Column( + children: [ + Align( + alignment: Alignment.centerLeft, + child: Text( + text, + maxLines: 3, + softWrap: true, + overflow: TextOverflow.ellipsis, + style: TextStyle(fontSize: 16), + ), + ), + Container( + height: 8, + ), + ], + ); + } + + return CustomAlertDialog( + title: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.password_rounded, color: MyTheme.accent), + Text(translate('OS Account')).paddingOnly(left: 10), + ], + ), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + descWidget(translate("os_account_desk_tip")), + DialogTextField( + title: translate(DialogTextField.kUsernameTitle), + controller: usernameController, + prefixIcon: DialogTextField.kUsernameIcon, + errorText: null, + ), + PasswordWidget(controller: passwdController), + ], + ), + actions: [ + dialogButton( + "Cancel", + icon: Icon(Icons.close_rounded), + onPressed: close, + isOutline: true, + ), + dialogButton( + "OK", + icon: Icon(Icons.done_rounded), + onPressed: submit, + ), + ], + onSubmit: submit, + onCancel: close, + ); + }); +} diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index a593810fd..3f6bb7d16 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -225,9 +225,7 @@ class _ConnectionPageState extends State children: [ Button( isOutline: true, - onTap: () { - onConnect(isFileTransfer: true); - }, + onTap: () => onConnect(isFileTransfer: true), text: "Transfer File", ), const SizedBox( diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index cdbf8be27..75bae3d08 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -639,7 +639,7 @@ class _ControlMenu extends StatelessWidget { ffi: ffi, menuChildren: [ requestElevation(), - osPassword(), + ffi.ffiModel.pi.is_headless ? osAccount() : osPassword(), transferFile(context), tcpTunneling(context), note(), @@ -662,78 +662,20 @@ class _ControlMenu extends StatelessWidget { onPressed: () => showRequestElevationDialog(id, ffi.dialogManager)); } + osAccount() { + return _MenuItemButton( + child: Text(translate('OS Account')), + trailingIcon: Transform.scale(scale: 0.8, child: Icon(Icons.edit)), + ffi: ffi, + onPressed: () => showSetOSAccount(id, ffi.dialogManager)); + } + osPassword() { return _MenuItemButton( child: Text(translate('OS Password')), trailingIcon: Transform.scale(scale: 0.8, child: Icon(Icons.edit)), ffi: ffi, - onPressed: () => _showSetOSPassword(id, false, ffi.dialogManager)); - } - - _showSetOSPassword( - String id, bool login, OverlayDialogManager dialogManager) async { - final controller = TextEditingController(); - var password = - await bind.sessionGetOption(id: id, arg: 'os-password') ?? ''; - var autoLogin = - await bind.sessionGetOption(id: id, arg: 'auto-login') != ''; - controller.text = password; - dialogManager.show((setState, close) { - submit() { - var text = controller.text.trim(); - bind.sessionPeerOption(id: id, name: 'os-password', value: text); - bind.sessionPeerOption( - id: id, name: 'auto-login', value: autoLogin ? 'Y' : ''); - if (text != '' && login) { - bind.sessionInputOsPassword(id: id, value: text); - } - close(); - } - - return CustomAlertDialog( - title: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.password_rounded, color: MyTheme.accent), - Text(translate('OS Password')).paddingOnly(left: 10), - ], - ), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - PasswordWidget(controller: controller), - CheckboxListTile( - contentPadding: const EdgeInsets.all(0), - dense: true, - controlAffinity: ListTileControlAffinity.leading, - title: Text( - translate('Auto Login'), - ), - value: autoLogin, - onChanged: (v) { - if (v == null) return; - setState(() => autoLogin = v); - }, - ), - ], - ), - actions: [ - dialogButton( - "Cancel", - icon: Icon(Icons.close_rounded), - onPressed: close, - isOutline: true, - ), - dialogButton( - "OK", - icon: Icon(Icons.done_rounded), - onPressed: submit, - ), - ], - onSubmit: submit, - onCancel: close, - ); - }); + onPressed: () => showSetOSPassword(id, false, ffi.dialogManager)); } transferFile(BuildContext context) { diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 35959f407..776aa5455 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -548,19 +548,39 @@ class _RemotePageState extends State { more.add(PopupMenuItem( child: Text(translate('Refresh')), value: 'refresh')); } - more.add(PopupMenuItem( - child: Row( - children: ([ - Text(translate('OS Password')), - TextButton( - style: flatButtonStyle, - onPressed: () { - showSetOSPassword(id, false, gFFI.dialogManager); - }, - child: Icon(Icons.edit, color: MyTheme.accent), - ) - ])), - value: 'enter_os_password')); + if (gFFI.ffiModel.pi.is_headless) { + more.add( + PopupMenuItem( + child: Row( + children: ([ + Text(translate('OS Account')), + TextButton( + style: flatButtonStyle, + onPressed: () { + showSetOSAccount(id, gFFI.dialogManager); + }, + child: Icon(Icons.edit, color: MyTheme.accent), + ) + ])), + value: 'enter_os_account'), + ); + } else { + more.add( + PopupMenuItem( + child: Row( + children: ([ + Text(translate('OS Password')), + TextButton( + style: flatButtonStyle, + onPressed: () { + showSetOSPassword(id, false, gFFI.dialogManager); + }, + child: Icon(Icons.edit, color: MyTheme.accent), + ) + ])), + value: 'enter_os_password'), + ); + } if (!isWebDesktop) { if (perms['keyboard'] != false && perms['clipboard'] != false) { more.add(PopupMenuItem( @@ -657,6 +677,8 @@ class _RemotePageState extends State { } else { showSetOSPassword(id, true, gFFI.dialogManager); } + } else if (value == 'enter_os_account') { + showSetOSAccount(id, gFFI.dialogManager); } else if (value == 'reset_canvas') { gFFI.cursorModel.reset(); } else if (value == 'restart') { @@ -1072,50 +1094,6 @@ void showOptions( }, clickMaskDismiss: true, backDismiss: true); } -void showSetOSPassword( - String id, bool login, OverlayDialogManager dialogManager) async { - final controller = TextEditingController(); - var password = await bind.sessionGetOption(id: id, arg: "os-password") ?? ""; - var autoLogin = await bind.sessionGetOption(id: id, arg: "auto-login") != ""; - controller.text = password; - dialogManager.show((setState, close) { - return CustomAlertDialog( - title: Text(translate('OS Password')), - content: Column(mainAxisSize: MainAxisSize.min, children: [ - PasswordWidget(controller: controller), - CheckboxListTile( - contentPadding: const EdgeInsets.all(0), - dense: true, - controlAffinity: ListTileControlAffinity.leading, - title: Text( - translate('Auto Login'), - ), - value: autoLogin, - onChanged: (v) { - if (v == null) return; - setState(() => autoLogin = v); - }, - ), - ]), - actions: [ - dialogButton('Cancel', onPressed: close, isOutline: true), - dialogButton( - 'OK', - onPressed: () { - var text = controller.text.trim(); - bind.sessionPeerOption(id: id, name: "os-password", value: text); - bind.sessionPeerOption( - id: id, name: "auto-login", value: autoLogin ? 'Y' : ''); - if (text != "" && login) { - bind.sessionInputOsPassword(id: id, value: text); - } - close(); - }, - ), - ]); - }); -} - void sendPrompt(bool isMac, String key) { final old = isMac ? gFFI.inputModel.command : gFFI.inputModel.ctrl; if (isMac) { diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 06bfeec45..77e1b8fbf 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -293,11 +293,11 @@ class FfiModel with ChangeNotifier { wrongPasswordDialog(id, dialogManager, type, title, text); } else if (type == 'input-password') { enterPasswordDialog(id, dialogManager); - } else if (type == 'xsession-login' || type == 'xsession-re-login') { - // to-do - } else if (type == 'xsession-login-password' || - type == 'xsession-login-password') { - // to-do + } else if (type == 'session-login' || type == 'session-re-login') { + enterUserLoginDialog(id, dialogManager); + } else if (type == 'session-login-password' || + type == 'session-login-password') { + enterUserLoginAndPasswordDialog(id, dialogManager); } else if (type == 'restarting') { showMsgBox(id, type, title, text, link, false, dialogManager, hasCancel: false); @@ -1631,8 +1631,14 @@ class FFI { } /// Login with [password], choose if the client should [remember] it. - void login(String id, String password, bool remember) { - bind.sessionLogin(id: id, password: password, remember: remember); + void login(String osUsername, String osPassword, String id, String password, + bool remember) { + bind.sessionLogin( + id: id, + osUsername: osUsername, + osPassword: osPassword, + password: password, + remember: remember); } /// Close the remote session. @@ -1719,6 +1725,7 @@ class PeerInfo { Map platform_additions = {}; bool get is_wayland => platform_additions['is_wayland'] == true; + bool get is_headless => platform_additions['headless'] == true; } const canvasKey = 'canvas'; diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index cd86111ec..42d2d7c09 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -53,6 +53,11 @@ message FileTransfer { bool show_hidden = 2; } +message OSLogin { + string username = 1; + string password = 2; +} + message LoginRequest { string username = 1; bytes password = 2; @@ -66,6 +71,7 @@ message LoginRequest { bool video_ack_required = 9; uint64 session_id = 10; string version = 11; + OSLogin os_login = 12; } message ChatMessage { string text = 1; } diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 960074a8f..4c60e1eb3 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -64,11 +64,15 @@ lazy_static::lazy_static! { pub static ref APP_HOME_DIR: Arc> = Default::default(); } -// #[cfg(any(target_os = "android", target_os = "ios"))] +pub const LINK_DOCS_HOME: &str = "https://rustdesk.com/docs/en/"; +pub const LINK_DOCS_X11_REQUIRED: &str = "https://rustdesk.com/docs/en/manual/linux/#x11-required"; +pub const LINK_HEADLESS_LINUX_SUPPORT: &str = + "https://github.com/rustdesk/rustdesk/wiki/Headless-Linux-Support"; lazy_static::lazy_static! { pub static ref HELPER_URL: HashMap<&'static str, &'static str> = HashMap::from([ - ("rustdesk docs home", "https://rustdesk.com/docs/en/"), - ("rustdesk docs x11-required", "https://rustdesk.com/docs/en/manual/linux/#x11-required"), + ("rustdesk docs home", LINK_DOCS_HOME), + ("rustdesk docs x11-required", LINK_DOCS_X11_REQUIRED), + ("rustdesk x11 headless", LINK_HEADLESS_LINUX_SUPPORT), ]); } @@ -915,7 +919,7 @@ impl PeerConfig { decrypt_vec_or_original(&config.password, PASSWORD_ENC_VERSION); config.password = password; store = store || store2; - for opt in ["rdp_password", "os-password"] { + for opt in ["rdp_password", "os-username", "os-password"] { if let Some(v) = config.options.get_mut(opt) { let (encrypted, _, store2) = decrypt_str_or_original(v, PASSWORD_ENC_VERSION); @@ -939,7 +943,7 @@ impl PeerConfig { let _lock = CONFIG.read().unwrap(); let mut config = self.clone(); config.password = encrypt_vec_or_original(&config.password, PASSWORD_ENC_VERSION); - for opt in ["rdp_password", "os-password"] { + for opt in ["rdp_password", "os-username", "os-password"] { if let Some(v) = config.options.get_mut(opt) { *v = encrypt_str_or_original(v, PASSWORD_ENC_VERSION) } diff --git a/res/pam.d/rustdesk.debian b/res/pam.d/rustdesk.debian new file mode 100644 index 000000000..789ce8f7c --- /dev/null +++ b/res/pam.d/rustdesk.debian @@ -0,0 +1,5 @@ +#%PAM-1.0 +@include common-auth +@include common-account +@include common-session +@include common-password diff --git a/res/pam.d/rustdesk.suse b/res/pam.d/rustdesk.suse new file mode 100644 index 000000000..a7c7836ce --- /dev/null +++ b/res/pam.d/rustdesk.suse @@ -0,0 +1,5 @@ +#%PAM-1.0 +auth include common-auth +account include common-account +session include common-session +password include common-password diff --git a/res/startwm.sh b/res/startwm.sh new file mode 100755 index 000000000..7cdaf07ce --- /dev/null +++ b/res/startwm.sh @@ -0,0 +1,130 @@ +#!/usr/bin/env bash + +# This script is derived from https://github.com/neutrinolabs/xrdp/sesman/startwm.sh. + +# +# This script is an example. You might need to edit this script +# depending on your distro if it doesn't work for you. +# +# Uncomment the following line for debug: +# exec xterm + + +# Execution sequence for interactive login shell - pseudocode +# +# IF /etc/profile is readable THEN +# execute ~/.bash_profile +# END IF +# IF ~/.bash_profile is readable THEN +# execute ~/.bash_profile +# ELSE +# IF ~/.bash_login is readable THEN +# execute ~/.bash_login +# ELSE +# IF ~/.profile is readable THEN +# execute ~/.profile +# END IF +# END IF +# END IF +pre_start() +{ + if [ -r /etc/profile ]; then + . /etc/profile + fi + if [ -r ~/.bash_profile ]; then + . ~/.bash_profile + else + if [ -r ~/.bash_login ]; then + . ~/.bash_login + else + if [ -r ~/.profile ]; then + . ~/.profile + fi + fi + fi + return 0 +} + +# When loging out from the interactive shell, the execution sequence is: +# +# IF ~/.bash_logout exists THEN +# execute ~/.bash_logout +# END IF +post_start() +{ + if [ -r ~/.bash_logout ]; then + . ~/.bash_logout + fi + return 0 +} + +#start the window manager +wm_start() +{ + if [ -r /etc/default/locale ]; then + . /etc/default/locale + export LANG LANGUAGE + fi + + # debian + if [ -r /etc/X11/Xsession ]; then + pre_start + . /etc/X11/Xsession + post_start + exit 0 + fi + + # alpine + # Don't use /etc/X11/xinit/Xsession - it doesn't work + if [ -f /etc/alpine-release ]; then + if [ -f /etc/X11/xinit/xinitrc ]; then + pre_start + /etc/X11/xinit/xinitrc + post_start + else + echo "** xinit package isn't installed" >&2 + exit 1 + fi + fi + + # el + if [ -r /etc/X11/xinit/Xsession ]; then + pre_start + . /etc/X11/xinit/Xsession + post_start + exit 0 + fi + + # suse + if [ -r /etc/X11/xdm/Xsession ]; then + # since the following script run a user login shell, + # do not execute the pseudo login shell scripts + . /etc/X11/xdm/Xsession + exit 0 + elif [ -r /usr/etc/X11/xdm/Xsession ]; then + . /usr/etc/X11/xdm/Xsession + exit 0 + fi + + pre_start + xterm + post_start +} + +#. /etc/environment +#export PATH=$PATH +#export LANG=$LANG + +# change PATH to be what your environment needs usually what is in +# /etc/environment +#PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games" +#export PATH=$PATH + +# for PATH and LANG from /etc/environment +# pam will auto process the environment file if /etc/pam.d/xrdp-sesman +# includes +# auth required pam_env.so readenv=1 + +wm_start + +exit 1 diff --git a/res/xorg.conf b/res/xorg.conf new file mode 100644 index 000000000..fe1539995 --- /dev/null +++ b/res/xorg.conf @@ -0,0 +1,30 @@ +Section "Monitor" + Identifier "Dummy Monitor" + + # Default HorizSync 31.50 - 48.00 kHz + HorizSync 5.0 - 150.0 + # Default VertRefresh 50.00 - 70.00 Hz + VertRefresh 5.0 - 100.0 + + # Taken from https://www.xpra.org/xorg.conf + Modeline "1920x1080" 23.53 1920 1952 2040 2072 1080 1106 1108 1135 + Modeline "1280x720" 27.41 1280 1312 1416 1448 720 737 740 757 +EndSection + +Section "Device" + Identifier "Dummy VideoCard" + Driver "dummy" + # Default VideoRam 4096 + # (1920 * 1080 * 4) / 1024 = 8100 + VideoRam 8100 +EndSection + +Section "Screen" + Identifier "Dummy Screen" + Device "Dummy VideoCard" + Monitor "Dummy Monitor" + SubSection "Display" + Depth 24 + Modes "1920x1080" "1280x720" + EndSubSection +EndSection \ No newline at end of file diff --git a/src/cli.rs b/src/cli.rs index 454eec1ee..13e70987b 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -85,8 +85,8 @@ impl Interface for Session { handle_hash(self.lc.clone(), &pass, hash, self, peer).await; } - async fn handle_login_from_ui(&mut self, password: String, remember: bool, peer: &mut Stream) { - handle_login_from_ui(self.lc.clone(), password, remember, peer).await; + async fn handle_login_from_ui(&mut 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(&mut self, t: TestDelay, peer: &mut Stream) { diff --git a/src/client.rs b/src/client.rs index ae93ccfcf..faa25be95 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1578,7 +1578,12 @@ impl LoginConfigHandler { } /// Create a [`Message`] for login. - fn create_login_msg(&self, password: Vec) -> Message { + fn create_login_msg( + &self, + os_username: String, + os_password: String, + password: Vec, + ) -> Message { #[cfg(any(target_os = "android", target_os = "ios"))] let my_id = Config::get_id_or(crate::common::DEVICE_ID.lock().unwrap().clone()); #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -1591,6 +1596,12 @@ impl LoginConfigHandler { option: self.get_option_message(true).into(), session_id: self.session_id, version: crate::VERSION.to_string(), + os_login: Some(OSLogin { + username: os_username, + password: os_password, + ..Default::default() + }) + .into(), ..Default::default() }; match self.conn_type { @@ -1888,6 +1899,71 @@ fn _input_os_password(p: String, activate: bool, interface: impl Interface) { interface.send(Data::Message(msg_out)); } +#[derive(Copy, Clone)] +struct LoginErrorMsgBox { + msgtype: &'static str, + title: &'static str, + text: &'static str, + link: &'static str, + try_again: bool, +} + +lazy_static::lazy_static! { + static ref LOGIN_ERROR_MAP: Arc> = { + use hbb_common::config::LINK_HEADLESS_LINUX_SUPPORT; + let map = HashMap::from([(crate::server::LOGIN_MSG_DESKTOP_SESSION_NOT_READY, LoginErrorMsgBox{ + msgtype: "session-login", + title: "", + text: "", + link: "", + try_again: true, + }), (crate::server::LOGIN_MSG_DESKTOP_XSESSION_FAILED, LoginErrorMsgBox{ + msgtype: "session-re-login", + title: "xsession_failed_title_tip", + text: "xsession_failed_text_tip", + link: "", + try_again: true, + }), (crate::server::LOGIN_MSG_DESKTOP_SESSION_ANOTHER_USER, LoginErrorMsgBox{ + msgtype: "info-nocancel", + title: "another_user_login_title_tip", + text: "another_user_login_text_tip", + link: "", + try_again: false, + }), (crate::server::LOGIN_MSG_DESKTOP_XORG_NOT_FOUND, LoginErrorMsgBox{ + msgtype: "info-nocancel", + title: "xorg_not_found_title_tip", + text: "xorg_not_found_text_tip", + link: LINK_HEADLESS_LINUX_SUPPORT, + try_again: true, + }), (crate::server::LOGIN_MSG_DESKTOP_NO_DESKTOP, LoginErrorMsgBox{ + msgtype: "info-nocancel", + title: "no_desktop_title_tip", + text: "no_desktop_text_tip", + link: LINK_HEADLESS_LINUX_SUPPORT, + try_again: true, + }), (crate::server::LOGIN_MSG_DESKTOP_SESSION_NOT_READY_PASSWORD_EMPTY, LoginErrorMsgBox{ + msgtype: "session-login-password", + title: "session_not_ready_no_password_title_tip", + text: "session_not_ready_no_password_text_tip", + link: "", + try_again: true, + }), (crate::server::LOGIN_MSG_DESKTOP_SESSION_NOT_READY_PASSWORD_WRONG, LoginErrorMsgBox{ + msgtype: "session-login-re-password", + title: "session_not_ready_wrong_password_title_tip", + text: "session_not_ready_wrong_password_text_tip", + link: "", + try_again: true, + }), (crate::server::LOGIN_MSG_NO_PASSWORD_ACCESS, LoginErrorMsgBox{ + msgtype: "wait-remote-accept-nook", + title: "Prompt", + text: "Please wait for the remote side to accept your session request...", + link: "", + try_again: true, + })]); + Arc::new(map) + }; +} + /// Handle login error. /// Return true if the password is wrong, return false if there's an actual error. pub fn handle_login_error( @@ -1895,19 +1971,27 @@ pub fn handle_login_error( err: &str, interface: &impl Interface, ) -> bool { - if err == "Wrong Password" { + if err == crate::server::LOGIN_MSG_PASSWORD_EMPTY { + lc.write().unwrap().password = Default::default(); + interface.msgbox("input-password", "Password Required", "", ""); + true + } else if err == crate::server::LOGIN_MSG_PASSWORD_WRONG { lc.write().unwrap().password = Default::default(); interface.msgbox("re-input-password", err, "Do you want to enter again?", ""); true - } else if err == "No Password Access" { - lc.write().unwrap().password = Default::default(); - interface.msgbox( - "wait-remote-accept-nook", - "Prompt", - "Please wait for the remote side to accept your session request...", - "", - ); - true + } else if LOGIN_ERROR_MAP.contains_key(err) { + if let Some(msgbox_info) = LOGIN_ERROR_MAP.get(err) { + interface.msgbox( + msgbox_info.msgtype, + msgbox_info.title, + msgbox_info.text, + msgbox_info.link, + ); + msgbox_info.try_again + } else { + // unreachable! + false + } } else { if err.contains(SCRAP_X11_REQUIRED) { interface.msgbox("error", "Login Error", err, SCRAP_X11_REF_URL); @@ -1955,16 +2039,21 @@ pub async fn handle_hash( if password.is_empty() { password = lc.read().unwrap().config.password.clone(); } - if password.is_empty() { + let password = if password.is_empty() { // login without password, the remote side can click accept - send_login(lc.clone(), Vec::new(), peer).await; interface.msgbox("input-password", "Password Required", "", ""); + Vec::new() } else { let mut hasher = Sha256::new(); hasher.update(&password); hasher.update(&hash.challenge); - send_login(lc.clone(), hasher.finalize()[..].into(), peer).await; - } + hasher.finalize()[..].into() + }; + + let os_username = lc.read().unwrap().get_option("os-username"); + let os_password = lc.read().unwrap().get_option("os-password"); + + send_login(lc.clone(), os_username, os_password, password, peer).await; lc.write().unwrap().hash = hash; } @@ -1973,10 +2062,21 @@ pub async fn handle_hash( /// # Arguments /// /// * `lc` - Login config. +/// * `os_username` - OS username. +/// * `os_password` - OS password. /// * `password` - Password. /// * `peer` - [`Stream`] for communicating with peer. -async fn send_login(lc: Arc>, password: Vec, peer: &mut Stream) { - let msg_out = lc.read().unwrap().create_login_msg(password); +async fn send_login( + lc: Arc>, + os_username: String, + os_password: String, + password: Vec, + peer: &mut Stream, +) { + let msg_out = lc + .read() + .unwrap() + .create_login_msg(os_username, os_password, password); allow_err!(peer.send(&msg_out).await); } @@ -1985,25 +2085,40 @@ async fn send_login(lc: Arc>, password: Vec, peer /// # Arguments /// /// * `lc` - Login config. +/// * `os_username` - OS username. +/// * `os_password` - OS password. /// * `password` - Password. /// * `remember` - Whether to remember password. /// * `peer` - [`Stream`] for communicating with peer. pub async fn handle_login_from_ui( lc: Arc>, + os_username: String, + os_password: String, password: String, remember: bool, peer: &mut Stream, ) { - let mut hasher = Sha256::new(); - hasher.update(password); - hasher.update(&lc.read().unwrap().hash.salt); - let res = hasher.finalize(); - lc.write().unwrap().remember = remember; - lc.write().unwrap().password = res[..].into(); + let mut hash_password = if password.is_empty() { + let mut password2 = lc.read().unwrap().password.clone(); + if password2.is_empty() { + password2 = lc.read().unwrap().config.password.clone(); + } + password2 + } else { + let mut hasher = Sha256::new(); + hasher.update(password); + hasher.update(&lc.read().unwrap().hash.salt); + let res = hasher.finalize(); + lc.write().unwrap().remember = remember; + lc.write().unwrap().password = res[..].into(); + res[..].into() + }; let mut hasher2 = Sha256::new(); - hasher2.update(&res[..]); + hasher2.update(&hash_password[..]); hasher2.update(&lc.read().unwrap().hash.challenge); - send_login(lc.clone(), hasher2.finalize()[..].into(), peer).await; + hash_password = hasher2.finalize()[..].to_vec(); + + send_login(lc.clone(), os_username, os_password, hash_password, peer).await; } async fn send_switch_login_request( @@ -2017,7 +2132,7 @@ async fn send_switch_login_request( lr: hbb_common::protobuf::MessageField::some( lc.read() .unwrap() - .create_login_msg(vec![]) + .create_login_msg("".to_owned(), "".to_owned(), vec![]) .login_request() .to_owned(), ), @@ -2038,7 +2153,14 @@ pub trait Interface: Send + Clone + 'static + Sized { self.msgbox("error", "Error", err, ""); } async fn handle_hash(&mut self, pass: &str, hash: Hash, peer: &mut Stream); - async fn handle_login_from_ui(&mut self, password: String, remember: bool, peer: &mut Stream); + async fn handle_login_from_ui( + &mut self, + os_username: String, + os_password: String, + password: String, + remember: bool, + peer: &mut Stream, + ); async fn handle_test_delay(&mut self, t: TestDelay, peer: &mut Stream); fn get_login_config_handler(&self) -> Arc>; @@ -2058,7 +2180,7 @@ pub trait Interface: Send + Clone + 'static + Sized { #[derive(Clone)] pub enum Data { Close, - Login((String, bool)), + Login((String, String, String, bool)), Message(Message), SendFiles((i32, String, String, i32, bool, bool)), RemoveDirAll((i32, String, bool, bool)), diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 0f662e015..bc6ad8a80 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -360,9 +360,9 @@ impl Remote { allow_err!(peer.send(&msg).await); return false; } - Data::Login((password, remember)) => { + Data::Login((os_username, os_password, password, remember)) => { self.handler - .handle_login_from_ui(password, remember, peer) + .handle_login_from_ui(os_username, os_password, password, remember, peer) .await; } Data::ToggleClipboardFile => { @@ -1255,6 +1255,7 @@ impl Remote { }, Some(message::Union::MessageBox(msgbox)) => { let mut link = msgbox.link; + // Links from the remote side must be verified. if !link.starts_with("rustdesk://") { if let Some(v) = hbb_common::config::HELPER_URL.get(&link as &str) { link = v.to_string(); diff --git a/src/core_main.rs b/src/core_main.rs index e5eb64136..05a5ad769 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -224,6 +224,11 @@ pub fn core_main() -> Option> { // call connection manager to establish connections // meanwhile, return true to call flutter window to show control panel crate::ui_interface::start_option_status_sync(); + } else if args[0] == "--cm-no-ui" { + #[cfg(feature = "flutter")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + crate::flutter::connection_manager::start_cm_no_ui(); + return None; } } //_async_logger_holder.map(|x| x.flush()); diff --git a/src/flutter.rs b/src/flutter.rs index b293059d0..6c9ff7f37 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -815,8 +815,20 @@ pub mod connection_manager { } } + #[inline] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + pub fn start_cm_no_ui() { + start_listen_ipc(false); + } + + #[inline] #[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn start_listen_ipc_thread() { + start_listen_ipc(true); + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + fn start_listen_ipc(new_thread: bool) { use crate::ui_cm_interface::{start_ipc, ConnectionManager}; #[cfg(target_os = "linux")] @@ -825,7 +837,11 @@ pub mod connection_manager { let cm = ConnectionManager { ui_handler: FlutterHandler {}, }; - std::thread::spawn(move || start_ipc(cm)); + if new_thread { + std::thread::spawn(move || start_ipc(cm)); + } else { + start_ipc(cm); + } } #[cfg(target_os = "android")] diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 1d965ebc5..6d4e468ab 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -136,9 +136,15 @@ pub fn session_get_option(id: String, arg: String) -> Option { } } -pub fn session_login(id: String, password: String, remember: bool) { +pub fn session_login( + id: String, + os_username: String, + os_password: String, + password: String, + remember: bool, +) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { - session.login(password, remember); + session.login(os_username, os_password, password, remember); } } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 162a48883..f4a8a1204 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -480,8 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("identical_file_tip", ""), ("show_monitors_tip", ""), ("View Mode", ""), - ("enter_rustdesk_passwd_tip", ""), - ("remember_rustdesk_passwd_tip", ""), ("login_linux_tip", ""), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 250f9a5b6..11b3144c7 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -480,9 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("identical_file_tip", "此文件与对方的一致"), ("show_monitors_tip", ""), ("View Mode", "浏览模式"), - ("enter_rustdesk_passwd_tip", "请输入 RustDesk 密码"), - ("remember_rustdesk_passwd_tip", "记住 RustDesk 密码"), - ("login_linux_tip", "登录被控端的 Linux 账户"), - ("login_linux_tooltip_tip", "登录被控端的 Linux 账户,才能启用 X 桌面"), + ("login_linux_tip", "登录被控端的 Linux 账户,才能启用 X 桌面"), + ("verify_rustdesk_password_tip", "验证 RustDesk 密码"), + ("remember_account_tip", "记住此账户"), + ("os_account_desk_tip", "在无显示器的环境下,此账户用于登录被控系统,并启用桌面"), + ("OS Account", "系统账户"), + ("another_user_login_title_tip", "其他用户已登录"), + ("another_user_login_text_tip", "断开"), + ("xorg_not_found_title_tip", "Xorg 未安装"), + ("xorg_not_found_text_tip", "请安装 Xorg"), + ("no_desktop_title_tip", "desktop 未安装"), + ("no_desktop_text_tip", "请安装 desktop"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 2f9e52f6c..acfbed636 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -480,8 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("identical_file_tip", ""), ("show_monitors_tip", ""), ("View Mode", ""), - ("enter_rustdesk_passwd_tip", ""), - ("remember_rustdesk_passwd_tip", ""), ("login_linux_tip", ""), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index a9ca8e8a6..b165540e8 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -480,8 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("identical_file_tip", ""), ("show_monitors_tip", ""), ("View Mode", ""), - ("enter_rustdesk_passwd_tip", ""), - ("remember_rustdesk_passwd_tip", ""), ("login_linux_tip", ""), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index b70e9f872..64ac4d9d4 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -480,9 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("identical_file_tip", "Diese Datei ist identisch mit der Datei der Gegenstelle."), ("show_monitors_tip", "Monitore in der Symbolleiste anzeigen"), ("View Mode", "Ansichtsmodus"), - ("enter_rustdesk_passwd_tip", "RustDesk-Passwort eingeben."), - ("remember_rustdesk_passwd_tip", "RustDesk-Passwort merken."), - ("login_linux_tip", "Anmeldung am entfernten Linux-Konto"), - ("login_linux_tooltip_tip", "Sie müssen sich an einem entfernten Linux-Konto anmelden, um eine X-Desktop-Sitzung zu eröffnen."), + ("login_linux_tip", "Sie müssen sich an einem entfernten Linux-Konto anmelden, um eine X-Desktop-Sitzung zu eröffnen."), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index 85c269d29..e74551046 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -480,9 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("identical_file_tip", "Το αρχείο είναι πανομοιότυπο με αυτό του άλλου υπολογιστή."), ("show_monitors_tip", "Εμφάνιση οθονών στη γραμμή εργαλείων"), ("View Mode", "Λειτουργία προβολής"), - ("enter_rustdesk_passwd_tip", "Εισαγωγή του κωδικού RustDesk."), - ("remember_rustdesk_passwd_tip", "Να θυμάσαι τον κωδικό του RustDesk."), - ("login_linux_tip", "Είσοδος σε απομακρυσμένο λογαριασμό Linux"), - ("login_linux_tooltip_tip", "Απαιτείται είσοδος σε απομακρυσμένο λογαριασμό Linux για την ενεργοποίηση του περιβάλλον εργασίας Χ."), + ("login_linux_tip", "Απαιτείται είσοδος σε απομακρυσμένο λογαριασμό Linux για την ενεργοποίηση του περιβάλλον εργασίας Χ."), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index 8a981564b..b20035dc9 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -54,9 +54,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_address_book_tip", "Oh dear, it appears that there are currently no peers listed in your address book."), ("identical_file_tip", "This file is identical with the peer's one."), ("show_monitors_tip", "Show monitors in toolbar."), - ("enter_rustdesk_passwd_tip", "Enter RustDesk password."), - ("remember_rustdesk_passwd_tip", "Remember RustDesk password."), - ("login_linux_tip", "Login to remote Linux account"), - ("login_linux_tooltip_tip", "You need to login to remote Linux account to enable a X desktop session."), + ("enter_rustdesk_passwd_tip", "Enter RustDesk password"), + ("remember_rustdesk_passwd_tip", "Remember RustDesk password"), + ("login_linux_tip", "You need to login to remote Linux account to enable a X desktop session"), + ("verify_rustdesk_password_tip", "Verify RustDesk password"), + ("remember_account_tip", "Remember this account"), + ("os_account_desk_tip", "This account is used to login the remote OS and enable the desktop session in headless"), + ("another_user_login_title_tip", "Another user already logged in"), + ("another_user_login_text_tip", "Disconnect"), + ("xorg_not_found_title_tip", "Xorg not found"), + ("xorg_not_found_text_tip", "Please install Xorg"), + ("no_desktop_title_tip", "No desktop is avaliable"), + ("no_desktop_text_tip", "Please install GNOME desktop"), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index e430f8f67..435bc6b0a 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -480,8 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("identical_file_tip", ""), ("show_monitors_tip", ""), ("View Mode", ""), - ("enter_rustdesk_passwd_tip", ""), - ("remember_rustdesk_passwd_tip", ""), ("login_linux_tip", ""), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index a85a5e491..6207fac3b 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -480,8 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("identical_file_tip", "Este archivo es idéntico al del par."), ("show_monitors_tip", "Mostrar monitores en la barra de herramientas"), ("View Mode", "Modo Vista"), - ("enter_rustdesk_passwd_tip", "Introduzca la contraseña de RustDesk"), - ("remember_rustdesk_passwd_tip", "Recordar la contraseña de RustDesk"), - ("login_linux_tip", "Iniciar sesión para la cuenta remota de Linux"), + ("login_linux_tip", ""), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index e4d026586..d78cc8b37 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -480,8 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("identical_file_tip", "این فایل با فایل همتا یکسان است."), ("show_monitors_tip", "نمایش مانیتورها در نوار ابزار"), ("View Mode", ""), - ("enter_rustdesk_passwd_tip", ""), - ("remember_rustdesk_passwd_tip", ""), ("login_linux_tip", ""), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 3279b9175..cd2a6c3cc 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -480,9 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("identical_file_tip", "Ce fichier est identique à celui du pair."), ("show_monitors_tip", "Afficher les moniteurs dans la barre d'outils."), ("View Mode", "Mode vue"), - ("enter_rustdesk_passwd_tip", "Saisissez le mot de passe RustDesk."), - ("remember_rustdesk_passwd_tip", "Se rappeler du mot de passe RustDesk."), ("login_linux_tip", "Se connecter au compte Linux distant"), - ("login_linux_tooltip_tip", "Vous devez vous connecter à un compte Linux distant pour activer une session de bureau X."), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 2e6a20b56..e8efc6aa3 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -480,8 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("identical_file_tip", ""), ("show_monitors_tip", ""), ("View Mode", ""), - ("enter_rustdesk_passwd_tip", ""), - ("remember_rustdesk_passwd_tip", ""), ("login_linux_tip", ""), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index cca4ba6c1..97cab077c 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -480,8 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("identical_file_tip", ""), ("show_monitors_tip", ""), ("View Mode", ""), - ("enter_rustdesk_passwd_tip", ""), - ("remember_rustdesk_passwd_tip", ""), ("login_linux_tip", ""), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 3c5973053..1211ceb3c 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -480,8 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("identical_file_tip", "Questo file è identico a quello del peer."), ("show_monitors_tip", "Mostra schermi nella barra degli strumenti"), ("View Mode", "Modalità di visualizzazione"), - ("enter_rustdesk_passwd_tip", "Inserisci la password di RustDesk."), - ("remember_rustdesk_passwd_tip", "Ricorda la passowrd di RustDesk."), - ("login_linux_tip", "Effettua l'accesso sul tuo account Linux"), + ("login_linux_tip", ""), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 29bf3c346..cfe029cc4 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -480,8 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("identical_file_tip", ""), ("show_monitors_tip", ""), ("View Mode", ""), - ("enter_rustdesk_passwd_tip", ""), - ("remember_rustdesk_passwd_tip", ""), ("login_linux_tip", ""), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 929934016..9e1278c6e 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -480,8 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("identical_file_tip", ""), ("show_monitors_tip", ""), ("View Mode", ""), - ("enter_rustdesk_passwd_tip", ""), - ("remember_rustdesk_passwd_tip", ""), ("login_linux_tip", ""), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index a2c7c5983..a86d1c81a 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -480,8 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("identical_file_tip", ""), ("show_monitors_tip", ""), ("View Mode", ""), - ("enter_rustdesk_passwd_tip", ""), - ("remember_rustdesk_passwd_tip", ""), ("login_linux_tip", ""), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index f31135c6c..329c4dd70 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -480,8 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("identical_file_tip", "Dit bestand is identiek aan het bestand van het externe station."), ("show_monitors_tip", "Monitoren weergeven in de werkbalk"), ("View Mode", "Weergave Mode"), - ("enter_rustdesk_passwd_tip", "Geef het RustDesk-wachtwoord op."), - ("remember_rustdesk_passwd_tip", "RustDesk Wachtwoord onthouden."), - ("login_linux_tip", "Je moet inloggen op een Linux Account op afstand om een X desktop sessie te openen."), + ("login_linux_tip", ""), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 852aa520d..afa07046a 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -480,8 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("identical_file_tip", "Ten plik jest identyczny jak plik na drugim komputerze."), ("show_monitors_tip", "Pokaż monitory w zasobniku."), ("View Mode", "Tryb widoku"), - ("enter_rustdesk_passwd_tip", "Podaj hasło RustDesk."), - ("remember_rustdesk_passwd_tip", "Zapamiętaj hasło RustDesk."), ("login_linux_tip", "Zaloguj do zdalnego konta Linux"), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 45226dec6..6a44584a0 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -480,8 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("identical_file_tip", ""), ("show_monitors_tip", ""), ("View Mode", ""), - ("enter_rustdesk_passwd_tip", ""), - ("remember_rustdesk_passwd_tip", ""), ("login_linux_tip", ""), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 8cf8ca6ef..51ef174f5 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -480,8 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("identical_file_tip", ""), ("show_monitors_tip", ""), ("View Mode", ""), - ("enter_rustdesk_passwd_tip", ""), - ("remember_rustdesk_passwd_tip", ""), ("login_linux_tip", ""), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index e81fa0cfd..5de632ec4 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -480,8 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("identical_file_tip", ""), ("show_monitors_tip", ""), ("View Mode", ""), - ("enter_rustdesk_passwd_tip", ""), - ("remember_rustdesk_passwd_tip", ""), ("login_linux_tip", ""), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 12c3dfb34..f813c4b2f 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -480,9 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("identical_file_tip", "Файл идентичен файлу на удалённом узле."), ("show_monitors_tip", "Показывать мониторы на панели инструментов"), ("View Mode", "Режим просмотра"), - ("enter_rustdesk_passwd_tip", "Введите пароль RustDesk"), - ("remember_rustdesk_passwd_tip", "Запомнить пароль RustDesk"), - ("login_linux_tip", "Вход в удалённый аккаунт Linux"), - ("login_linux_tooltip_tip", "Чтобы включить сеанс рабочего стола X, необходимо войти в удалённый аккаунт Linux."), + ("login_linux_tip", "Чтобы включить сеанс рабочего стола X, необходимо войти в удалённый аккаунт Linux."), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 7d28cfb72..3b7f62b74 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -480,8 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("identical_file_tip", ""), ("show_monitors_tip", ""), ("View Mode", ""), - ("enter_rustdesk_passwd_tip", ""), - ("remember_rustdesk_passwd_tip", ""), ("login_linux_tip", ""), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 544e544f7..2f050333c 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -480,8 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("identical_file_tip", ""), ("show_monitors_tip", ""), ("View Mode", ""), - ("enter_rustdesk_passwd_tip", ""), - ("remember_rustdesk_passwd_tip", ""), ("login_linux_tip", ""), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index afd5c2c2d..453534a86 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -480,8 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("identical_file_tip", ""), ("show_monitors_tip", ""), ("View Mode", ""), - ("enter_rustdesk_passwd_tip", ""), - ("remember_rustdesk_passwd_tip", ""), ("login_linux_tip", ""), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 05204f728..e2728caaa 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -480,8 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("identical_file_tip", ""), ("show_monitors_tip", ""), ("View Mode", ""), - ("enter_rustdesk_passwd_tip", ""), - ("remember_rustdesk_passwd_tip", ""), ("login_linux_tip", ""), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 7941c2f58..802eff33d 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -480,8 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("identical_file_tip", ""), ("show_monitors_tip", ""), ("View Mode", ""), - ("enter_rustdesk_passwd_tip", ""), - ("remember_rustdesk_passwd_tip", ""), ("login_linux_tip", ""), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 56508d80a..8941c00d4 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -480,9 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("identical_file_tip", ""), ("show_monitors_tip", ""), ("View Mode", ""), - ("enter_rustdesk_passwd_tip", ""), - ("remember_rustdesk_passwd_tip", ""), ("login_linux_tip", ""), - ("login_linux_tooltip_tip", ""), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 4f178bf1d..1bdece3b7 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -480,8 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("identical_file_tip", ""), ("show_monitors_tip", ""), ("View Mode", ""), - ("enter_rustdesk_passwd_tip", ""), - ("remember_rustdesk_passwd_tip", ""), ("login_linux_tip", ""), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index f5209129b..99977515b 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -480,8 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("identical_file_tip", ""), ("show_monitors_tip", ""), ("View Mode", ""), - ("enter_rustdesk_passwd_tip", ""), - ("remember_rustdesk_passwd_tip", ""), ("login_linux_tip", ""), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 870105b09..bff471b37 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -1,488 +1,495 @@ -lazy_static::lazy_static! { -pub static ref T: std::collections::HashMap<&'static str, &'static str> = - [ - ("Status", "狀態"), - ("Your Desktop", "您的桌面"), - ("desk_tip", "您可以透過此 ID 及密碼存取您的桌面"), - ("Password", "密碼"), - ("Ready", "就緒"), - ("Established", "已建立"), - ("connecting_status", "正在連線到 RustDesk 網路 ..."), - ("Enable Service", "啟用服務"), - ("Start Service", "啟動服務"), - ("Service is running", "服務正在執行"), - ("Service is not running", "服務尚未執行"), - ("not_ready_status", "尚未就緒,請檢查您的網路連線。"), - ("Control Remote Desktop", "控制遠端桌面"), - ("Transfer File", "傳輸檔案"), - ("Connect", "連線"), - ("Recent Sessions", "近期的工作階段"), - ("Address Book", "通訊錄"), - ("Confirmation", "確認"), - ("TCP Tunneling", "TCP 通道"), - ("Remove", "移除"), - ("Refresh random password", "重新產生隨機密碼"), - ("Set your own password", "自行設定密碼"), - ("Enable Keyboard/Mouse", "啟用鍵盤和滑鼠"), - ("Enable Clipboard", "啟用剪貼簿"), - ("Enable File Transfer", "啟用檔案傳輸"), - ("Enable TCP Tunneling", "啟用 TCP 通道"), - ("IP Whitelisting", "IP 白名單"), - ("ID/Relay Server", "ID / 轉送伺服器"), - ("Import Server Config", "匯入伺服器設定"), - ("Export Server Config", "匯出伺服器設定"), - ("Import server configuration successfully", "匯入伺服器設定成功"), - ("Export server configuration successfully", "匯出伺服器設定成功"), - ("Invalid server configuration", "無效的伺服器設定"), - ("Clipboard is empty", "剪貼簿是空的"), - ("Stop service", "停止服務"), - ("Change ID", "更改 ID"), - ("Your new ID", "您的新 ID"), - ("length %min% to %max%", "長度在 %min% 與 %max% 之間"), - ("starts with a letter", "以字母開頭"), - ("allowed characters", "使用允許的字元"), - ("id_change_tip", "僅能使用以下字元:a-z、A-Z、0-9、_ (底線)。首字元必須為 a-z 或 A-Z。長度介於 6 到 16 之間。"), - ("Website", "網站"), - ("About", "關於"), - ("Slogan_tip", ""), - ("Privacy Statement", "隱私權聲明"), - ("Mute", "靜音"), - ("Build Date", "構建日期"), - ("Version", "版本"), - ("Home", "首頁"), - ("Audio Input", "音訊輸入"), - ("Enhancements", "增強功能"), - ("Hardware Codec", "硬體編解碼器"), - ("Adaptive Bitrate", "自適應位元速率"), - ("ID Server", "ID 伺服器"), - ("Relay Server", "轉送伺服器"), - ("API Server", "API 伺服器"), - ("invalid_http", "開頭必須為 http:// 或 https://"), - ("Invalid IP", "IP 無效"), - ("Invalid format", "格式無效"), - ("server_not_support", "伺服器暫不支持"), - ("Not available", "無法使用"), - ("Too frequent", "修改過於頻繁,請稍後再試。"), - ("Cancel", "取消"), - ("Skip", "跳過"), - ("Close", "關閉"), - ("Retry", "重試"), - ("OK", "確定"), - ("Password Required", "需要密碼"), - ("Please enter your password", "請輸入您的密碼"), - ("Remember password", "記住密碼"), - ("Wrong Password", "密碼錯誤"), - ("Do you want to enter again?", "您要重新輸入嗎?"), - ("Connection Error", "連線錯誤"), - ("Error", "錯誤"), - ("Reset by the peer", "對方重設了連線"), - ("Connecting...", "正在連線 ..."), - ("Connection in progress. Please wait.", "正在連線,請稍候。"), - ("Please try 1 minute later", "請於 1 分鐘後再試"), - ("Login Error", "登入錯誤"), - ("Successful", "成功"), - ("Connected, waiting for image...", "已連線,等待畫面傳輸 ..."), - ("Name", "名稱"), - ("Type", "類型"), - ("Modified", "修改時間"), - ("Size", "大小"), - ("Show Hidden Files", "顯示隱藏檔案"), - ("Receive", "接收"), - ("Send", "傳送"), - ("Refresh File", "重新整理檔案"), - ("Local", "本地"), - ("Remote", "遠端"), - ("Remote Computer", "遠端電腦"), - ("Local Computer", "本地電腦"), - ("Confirm Delete", "確認刪除"), - ("Delete", "刪除"), - ("Properties", "屬性"), - ("Multi Select", "多選"), - ("Select All", "全選"), - ("Unselect All", "取消全選"), - ("Empty Directory", "空資料夾"), - ("Not an empty directory", "不是一個空資料夾"), - ("Are you sure you want to delete this file?", "您確定要刪除此檔案嗎?"), - ("Are you sure you want to delete this empty directory?", "您確定要刪除此空資料夾嗎?"), - ("Are you sure you want to delete the file of this directory?", "您確定要刪除此資料夾中的檔案嗎?"), - ("Do this for all conflicts", "套用到其他衝突"), - ("This is irreversible!", "此操作不可逆!"), - ("Deleting", "正在刪除 ..."), - ("files", "檔案"), - ("Waiting", "正在等候 ..."), - ("Finished", "已完成"), - ("Speed", "速度"), - ("Custom Image Quality", "自訂畫面品質"), - ("Privacy mode", "隱私模式"), - ("Block user input", "封鎖使用者輸入"), - ("Unblock user input", "取消封鎖使用者輸入"), - ("Adjust Window", "調整視窗"), - ("Original", "原始"), - ("Shrink", "縮減"), - ("Stretch", "延展"), - ("Scrollbar", "滾動條"), - ("ScrollAuto", "自動滾動"), - ("Good image quality", "最佳化畫面品質"), - ("Balanced", "平衡"), - ("Optimize reaction time", "最佳化反應時間"), - ("Custom", "自訂"), - ("Show remote cursor", "顯示遠端游標"), - ("Show quality monitor", "顯示質量監測"), - ("Disable clipboard", "停用剪貼簿"), - ("Lock after session end", "工作階段結束後鎖定電腦"), - ("Insert", "插入"), - ("Insert Lock", "鎖定遠端電腦"), - ("Refresh", "重新載入"), - ("ID does not exist", "ID 不存在"), - ("Failed to connect to rendezvous server", "無法連線到 rendezvous 伺服器"), - ("Please try later", "請稍候再試"), - ("Remote desktop is offline", "遠端桌面已離線"), - ("Key mismatch", "金鑰不符"), - ("Timeout", "逾時"), - ("Failed to connect to relay server", "無法連線到轉送伺服器"), - ("Failed to connect via rendezvous server", "無法透過 rendezvous 伺服器連線"), - ("Failed to connect via relay server", "無法透過轉送伺服器連線"), - ("Failed to make direct connection to remote desktop", "無法直接連線到遠端桌面"), - ("Set Password", "設定密碼"), - ("OS Password", "作業系統密碼"), - ("install_tip", "UAC 會導致 RustDesk 在某些情況下無法正常以遠端電腦執行。若要避開 UAC,請點擊下方按鈕將 RustDesk 安裝到系統中。"), - ("Click to upgrade", "點擊以升級"), - ("Click to download", "點擊以下載"), - ("Click to update", "點擊以更新"), - ("Configure", "設定"), - ("config_acc", "您需要授予 RustDesk「協助工具」權限才能存取遠端電腦。"), - ("config_screen", "您需要授予 RustDesk「畫面錄製」權限才能存取遠端電腦。"), - ("Installing ...", "正在安裝 ..."), - ("Install", "安裝"), - ("Installation", "安裝"), - ("Installation Path", "安裝路徑"), - ("Create start menu shortcuts", "新增開始功能表捷徑"), - ("Create desktop icon", "新增桌面捷徑"), - ("agreement_tip", "開始安裝即表示接受許可協議"), - ("Accept and Install", "接受並安裝"), - ("End-user license agreement", "使用者授權合約"), - ("Generating ...", "正在產生 ..."), - ("Your installation is lower version.", "您安裝的版本過舊。"), - ("not_close_tcp_tip", "使用通道時請不要關閉此視窗"), - ("Listening ...", "正在等待通道連線 ..."), - ("Remote Host", "遠端主機"), - ("Remote Port", "遠端連線端口"), - ("Action", "操作"), - ("Add", "新增"), - ("Local Port", "本機連線端口"), - ("Local Address", "本機地址"), - ("Change Local Port", "修改本機連線端口"), - ("setup_server_tip", "若您需要更快的連線速度,可以選擇自行建立伺服器"), - ("Too short, at least 6 characters.", "過短,至少需要 6 個字元。"), - ("The confirmation is not identical.", "兩次輸入不相符"), - ("Permissions", "權限"), - ("Accept", "接受"), - ("Dismiss", "關閉"), - ("Disconnect", "中斷連線"), - ("Allow using keyboard and mouse", "允許使用鍵盤和滑鼠"), - ("Allow using clipboard", "允許使用剪貼簿"), - ("Allow hearing sound", "允許分享音訊"), - ("Allow file copy and paste", "允許檔案複製和貼上"), - ("Connected", "已連線"), - ("Direct and encrypted connection", "加密直接連線"), - ("Relayed and encrypted connection", "加密轉送連線"), - ("Direct and unencrypted connection", "未加密直接連線"), - ("Relayed and unencrypted connection", "未加密轉送連線"), - ("Enter Remote ID", "輸入遠端 ID"), - ("Enter your password", "輸入您的密碼"), - ("Logging in...", "正在登入 ..."), - ("Enable RDP session sharing", "啟用 RDP 工作階段共享"), - ("Auto Login", "自動登入 (鎖定將在設定關閉後套用)"), - ("Enable Direct IP Access", "允許 IP 直接存取"), - ("Rename", "重新命名"), - ("Space", "空白"), - ("Create Desktop Shortcut", "新增桌面捷徑"), - ("Change Path", "更改路徑"), - ("Create Folder", "新增資料夾"), - ("Please enter the folder name", "請輸入資料夾名稱"), - ("Fix it", "修復"), - ("Warning", "警告"), - ("Login screen using Wayland is not supported", "不支援使用 Wayland 的登入畫面"), - ("Reboot required", "需要重新啟動"), - ("Unsupported display server", "不支援顯示伺服器"), - ("x11 expected", "預期 x11"), - ("Port", "端口"), - ("Settings", "設定"), - ("Username", "使用者名稱"), - ("Invalid port", "連線端口無效"), - ("Closed manually by the peer", "遠端使用者關閉了工作階段"), - ("Enable remote configuration modification", "允許遠端使用者更改設定"), - ("Run without install", "跳過安裝直接執行"), - ("Connect via relay", "中繼連線"), - ("Always connect via relay", "一律透過轉送連線"), - ("whitelist_tip", "只有白名單中的 IP 可以存取"), - ("Login", "登入"), - ("Verify", "驗證"), - ("Remember me", "記住我"), - ("Trust this device", "信任此裝置"), - ("Verification code", "驗證碼"), - ("verification_tip", "檢測到新裝置登入,已向註冊電子信箱發送了登入驗證碼,請輸入驗證碼以繼續登入"), - ("Logout", "登出"), - ("Tags", "標籤"), - ("Search ID", "搜尋 ID"), - ("whitelist_sep", "使用逗號、分號、空白,或是換行來分隔"), - ("Add ID", "新增 ID"), - ("Add Tag", "新增標籤"), - ("Unselect all tags", "取消選取所有標籤"), - ("Network error", "網路錯誤"), - ("Username missed", "缺少使用者名稱"), - ("Password missed", "缺少密碼"), - ("Wrong credentials", "提供的登入資訊有誤"), - ("Edit Tag", "編輯標籤"), - ("Unremember Password", "忘掉密碼"), - ("Favorites", "我的最愛"), - ("Add to Favorites", "新增到我的最愛"), - ("Remove from Favorites", "從我的最愛中刪除"), - ("Empty", "空空如也"), - ("Invalid folder name", "資料夾名稱無效"), - ("Socks5 Proxy", "Socks5 代理"), - ("Hostname", "主機名稱"), - ("Discovered", "已探索"), - ("install_daemon_tip", "為了能夠開機時自動啟動,請先安裝系統服務。"), - ("Remote ID", "遠端 ID"), - ("Paste", "貼上"), - ("Paste here?", "貼上到這裡?"), - ("Are you sure to close the connection?", "您確定要關閉連線嗎?"), - ("Download new version", "下載新版本"), - ("Touch mode", "觸控模式"), - ("Mouse mode", "滑鼠模式"), - ("One-Finger Tap", "單指輕觸"), - ("Left Mouse", "滑鼠左鍵"), - ("One-Long Tap", "單指長按"), - ("Two-Finger Tap", "雙指輕觸"), - ("Right Mouse", "滑鼠右鍵"), - ("One-Finger Move", "單指移動"), - ("Double Tap & Move", "雙擊並移動"), - ("Mouse Drag", "滑鼠選中拖動"), - ("Three-Finger vertically", "三指垂直滑動"), - ("Mouse Wheel", "滑鼠滾輪"), - ("Two-Finger Move", "雙指移動"), - ("Canvas Move", "移動畫布"), - ("Pinch to Zoom", "雙指縮放"), - ("Canvas Zoom", "縮放畫布"), - ("Reset canvas", "重設畫布"), - ("No permission of file transfer", "沒有檔案傳輸權限"), - ("Note", "備註"), - ("Connection", "連線"), - ("Share Screen", "共享螢幕畫面"), - ("Chat", "聊天訊息"), - ("Total", "總計"), - ("items", "個項目"), - ("Selected", "已選擇"), - ("Screen Capture", "畫面錄製"), - ("Input Control", "輸入控制"), - ("Audio Capture", "音訊錄製"), - ("File Connection", "檔案連線"), - ("Screen Connection", "畫面連線"), - ("Do you accept?", "是否接受?"), - ("Open System Setting", "開啟系統設定"), - ("How to get Android input permission?", "如何獲取 Android 的輸入權限?"), - ("android_input_permission_tip1", "取得輸入權限後可以讓遠端裝置透過滑鼠控制此 Android 裝置"), - ("android_input_permission_tip2", "請在接下來的系統設定頁面中,找到並進入「已安裝的服務」頁面,並將「RustDesk Input」服務開啟"), - ("android_new_connection_tip", "收到新的連線控制請求,對方想要控制您目前的裝置"), - ("android_service_will_start_tip", "開啟畫面錄製權限將自動開啟服務,允許其他裝置向此裝置請求建立連線。"), - ("android_stop_service_tip", "關閉服務將自動關閉所有已建立的連線。"), - ("android_version_audio_tip", "目前的 Android 版本不支援音訊錄製,請升級到 Android 10 或以上版本。"), - ("android_start_service_tip", "點擊「啟動服務」或啟用「螢幕錄製」權限,以啟動螢幕共享服務。"), - ("android_permission_may_not_change_tip", "對於已經建立的連線,權限可能不會立即發生改變,除非重新建立連線。"), - ("Account", "帳號"), - ("Overwrite", "取代"), - ("This file exists, skip or overwrite this file?", "此檔案/資料夾已存在,要略過或是取代此檔案嗎?"), - ("Quit", "退出"), - ("doc_mac_permission", "https://rustdesk.com/docs/zh-tw/manual/mac/#啟用權限"), - ("Help", "幫助"), - ("Failed", "失敗"), - ("Succeeded", "成功"), - ("Someone turns on privacy mode, exit", "其他使用者開啟隱私模式,退出"), - ("Unsupported", "不支援"), - ("Peer denied", "被控端拒絕"), - ("Please install plugins", "請安裝插件"), - ("Peer exit", "被控端退出"), - ("Failed to turn off", "退出失敗"), - ("Turned off", "退出"), - ("In privacy mode", "開啟隱私模式"), - ("Out privacy mode", "退出隱私模式"), - ("Language", "語言"), - ("Keep RustDesk background service", "保持 RustDesk 後台服務"), - ("Ignore Battery Optimizations", "忽略電池最佳化"), - ("android_open_battery_optimizations_tip", "如需關閉此功能,請在接下來的 RustDesk 應用設定頁面中,找到並進入 [電源] 頁面,取消勾選 [不受限制]"), - ("Start on Boot", "開機自動啟動"), - ("Start the screen sharing service on boot, requires special permissions", "開機自動啟動螢幕共享服務,此功能需要一些特殊權限。"), - ("Connection not allowed", "對方不允許連線"), - ("Legacy mode", "傳統模式"), - ("Map mode", "1:1 傳輸模式"), - ("Translate mode", "翻譯模式"), - ("Use permanent password", "使用固定密碼"), - ("Use both passwords", "同時使用兩種密碼"), - ("Set permanent password", "設定固定密碼"), - ("Enable Remote Restart", "啟用遠端重新啟動"), - ("Allow remote restart", "允許遠端重新啟動"), - ("Restart Remote Device", "重新啟動遠端電腦"), - ("Are you sure you want to restart", "確定要重新啟動"), - ("Restarting Remote Device", "正在重新啟動遠端裝置"), - ("remote_restarting_tip", "遠端裝置正在重新啟動,請關閉當前提示框,並在一段時間後使用永久密碼重新連線"), - ("Copied", "已複製"), - ("Exit Fullscreen", "退出全螢幕"), - ("Fullscreen", "全螢幕"), - ("Mobile Actions", "手機操作"), - ("Select Monitor", "選擇顯示器"), - ("Control Actions", "控制操作"), - ("Display Settings", "顯示設定"), - ("Ratio", "比例"), - ("Image Quality", "畫質"), - ("Scroll Style", "滾動樣式"), - ("Show Menubar", "顯示選單欄"), - ("Hide Menubar", "隱藏選單欄"), - ("Direct Connection", "直接連線"), - ("Relay Connection", "中繼連線"), - ("Secure Connection", "安全連線"), - ("Insecure Connection", "非安全連線"), - ("Scale original", "原始尺寸"), - ("Scale adaptive", "適應視窗"), - ("General", "通用"), - ("Security", "安全"), - ("Theme", "主題"), - ("Dark Theme", "黑暗主題"), - ("Light Theme", "明亮主題"), - ("Dark", "黑暗"), - ("Light", "明亮"), - ("Follow System", "跟隨系統"), - ("Enable hardware codec", "使用硬體編解碼器"), - ("Unlock Security Settings", "解鎖安全設定"), - ("Enable Audio", "允許傳輸音訊"), - ("Unlock Network Settings", "解鎖網路設定"), - ("Server", "伺服器"), - ("Direct IP Access", "IP 直接連線"), - ("Proxy", "代理"), - ("Apply", "應用"), - ("Disconnect all devices?", "中斷所有遠端連線?"), - ("Clear", "清空"), - ("Audio Input Device", "音訊輸入裝置"), - ("Deny remote access", "拒絕遠端存取"), - ("Use IP Whitelisting", "只允許白名單上的 IP 進行連線"), - ("Network", "網路"), - ("Enable RDP", "允許 RDP 訪問"), - ("Pin menubar", "固定選單欄"), - ("Unpin menubar", "取消固定選單欄"), - ("Recording", "錄製"), - ("Directory", "路徑"), - ("Automatically record incoming sessions", "自動錄製連入的工作階段"), - ("Change", "變更"), - ("Start session recording", "開始錄影"), - ("Stop session recording", "停止錄影"), - ("Enable Recording Session", "啟用錄製工作階段"), - ("Allow recording session", "允許錄製工作階段"), - ("Enable LAN Discovery", "允許區域網路探索"), - ("Deny LAN Discovery", "拒絕區域網路探索"), - ("Write a message", "輸入聊天訊息"), - ("Prompt", "提示"), - ("Please wait for confirmation of UAC...", "請等待對方確認 UAC ..."), - ("elevated_foreground_window_tip", "目前的遠端桌面視窗需要更高的權限才能繼續操作,暫時無法使用滑鼠、鍵盤,可以請求對方最小化目前視窗,或者在連線管理視窗點擊提升權限。為了避免這個問題,建議在遠端裝置上安裝本軟體。"), - ("Disconnected", "斷開連線"), - ("Other", "其他"), - ("Confirm before closing multiple tabs", "關閉多個分頁前詢問我"), - ("Keyboard Settings", "鍵盤設定"), - ("Full Access", "完全訪問"), - ("Screen Share", "僅分享螢幕畫面"), - ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland 需要 Ubuntu 21.04 或更高版本。"), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland 需要更高版本的 Linux 發行版。請嘗試使用 X11 桌面或更改您的作業系統。"), - ("JumpLink", "查看"), - ("Please Select the screen to be shared(Operate on the peer side).", "請選擇要分享的螢幕畫面(在對端操作)。"), - ("Show RustDesk", "顯示 RustDesk"), - ("This PC", "此電腦"), - ("or", "或"), - ("Continue with", "繼續"), - ("Elevate", "提升權限"), - ("Zoom cursor", "縮放游標"), - ("Accept sessions via password", "只允許透過輸入密碼進行連線"), - ("Accept sessions via click", "只允許透過點擊接受進行連線"), - ("Accept sessions via both", "允許輸入密碼或點擊接受進行連線"), - ("Please wait for the remote side to accept your session request...", "請等待對方接受您的連線請求 ..."), - ("One-time Password", "一次性密碼"), - ("Use one-time password", "使用一次性密碼"), - ("One-time password length", "一次性密碼長度"), - ("Request access to your device", "請求訪問您的裝置"), - ("Hide connection management window", "隱藏連線管理視窗"), - ("hide_cm_tip", "在只允許密碼連線並且只用固定密碼的情況下才允許隱藏"), - ("wayland_experiment_tip", "Wayland 支援處於實驗階段,如果您需要使用無人值守訪問,請使用 X11。"), - ("Right click to select tabs", "右鍵選擇分頁"), - ("Skipped", "已略過"), - ("Add to Address Book", "新增到通訊錄"), - ("Group", "群組"), - ("Search", "搜尋"), - ("Closed manually by web console", "被 Web 控制台手動關閉"), - ("Local keyboard type", "本地鍵盤類型"), - ("Select local keyboard type", "請選擇本地鍵盤類型"), - ("software_render_tip", "如果您使用 NVIDIA 顯示卡,並且遠端視窗在建立連線後會立刻關閉,那麼請安裝 nouveau 顯示卡驅動程式並且選擇使用軟體渲染可能會有幫助。重新啟動軟體後生效。"), - ("Always use software rendering", "使用軟體渲染"), - ("config_input", "為了能夠透過鍵盤控制遠端桌面,請給予 RustDesk \"輸入監控\" 權限。"), - ("config_microphone", "為了支援透過麥克風進行音訊傳輸,請給予 RustDesk \"錄音\"權限。"), - ("request_elevation_tip", "如果遠端使用者可以操作電腦,也可以請求提升權限。"), - ("Wait", "等待"), - ("Elevation Error", "權限提升失敗"), - ("Ask the remote user for authentication", "請求遠端使用者進行身分驗證"), - ("Choose this if the remote account is administrator", "當遠端使用者帳戶是管理員時,請選擇此選項"), - ("Transmit the username and password of administrator", "發送管理員的使用者名稱和密碼"), - ("still_click_uac_tip", "依然需要遠端使用者在 UAC 視窗點擊確認。"), - ("Request Elevation", "請求權限提升"), - ("wait_accept_uac_tip", "請等待遠端使用者確認 UAC 對話框。"), - ("Elevate successfully", "權限提升成功"), - ("uppercase", "大寫字母"), - ("lowercase", "小寫字母"), - ("digit", "數字"), - ("special character", "特殊字元"), - ("length>=8", "長度不能小於 8"), - ("Weak", "弱"), - ("Medium", "中"), - ("Strong", "強"), - ("Switch Sides", "反轉存取方向"), - ("Please confirm if you want to share your desktop?", "請確認是否要讓對方存取您的桌面?"), - ("Display", "顯示"), - ("Default View Style", "預設顯示方式"), - ("Default Scroll Style", "預設滾動方式"), - ("Default Image Quality", "預設圖像質量"), - ("Default Codec", "預設編解碼器"), - ("Bitrate", "位元速率"), - ("FPS", "幀率"), - ("Auto", "自動"), - ("Other Default Options", "其他預設選項"), - ("Voice call", "語音通話"), - ("Text chat", "文字聊天"), - ("Stop voice call", "停止語音通話"), - ("relay_hint_tip", "可能無法直接連線,可以嘗試中繼連線。\n另外,如果想要直接使用中繼連線,可以在 ID 後面新增/r,或者在卡片選項裡選擇強制走中繼連線。"), - ("Reconnect", "重新連線"), - ("Codec", "編解碼器"), - ("Resolution", "解析度"), - ("No transfers in progress", "沒有正在進行的傳輸"), - ("Set one-time password length", "設定一次性密碼長度"), - ("idd_driver_tip", "安裝虛擬顯示器驅動程式,以便在沒有連接顯示器的情況下啟動虛擬顯示器進行控制。"), - ("confirm_idd_driver_tip", "安裝虛擬顯示器驅動程式的選項已勾選。請注意,測試證書將被安裝以信任虛擬顯示器驅動。測試證書僅會用於信任 RustDesk 的驅動程式。"), - ("RDP Settings", "RDP 設定"), - ("Sort by", "排序方式"), - ("New Connection", "新連線"), - ("Restore", "還原"), - ("Minimize", "最小化"), - ("Maximize", "最大化"), - ("Your Device", "您的裝置"), - ("empty_recent_tip", "空空如也"), - ("empty_favorite_tip", "空空如也"), - ("empty_lan_tip", "空空如也"), - ("empty_address_book_tip", "空空如也"), - ("eg: admin", "例如:admin"), - ("Empty Username", "空使用者帳號"), - ("Empty Password", "空密碼"), - ("Me", "我"), - ("identical_file_tip", "此檔案與對方的檔案一致"), - ("show_monitors_tip", "在工具列中顯示顯示器"), - ("View Mode", "瀏覽模式"), - ("enter_rustdesk_passwd_tip", "輸入 RustDesk 密碼"), - ("remember_rustdesk_passwd_tip", "記住 RustDesk 密碼"), - ("login_linux_tip", "登入到遠端 Linux 使用者帳戶"), - ("login_linux_tooltip_tip", "需要登入到遠端 Linux 使用者帳戶才能啟用 X 介面。"), - ].iter().cloned().collect(); -} +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "狀態"), + ("Your Desktop", "您的桌面"), + ("desk_tip", "您可以透過此 ID 及密碼存取您的桌面"), + ("Password", "密碼"), + ("Ready", "就緒"), + ("Established", "已建立"), + ("connecting_status", "正在連線到 RustDesk 網路 ..."), + ("Enable Service", "啟用服務"), + ("Start Service", "啟動服務"), + ("Service is running", "服務正在執行"), + ("Service is not running", "服務尚未執行"), + ("not_ready_status", "尚未就緒,請檢查您的網路連線。"), + ("Control Remote Desktop", "控制遠端桌面"), + ("Transfer File", "傳輸檔案"), + ("Connect", "連線"), + ("Recent Sessions", "近期的工作階段"), + ("Address Book", "通訊錄"), + ("Confirmation", "確認"), + ("TCP Tunneling", "TCP 通道"), + ("Remove", "移除"), + ("Refresh random password", "重新產生隨機密碼"), + ("Set your own password", "自行設定密碼"), + ("Enable Keyboard/Mouse", "啟用鍵盤和滑鼠"), + ("Enable Clipboard", "啟用剪貼簿"), + ("Enable File Transfer", "啟用檔案傳輸"), + ("Enable TCP Tunneling", "啟用 TCP 通道"), + ("IP Whitelisting", "IP 白名單"), + ("ID/Relay Server", "ID / 轉送伺服器"), + ("Import Server Config", "匯入伺服器設定"), + ("Export Server Config", "匯出伺服器設定"), + ("Import server configuration successfully", "匯入伺服器設定成功"), + ("Export server configuration successfully", "匯出伺服器設定成功"), + ("Invalid server configuration", "無效的伺服器設定"), + ("Clipboard is empty", "剪貼簿是空的"), + ("Stop service", "停止服務"), + ("Change ID", "更改 ID"), + ("Your new ID", "您的新 ID"), + ("length %min% to %max%", "長度在 %min% 與 %max% 之間"), + ("starts with a letter", "以字母開頭"), + ("allowed characters", "使用允許的字元"), + ("id_change_tip", "僅能使用以下字元:a-z、A-Z、0-9、_ (底線)。首字元必須為 a-z 或 A-Z。長度介於 6 到 16 之間。"), + ("Website", "網站"), + ("About", "關於"), + ("Slogan_tip", ""), + ("Privacy Statement", "隱私權聲明"), + ("Mute", "靜音"), + ("Build Date", "構建日期"), + ("Version", "版本"), + ("Home", "首頁"), + ("Audio Input", "音訊輸入"), + ("Enhancements", "增強功能"), + ("Hardware Codec", "硬體編解碼器"), + ("Adaptive Bitrate", "自適應位元速率"), + ("ID Server", "ID 伺服器"), + ("Relay Server", "轉送伺服器"), + ("API Server", "API 伺服器"), + ("invalid_http", "開頭必須為 http:// 或 https://"), + ("Invalid IP", "IP 無效"), + ("Invalid format", "格式無效"), + ("server_not_support", "伺服器暫不支持"), + ("Not available", "無法使用"), + ("Too frequent", "修改過於頻繁,請稍後再試。"), + ("Cancel", "取消"), + ("Skip", "跳過"), + ("Close", "關閉"), + ("Retry", "重試"), + ("OK", "確定"), + ("Password Required", "需要密碼"), + ("Please enter your password", "請輸入您的密碼"), + ("Remember password", "記住密碼"), + ("Wrong Password", "密碼錯誤"), + ("Do you want to enter again?", "您要重新輸入嗎?"), + ("Connection Error", "連線錯誤"), + ("Error", "錯誤"), + ("Reset by the peer", "對方重設了連線"), + ("Connecting...", "正在連線 ..."), + ("Connection in progress. Please wait.", "正在連線,請稍候。"), + ("Please try 1 minute later", "請於 1 分鐘後再試"), + ("Login Error", "登入錯誤"), + ("Successful", "成功"), + ("Connected, waiting for image...", "已連線,等待畫面傳輸 ..."), + ("Name", "名稱"), + ("Type", "類型"), + ("Modified", "修改時間"), + ("Size", "大小"), + ("Show Hidden Files", "顯示隱藏檔案"), + ("Receive", "接收"), + ("Send", "傳送"), + ("Refresh File", "重新整理檔案"), + ("Local", "本地"), + ("Remote", "遠端"), + ("Remote Computer", "遠端電腦"), + ("Local Computer", "本地電腦"), + ("Confirm Delete", "確認刪除"), + ("Delete", "刪除"), + ("Properties", "屬性"), + ("Multi Select", "多選"), + ("Select All", "全選"), + ("Unselect All", "取消全選"), + ("Empty Directory", "空資料夾"), + ("Not an empty directory", "不是一個空資料夾"), + ("Are you sure you want to delete this file?", "您確定要刪除此檔案嗎?"), + ("Are you sure you want to delete this empty directory?", "您確定要刪除此空資料夾嗎?"), + ("Are you sure you want to delete the file of this directory?", "您確定要刪除此資料夾中的檔案嗎?"), + ("Do this for all conflicts", "套用到其他衝突"), + ("This is irreversible!", "此操作不可逆!"), + ("Deleting", "正在刪除 ..."), + ("files", "檔案"), + ("Waiting", "正在等候 ..."), + ("Finished", "已完成"), + ("Speed", "速度"), + ("Custom Image Quality", "自訂畫面品質"), + ("Privacy mode", "隱私模式"), + ("Block user input", "封鎖使用者輸入"), + ("Unblock user input", "取消封鎖使用者輸入"), + ("Adjust Window", "調整視窗"), + ("Original", "原始"), + ("Shrink", "縮減"), + ("Stretch", "延展"), + ("Scrollbar", "滾動條"), + ("ScrollAuto", "自動滾動"), + ("Good image quality", "最佳化畫面品質"), + ("Balanced", "平衡"), + ("Optimize reaction time", "最佳化反應時間"), + ("Custom", "自訂"), + ("Show remote cursor", "顯示遠端游標"), + ("Show quality monitor", "顯示質量監測"), + ("Disable clipboard", "停用剪貼簿"), + ("Lock after session end", "工作階段結束後鎖定電腦"), + ("Insert", "插入"), + ("Insert Lock", "鎖定遠端電腦"), + ("Refresh", "重新載入"), + ("ID does not exist", "ID 不存在"), + ("Failed to connect to rendezvous server", "無法連線到 rendezvous 伺服器"), + ("Please try later", "請稍候再試"), + ("Remote desktop is offline", "遠端桌面已離線"), + ("Key mismatch", "金鑰不符"), + ("Timeout", "逾時"), + ("Failed to connect to relay server", "無法連線到轉送伺服器"), + ("Failed to connect via rendezvous server", "無法透過 rendezvous 伺服器連線"), + ("Failed to connect via relay server", "無法透過轉送伺服器連線"), + ("Failed to make direct connection to remote desktop", "無法直接連線到遠端桌面"), + ("Set Password", "設定密碼"), + ("OS Password", "作業系統密碼"), + ("install_tip", "UAC 會導致 RustDesk 在某些情況下無法正常以遠端電腦執行。若要避開 UAC,請點擊下方按鈕將 RustDesk 安裝到系統中。"), + ("Click to upgrade", "點擊以升級"), + ("Click to download", "點擊以下載"), + ("Click to update", "點擊以更新"), + ("Configure", "設定"), + ("config_acc", "您需要授予 RustDesk「協助工具」權限才能存取遠端電腦。"), + ("config_screen", "您需要授予 RustDesk「畫面錄製」權限才能存取遠端電腦。"), + ("Installing ...", "正在安裝 ..."), + ("Install", "安裝"), + ("Installation", "安裝"), + ("Installation Path", "安裝路徑"), + ("Create start menu shortcuts", "新增開始功能表捷徑"), + ("Create desktop icon", "新增桌面捷徑"), + ("agreement_tip", "開始安裝即表示接受許可協議"), + ("Accept and Install", "接受並安裝"), + ("End-user license agreement", "使用者授權合約"), + ("Generating ...", "正在產生 ..."), + ("Your installation is lower version.", "您安裝的版本過舊。"), + ("not_close_tcp_tip", "使用通道時請不要關閉此視窗"), + ("Listening ...", "正在等待通道連線 ..."), + ("Remote Host", "遠端主機"), + ("Remote Port", "遠端連線端口"), + ("Action", "操作"), + ("Add", "新增"), + ("Local Port", "本機連線端口"), + ("Local Address", "本機地址"), + ("Change Local Port", "修改本機連線端口"), + ("setup_server_tip", "若您需要更快的連線速度,可以選擇自行建立伺服器"), + ("Too short, at least 6 characters.", "過短,至少需要 6 個字元。"), + ("The confirmation is not identical.", "兩次輸入不相符"), + ("Permissions", "權限"), + ("Accept", "接受"), + ("Dismiss", "關閉"), + ("Disconnect", "中斷連線"), + ("Allow using keyboard and mouse", "允許使用鍵盤和滑鼠"), + ("Allow using clipboard", "允許使用剪貼簿"), + ("Allow hearing sound", "允許分享音訊"), + ("Allow file copy and paste", "允許檔案複製和貼上"), + ("Connected", "已連線"), + ("Direct and encrypted connection", "加密直接連線"), + ("Relayed and encrypted connection", "加密轉送連線"), + ("Direct and unencrypted connection", "未加密直接連線"), + ("Relayed and unencrypted connection", "未加密轉送連線"), + ("Enter Remote ID", "輸入遠端 ID"), + ("Enter your password", "輸入您的密碼"), + ("Logging in...", "正在登入 ..."), + ("Enable RDP session sharing", "啟用 RDP 工作階段共享"), + ("Auto Login", "自動登入 (鎖定將在設定關閉後套用)"), + ("Enable Direct IP Access", "允許 IP 直接存取"), + ("Rename", "重新命名"), + ("Space", "空白"), + ("Create Desktop Shortcut", "新增桌面捷徑"), + ("Change Path", "更改路徑"), + ("Create Folder", "新增資料夾"), + ("Please enter the folder name", "請輸入資料夾名稱"), + ("Fix it", "修復"), + ("Warning", "警告"), + ("Login screen using Wayland is not supported", "不支援使用 Wayland 的登入畫面"), + ("Reboot required", "需要重新啟動"), + ("Unsupported display server", "不支援顯示伺服器"), + ("x11 expected", "預期 x11"), + ("Port", "端口"), + ("Settings", "設定"), + ("Username", "使用者名稱"), + ("Invalid port", "連線端口無效"), + ("Closed manually by the peer", "遠端使用者關閉了工作階段"), + ("Enable remote configuration modification", "允許遠端使用者更改設定"), + ("Run without install", "跳過安裝直接執行"), + ("Connect via relay", "中繼連線"), + ("Always connect via relay", "一律透過轉送連線"), + ("whitelist_tip", "只有白名單中的 IP 可以存取"), + ("Login", "登入"), + ("Verify", "驗證"), + ("Remember me", "記住我"), + ("Trust this device", "信任此裝置"), + ("Verification code", "驗證碼"), + ("verification_tip", "檢測到新裝置登入,已向註冊電子信箱發送了登入驗證碼,請輸入驗證碼以繼續登入"), + ("Logout", "登出"), + ("Tags", "標籤"), + ("Search ID", "搜尋 ID"), + ("whitelist_sep", "使用逗號、分號、空白,或是換行來分隔"), + ("Add ID", "新增 ID"), + ("Add Tag", "新增標籤"), + ("Unselect all tags", "取消選取所有標籤"), + ("Network error", "網路錯誤"), + ("Username missed", "缺少使用者名稱"), + ("Password missed", "缺少密碼"), + ("Wrong credentials", "提供的登入資訊有誤"), + ("Edit Tag", "編輯標籤"), + ("Unremember Password", "忘掉密碼"), + ("Favorites", "我的最愛"), + ("Add to Favorites", "新增到我的最愛"), + ("Remove from Favorites", "從我的最愛中刪除"), + ("Empty", "空空如也"), + ("Invalid folder name", "資料夾名稱無效"), + ("Socks5 Proxy", "Socks5 代理"), + ("Hostname", "主機名稱"), + ("Discovered", "已探索"), + ("install_daemon_tip", "為了能夠開機時自動啟動,請先安裝系統服務。"), + ("Remote ID", "遠端 ID"), + ("Paste", "貼上"), + ("Paste here?", "貼上到這裡?"), + ("Are you sure to close the connection?", "您確定要關閉連線嗎?"), + ("Download new version", "下載新版本"), + ("Touch mode", "觸控模式"), + ("Mouse mode", "滑鼠模式"), + ("One-Finger Tap", "單指輕觸"), + ("Left Mouse", "滑鼠左鍵"), + ("One-Long Tap", "單指長按"), + ("Two-Finger Tap", "雙指輕觸"), + ("Right Mouse", "滑鼠右鍵"), + ("One-Finger Move", "單指移動"), + ("Double Tap & Move", "雙擊並移動"), + ("Mouse Drag", "滑鼠選中拖動"), + ("Three-Finger vertically", "三指垂直滑動"), + ("Mouse Wheel", "滑鼠滾輪"), + ("Two-Finger Move", "雙指移動"), + ("Canvas Move", "移動畫布"), + ("Pinch to Zoom", "雙指縮放"), + ("Canvas Zoom", "縮放畫布"), + ("Reset canvas", "重設畫布"), + ("No permission of file transfer", "沒有檔案傳輸權限"), + ("Note", "備註"), + ("Connection", "連線"), + ("Share Screen", "共享螢幕畫面"), + ("Chat", "聊天訊息"), + ("Total", "總計"), + ("items", "個項目"), + ("Selected", "已選擇"), + ("Screen Capture", "畫面錄製"), + ("Input Control", "輸入控制"), + ("Audio Capture", "音訊錄製"), + ("File Connection", "檔案連線"), + ("Screen Connection", "畫面連線"), + ("Do you accept?", "是否接受?"), + ("Open System Setting", "開啟系統設定"), + ("How to get Android input permission?", "如何獲取 Android 的輸入權限?"), + ("android_input_permission_tip1", "取得輸入權限後可以讓遠端裝置透過滑鼠控制此 Android 裝置"), + ("android_input_permission_tip2", "請在接下來的系統設定頁面中,找到並進入「已安裝的服務」頁面,並將「RustDesk Input」服務開啟"), + ("android_new_connection_tip", "收到新的連線控制請求,對方想要控制您目前的裝置"), + ("android_service_will_start_tip", "開啟畫面錄製權限將自動開啟服務,允許其他裝置向此裝置請求建立連線。"), + ("android_stop_service_tip", "關閉服務將自動關閉所有已建立的連線。"), + ("android_version_audio_tip", "目前的 Android 版本不支援音訊錄製,請升級到 Android 10 或以上版本。"), + ("android_start_service_tip", "點擊「啟動服務」或啟用「螢幕錄製」權限,以啟動螢幕共享服務。"), + ("android_permission_may_not_change_tip", "對於已經建立的連線,權限可能不會立即發生改變,除非重新建立連線。"), + ("Account", "帳號"), + ("Overwrite", "取代"), + ("This file exists, skip or overwrite this file?", "此檔案/資料夾已存在,要略過或是取代此檔案嗎?"), + ("Quit", "退出"), + ("doc_mac_permission", "https://rustdesk.com/docs/zh-tw/manual/mac/#啟用權限"), + ("Help", "幫助"), + ("Failed", "失敗"), + ("Succeeded", "成功"), + ("Someone turns on privacy mode, exit", "其他使用者開啟隱私模式,退出"), + ("Unsupported", "不支援"), + ("Peer denied", "被控端拒絕"), + ("Please install plugins", "請安裝插件"), + ("Peer exit", "被控端退出"), + ("Failed to turn off", "退出失敗"), + ("Turned off", "退出"), + ("In privacy mode", "開啟隱私模式"), + ("Out privacy mode", "退出隱私模式"), + ("Language", "語言"), + ("Keep RustDesk background service", "保持 RustDesk 後台服務"), + ("Ignore Battery Optimizations", "忽略電池最佳化"), + ("android_open_battery_optimizations_tip", "如需關閉此功能,請在接下來的 RustDesk 應用設定頁面中,找到並進入 [電源] 頁面,取消勾選 [不受限制]"), + ("Start on Boot", "開機自動啟動"), + ("Start the screen sharing service on boot, requires special permissions", "開機自動啟動螢幕共享服務,此功能需要一些特殊權限。"), + ("Connection not allowed", "對方不允許連線"), + ("Legacy mode", "傳統模式"), + ("Map mode", "1:1 傳輸模式"), + ("Translate mode", "翻譯模式"), + ("Use permanent password", "使用固定密碼"), + ("Use both passwords", "同時使用兩種密碼"), + ("Set permanent password", "設定固定密碼"), + ("Enable Remote Restart", "啟用遠端重新啟動"), + ("Allow remote restart", "允許遠端重新啟動"), + ("Restart Remote Device", "重新啟動遠端電腦"), + ("Are you sure you want to restart", "確定要重新啟動"), + ("Restarting Remote Device", "正在重新啟動遠端裝置"), + ("remote_restarting_tip", "遠端裝置正在重新啟動,請關閉當前提示框,並在一段時間後使用永久密碼重新連線"), + ("Copied", "已複製"), + ("Exit Fullscreen", "退出全螢幕"), + ("Fullscreen", "全螢幕"), + ("Mobile Actions", "手機操作"), + ("Select Monitor", "選擇顯示器"), + ("Control Actions", "控制操作"), + ("Display Settings", "顯示設定"), + ("Ratio", "比例"), + ("Image Quality", "畫質"), + ("Scroll Style", "滾動樣式"), + ("Show Menubar", "顯示選單欄"), + ("Hide Menubar", "隱藏選單欄"), + ("Direct Connection", "直接連線"), + ("Relay Connection", "中繼連線"), + ("Secure Connection", "安全連線"), + ("Insecure Connection", "非安全連線"), + ("Scale original", "原始尺寸"), + ("Scale adaptive", "適應視窗"), + ("General", "通用"), + ("Security", "安全"), + ("Theme", "主題"), + ("Dark Theme", "黑暗主題"), + ("Light Theme", "明亮主題"), + ("Dark", "黑暗"), + ("Light", "明亮"), + ("Follow System", "跟隨系統"), + ("Enable hardware codec", "使用硬體編解碼器"), + ("Unlock Security Settings", "解鎖安全設定"), + ("Enable Audio", "允許傳輸音訊"), + ("Unlock Network Settings", "解鎖網路設定"), + ("Server", "伺服器"), + ("Direct IP Access", "IP 直接連線"), + ("Proxy", "代理"), + ("Apply", "應用"), + ("Disconnect all devices?", "中斷所有遠端連線?"), + ("Clear", "清空"), + ("Audio Input Device", "音訊輸入裝置"), + ("Deny remote access", "拒絕遠端存取"), + ("Use IP Whitelisting", "只允許白名單上的 IP 進行連線"), + ("Network", "網路"), + ("Enable RDP", "允許 RDP 訪問"), + ("Pin menubar", "固定選單欄"), + ("Unpin menubar", "取消固定選單欄"), + ("Recording", "錄製"), + ("Directory", "路徑"), + ("Automatically record incoming sessions", "自動錄製連入的工作階段"), + ("Change", "變更"), + ("Start session recording", "開始錄影"), + ("Stop session recording", "停止錄影"), + ("Enable Recording Session", "啟用錄製工作階段"), + ("Allow recording session", "允許錄製工作階段"), + ("Enable LAN Discovery", "允許區域網路探索"), + ("Deny LAN Discovery", "拒絕區域網路探索"), + ("Write a message", "輸入聊天訊息"), + ("Prompt", "提示"), + ("Please wait for confirmation of UAC...", "請等待對方確認 UAC ..."), + ("elevated_foreground_window_tip", "目前的遠端桌面視窗需要更高的權限才能繼續操作,暫時無法使用滑鼠、鍵盤,可以請求對方最小化目前視窗,或者在連線管理視窗點擊提升權限。為了避免這個問題,建議在遠端裝置上安裝本軟體。"), + ("Disconnected", "斷開連線"), + ("Other", "其他"), + ("Confirm before closing multiple tabs", "關閉多個分頁前詢問我"), + ("Keyboard Settings", "鍵盤設定"), + ("Full Access", "完全訪問"), + ("Screen Share", "僅分享螢幕畫面"), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland 需要 Ubuntu 21.04 或更高版本。"), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland 需要更高版本的 Linux 發行版。請嘗試使用 X11 桌面或更改您的作業系統。"), + ("JumpLink", "查看"), + ("Please Select the screen to be shared(Operate on the peer side).", "請選擇要分享的螢幕畫面(在對端操作)。"), + ("Show RustDesk", "顯示 RustDesk"), + ("This PC", "此電腦"), + ("or", "或"), + ("Continue with", "繼續"), + ("Elevate", "提升權限"), + ("Zoom cursor", "縮放游標"), + ("Accept sessions via password", "只允許透過輸入密碼進行連線"), + ("Accept sessions via click", "只允許透過點擊接受進行連線"), + ("Accept sessions via both", "允許輸入密碼或點擊接受進行連線"), + ("Please wait for the remote side to accept your session request...", "請等待對方接受您的連線請求 ..."), + ("One-time Password", "一次性密碼"), + ("Use one-time password", "使用一次性密碼"), + ("One-time password length", "一次性密碼長度"), + ("Request access to your device", "請求訪問您的裝置"), + ("Hide connection management window", "隱藏連線管理視窗"), + ("hide_cm_tip", "在只允許密碼連線並且只用固定密碼的情況下才允許隱藏"), + ("wayland_experiment_tip", "Wayland 支援處於實驗階段,如果您需要使用無人值守訪問,請使用 X11。"), + ("Right click to select tabs", "右鍵選擇分頁"), + ("Skipped", "已略過"), + ("Add to Address Book", "新增到通訊錄"), + ("Group", "群組"), + ("Search", "搜尋"), + ("Closed manually by web console", "被 Web 控制台手動關閉"), + ("Local keyboard type", "本地鍵盤類型"), + ("Select local keyboard type", "請選擇本地鍵盤類型"), + ("software_render_tip", "如果您使用 NVIDIA 顯示卡,並且遠端視窗在建立連線後會立刻關閉,那麼請安裝 nouveau 顯示卡驅動程式並且選擇使用軟體渲染可能會有幫助。重新啟動軟體後生效。"), + ("Always use software rendering", "使用軟體渲染"), + ("config_input", "為了能夠透過鍵盤控制遠端桌面,請給予 RustDesk \"輸入監控\" 權限。"), + ("config_microphone", "為了支援透過麥克風進行音訊傳輸,請給予 RustDesk \"錄音\"權限。"), + ("request_elevation_tip", "如果遠端使用者可以操作電腦,也可以請求提升權限。"), + ("Wait", "等待"), + ("Elevation Error", "權限提升失敗"), + ("Ask the remote user for authentication", "請求遠端使用者進行身分驗證"), + ("Choose this if the remote account is administrator", "當遠端使用者帳戶是管理員時,請選擇此選項"), + ("Transmit the username and password of administrator", "發送管理員的使用者名稱和密碼"), + ("still_click_uac_tip", "依然需要遠端使用者在 UAC 視窗點擊確認。"), + ("Request Elevation", "請求權限提升"), + ("wait_accept_uac_tip", "請等待遠端使用者確認 UAC 對話框。"), + ("Elevate successfully", "權限提升成功"), + ("uppercase", "大寫字母"), + ("lowercase", "小寫字母"), + ("digit", "數字"), + ("special character", "特殊字元"), + ("length>=8", "長度不能小於 8"), + ("Weak", "弱"), + ("Medium", "中"), + ("Strong", "強"), + ("Switch Sides", "反轉存取方向"), + ("Please confirm if you want to share your desktop?", "請確認是否要讓對方存取您的桌面?"), + ("Display", "顯示"), + ("Default View Style", "預設顯示方式"), + ("Default Scroll Style", "預設滾動方式"), + ("Default Image Quality", "預設圖像質量"), + ("Default Codec", "預設編解碼器"), + ("Bitrate", "位元速率"), + ("FPS", "幀率"), + ("Auto", "自動"), + ("Other Default Options", "其他預設選項"), + ("Voice call", "語音通話"), + ("Text chat", "文字聊天"), + ("Stop voice call", "停止語音通話"), + ("relay_hint_tip", "可能無法直接連線,可以嘗試中繼連線。\n另外,如果想要直接使用中繼連線,可以在 ID 後面新增/r,或者在卡片選項裡選擇強制走中繼連線。"), + ("Reconnect", "重新連線"), + ("Codec", "編解碼器"), + ("Resolution", "解析度"), + ("No transfers in progress", "沒有正在進行的傳輸"), + ("Set one-time password length", "設定一次性密碼長度"), + ("idd_driver_tip", "安裝虛擬顯示器驅動程式,以便在沒有連接顯示器的情況下啟動虛擬顯示器進行控制。"), + ("confirm_idd_driver_tip", "安裝虛擬顯示器驅動程式的選項已勾選。請注意,測試證書將被安裝以信任虛擬顯示器驅動。測試證書僅會用於信任 RustDesk 的驅動程式。"), + ("RDP Settings", "RDP 設定"), + ("Sort by", "排序方式"), + ("New Connection", "新連線"), + ("Restore", "還原"), + ("Minimize", "最小化"), + ("Maximize", "最大化"), + ("Your Device", "您的裝置"), + ("empty_recent_tip", "空空如也"), + ("empty_favorite_tip", "空空如也"), + ("empty_lan_tip", "空空如也"), + ("empty_address_book_tip", "空空如也"), + ("eg: admin", "例如:admin"), + ("Empty Username", "空使用者帳號"), + ("Empty Password", "空密碼"), + ("Me", "我"), + ("identical_file_tip", "此檔案與對方的檔案一致"), + ("show_monitors_tip", "在工具列中顯示顯示器"), + ("View Mode", "瀏覽模式"), + ("login_linux_tip", "需要登入到遠端 Linux 使用者帳戶才能啟用 X 介面。"), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), + ].iter().cloned().collect(); +} diff --git a/src/lang/ua.rs b/src/lang/ua.rs index e21cd88df..755e7b041 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -480,8 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("identical_file_tip", ""), ("show_monitors_tip", ""), ("View Mode", ""), - ("enter_rustdesk_passwd_tip", ""), - ("remember_rustdesk_passwd_tip", ""), ("login_linux_tip", ""), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index f716bd536..b8086f126 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -480,8 +480,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("identical_file_tip", ""), ("show_monitors_tip", ""), ("View Mode", ""), - ("enter_rustdesk_passwd_tip", ""), - ("remember_rustdesk_passwd_tip", ""), ("login_linux_tip", ""), + ("verify_rustdesk_password_tip", ""), + ("remember_account_tip", ""), + ("os_account_desk_tip", ""), + ("OS Account", ""), + ("another_user_login_title_tip", ""), + ("another_user_login_text_tip", ""), + ("xorg_not_found_title_tip", ""), + ("xorg_not_found_text_tip", ""), + ("no_desktop_title_tip", ""), + ("no_desktop_text_tip", ""), ].iter().cloned().collect(); } diff --git a/src/platform/linux.rs b/src/platform/linux.rs index dda4b115b..33ebcc82c 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -225,6 +225,16 @@ fn stop_rustdesk_servers() { )); } +#[inline] +fn stop_subprocess() { + let _ = run_cmds(&format!( + r##"ps -ef | grep '/etc/rustdesk/xorg.conf' | grep -v grep | awk '{{printf("kill -9 %d\n", $2)}}' | bash"##, + )); + let _ = run_cmds(&format!( + r##"ps -ef | grep -E 'rustdesk +--cm-no-ui' | grep -v grep | awk '{{printf("kill -9 %d\n", $2)}}' | bash"##, + )); +} + fn should_start_server( try_x11: bool, uid: &mut String, @@ -295,6 +305,7 @@ fn force_stop_server() { pub fn start_os_service() { stop_rustdesk_servers(); + stop_subprocess(); start_uinput_service(); let running = Arc::new(AtomicBool::new(true)); @@ -329,6 +340,7 @@ pub fn start_os_service() { &mut last_restart, &mut server, ) { + stop_subprocess(); force_stop_server(); start_server(None, &mut server); } @@ -345,6 +357,7 @@ pub fn start_os_service() { &mut last_restart, &mut user_server, ) { + stop_subprocess(); force_stop_server(); start_server( Some((desktop.uid.clone(), desktop.username.clone())), @@ -454,6 +467,7 @@ pub fn get_env_var(k: &str) -> String { } } +// Headless is enabled, always return true. pub fn is_prelogin() -> bool { let n = get_active_userid().len(); n < 4 && n > 1 @@ -769,6 +783,7 @@ mod desktop { pub protocal: String, pub display: String, pub xauth: String, + pub is_rustdesk_subprocess: bool, } impl Desktop { @@ -784,7 +799,7 @@ mod desktop { #[inline] pub fn is_headless(&self) -> bool { - self.sid.is_empty() + self.sid.is_empty() || self.is_rustdesk_subprocess } fn get_display(&mut self) { @@ -901,6 +916,16 @@ mod desktop { last } + fn set_is_subprocess(&mut self) { + self.is_rustdesk_subprocess = false; + let cmd = "ps -ef | grep 'rustdesk/xorg.conf' | grep -v grep | wc -l"; + if let Ok(res) = run_cmds(cmd) { + if res.trim() != "0" { + self.is_rustdesk_subprocess = true; + } + } + } + pub fn refresh(&mut self) { if !self.sid.is_empty() && is_active(&self.sid) { return; @@ -909,6 +934,7 @@ mod desktop { let seat0_values = get_values_of_seat0(&[0, 1, 2]); if seat0_values[0].is_empty() { *self = Self::default(); + self.is_rustdesk_subprocess = false; return; } @@ -919,11 +945,13 @@ mod desktop { if self.is_login_wayland() { self.display = "".to_owned(); self.xauth = "".to_owned(); + self.is_rustdesk_subprocess = false; return; } self.get_display(); self.get_xauth(); + self.set_is_subprocess(); } } } diff --git a/src/platform/linux_desktop_manager.rs b/src/platform/linux_desktop_manager.rs new file mode 100644 index 000000000..25fada503 --- /dev/null +++ b/src/platform/linux_desktop_manager.rs @@ -0,0 +1,733 @@ +use super::{linux::*, ResultType}; +use crate::server::{ + LOGIN_MSG_DESKTOP_NO_DESKTOP, LOGIN_MSG_DESKTOP_SESSION_ANOTHER_USER, + LOGIN_MSG_DESKTOP_SESSION_NOT_READY, LOGIN_MSG_DESKTOP_XORG_NOT_FOUND, + LOGIN_MSG_DESKTOP_XSESSION_FAILED, +}; +use hbb_common::{allow_err, bail, log, rand::prelude::*, tokio::time}; +use pam; +use std::{ + collections::HashMap, + os::unix::process::CommandExt, + path::Path, + process::{Child, Command}, + sync::{ + atomic::{AtomicBool, Ordering}, + mpsc::{sync_channel, SyncSender}, + Arc, Mutex, + }, + time::{Duration, Instant}, +}; +use users::{get_user_by_name, os::unix::UserExt, User}; + +lazy_static::lazy_static! { + static ref DESKTOP_RUNNING: Arc = Arc::new(AtomicBool::new(false)); + static ref DESKTOP_MANAGER: Arc>> = Arc::new(Mutex::new(None)); +} + +#[derive(Debug)] +struct DesktopManager { + seat0_username: String, + seat0_display_server: String, + child_username: String, + child_exit: Arc, + is_child_running: Arc, +} + +fn check_desktop_manager() { + let mut desktop_manager = DESKTOP_MANAGER.lock().unwrap(); + if let Some(desktop_manager) = &mut (*desktop_manager) { + if desktop_manager.is_child_running.load(Ordering::SeqCst) { + return; + } + desktop_manager.child_exit.store(true, Ordering::SeqCst); + } +} + +// --server process +pub fn start_xdesktop() { + std::thread::spawn(|| { + *DESKTOP_MANAGER.lock().unwrap() = Some(DesktopManager::new()); + + let interval = time::Duration::from_millis(super::SERVICE_INTERVAL); + DESKTOP_RUNNING.store(true, Ordering::SeqCst); + while DESKTOP_RUNNING.load(Ordering::SeqCst) { + check_desktop_manager(); + std::thread::sleep(interval); + } + log::info!("xdesktop child thread exit"); + }); +} + +pub fn stop_xdesktop() { + DESKTOP_RUNNING.store(false, Ordering::SeqCst); + *DESKTOP_MANAGER.lock().unwrap() = None; +} + +fn detect_headless() -> Option<&'static str> { + match run_cmds(&format!("which {}", DesktopManager::get_xorg())) { + Ok(output) => { + if output.trim().is_empty() { + return Some(LOGIN_MSG_DESKTOP_XORG_NOT_FOUND); + } + } + _ => { + return Some(LOGIN_MSG_DESKTOP_XORG_NOT_FOUND); + } + } + + match run_cmds("ls /usr/share/xsessions/") { + Ok(output) => { + if output.trim().is_empty() { + return Some(LOGIN_MSG_DESKTOP_NO_DESKTOP); + } + } + _ => { + return Some(LOGIN_MSG_DESKTOP_NO_DESKTOP); + } + } + + None +} + +pub fn try_start_desktop(_username: &str, _passsword: &str) -> String { + if _username.is_empty() { + let username = get_username(); + if username.is_empty() { + if let Some(msg) = detect_headless() { + msg + } else { + LOGIN_MSG_DESKTOP_SESSION_NOT_READY + } + } else { + "" + } + .to_owned() + } else { + let username = get_username(); + if username == _username { + // No need to verify password here. + return "".to_owned(); + } + + if let Some(msg) = detect_headless() { + return msg.to_owned(); + } + + match try_start_x_session(_username, _passsword) { + Ok((username, x11_ready)) => { + if x11_ready { + if _username != username { + LOGIN_MSG_DESKTOP_SESSION_ANOTHER_USER.to_owned() + } else { + "".to_owned() + } + } else { + LOGIN_MSG_DESKTOP_SESSION_NOT_READY.to_owned() + } + } + Err(e) => { + log::error!("Failed to start xsession {}", e); + LOGIN_MSG_DESKTOP_XSESSION_FAILED.to_owned() + } + } + } +} + +fn try_start_x_session(username: &str, password: &str) -> ResultType<(String, bool)> { + let mut desktop_manager = DESKTOP_MANAGER.lock().unwrap(); + if let Some(desktop_manager) = &mut (*desktop_manager) { + if let Some(seat0_username) = desktop_manager.get_supported_display_seat0_username() { + return Ok((seat0_username, true)); + } + + let _ = desktop_manager.try_start_x_session(username, password)?; + log::debug!( + "try_start_x_session, username: {}, {:?}", + &username, + &desktop_manager + ); + Ok(( + desktop_manager.child_username.clone(), + desktop_manager.is_running(), + )) + } else { + bail!(crate::server::LOGIN_MSG_DESKTOP_NOT_INITED); + } +} + +#[inline] +pub fn is_headless() -> bool { + DESKTOP_MANAGER + .lock() + .unwrap() + .as_ref() + .map_or(false, |manager| { + manager.get_supported_display_seat0_username().is_none() + }) +} + +pub fn get_username() -> String { + match &*DESKTOP_MANAGER.lock().unwrap() { + Some(manager) => { + if let Some(seat0_username) = manager.get_supported_display_seat0_username() { + seat0_username + } else { + if manager.is_running() && !manager.child_username.is_empty() { + manager.child_username.clone() + } else { + "".to_owned() + } + } + } + None => "".to_owned(), + } +} + +impl Drop for DesktopManager { + fn drop(&mut self) { + self.stop_children(); + } +} + +impl DesktopManager { + fn fatal_exit() { + std::process::exit(0); + } + + pub fn new() -> Self { + let mut seat0_username = "".to_owned(); + let mut seat0_display_server = "".to_owned(); + let seat0_values = get_values_of_seat0(&[0, 2]); + if !seat0_values[0].is_empty() { + seat0_username = seat0_values[1].clone(); + seat0_display_server = get_display_server_of_session(&seat0_values[0]); + } + Self { + seat0_username, + seat0_display_server, + child_username: "".to_owned(), + child_exit: Arc::new(AtomicBool::new(true)), + is_child_running: Arc::new(AtomicBool::new(false)), + } + } + + fn get_supported_display_seat0_username(&self) -> Option { + if is_gdm_user(&self.seat0_username) && self.seat0_display_server == DISPLAY_SERVER_WAYLAND + { + None + } else if self.seat0_username.is_empty() { + None + } else { + Some(self.seat0_username.clone()) + } + } + + #[inline] + fn get_xauth() -> String { + let xauth = get_env_var("XAUTHORITY"); + if xauth.is_empty() { + "/tmp/.Xauthority".to_owned() + } else { + xauth + } + } + + #[inline] + fn is_running(&self) -> bool { + self.is_child_running.load(Ordering::SeqCst) + } + + fn try_start_x_session(&mut self, username: &str, password: &str) -> ResultType<()> { + match get_user_by_name(username) { + Some(userinfo) => { + let mut client = pam::Client::with_password(pam_get_service_name())?; + client + .conversation_mut() + .set_credentials(username, password); + match client.authenticate() { + Ok(_) => { + if self.is_running() { + return Ok(()); + } + + match self.start_x_session(&userinfo, username, password) { + Ok(_) => { + log::info!("Succeeded to start x11"); + self.child_username = username.to_string(); + Ok(()) + } + Err(e) => { + bail!("failed to start x session, {}", e); + } + } + } + Err(e) => { + bail!("failed to check user pass for {}, {}", username, e); + } + } + } + None => { + bail!("failed to get userinfo of {}", username); + } + } + } + + // The logic mainly fron https://github.com/neutrinolabs/xrdp/blob/34fe9b60ebaea59e8814bbc3ca5383cabaa1b869/sesman/session.c#L334. + fn get_avail_display() -> ResultType { + let display_range = 0..51; + for i in display_range.clone() { + if Self::is_x_server_running(i) { + continue; + } + return Ok(i); + } + bail!("No avaliable display found in range {:?}", display_range) + } + + #[inline] + fn is_x_server_running(display: u32) -> bool { + Path::new(&format!("/tmp/.X11-unix/X{}", display)).exists() + || Path::new(&format!("/tmp/.X{}-lock", display)).exists() + } + + fn start_x_session( + &mut self, + userinfo: &User, + username: &str, + password: &str, + ) -> ResultType<()> { + self.stop_children(); + + let display_num = Self::get_avail_display()?; + // "xServer_ip:display_num.screen_num" + + let uid = userinfo.uid(); + let gid = userinfo.primary_group_id(); + let envs = HashMap::from([ + ("SHELL", userinfo.shell().to_string_lossy().to_string()), + ("PATH", "/sbin:/bin:/usr/bin:/usr/local/bin".to_owned()), + ("USER", username.to_string()), + ("UID", userinfo.uid().to_string()), + ("HOME", userinfo.home_dir().to_string_lossy().to_string()), + ( + "XDG_RUNTIME_DIR", + format!("/run/user/{}", userinfo.uid().to_string()), + ), + // ("DISPLAY", self.display.clone()), + // ("XAUTHORITY", self.xauth.clone()), + // (ENV_DESKTOP_PROTOCAL, XProtocal::X11.to_string()), + ]); + self.child_exit.store(false, Ordering::SeqCst); + let is_child_running = self.is_child_running.clone(); + + let (tx_res, rx_res) = sync_channel(1); + let password = password.to_string(); + let username = username.to_string(); + // start x11 + std::thread::spawn(move || { + match Self::start_x_session_thread( + tx_res.clone(), + is_child_running, + uid, + gid, + display_num, + username, + password, + envs, + ) { + Ok(_) => {} + Err(e) => { + log::error!("Failed to start x session thread"); + allow_err!(tx_res.send(format!("Failed to start x session thread, {}", e))); + } + } + }); + + // wait x11 + match rx_res.recv_timeout(Duration::from_millis(10_000)) { + Ok(res) => { + if res == "" { + Ok(()) + } else { + bail!(res) + } + } + Err(e) => { + bail!("Failed to recv x11 result {}", e) + } + } + } + + #[inline] + fn display_from_num(num: u32) -> String { + format!(":{num}") + } + + fn start_x_session_thread( + tx_res: SyncSender, + is_child_running: Arc, + uid: u32, + gid: u32, + display_num: u32, + username: String, + password: String, + envs: HashMap<&str, String>, + ) -> ResultType<()> { + let mut client = pam::Client::with_password(pam_get_service_name())?; + client + .conversation_mut() + .set_credentials(&username, &password); + client.authenticate()?; + + client.set_item(pam::PamItemType::TTY, &Self::display_from_num(display_num))?; + client.open_session()?; + + // fixme: FreeBSD kernel needs to login here. + // see: https://github.com/neutrinolabs/xrdp/blob/a64573b596b5fb07ca3a51590c5308d621f7214e/sesman/session.c#L556 + + let (child_xorg, child_wm) = Self::start_x11(uid, gid, username, display_num, &envs)?; + is_child_running.store(true, Ordering::SeqCst); + + log::info!("Start xorg and wm done, notify and wait xtop x11"); + allow_err!(tx_res.send("".to_owned())); + + Self::wait_stop_x11(child_xorg, child_wm); + log::info!("Wait x11 stop done"); + Ok(()) + } + + fn wait_xorg_exit(child_xorg: &mut Child) -> ResultType { + if let Ok(_) = child_xorg.kill() { + for _ in 0..3 { + match child_xorg.try_wait() { + Ok(Some(status)) => return Ok(format!("Xorg exit with {}", status)), + Ok(None) => {} + Err(e) => { + // fatal error + log::error!("Failed to wait xorg process, {}", e); + bail!("Failed to wait xorg process, {}", e) + } + } + std::thread::sleep(std::time::Duration::from_millis(1_000)); + } + log::error!("Failed to wait xorg process, not exit"); + bail!("Failed to wait xorg process, not exit") + } else { + Ok("Xorg is already exited".to_owned()) + } + } + + fn add_xauth_cookie( + file: &str, + display: &str, + uid: u32, + gid: u32, + envs: &HashMap<&str, String>, + ) -> ResultType<()> { + let randstr = (0..16) + .map(|_| format!("{:02x}", random::())) + .collect::(); + let output = Command::new("xauth") + .uid(uid) + .gid(gid) + .envs(envs) + .args(vec!["-q", "-f", file, "add", display, ".", &randstr]) + .output()?; + // xauth run success, even the following error occurs. + // Ok(Output { status: ExitStatus(unix_wait_status(0)), stdout: "", stderr: "xauth: file .Xauthority does not exist\n" }) + let errmsg = String::from_utf8_lossy(&output.stderr).to_string(); + if !errmsg.is_empty() { + if !errmsg.contains("does not exist") { + bail!("Failed to launch xauth, {}", errmsg) + } + } + Ok(()) + } + + fn wait_x_server_running(pid: u32, display_num: u32, max_wait_secs: u64) -> ResultType<()> { + let wait_begin = Instant::now(); + loop { + if run_cmds(&format!("ls /proc/{}", pid))?.is_empty() { + bail!("X server exit"); + } + + if Self::is_x_server_running(display_num) { + return Ok(()); + } + if wait_begin.elapsed().as_secs() > max_wait_secs { + bail!("Failed to wait xserver after {} seconds", max_wait_secs); + } + std::thread::sleep(Duration::from_millis(300)); + } + } + + fn start_x11( + uid: u32, + gid: u32, + username: String, + display_num: u32, + envs: &HashMap<&str, String>, + ) -> ResultType<(Child, Child)> { + log::debug!("envs of user {}: {:?}", &username, &envs); + + let xauth = Self::get_xauth(); + let display = Self::display_from_num(display_num); + + Self::add_xauth_cookie(&xauth, &display, uid, gid, &envs)?; + + // Start Xorg + let mut child_xorg = Self::start_x_server(&xauth, &display, uid, gid, &envs)?; + + log::info!("xorg started, wait 10 secs to ensuer x server is running"); + + let max_wait_secs = 10; + // wait x server running + if let Err(e) = Self::wait_x_server_running(child_xorg.id(), display_num, max_wait_secs) { + match Self::wait_xorg_exit(&mut child_xorg) { + Ok(msg) => log::info!("{}", msg), + Err(e) => { + log::error!("{}", e); + Self::fatal_exit(); + } + } + bail!(e) + } + + log::info!( + "xorg is running, start x window manager with DISPLAY: {}, XAUTHORITY: {}", + &display, + &xauth + ); + + std::env::set_var("DISPLAY", &display); + std::env::set_var("XAUTHORITY", &xauth); + // start window manager (startwm.sh) + let child_wm = match Self::start_x_window_manager(uid, gid, &envs) { + Ok(c) => c, + Err(e) => { + match Self::wait_xorg_exit(&mut child_xorg) { + Ok(msg) => log::info!("{}", msg), + Err(e) => { + log::error!("{}", e); + Self::fatal_exit(); + } + } + bail!(e) + } + }; + log::info!("x window manager is started"); + + Ok((child_xorg, child_wm)) + } + + fn try_wait_x11_child_exit(child_xorg: &mut Child, child_wm: &mut Child) -> bool { + match child_xorg.try_wait() { + Ok(Some(status)) => { + log::info!("Xorg exit with {}", status); + return true; + } + Ok(None) => {} + Err(e) => log::error!("Failed to wait xorg process, {}", e), + } + + match child_wm.try_wait() { + Ok(Some(status)) => { + // Logout may result "wm exit with signal: 11 (SIGSEGV) (core dumped)" + log::info!("wm exit with {}", status); + return true; + } + Ok(None) => {} + Err(e) => log::error!("Failed to wait xorg process, {}", e), + } + false + } + + fn wait_x11_children_exit(child_xorg: &mut Child, child_wm: &mut Child) { + log::debug!("Try kill child process xorg"); + if let Ok(_) = child_xorg.kill() { + let mut exited = false; + for _ in 0..2 { + match child_xorg.try_wait() { + Ok(Some(status)) => { + log::info!("Xorg exit with {}", status); + exited = true; + break; + } + Ok(None) => {} + Err(e) => { + log::error!("Failed to wait xorg process, {}", e); + Self::fatal_exit(); + } + } + std::thread::sleep(std::time::Duration::from_millis(1_000)); + } + if !exited { + log::error!("Failed to wait child xorg, after kill()"); + // try kill -9? + } + } + log::debug!("Try kill child process wm"); + if let Ok(_) = child_wm.kill() { + let mut exited = false; + for _ in 0..2 { + match child_wm.try_wait() { + Ok(Some(status)) => { + // Logout may result "wm exit with signal: 11 (SIGSEGV) (core dumped)" + log::info!("wm exit with {}", status); + exited = true; + } + Ok(None) => {} + Err(e) => { + log::error!("Failed to wait wm process, {}", e); + Self::fatal_exit(); + } + } + std::thread::sleep(std::time::Duration::from_millis(1_000)); + } + if !exited { + log::error!("Failed to wait child xorg, after kill()"); + // try kill -9? + } + } + } + + fn try_wait_stop_x11(child_xorg: &mut Child, child_wm: &mut Child) -> bool { + let mut desktop_manager = DESKTOP_MANAGER.lock().unwrap(); + let mut exited = true; + if let Some(desktop_manager) = &mut (*desktop_manager) { + if desktop_manager.child_exit.load(Ordering::SeqCst) { + exited = true; + } else { + exited = Self::try_wait_x11_child_exit(child_xorg, child_wm); + } + if exited { + log::debug!("Wait x11 children exiting"); + Self::wait_x11_children_exit(child_xorg, child_wm); + desktop_manager + .is_child_running + .store(false, Ordering::SeqCst); + desktop_manager.child_exit.store(true, Ordering::SeqCst); + } + } + exited + } + + fn wait_stop_x11(mut child_xorg: Child, mut child_wm: Child) { + loop { + if Self::try_wait_stop_x11(&mut child_xorg, &mut child_wm) { + break; + } + std::thread::sleep(Duration::from_millis(super::SERVICE_INTERVAL)); + } + } + + fn get_xorg() -> &'static str { + // Fedora 26 or later + let xorg = "/usr/libexec/Xorg"; + if Path::new(xorg).is_file() { + return xorg; + } + // Debian 9 or later + let xorg = "/usr/lib/xorg/Xorg"; + if Path::new(xorg).is_file() { + return xorg; + } + // Ubuntu 16.04 or later + let xorg = "/usr/lib/xorg/Xorg"; + if Path::new(xorg).is_file() { + return xorg; + } + // Arch Linux + let xorg = "/usr/lib/xorg-server/Xorg"; + if Path::new(xorg).is_file() { + return xorg; + } + // Arch Linux + let xorg = "/usr/lib/Xorg"; + if Path::new(xorg).is_file() { + return xorg; + } + // CentOS 7 /usr/bin/Xorg or param=Xorg + + log::warn!("Failed to find xorg, use default Xorg.\n Please add \"allowed_users=anybody\" to \"/etc/X11/Xwrapper.config\"."); + "Xorg" + } + + fn start_x_server( + xauth: &str, + display: &str, + uid: u32, + gid: u32, + envs: &HashMap<&str, String>, + ) -> ResultType { + let xorg = Self::get_xorg(); + log::info!("Use xorg: {}", &xorg); + match Command::new(xorg) + .envs(envs) + .uid(uid) + .gid(gid) + .args(vec![ + "-noreset", + "+extension", + "GLX", + "+extension", + "RANDR", + "+extension", + "RENDER", + //"-logfile", + //"/tmp/RustDesk_xorg.log", + "-config", + "/etc/rustdesk/xorg.conf", + "-auth", + xauth, + display, + ]) + .spawn() + { + Ok(c) => Ok(c), + Err(e) => { + bail!("Failed to start Xorg with display {}, {}", display, e); + } + } + } + + fn start_x_window_manager( + uid: u32, + gid: u32, + envs: &HashMap<&str, String>, + ) -> ResultType { + match Command::new("/etc/rustdesk/startwm.sh") + .envs(envs) + .uid(uid) + .gid(gid) + .spawn() + { + Ok(c) => Ok(c), + Err(e) => { + bail!("Failed to start window manager, {}", e); + } + } + } + + fn stop_children(&mut self) { + self.child_exit.store(true, Ordering::SeqCst); + for _i in 1..10 { + if !self.is_child_running.load(Ordering::SeqCst) { + break; + } + std::thread::sleep(Duration::from_millis(super::SERVICE_INTERVAL)); + } + if self.is_child_running.load(Ordering::SeqCst) { + log::warn!("xdesktop child is still running!"); + } + } +} + +fn pam_get_service_name() -> &'static str { + if Path::new("/etc/pam.d/rustdesk").is_file() { + "rustdesk" + } else { + "gdm" + } +} diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 777de3b01..fc27be098 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -17,6 +17,9 @@ pub mod delegate; #[cfg(target_os = "linux")] pub mod linux; +#[cfg(all(target_os = "linux", feature = "linux_headless"))] +pub mod linux_desktop_manager; + #[cfg(not(any(target_os = "android", target_os = "ios")))] use hbb_common::{message_proto::CursorData, ResultType}; #[cfg(not(any(target_os = "macos", target_os = "android", target_os = "ios")))] diff --git a/src/port_forward.rs b/src/port_forward.rs index f50f40db8..4e05ad92f 100644 --- a/src/port_forward.rs +++ b/src/port_forward.rs @@ -199,8 +199,8 @@ async fn connect_and_login_2( }, d = ui_receiver.recv() => { match d { - Some(Data::Login((password, remember))) => { - interface.handle_login_from_ui(password, remember, &mut stream).await; + Some(Data::Login((os_username, os_password, password, remember))) => { + interface.handle_login_from_ui(os_username, os_password, password, remember, &mut stream).await; } _ => {} } diff --git a/src/rendezvous_mediator.rs b/src/rendezvous_mediator.rs index 8b7dae1ba..6dd0a1284 100644 --- a/src/rendezvous_mediator.rs +++ b/src/rendezvous_mediator.rs @@ -72,6 +72,8 @@ impl RendezvousMediator { allow_err!(super::lan::start_listening()); }); } + #[cfg(all(target_os = "linux", feature = "linux_headless"))] + crate::platform::linux_desktop_manager::start_xdesktop(); loop { Config::reset_online(); if Config::get_option("stop-service").is_empty() { @@ -96,6 +98,10 @@ impl RendezvousMediator { } sleep(1.).await; } + // It should be better to call stop_xdesktop. + // But for server, it also is Ok without calling this method. + // #[cfg(all(target_os = "linux", feature = "linux_headless"))] + // crate::platform::linux_desktop_manager::stop_xdesktop(); } pub async fn start(server: ServerPtr, host: String) -> ResultType<()> { diff --git a/src/server/connection.rs b/src/server/connection.rs index d1ae4d6a3..53dccc3ca 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -3,6 +3,8 @@ use super::{input_service::*, *}; use crate::clipboard_file::*; #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::common::update_clipboard; +#[cfg(all(target_os = "linux", feature = "linux_headless"))] +use crate::platform::linux_desktop_manager; #[cfg(windows)] use crate::portable_service::client as portable_client; use crate::{ @@ -16,6 +18,8 @@ use crate::{ use crate::{common::DEVICE_NAME, flutter::connection_manager::start_channel}; use crate::{ipc, VERSION}; use cidr_utils::cidr::IpCidr; +#[cfg(all(target_os = "linux", feature = "linux_headless"))] +use hbb_common::platform::linux::run_cmds; use hbb_common::{ config::Config, fs, @@ -57,6 +61,22 @@ lazy_static::lazy_static! { pub static CLICK_TIME: AtomicI64 = AtomicI64::new(0); pub static MOUSE_MOVE_TIME: AtomicI64 = AtomicI64::new(0); +pub const LOGIN_MSG_DESKTOP_NOT_INITED: &str = "Desktop env is not inited"; +pub const LOGIN_MSG_DESKTOP_SESSION_NOT_READY: &str = "Desktop session not ready"; +pub const LOGIN_MSG_DESKTOP_XSESSION_FAILED: &str = "Desktop xsession failed"; +pub const LOGIN_MSG_DESKTOP_SESSION_ANOTHER_USER: &str = "Desktop session another user login"; +pub const LOGIN_MSG_DESKTOP_XORG_NOT_FOUND: &str = "Desktop xorg not found"; +// ls /usr/share/xsessions/ +pub const LOGIN_MSG_DESKTOP_NO_DESKTOP: &str = "Desktop none"; +pub const LOGIN_MSG_DESKTOP_SESSION_NOT_READY_PASSWORD_EMPTY: &str = + "Desktop session not ready, password empty"; +pub const LOGIN_MSG_DESKTOP_SESSION_NOT_READY_PASSWORD_WRONG: &str = + "Desktop session not ready, password wrong"; +pub const LOGIN_MSG_PASSWORD_EMPTY: &str = "Empty Password"; +pub const LOGIN_MSG_PASSWORD_WRONG: &str = "Wrong Password"; +pub const LOGIN_MSG_NO_PASSWORD_ACCESS: &str = "No Password Access"; +pub const LOGIN_MSG_OFFLINE: &str = "Offline"; + #[derive(Clone, Default)] pub struct ConnInner { id: i32, @@ -134,6 +154,10 @@ pub struct Connection { audio_input_device_before_voice_call: Option, options_in_login: Option, pressed_modifiers: HashSet, + #[cfg(all(target_os = "linux", feature = "linux_headless"))] + rx_cm_stream_ready: mpsc::Receiver<()>, + #[cfg(all(target_os = "linux", feature = "linux_headless"))] + tx_desktop_ready: mpsc::Sender<()>, } impl ConnInner { @@ -194,6 +218,10 @@ impl Connection { let (tx_video, mut rx_video) = mpsc::unbounded_channel::<(Instant, Arc)>(); let (tx_input, _rx_input) = std_mpsc::channel(); let mut hbbs_rx = crate::hbbs_http::sync::signal_receiver(); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + let (tx_cm_stream_ready, _rx_cm_stream_ready) = mpsc::channel(1); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + let (_tx_desktop_ready, rx_desktop_ready) = mpsc::channel(1); #[cfg(not(any(target_os = "android", target_os = "ios")))] let tx_cloned = tx.clone(); @@ -246,10 +274,16 @@ impl Connection { audio_input_device_before_voice_call: None, options_in_login: None, pressed_modifiers: Default::default(), + #[cfg(all(target_os = "linux", feature = "linux_headless"))] + rx_cm_stream_ready: _rx_cm_stream_ready, + #[cfg(all(target_os = "linux", feature = "linux_headless"))] + tx_desktop_ready: _tx_desktop_ready, }; #[cfg(not(any(target_os = "android", target_os = "ios")))] tokio::spawn(async move { - if let Err(err) = start_ipc(rx_to_cm, tx_from_cm).await { + if let Err(err) = + start_ipc(rx_to_cm, tx_from_cm, rx_desktop_ready, tx_cm_stream_ready).await + { log::error!("ipc to connection manager exit: {}", err); } }); @@ -856,6 +890,10 @@ impl Connection { if crate::platform::current_is_wayland() { platform_additions.insert("is_wayland".into(), json!(true)); } + #[cfg(feature = "linux_headless")] + if linux_desktop_manager::is_headless() { + platform_additions.insert("headless".into(), json!(true)); + } if !platform_additions.is_empty() { pi.platform_additions = serde_json::to_string(&platform_additions).unwrap_or("".into()); @@ -874,7 +912,9 @@ impl Connection { #[cfg(target_os = "linux")] if !self.file_transfer.is_some() && !self.port_forward_socket.is_some() { let dtype = crate::platform::linux::get_display_server(); - if dtype != "x11" && dtype != "wayland" { + if dtype != crate::platform::linux::DISPLAY_SERVER_X11 + && dtype != crate::platform::linux::DISPLAY_SERVER_WAYLAND + { res.set_error(format!( "Unsupported display server type \"{}\", x11 or wayland expected", dtype @@ -1216,8 +1256,28 @@ impl Connection { } _ => {} } + + #[cfg(all(target_os = "linux", feature = "linux_headless"))] + let desktop_err = match lr.os_login.as_ref() { + Some(os_login) => { + linux_desktop_manager::try_start_desktop(&os_login.username, &os_login.password) + } + None => linux_desktop_manager::try_start_desktop("", ""), + }; + #[cfg(all(target_os = "linux", feature = "linux_headless"))] + let is_headless = linux_desktop_manager::is_headless(); + #[cfg(all(target_os = "linux", feature = "linux_headless"))] + let wait_ipc_timeout = 10_000; + + // If err is LOGIN_MSG_DESKTOP_SESSION_NOT_READY, just keep this msg and go on checking password. + #[cfg(all(target_os = "linux", feature = "linux_headless"))] + if !desktop_err.is_empty() && desktop_err != LOGIN_MSG_DESKTOP_SESSION_NOT_READY { + self.send_login_error(desktop_err).await; + return true; + } + if !hbb_common::is_ipv4_str(&lr.username) && lr.username != Config::get_id() { - self.send_login_error("Offline").await; + self.send_login_error(LOGIN_MSG_OFFLINE).await; } else if password::approve_mode() == ApproveMode::Click || password::approve_mode() == ApproveMode::Both && !password::has_valid_password() { @@ -1225,7 +1285,7 @@ impl Connection { if hbb_common::get_version_number(&lr.version) >= hbb_common::get_version_number("1.2.0") { - self.send_login_error("No Password Access").await; + self.send_login_error(LOGIN_MSG_NO_PASSWORD_ACCESS).await; } return true; } else if password::approve_mode() == ApproveMode::Password @@ -1234,12 +1294,38 @@ impl Connection { self.send_login_error("Connection not allowed").await; return false; } else if self.is_recent_session() { - self.try_start_cm(lr.my_id, lr.my_name, true); - self.send_logon_response().await; - if self.port_forward_socket.is_some() { - return false; + #[cfg(all(target_os = "linux", feature = "linux_headless"))] + if desktop_err.is_empty() { + #[cfg(target_os = "linux")] + if is_headless { + self.tx_desktop_ready.send(()).await.ok(); + let _res = timeout(wait_ipc_timeout, self.rx_cm_stream_ready.recv()).await; + } + self.try_start_cm(lr.my_id, lr.my_name, true); + self.send_logon_response().await; + if self.port_forward_socket.is_some() { + return false; + } + } else { + self.send_login_error(desktop_err).await; + } + #[cfg(not(all(target_os = "linux", feature = "linux_headless")))] + { + self.try_start_cm(lr.my_id, lr.my_name, true); + self.send_logon_response().await; + if self.port_forward_socket.is_some() { + return false; + } } } else if lr.password.is_empty() { + #[cfg(all(target_os = "linux", feature = "linux_headless"))] + if desktop_err.is_empty() { + self.try_start_cm(lr.my_id, lr.my_name, false); + } else { + self.send_login_error(LOGIN_MSG_DESKTOP_SESSION_NOT_READY_PASSWORD_EMPTY) + .await; + } + #[cfg(not(all(target_os = "linux", feature = "linux_headless")))] self.try_start_cm(lr.my_id, lr.my_name, false); } else { let mut failure = LOGIN_FAILURES @@ -1281,16 +1367,46 @@ impl Connection { .lock() .unwrap() .insert(self.ip.clone(), failure); - self.send_login_error("Wrong Password").await; - self.try_start_cm(lr.my_id, lr.my_name, false); + #[cfg(all(target_os = "linux", feature = "linux_headless"))] + if desktop_err.is_empty() { + self.send_login_error(LOGIN_MSG_PASSWORD_WRONG).await; + self.try_start_cm(lr.my_id, lr.my_name, false); + } else { + self.send_login_error(LOGIN_MSG_DESKTOP_SESSION_NOT_READY_PASSWORD_WRONG) + .await; + } + #[cfg(not(all(target_os = "linux", feature = "linux_headless")))] + { + self.send_login_error(LOGIN_MSG_PASSWORD_WRONG).await; + self.try_start_cm(lr.my_id, lr.my_name, false); + } } else { if failure.0 != 0 { LOGIN_FAILURES.lock().unwrap().remove(&self.ip); } - self.try_start_cm(lr.my_id, lr.my_name, true); - self.send_logon_response().await; - if self.port_forward_socket.is_some() { - return false; + #[cfg(all(target_os = "linux", feature = "linux_headless"))] + if desktop_err.is_empty() { + #[cfg(target_os = "linux")] + if is_headless { + self.tx_desktop_ready.send(()).await.ok(); + let _res = + timeout(wait_ipc_timeout, self.rx_cm_stream_ready.recv()).await; + } + self.send_logon_response().await; + self.try_start_cm(lr.my_id, lr.my_name, true); + if self.port_forward_socket.is_some() { + return false; + } + } else { + self.send_login_error(desktop_err).await; + } + #[cfg(not(all(target_os = "linux", feature = "linux_headless")))] + { + self.send_logon_response().await; + self.try_start_cm(lr.my_id, lr.my_name, true); + if self.port_forward_socket.is_some() { + return false; + } } } } @@ -2058,6 +2174,8 @@ pub fn insert_switch_sides_uuid(id: String, uuid: uuid::Uuid) { async fn start_ipc( mut rx_to_cm: mpsc::UnboundedReceiver, tx_from_cm: mpsc::UnboundedSender, + mut _rx_desktop_ready: mpsc::Receiver<()>, + tx_stream_ready: mpsc::Sender<()>, ) -> ResultType<()> { loop { if !crate::platform::is_prelogin() { @@ -2073,6 +2191,34 @@ async fn start_ipc( if password::hide_cm() { args.push("--hide"); }; + + #[cfg(target_os = "linux")] + #[cfg(not(feature = "linux_headless"))] + let user = None; + #[cfg(all(target_os = "linux", feature = "linux_headless"))] + let mut user = None; + // Cm run as user, wait until desktop session is ready. + #[cfg(all(target_os = "linux", feature = "linux_headless"))] + if linux_desktop_manager::is_headless() { + let mut username = linux_desktop_manager::get_username(); + loop { + if !username.is_empty() { + break; + } + let _res = timeout(1_000, _rx_desktop_ready.recv()).await; + username = linux_desktop_manager::get_username(); + } + let uid = { + let output = run_cmds(&format!("id -u {}", &username))?; + let output = output.trim(); + if output.is_empty() || !output.parse::().is_ok() { + bail!("Invalid username {}", &username); + } + output.to_string() + }; + user = Some((uid, username)); + args = vec!["--cm-no-ui"]; + } let run_done; if crate::platform::is_root() { let mut res = Ok(None); @@ -2085,7 +2231,7 @@ async fn start_ipc( #[cfg(target_os = "linux")] { log::debug!("Start cm"); - res = crate::platform::run_as_user(args.clone(), None); + res = crate::platform::run_as_user(args.clone(), user.clone()); } if res.is_ok() { break; @@ -2117,6 +2263,8 @@ async fn start_ipc( bail!("Failed to connect to connection manager"); } } + + let _res = tx_stream_ready.send(()).await; let mut stream = stream.unwrap(); loop { tokio::select! { diff --git a/src/server/service.rs b/src/server/service.rs index 9cc1e860c..fe038f3c0 100644 --- a/src/server/service.rs +++ b/src/server/service.rs @@ -221,6 +221,7 @@ impl> ServiceTmpl { thread::sleep(interval - elapsed); } } + log::info!("Service {} exit", sp.name()); }); self.0.write().unwrap().handle = Some(thread); } @@ -256,6 +257,7 @@ impl> ServiceTmpl { } thread::sleep(time::Duration::from_millis(HIBERNATE_TIMEOUT)); } + log::info!("Service {} exit", sp.name()); }); self.0.write().unwrap().handle = Some(thread); } diff --git a/src/ui/common.css b/src/ui/common.css index 0fb9afcb1..9845ff104 100644 --- a/src/ui/common.css +++ b/src/ui/common.css @@ -142,6 +142,10 @@ div.password input { font-size: 1.2em; } +div.username input { + font-size: 1.2em; +} + svg { background: none; } diff --git a/src/ui/common.tis b/src/ui/common.tis index b6723b131..92e704052 100644 --- a/src/ui/common.tis +++ b/src/ui/common.tis @@ -252,7 +252,7 @@ function msgbox(type, title, content, link="", callback=null, height=180, width= view.close(); return; } - handler.login(res.password, res.remember); + handler.login("", "", res.password, res.remember); if (!is_port_forward) { // Specially handling file transfer for no permission hanging issue (including 60ms // timer in setPermission. @@ -262,6 +262,30 @@ function msgbox(type, title, content, link="", callback=null, height=180, width= else msgbox("connecting", "Connecting...", "Logging in..."); } }; + } else if (type == "session-login" || type == "session-re-login") { + callback = function (res) { + if (!res) { + view.close(); + return; + } + handler.login(res.osusername, res.ospassword, "", false); + if (!is_port_forward) { + if (is_file_transfer) handler.msgbox("connecting", "Connecting...", "Logging in..."); + else msgbox("connecting", "Connecting...", "Logging in..."); + } + }; + } else if (type.indexOf("session-login") >= 0) { + callback = function (res) { + if (!res) { + view.close(); + return; + } + handler.login(res.osusername, res.ospassword, res.password, res.remember); + if (!is_port_forward) { + if (is_file_transfer) handler.msgbox("connecting", "Connecting...", "Logging in..."); + else msgbox("connecting", "Connecting...", "Logging in..."); + } + }; } else if (type.indexOf("custom") < 0 && !is_port_forward && !callback) { callback = function() { view.close(); } } else if (type == 'wait-remote-accept-nook') { diff --git a/src/ui/msgbox.tis b/src/ui/msgbox.tis index d5c60d95c..2099a8e7b 100644 --- a/src/ui/msgbox.tis +++ b/src/ui/msgbox.tis @@ -32,7 +32,7 @@ class MsgboxComponent: Reactor.Component { } function getIcon(color) { - if (this.type == "input-password") { + if (this.type == "input-password" || this.type == "session-login" || this.type == "session-login-password") { return ; } if (this.type == "connecting") { @@ -41,7 +41,7 @@ class MsgboxComponent: Reactor.Component { if (this.type == "success") { return ; } - if (this.type.indexOf("error") >= 0 || this.type == "re-input-password") { + if (this.type.indexOf("error") >= 0 || this.type == "re-input-password" || this.type == "session-re-login" || this.type == "session-login-re-password") { return ; } return null; @@ -56,11 +56,36 @@ class MsgboxComponent: Reactor.Component { ; } + function getInputUserPasswordContent() { + return
+
{translate("OS Username")}
+
+
{translate("OS Password")}
+ +
+
; + } + + function getXsessionPasswordContent() { + return
+
{translate("OS Username")}
+
+
{translate("OS Password")}
+ +
{translate('Please enter your password')}
+ +
{translate('Remember password')}
+
; + } + function getContent() { if (this.type == "input-password") { return this.getInputPasswordContent(); - } - if (this.type == "custom-os-password") { + } else if (this.type == "session-login") { + return this.getInputUserPasswordContent(); + } else if (this.type == "session-login-password") { + return this.getXsessionPasswordContent(); + } else if (this.type == "custom-os-password") { var ts = this.auto_login ? { checked: true } : {}; return
@@ -71,13 +96,13 @@ class MsgboxComponent: Reactor.Component { } function getColor() { - if (this.type == "input-password" || this.type == "custom-os-password") { + if (this.type == "input-password" || this.type == "custom-os-password" || this.type == "session-login" || this.type == "session-login-password") { return "#AD448E"; } if (this.type == "success") { return "#32bea6"; } - if (this.type.indexOf("error") >= 0 || this.type == "re-input-password") { + if (this.type.indexOf("error") >= 0 || this.type == "re-input-password" || this.type == "session-re-login" || this.type == "session-login-re-password") { return "#e04f5f"; } return "#2C8CFF"; @@ -177,6 +202,16 @@ class MsgboxComponent: Reactor.Component { this.update(); return; } + if (this.type == "session-re-login") { + this.type = "session-login"; + this.update(); + return; + } + if (this.type == "session-login-re-password") { + this.type = "session-login-password"; + this.update(); + return; + } var values = this.getValues(); if (this.callback) { var self = this; @@ -238,6 +273,21 @@ class MsgboxComponent: Reactor.Component { return; } } + if (this.type == "session-login") { + values.osusername = (values.osusername || "").trim(); + values.ospassword = (values.ospassword || "").trim(); + if (!values.osusername || !values.ospassword) { + return; + } + } + if (this.type == "session-login-password") { + values.password = (values.password || "").trim(); + values.osusername = (values.osusername || "").trim(); + values.ospassword = (values.ospassword || "").trim(); + if (!values.osusername || !values.ospassword || !values.password) { + return; + } + } return values; } diff --git a/src/ui/remote.rs b/src/ui/remote.rs index fc878cf1f..6d7dc7b42 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -404,7 +404,7 @@ impl sciter::EventHandler for SciterSession { fn is_file_transfer(); fn is_port_forward(); fn is_rdp(); - fn login(String, bool); + fn login(String, String, String, bool); fn new_rdp(); fn send_mouse(i32, i32, i32, bool, bool, bool, bool); fn enter(); diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 26d470fe0..3749b4918 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -512,10 +512,11 @@ pub fn get_error() -> String { #[cfg(target_os = "linux")] { let dtype = crate::platform::linux::get_display_server(); - if "wayland" == dtype { + if crate::platform::linux::DISPLAY_SERVER_WAYLAND == dtype + { return crate::server::wayland::common_get_error(); } - if dtype != "x11" { + if dtype != crate::platform::linux::DISPLAY_SERVER_X11 { return format!( "{} {}, {}", crate::client::translate("Unsupported display server".to_owned()), diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 3dee89a6e..796e13246 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -703,8 +703,14 @@ impl Session { fs::get_string(&path) } - pub fn login(&self, password: String, remember: bool) { - self.send(Data::Login((password, remember))); + 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 new_rdp(&self) { @@ -997,8 +1003,23 @@ impl Interface for Session { handle_hash(self.lc.clone(), pass, hash, self, peer).await; } - async fn handle_login_from_ui(&mut self, password: String, remember: bool, peer: &mut Stream) { - handle_login_from_ui(self.lc.clone(), password, remember, peer).await; + async fn handle_login_from_ui( + &mut 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(&mut self, t: TestDelay, peer: &mut Stream) {