Merge pull request #3902 from fufesou/feat/linux_virtual_display

Feat/linux virtual display (headless linux)
This commit is contained in:
RustDesk 2023-04-03 14:28:50 +08:00 committed by GitHub
commit cb66c6f9f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
69 changed files with 2662 additions and 849 deletions

View File

@ -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 "*"

View File

@ -11,4 +11,3 @@ jobs:
uses: ./.github/workflows/flutter-build.yml
with:
upload-artifact: true

54
Cargo.lock generated
View File

@ -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"

View File

@ -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"

View File

@ -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)

View File

@ -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<bool?>? 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,
);
});
}

View File

@ -225,9 +225,7 @@ class _ConnectionPageState extends State<ConnectionPage>
children: [
Button(
isOutline: true,
onTap: () {
onConnect(isFileTransfer: true);
},
onTap: () => onConnect(isFileTransfer: true),
text: "Transfer File",
),
const SizedBox(

View File

@ -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) {

View File

@ -548,19 +548,39 @@ class _RemotePageState extends State<RemotePage> {
more.add(PopupMenuItem<String>(
child: Text(translate('Refresh')), value: 'refresh'));
}
more.add(PopupMenuItem<String>(
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<String>(
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<String>(
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<String>(
@ -657,6 +677,8 @@ class _RemotePageState extends State<RemotePage> {
} 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) {

View File

@ -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<String, dynamic> platform_additions = {};
bool get is_wayland => platform_additions['is_wayland'] == true;
bool get is_headless => platform_additions['headless'] == true;
}
const canvasKey = 'canvas';

View File

@ -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; }

View File

@ -64,11 +64,15 @@ lazy_static::lazy_static! {
pub static ref APP_HOME_DIR: Arc<RwLock<String>> = 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)
}

View File

@ -0,0 +1,5 @@
#%PAM-1.0
@include common-auth
@include common-account
@include common-session
@include common-password

5
res/pam.d/rustdesk.suse Normal file
View File

@ -0,0 +1,5 @@
#%PAM-1.0
auth include common-auth
account include common-account
session include common-session
password include common-password

130
res/startwm.sh Executable file
View File

@ -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

30
res/xorg.conf Normal file
View File

@ -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"
SubSectionSub "Display"
Depth 24
Modes "1920x1080" "1280x720"
EndSubSection
EndSection

View File

@ -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) {

View File

@ -1578,7 +1578,12 @@ impl LoginConfigHandler {
}
/// Create a [`Message`] for login.
fn create_login_msg(&self, password: Vec<u8>) -> Message {
fn create_login_msg(
&self,
os_username: String,
os_password: String,
password: Vec<u8>,
) -> 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<HashMap<&'static str, LoginErrorMsgBox>> = {
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<RwLock<LoginConfigHandler>>, password: Vec<u8>, peer: &mut Stream) {
let msg_out = lc.read().unwrap().create_login_msg(password);
async fn send_login(
lc: Arc<RwLock<LoginConfigHandler>>,
os_username: String,
os_password: String,
password: Vec<u8>,
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<RwLock<LoginConfigHandler>>, password: Vec<u8>, 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<RwLock<LoginConfigHandler>>,
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<RwLock<LoginConfigHandler>>;
@ -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)),

View File

@ -360,9 +360,9 @@ impl<T: InvokeUiSession> Remote<T> {
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<T: InvokeUiSession> Remote<T> {
},
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();

View File

@ -224,6 +224,11 @@ pub fn core_main() -> Option<Vec<String>> {
// 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());

View File

@ -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")]

View File

@ -136,9 +136,15 @@ pub fn session_get_option(id: String, arg: String) -> Option<String> {
}
}
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);
}
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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", "11 傳輸模式"),
("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", "11 傳輸模式"),
("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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}
}
}

View File

@ -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<AtomicBool> = Arc::new(AtomicBool::new(false));
static ref DESKTOP_MANAGER: Arc<Mutex<Option<DesktopManager>>> = Arc::new(Mutex::new(None));
}
#[derive(Debug)]
struct DesktopManager {
seat0_username: String,
seat0_display_server: String,
child_username: String,
child_exit: Arc<AtomicBool>,
is_child_running: Arc<AtomicBool>,
}
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<String> {
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<u32> {
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<String>,
is_child_running: Arc<AtomicBool>,
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<String> {
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::<u8>()))
.collect::<String>();
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<Child> {
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<Child> {
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"
}
}

View File

@ -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")))]

View File

@ -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;
}
_ => {}
}

View File

@ -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<()> {

View File

@ -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<String>,
options_in_login: Option<OptionMessage>,
pressed_modifiers: HashSet<rdev::Key>,
#[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<Message>)>();
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<ipc::Data>,
tx_from_cm: mpsc::UnboundedSender<ipc::Data>,
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::<i32>().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! {

View File

@ -221,6 +221,7 @@ impl<T: Subscriber + From<ConnInner>> ServiceTmpl<T> {
thread::sleep(interval - elapsed);
}
}
log::info!("Service {} exit", sp.name());
});
self.0.write().unwrap().handle = Some(thread);
}
@ -256,6 +257,7 @@ impl<T: Subscriber + From<ConnInner>> ServiceTmpl<T> {
}
thread::sleep(time::Duration::from_millis(HIBERNATE_TIMEOUT));
}
log::info!("Service {} exit", sp.name());
});
self.0.write().unwrap().handle = Some(thread);
}

View File

@ -142,6 +142,10 @@ div.password input {
font-size: 1.2em;
}
div.username input {
font-size: 1.2em;
}
svg {
background: none;
}

View File

@ -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') {

View File

@ -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 <svg viewBox="0 0 505 505"><circle cx="252.5" cy="252.5" r="252.5" fill={color}/><path d="M271.9 246.1c29.2 17.5 67.6 13.6 92.7-11.5 29.7-29.7 29.7-77.8 0-107.4s-77.8-29.7-107.4 0c-25.1 25.1-29 63.5-11.5 92.7L118.1 347.4l26.2 26.2 26.4 26.4 10.6-10.6-10.1-10.1 9.7-9.7 10.1 10.1 10.6-10.6-10.1-10 9.7-9.7 10.1 10.1 10.6-10.6-26.4-26.3 76.4-76.5z" fill="#fff"/><circle cx="337.4" cy="154.4" r="17.7" fill={color}/></svg>;
}
if (this.type == "connecting") {
@ -41,7 +41,7 @@ class MsgboxComponent: Reactor.Component {
if (this.type == "success") {
return <svg viewBox="0 0 512 512"><circle cx="256" cy="256" r="256" fill={color} /><path fill="#fff" d="M235.472 392.08l-121.04-94.296 34.416-44.168 74.328 57.904 122.672-177.016 46.032 31.888z"/></svg>;
}
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 <svg viewBox="0 0 512 512"><ellipse cx="256" cy="256" rx="256" ry="255.832" fill={color}/><g fill="#fff"><path d="M376.812 337.18l-39.592 39.593-201.998-201.999 39.592-39.592z"/><path d="M376.818 174.825L174.819 376.824l-39.592-39.592 201.999-201.999z"/></g></svg>;
}
return null;
@ -56,11 +56,36 @@ class MsgboxComponent: Reactor.Component {
</div>;
}
function getInputUserPasswordContent() {
return <div .form>
<div>{translate("OS Username")}</div>
<div .username><input name='osusername' type='text' .outline-focus /></div>
<div>{translate("OS Password")}</div>
<PasswordComponent name='ospassword' />
<div></div>
</div>;
}
function getXsessionPasswordContent() {
return <div .form>
<div>{translate("OS Username")}</div>
<div .username><input name='osusername' type='text' .outline-focus /></div>
<div>{translate("OS Password")}</div>
<PasswordComponent name='ospassword' />
<div>{translate('Please enter your password')}</div>
<PasswordComponent />
<div><button|checkbox(remember) {ts}>{translate('Remember password')}</button></div>
</div>;
}
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 <div .form>
<PasswordComponent value={this.content} />
@ -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;
}

View File

@ -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();

View File

@ -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()),

View File

@ -703,8 +703,14 @@ impl<T: InvokeUiSession> Session<T> {
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<T: InvokeUiSession> Interface for Session<T> {
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) {