From abb7748ee92d39998e702695fb85b4c02c6bc213 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Tue, 15 Jul 2025 16:32:14 +0800 Subject: [PATCH] refact: terminal, win, run as admin (#12300) Signed-off-by: fufesou --- Cargo.lock | 8 +- Cargo.toml | 2 +- flutter/lib/common.dart | 17 +- flutter/lib/common/widgets/dialog.dart | 67 ++++++-- flutter/lib/common/widgets/peer_card.dart | 38 ++++- flutter/lib/mobile/pages/home_page.dart | 6 + flutter/lib/models/model.dart | 14 +- src/client.rs | 33 +++- src/flutter_ffi.rs | 40 ++++- src/lang/ar.rs | 7 + src/lang/be.rs | 7 + src/lang/bg.rs | 7 + src/lang/ca.rs | 7 + src/lang/cn.rs | 7 + src/lang/cs.rs | 7 + src/lang/da.rs | 7 + src/lang/de.rs | 7 + src/lang/el.rs | 7 + src/lang/en.rs | 1 + src/lang/eo.rs | 7 + src/lang/es.rs | 7 + src/lang/et.rs | 7 + src/lang/eu.rs | 7 + src/lang/fa.rs | 7 + src/lang/fr.rs | 7 + src/lang/ge.rs | 7 + src/lang/he.rs | 7 + src/lang/hr.rs | 7 + src/lang/hu.rs | 7 + src/lang/id.rs | 7 + src/lang/it.rs | 7 + src/lang/ja.rs | 7 + src/lang/ko.rs | 7 + src/lang/kz.rs | 7 + src/lang/lt.rs | 7 + src/lang/lv.rs | 7 + src/lang/nb.rs | 7 + src/lang/nl.rs | 7 + src/lang/pl.rs | 7 + src/lang/pt_PT.rs | 7 + src/lang/ptbr.rs | 7 + src/lang/ro.rs | 7 + src/lang/ru.rs | 7 + src/lang/sc.rs | 7 + src/lang/sk.rs | 7 + src/lang/sl.rs | 7 + src/lang/sq.rs | 7 + src/lang/sr.rs | 7 + src/lang/sv.rs | 7 + src/lang/ta.rs | 7 + src/lang/template.rs | 7 + src/lang/th.rs | 7 + src/lang/tr.rs | 7 + src/lang/tw.rs | 7 + src/lang/uk.rs | 7 + src/lang/vi.rs | 7 + src/platform/windows.rs | 187 +++++++++++++++++++++- src/server/connection.rs | 164 ++++++++++++++++++- src/server/terminal_service.rs | 63 +++++++- 59 files changed, 920 insertions(+), 42 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 20e1b34ab..d73b36b09 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2218,9 +2218,8 @@ dependencies = [ [[package]] name = "filedescriptor" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" +version = "0.8.2" +source = "git+https://github.com/rustdesk-org/wezterm?branch=rustdesk/pty_based_0.8.1#80174f8009f41565f0fa8c66dab90d4f9211ae16" dependencies = [ "libc", "thiserror 1.0.61", @@ -5233,8 +5232,7 @@ dependencies = [ [[package]] name = "portable-pty" version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "806ee80c2a03dbe1a9fb9534f8d19e4c0546b790cde8fd1fea9d6390644cb0be" +source = "git+https://github.com/rustdesk-org/wezterm?branch=rustdesk/pty_based_0.8.1#80174f8009f41565f0fa8c66dab90d4f9211ae16" dependencies = [ "anyhow", "bitflags 1.3.2", diff --git a/Cargo.toml b/Cargo.toml index 06bfcaeb3..da8c3bff0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -98,7 +98,7 @@ ctrlc = "3.2" # arboard = { version = "3.4", features = ["wayland-data-control"] } arboard = { git = "https://github.com/rustdesk-org/arboard", features = ["wayland-data-control"] } clipboard-master = { git = "https://github.com/rustdesk-org/clipboard-master" } -portable-pty = "0.8.1" # higher version not work on rustc 1.75 +portable-pty = { git = "https://github.com/rustdesk-org/wezterm", branch = "rustdesk/pty_based_0.8.1", package = "portable-pty" } system_shutdown = "4.0" qrcode-generator = "4.1" diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 0fc8aa6c0..f54b88e88 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -2124,6 +2124,10 @@ enum UriLinkType { terminal, } +setEnvTerminalAdmin() { + bind.mainSetEnv(key: 'IS_TERMINAL_ADMIN', value: 'Y'); +} + // uri link handler bool handleUriLink({List? cmdArgs, Uri? uri, String? uriString}) { List? args; @@ -2191,6 +2195,12 @@ bool handleUriLink({List? cmdArgs, Uri? uri, String? uriString}) { id = args[i + 1]; i++; break; + case '--terminal-admin': + setEnvTerminalAdmin(); + type = UriLinkType.terminal; + id = args[i + 1]; + i++; + break; case '--password': password = args[i + 1]; i++; @@ -2264,7 +2274,8 @@ List? urlLinkToCmdArgs(Uri uri) { "view-camera", "port-forward", "rdp", - "terminal" + "terminal", + "terminal-admin", ]; if (uri.authority.isEmpty && uri.path.split('').every((char) => char == '/')) { @@ -2334,6 +2345,10 @@ List? urlLinkToCmdArgs(Uri uri) { } else if (command == '--terminal') { connect(Get.context!, id, isTerminal: true, forceRelay: forceRelay, password: password); + } else if (command == 'terminal-admin') { + setEnvTerminalAdmin(); + connect(Get.context!, id, + isTerminal: true, forceRelay: forceRelay, password: password); } else { // Default to remote desktop for '--connect', '--play', or direct connection connect(Get.context!, id, forceRelay: forceRelay, password: password); diff --git a/flutter/lib/common/widgets/dialog.dart b/flutter/lib/common/widgets/dialog.dart index 7c75f96f7..fc2334d58 100644 --- a/flutter/lib/common/widgets/dialog.dart +++ b/flutter/lib/common/widgets/dialog.dart @@ -819,23 +819,33 @@ void enterPasswordDialog( } void enterUserLoginDialog( - SessionID sessionId, OverlayDialogManager dialogManager) async { + SessionID sessionId, + OverlayDialogManager dialogManager, + String osAccountDescTip, + bool canRememberAccount) async { await _connectDialog( sessionId, dialogManager, osUsernameController: TextEditingController(), osPasswordController: TextEditingController(), + osAccountDescTip: osAccountDescTip, + canRememberAccount: canRememberAccount, ); } void enterUserLoginAndPasswordDialog( - SessionID sessionId, OverlayDialogManager dialogManager) async { + SessionID sessionId, + OverlayDialogManager dialogManager, + String osAccountDescTip, + bool canRememberAccount) async { await _connectDialog( sessionId, dialogManager, osUsernameController: TextEditingController(), osPasswordController: TextEditingController(), passwordController: TextEditingController(), + osAccountDescTip: osAccountDescTip, + canRememberAccount: canRememberAccount, ); } @@ -845,17 +855,28 @@ _connectDialog( TextEditingController? osUsernameController, TextEditingController? osPasswordController, TextEditingController? passwordController, + String? osAccountDescTip, + bool canRememberAccount = true, }) async { + final errUsername = ''.obs; var rememberPassword = false; if (passwordController != null) { rememberPassword = await bind.sessionGetRemember(sessionId: sessionId) ?? false; } var rememberAccount = false; - if (osUsernameController != null) { + if (canRememberAccount && osUsernameController != null) { rememberAccount = await bind.sessionGetRemember(sessionId: sessionId) ?? false; } + if (osUsernameController != null) { + osUsernameController.addListener(() { + if (errUsername.value.isNotEmpty) { + errUsername.value = ''; + } + }); + } + dialogManager.dismissAll(); dialogManager.show((setState, close, context) { cancel() { @@ -864,6 +885,13 @@ _connectDialog( } submit() { + if (osUsernameController != null) { + if (osUsernameController.text.trim().isEmpty) { + errUsername.value = translate('Empty Username'); + setState(() {}); + return; + } + } final osUsername = osUsernameController?.text.trim() ?? ''; final osPassword = osPasswordController?.text.trim() ?? ''; final password = passwordController?.text.trim() ?? ''; @@ -927,26 +955,39 @@ _connectDialog( } return Column( children: [ - descWidget(translate('login_linux_tip')), + if (osAccountDescTip != null) descWidget(translate(osAccountDescTip)), DialogTextField( title: translate(DialogTextField.kUsernameTitle), controller: osUsernameController, prefixIcon: DialogTextField.kUsernameIcon, errorText: null, ), + if (errUsername.value.isNotEmpty) + Align( + alignment: Alignment.centerLeft, + child: SelectableText( + errUsername.value, + style: TextStyle( + color: Theme.of(context).colorScheme.error, + fontSize: 12, + ), + textAlign: TextAlign.left, + ).paddingOnly(left: 12, bottom: 2), + ), PasswordWidget( controller: osPasswordController, autoFocus: false, ), - rememberWidget( - translate('remember_account_tip'), - rememberAccount, - (v) { - if (v != null) { - setState(() => rememberAccount = v); - } - }, - ), + if (canRememberAccount) + rememberWidget( + translate('remember_account_tip'), + rememberAccount, + (v) { + if (v != null) { + setState(() => rememberAccount = v); + } + }, + ), ], ); } diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index d664f3d80..4b52e6c46 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -492,6 +492,7 @@ abstract class BasePeerCard extends StatelessWidget { bool isTcpTunneling = false, bool isRDP = false, bool isTerminal = false, + bool isTerminalRunAsAdmin = false, }) { return MenuEntryButton( childBuilder: (TextStyle? style) => Text( @@ -499,6 +500,9 @@ abstract class BasePeerCard extends StatelessWidget { style: style, ), proc: () { + if (isTerminalRunAsAdmin) { + setEnvTerminalAdmin(); + } connectInPeerTab( context, peer, @@ -507,7 +511,7 @@ abstract class BasePeerCard extends StatelessWidget { isViewCamera: isViewCamera, isTcpTunneling: isTcpTunneling, isRDP: isRDP, - isTerminal: isTerminal, + isTerminal: isTerminal || isTerminalRunAsAdmin, ); }, padding: menuPadding, @@ -552,6 +556,15 @@ abstract class BasePeerCard extends StatelessWidget { ); } + @protected + MenuEntryBase _terminalRunAsAdminAction(BuildContext context) { + return _connectCommonAction( + context, + translate('Terminal (Run as administrator)'), + isTerminalRunAsAdmin: true, + ); + } + @protected MenuEntryBase _tcpTunnelingAction(BuildContext context) { return _connectCommonAction( @@ -906,6 +919,10 @@ class RecentPeerCard extends BasePeerCard { _terminalAction(context), ]; + if (peer.platform == kPeerPlatformWindows) { + menuItems.add(_terminalRunAsAdminAction(context)); + } + final List favs = (await bind.mainGetFav()).toList(); if (isDesktop && peer.platform != kPeerPlatformAndroid) { @@ -966,6 +983,11 @@ class FavoritePeerCard extends BasePeerCard { _viewCameraAction(context), _terminalAction(context), ]; + + if (peer.platform == kPeerPlatformWindows) { + menuItems.add(_terminalRunAsAdminAction(context)); + } + if (isDesktop && peer.platform != kPeerPlatformAndroid) { menuItems.add(_tcpTunnelingAction(context)); } @@ -1022,6 +1044,10 @@ class DiscoveredPeerCard extends BasePeerCard { _terminalAction(context), ]; + if (peer.platform == kPeerPlatformWindows) { + menuItems.add(_terminalRunAsAdminAction(context)); + } + final List favs = (await bind.mainGetFav()).toList(); if (isDesktop && peer.platform != kPeerPlatformAndroid) { @@ -1076,6 +1102,11 @@ class AddressBookPeerCard extends BasePeerCard { _viewCameraAction(context), _terminalAction(context), ]; + + if (peer.platform == kPeerPlatformWindows) { + menuItems.add(_terminalRunAsAdminAction(context)); + } + if (isDesktop && peer.platform != kPeerPlatformAndroid) { menuItems.add(_tcpTunnelingAction(context)); } @@ -1212,6 +1243,11 @@ class MyGroupPeerCard extends BasePeerCard { _viewCameraAction(context), _terminalAction(context), ]; + + if (peer.platform == kPeerPlatformWindows) { + menuItems.add(_terminalRunAsAdminAction(context)); + } + if (isDesktop && peer.platform != kPeerPlatformAndroid) { menuItems.add(_tcpTunnelingAction(context)); } diff --git a/flutter/lib/mobile/pages/home_page.dart b/flutter/lib/mobile/pages/home_page.dart index e35c8872c..651ec4f17 100644 --- a/flutter/lib/mobile/pages/home_page.dart +++ b/flutter/lib/mobile/pages/home_page.dart @@ -230,6 +230,12 @@ class WebHomePage extends StatelessWidget { id = args[i + 1]; i++; break; + case '--terminal-admin': + setEnvTerminalAdmin(); + isTerminal = true; + id = args[i + 1]; + i++; + break; case '--password': password = args[i + 1]; i++; diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index b15112025..017f2c9d1 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -836,10 +836,16 @@ class FfiModel with ChangeNotifier { } else if (type == 'input-password') { enterPasswordDialog(sessionId, dialogManager); } else if (type == 'session-login' || type == 'session-re-login') { - enterUserLoginDialog(sessionId, dialogManager); - } else if (type == 'session-login-password' || - type == 'session-login-password') { - enterUserLoginAndPasswordDialog(sessionId, dialogManager); + enterUserLoginDialog(sessionId, dialogManager, 'login_linux_tip', true); + } else if (type == 'session-login-password') { + enterUserLoginAndPasswordDialog( + sessionId, dialogManager, 'login_linux_tip', true); + } else if (type == 'terminal-admin-login') { + enterUserLoginDialog( + sessionId, dialogManager, 'terminal-admin-login-tip', false); + } else if (type == 'terminal-admin-login-password') { + enterUserLoginAndPasswordDialog( + sessionId, dialogManager, 'terminal-admin-login-tip', false); } else if (type == 'restarting') { showMsgBox(sessionId, type, title, text, link, false, dialogManager, hasCancel: false); diff --git a/src/client.rs b/src/client.rs index 4bda93d43..48d753756 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1611,6 +1611,7 @@ struct ConnToken { pub struct LoginConfigHandler { id: String, pub conn_type: ConnType, + pub is_terminal_admin: bool, hash: Hash, password: Vec, // remember password for reconnect pub remember: bool, @@ -1736,6 +1737,7 @@ impl LoginConfigHandler { self.other_server = Some((real_id.to_owned(), server.to_owned(), other_server_key)); } } + self.direct = None; self.received = false; self.switch_uuid = switch_uuid; @@ -1744,6 +1746,11 @@ impl LoginConfigHandler { self.shared_password = shared_password; self.record_state = false; self.record_permission = true; + + // `std::env::remove_var("IS_TERMINAL_ADMIN");` is called in `session_add_sync()` - `flutter_ffi.rs`. + let is_terminal_admin = conn_type == ConnType::TERMINAL + && std::env::var("IS_TERMINAL_ADMIN").map_or(false, |v| v == "Y"); + self.is_terminal_admin = is_terminal_admin; } /// Check if the client should auto login. @@ -1956,7 +1963,7 @@ impl LoginConfigHandler { .into(); } else if name == keys::OPTION_TERMINAL_PERSISTENT { config.terminal_persistent.v = !config.terminal_persistent.v; - option.terminal_persistent = (if config.terminal_persistent.v { + option.terminal_persistent = (if config.terminal_persistent.v { BoolOption::Yes } else { BoolOption::No @@ -3274,6 +3281,19 @@ pub async fn handle_hash( } lc.write().unwrap().password = password.clone(); + + let is_terminal_admin = lc.read().unwrap().is_terminal_admin; + let is_terminal = lc.read().unwrap().conn_type.eq(&ConnType::TERMINAL); + if is_terminal && is_terminal_admin { + if password.is_empty() { + interface.msgbox("terminal-admin-login-password", "", "", ""); + } else { + interface.msgbox("terminal-admin-login", "", "", ""); + } + lc.write().unwrap().hash = hash; + return; + } + let password = if password.is_empty() { // login without password, the remote side can click accept interface.msgbox("input-password", "Password Required", "", ""); @@ -3285,8 +3305,15 @@ pub async fn handle_hash( hasher.finalize()[..].into() }; - let os_username = lc.read().unwrap().get_option("os-username"); - let os_password = lc.read().unwrap().get_option("os-password"); + let is_terminal = lc.read().unwrap().conn_type.eq(&ConnType::TERMINAL); + let (os_username, os_password) = if is_terminal { + ("".to_owned(), "".to_owned()) + } else { + ( + lc.read().unwrap().get_option("os-username"), + lc.read().unwrap().get_option("os-password"), + ) + }; send_login(lc.clone(), os_username, os_password, password, peer).await; lc.write().unwrap().hash = hash; diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 5a6b66a0b..58afae528 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -138,7 +138,7 @@ pub fn session_add_sync( is_shared_password: bool, conn_token: Option, ) -> SyncReturn { - if let Err(e) = session_add( + let add_res = session_add( &session_id, &id, is_file_transfer, @@ -151,7 +151,14 @@ pub fn session_add_sync( password, is_shared_password, conn_token, - ) { + ); + // We can't put the remove call together with `std::env::var("IS_TERMINAL_ADMIN")`. + // Because there are some `bail!` in `session_add()`, we must make sure `IS_TERMINAL_ADMIN` is removed at last. + if is_terminal { + std::env::remove_var("IS_TERMINAL_ADMIN"); + } + + if let Err(e) = add_res { SyncReturn(format!("Failed to add session with id {}, {}", &id, e)) } else { SyncReturn("".to_owned()) @@ -1067,6 +1074,35 @@ pub fn main_get_env(key: String) -> SyncReturn { SyncReturn(std::env::var(key).unwrap_or_default()) } +// Dart does not support changing environment variables. +// `Platform.environment['MY_VAR'] = 'VAR';` will throw an error +// `Unsupported operation: Cannot modify unmodifiable map`. +// +// And we need to share the environment variables between rust and dart isolates sometimes. +pub fn main_set_env(key: String, value: Option) -> SyncReturn<()> { + let is_valid_key = !key.is_empty() && !key.contains('=') && !key.contains('\0'); + debug_assert!(is_valid_key, "Invalid environment variable key: {}", key); + if !is_valid_key { + log::error!("Invalid environment variable key: {}", key); + return SyncReturn(()); + } + + match value { + Some(v) => { + let is_valid_value = !v.contains('\0'); + debug_assert!(is_valid_value, "Invalid environment variable value: {}", v); + if !is_valid_value { + log::error!("Invalid environment variable value: {}", v); + return SyncReturn(()); + } + std::env::set_var(key, v); + } + None => std::env::remove_var(key), + } + + SyncReturn(()) +} + pub fn main_set_local_option(key: String, value: String) { let is_texture_render_key = key.eq(config::keys::OPTION_TEXTURE_RENDER); let is_d3d_render_key = key.eq(config::keys::OPTION_ALLOW_D3D_RENDER); diff --git a/src/lang/ar.rs b/src/lang/ar.rs index 38e212377..0560b1fb5 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", "تمكين الطرفية"), ("New tab", "تبويب جديد"), ("Keep terminal sessions on disconnect", "الاحتفاظ بجلسات الطرفية عند قطع الاتصال"), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/be.rs b/src/lang/be.rs index 85c424cae..d8973788c 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index 1bf1a4845..9863ac753 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index c6ece9ec1..9b0c94da3 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 18915464b..01b4f8ed8 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", "启用终端"), ("New tab", "新建选项卡"), ("Keep terminal sessions on disconnect", "断开连接时保持终端会话"), + ("Terminal (Run as administrator)", "终端(以管理员身份运行)"), + ("terminal-admin-login-tip", "请输入被控端的管理员账号密码。"), + ("Failed to get user token.", "获取用户令牌时出错。"), + ("Incorrect username or password.", "用户名或密码不正确。"), + ("The user is not an administrator.", "用户不是管理员。"), + ("Failed to check if the user is an administrator.", "检查用户是否为管理员时出错。"), + ("Supported only by the installation version.", "仅安装版本支持。"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 1b5a0b492..6faeed3c3 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 0aab88c9c..24ecd2eb8 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 54f32be63..73ea17877 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", "Terminal zulassen"), ("New tab", "Neuer Tab"), ("Keep terminal sessions on disconnect", "Terminalsitzungen beim Trennen der Verbindung beibehalten"), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index 2accbd9a7..c96e3f3af 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index 4570c8324..14904f076 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -256,5 +256,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("download-new-version-failed-tip", "Download failed. You can try again or click the \"Download\" button to download from the release page and upgrade manually."), ("update-failed-check-msi-tip", "Installation method check failed. Please click the \"Download\" button to download from the release page and upgrade manually."), ("websocket_tip", "When using WebSocket, only relay connections are supported."), + ("terminal-admin-login-tip", "Please input the administrator username and password of the controlled side."), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index a9dda55e7..b64926a72 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index ac588e710..7ef10e4b5 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index bde374601..bf6713833 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eu.rs b/src/lang/eu.rs index 46f3e8a9b..4309202db 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 1836c2742..9cd27927c 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", "فعال‌سازی ترمینال"), ("New tab", "زبانه جدید"), ("Keep terminal sessions on disconnect", "حفظ جلسات ترمینال پس از قطع اتصال"), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 5e1266fd8..7fbe290e3 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", "Activer le terminal"), ("New tab", "Nouvel onglet"), ("Keep terminal sessions on disconnect", "Maintenir les sessions du terminal lors de la déconnexion"), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ge.rs b/src/lang/ge.rs index 6adc2606d..f9fb90d06 100644 --- a/src/lang/ge.rs +++ b/src/lang/ge.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/he.rs b/src/lang/he.rs index af33c8c5f..c8254a324 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hr.rs b/src/lang/hr.rs index e1ea1837f..7063d3bda 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index df3044c6d..e88a4f59c 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", "Terminál engedélyezése"), ("New tab", "Új lap"), ("Keep terminal sessions on disconnect", "Terminál munkamenetek megtartása leválasztáskor"), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index fcc72431d..dde9c5d25 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 96974e36c..c9ebbcd87 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", "Abilita terminale"), ("New tab", "Nuova scheda"), ("Keep terminal sessions on disconnect", "Quando disconetti mantieni attiva sessione terminale"), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 14e322eeb..534b448e2 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 0efc42bdb..1ad948d5e 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", "터미널 사용함"), ("New tab", "새 탭"), ("Keep terminal sessions on disconnect", "터미널 세션 연결 해제 상태 유지"), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index b02bd1c0d..e0377af51 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index 598a54efd..963e8d48d 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 9ef5c38f0..d3f04a74a 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", "Iespējot termināli"), ("New tab", "Jauna cilne"), ("Keep terminal sessions on disconnect", "Atvienojoties saglabāt termināļa sesijas"), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nb.rs b/src/lang/nb.rs index c2719c86e..40c751283 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 0d4f6c808..8eef85b53 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", "Terminal inschakelen"), ("New tab", "Nieuw tabblad"), ("Keep terminal sessions on disconnect", "Terminalsessies bij verbreking van de verbinding behouden"), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 93b328f0a..1f5432fb0 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index c4fc78187..342656a65 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 1a715121d..e8ed440ac 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 41d86baab..adbc5c24f 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 0b40df0a7..cea299b15 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", "Включить терминал"), ("New tab", "Новая вкладка"), ("Keep terminal sessions on disconnect", "Сохранять сеансы терминала при отключении"), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sc.rs b/src/lang/sc.rs index 14d5b7e04..5405e8d42 100644 --- a/src/lang/sc.rs +++ b/src/lang/sc.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 439c90d00..25abf15d5 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 78551d37d..b4ff93c54 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index f8aa4e6b3..60bca13c5 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index f2be9629f..5b7cde1ac 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index ecd6122c5..a15047463 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ta.rs b/src/lang/ta.rs index 272766831..6ae5d833f 100644 --- a/src/lang/ta.rs +++ b/src/lang/ta.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index a4934e82e..c9ff2f20e 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 65774bffc..9d0c46809 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 8284dcc51..2fbcf78f9 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index b5e44d07a..bd23f3709 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/uk.rs b/src/lang/uk.rs index 7a5103ef3..a8aa6bd42 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vi.rs b/src/lang/vi.rs index 332c51d68..10690ef27 100644 --- a/src/lang/vi.rs +++ b/src/lang/vi.rs @@ -703,5 +703,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable terminal", ""), ("New tab", ""), ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only by the installation version.", ""), ].iter().cloned().collect(); } diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 45c5fc7ab..a00e9906b 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -40,7 +40,7 @@ use winapi::{ shared::{minwindef::*, ntdef::NULL, windef::*, winerror::*}, um::{ errhandlingapi::GetLastError, - handleapi::CloseHandle, + handleapi::{CloseHandle, INVALID_HANDLE_VALUE}, libloaderapi::{ GetProcAddress, LoadLibraryA, LoadLibraryExA, LOAD_LIBRARY_SEARCH_SYSTEM32, }, @@ -49,15 +49,19 @@ use winapi::{ GetCurrentProcess, GetCurrentProcessId, GetExitCodeProcess, OpenProcess, OpenProcessToken, ProcessIdToSessionId, PROCESS_INFORMATION, STARTUPINFOW, }, - securitybaseapi::GetTokenInformation, + securitybaseapi::{ + AllocateAndInitializeSid, DuplicateToken, EqualSid, FreeSid, GetTokenInformation, + }, shellapi::ShellExecuteW, sysinfoapi::{GetNativeSystemInfo, SYSTEM_INFO}, winbase::*, wingdi::*, winnt::{ - TokenElevation, ES_AWAYMODE_REQUIRED, ES_CONTINUOUS, ES_DISPLAY_REQUIRED, + SecurityImpersonation, TokenElevation, TokenGroups, TokenImpersonation, TokenType, + DOMAIN_ALIAS_RID_ADMINS, ES_AWAYMODE_REQUIRED, ES_CONTINUOUS, ES_DISPLAY_REQUIRED, ES_SYSTEM_REQUIRED, HANDLE, PROCESS_ALL_ACCESS, PROCESS_QUERY_LIMITED_INFORMATION, - TOKEN_ELEVATION, TOKEN_QUERY, + PSID, SECURITY_BUILTIN_DOMAIN_RID, SECURITY_NT_AUTHORITY, SID_IDENTIFIER_AUTHORITY, + TOKEN_ELEVATION, TOKEN_GROUPS, TOKEN_QUERY, TOKEN_TYPE, }, winreg::HKEY_CURRENT_USER, winspool::{ @@ -521,6 +525,10 @@ extern "C" { fn is_service_running_w(svc_name: *const u16) -> bool; } +pub fn get_current_session_id(share_rdp: bool) -> DWORD { + unsafe { get_current_session(if share_rdp { TRUE } else { FALSE }) } +} + extern "system" { fn BlockInput(v: BOOL) -> BOOL; } @@ -2158,6 +2166,177 @@ pub fn send_message_to_hnwd( return true; } +pub fn get_logon_user_token(user: &str, pwd: &str) -> ResultType { + let user_split = user.split("\\").collect::>(); + let wuser = wide_string(user_split.get(1).unwrap_or(&user)); + let wpc = wide_string(user_split.get(0).unwrap_or(&"")); + let wpwd = wide_string(pwd); + let mut ph_token: HANDLE = std::ptr::null_mut(); + let res = unsafe { + LogonUserW( + wuser.as_ptr(), + wpc.as_ptr(), + wpwd.as_ptr(), + LOGON32_LOGON_INTERACTIVE, + LOGON32_PROVIDER_DEFAULT, + &mut ph_token as _, + ) + }; + if res == FALSE { + bail!( + "Failed to log on user {}: {}", + user, + std::io::Error::last_os_error() + ); + } else { + if ph_token.is_null() { + bail!( + "Failed to log on user {}: {}", + user, + std::io::Error::last_os_error() + ); + } + Ok(ph_token) + } +} + +// Ensure the token returned is a primary token. +// If the provided token is an impersonation token, it duplicates it to a primary token. +// If the provided token is already a primary token, it returns it as is. +// The caller is responsible for closing the returned token handle. +pub fn ensure_primary_token(user_token: HANDLE) -> ResultType { + if user_token.is_null() || user_token == INVALID_HANDLE_VALUE { + bail!("Invalid user token provided"); + } + + unsafe { + let mut token_type: TOKEN_TYPE = 0; + let mut return_length: DWORD = 0; + + if GetTokenInformation( + user_token, + TokenType, + &mut token_type as *mut _ as *mut _, + std::mem::size_of::() as DWORD, + &mut return_length, + ) == FALSE + { + bail!( + "Failed to get token type, error {}", + io::Error::last_os_error() + ); + } + + if token_type == TokenImpersonation { + let mut duplicate_token: HANDLE = std::ptr::null_mut(); + let dup_res = DuplicateToken(user_token, SecurityImpersonation, &mut duplicate_token); + CloseHandle(user_token); + if dup_res == FALSE { + bail!( + "Failed to duplicate token, error {}", + io::Error::last_os_error() + ); + } + Ok(duplicate_token) + } else { + Ok(user_token) + } + } +} + +pub fn is_user_token_admin(user_token: HANDLE) -> ResultType { + if user_token.is_null() || user_token == INVALID_HANDLE_VALUE { + bail!("Invalid user token provided"); + } + + unsafe { + let mut dw_size: DWORD = 0; + GetTokenInformation( + user_token, + TokenGroups, + std::ptr::null_mut(), + 0, + &mut dw_size, + ); + + let last_error = GetLastError(); + if last_error != ERROR_INSUFFICIENT_BUFFER { + bail!( + "Failed to get token groups buffer size, error: {}", + last_error + ); + } + if dw_size == 0 { + bail!("Token groups buffer size is zero"); + } + + let mut buffer = vec![0u8; dw_size as usize]; + if GetTokenInformation( + user_token, + TokenGroups, + buffer.as_mut_ptr() as *mut _, + dw_size, + &mut dw_size, + ) == FALSE + { + bail!( + "Failed to get token groups information, error: {}", + io::Error::last_os_error() + ); + } + + let p_token_groups = buffer.as_ptr() as *const TOKEN_GROUPS; + let group_count = (*p_token_groups).GroupCount; + + if group_count == 0 { + return Ok(false); + } + + let mut nt_authority: SID_IDENTIFIER_AUTHORITY = SID_IDENTIFIER_AUTHORITY { + Value: SECURITY_NT_AUTHORITY, + }; + let mut administrators_group: PSID = std::ptr::null_mut(); + if AllocateAndInitializeSid( + &mut nt_authority, + 2, + SECURITY_BUILTIN_DOMAIN_RID, + DOMAIN_ALIAS_RID_ADMINS, + 0, + 0, + 0, + 0, + 0, + 0, + &mut administrators_group, + ) == FALSE + { + bail!( + "Failed to allocate administrators group SID, error: {}", + io::Error::last_os_error() + ); + } + if administrators_group.is_null() { + bail!("Failed to create administrators group SID"); + } + + let mut is_admin = false; + let groups = + std::slice::from_raw_parts((*p_token_groups).Groups.as_ptr(), group_count as usize); + for group in groups { + if EqualSid(administrators_group, group.Sid) == TRUE { + is_admin = true; + break; + } + } + + if !administrators_group.is_null() { + FreeSid(administrators_group); + } + + Ok(is_admin) + } +} + pub fn create_process_with_logon(user: &str, pwd: &str, exe: &str, arg: &str) -> ResultType<()> { let last_error_table = HashMap::from([ ( diff --git a/src/server/connection.rs b/src/server/connection.rs index 12a3061de..9daf24f78 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -56,6 +56,8 @@ use std::{ }; #[cfg(not(any(target_os = "android", target_os = "ios")))] use system_shutdown; +#[cfg(target_os = "windows")] +use windows::Win32::Foundation::{CloseHandle, HANDLE}; #[cfg(windows)] use crate::virtual_display_manager; @@ -172,6 +174,22 @@ pub enum AuthConnType { Terminal, } +#[cfg(not(any(target_os = "android", target_os = "ios")))] +#[derive(Clone, Debug)] +enum TerminalUserToken { + SelfUser, + CurrentLogonUser(crate::terminal_service::UserToken), +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +impl TerminalUserToken { + fn to_terminal_service_token(&self) -> Option { + match self { + TerminalUserToken::SelfUser => None, + TerminalUserToken::CurrentLogonUser(token) => Some(*token), + } + } +} pub struct Connection { inner: ConnInner, display_idx: usize, @@ -254,6 +272,11 @@ pub struct Connection { tx_post_seq: mpsc::UnboundedSender<(String, Value)>, terminal_service_id: String, terminal_persistent: bool, + // The user token must be set when terminal is enabled. + // 0 indicates SYSTEM user + // other values indicate current user + #[cfg(not(any(target_os = "android", target_os = "ios")))] + terminal_user_token: Option, terminal_generic_service: Option>, } @@ -418,6 +441,8 @@ impl Connection { tx_post_seq, terminal_service_id: "".to_owned(), terminal_persistent: false, + #[cfg(not(any(target_os = "android", target_os = "ios")))] + terminal_user_token: None, terminal_generic_service: None, }; let addr = hbb_common::try_into_v4(addr); @@ -1415,12 +1440,19 @@ impl Connection { .unwrap() .insert(self.lr.my_id.clone(), self.tx_input.clone()); + // Terminal feature is supported on desktop only + #[allow(unused_mut)] + let mut terminal = cfg!(not(any(target_os = "android", target_os = "ios"))); + #[cfg(target_os = "windows")] + { + terminal = terminal && portable_pty::win::check_support().is_ok(); + } pi.username = username; pi.sas_enabled = sas_enabled; pi.features = Some(Features { privacy_mode: privacy_mode::is_privacy_mode_supported(), #[cfg(not(any(target_os = "android", target_os = "ios")))] - terminal: true, // Terminal feature is supported on desktop only + terminal, ..Default::default() }) .into(); @@ -1429,7 +1461,9 @@ impl Connection { #[allow(unused_mut)] let mut wait_session_id_confirm = false; #[cfg(windows)] - self.handle_windows_specific_session(&mut pi, &mut wait_session_id_confirm); + if !self.terminal { + self.handle_windows_specific_session(&mut pi, &mut wait_session_id_confirm); + } if self.file_transfer.is_some() || self.terminal { res.set_peer_info(pi); } else if self.view_camera { @@ -1933,12 +1967,28 @@ impl Connection { sleep(1.).await; return false; } + #[cfg(target_os = "windows")] + if !lr.os_login.username.is_empty() && !crate::platform::is_installed() { + self.send_login_error("Supported only by the installation version.") + .await; + sleep(1.).await; + return false; + } + self.terminal = true; if let Some(o) = self.options_in_login.as_ref() { self.terminal_persistent = o.terminal_persistent.enum_value() == Ok(BoolOption::Yes); } self.terminal_service_id = terminal.service_id; + #[cfg(target_os = "windows")] + if let Some(msg) = + self.fill_terminal_user_token(&lr.os_login.username, &lr.os_login.password) + { + self.send_login_error(msg).await; + sleep(1.).await; + return false; + } } Some(login_request::Union::PortForward(mut pf)) => { if !Connection::permission("enable-tunnel") { @@ -2893,6 +2943,94 @@ impl Connection { true } + // Try to fill user token for terminal connection. + // If username is empty, use the user token of the current session. + // If username is not empty, try to logon and check if the user is an administrator. + // If the user is an administrator, use the user token of current process (SYSTEM). + // If the user is not an administrator, return an error message. + // Note: Only local and domain users are supported, Microsoft account (online account) not supported for now. + #[cfg(target_os = "windows")] + fn fill_terminal_user_token(&mut self, username: &str, password: &str) -> Option<&'static str> { + // No need to check if the password is empty. + if !username.is_empty() { + return self.handle_administrator_check(username, password); + } + + if crate::platform::is_prelogin() { + self.terminal_user_token = None; + return Some("No active console user logged on, please connect and logon first."); + } + + if crate::platform::is_installed() { + return self.handle_installed_user(); + } + + self.terminal_user_token = Some(TerminalUserToken::SelfUser); + None + } + + #[cfg(target_os = "windows")] + fn handle_administrator_check( + &mut self, + username: &str, + password: &str, + ) -> Option<&'static str> { + let check_admin_res = + crate::platform::get_logon_user_token(username, password).map(|token| { + let is_token_admin = crate::platform::is_user_token_admin(token); + unsafe { + hbb_common::allow_err!(CloseHandle(HANDLE(token as _))); + }; + is_token_admin + }); + match check_admin_res { + Ok(Ok(b)) => { + if b { + self.terminal_user_token = Some(TerminalUserToken::SelfUser); + None + } else { + Some("The user is not an administrator.") + } + } + Ok(Err(e)) => { + log::error!("Failed to check if the user is an administrator: {}", e); + Some("Failed to check if the user is an administrator.") + } + Err(e) => { + log::error!("Failed to get logon user token: {}", e); + Some("Incorrect username or password.") + } + } + } + + #[cfg(target_os = "windows")] + fn handle_installed_user(&mut self) -> Option<&'static str> { + let session_id = crate::platform::get_current_session_id(true); + if session_id == 0xFFFFFFFF { + return Some("Failed to get current session id."); + } + let token = crate::platform::get_user_token(session_id, true); + if !token.is_null() { + match crate::platform::ensure_primary_token(token) { + Ok(t) => { + self.terminal_user_token = Some(TerminalUserToken::CurrentLogonUser(t as _)); + } + Err(e) => { + log::error!("Failed to ensure primary token: {}", e); + self.terminal_user_token = + Some(TerminalUserToken::CurrentLogonUser(token as _)); + } + } + None + } else { + log::error!( + "Failed to get user token for terminal action, {}", + std::io::Error::last_os_error() + ); + Some("Failed to get user token.") + } + } + fn update_failure(&self, (mut failure, time): ((i32, i32, i32), i32), remove: bool, i: usize) { if remove { if failure.0 != 0 { @@ -3833,12 +3971,19 @@ impl Connection { #[cfg(not(any(target_os = "android", target_os = "ios")))] async fn init_terminal_service(&mut self) { + debug_assert!(self.terminal_user_token.is_some()); + let Some(user_token) = self.terminal_user_token.clone() else { + // unreachable, but keep it for safety + log::error!("Terminal user token is not set."); + return; + }; if self.terminal_service_id.is_empty() { self.terminal_service_id = terminal_service::generate_service_id(); } let s = Box::new(terminal_service::new( self.terminal_service_id.clone(), self.terminal_persistent, + user_token.to_terminal_service_token(), )); s.on_subscribe(self.inner.clone()); self.terminal_generic_service = Some(s); @@ -3846,9 +3991,15 @@ impl Connection { #[cfg(not(any(target_os = "android", target_os = "ios")))] async fn handle_terminal_action(&mut self, action: TerminalAction) -> ResultType<()> { + debug_assert!(self.terminal_user_token.is_some()); + let Some(user_token) = self.terminal_user_token.clone() else { + // unreacheable, but keep it for safety + bail!("Terminal user token is not set."); + }; let mut proxy = terminal_service::TerminalServiceProxy::new( self.terminal_service_id.clone(), Some(self.terminal_persistent), + user_token.to_terminal_service_token(), ); match proxy.handle_action(&action) { @@ -4249,6 +4400,15 @@ impl Drop for Connection { if let Some(s) = self.terminal_generic_service.as_ref() { s.join(); } + + #[cfg(target_os = "windows")] + if let Some(TerminalUserToken::CurrentLogonUser(token)) = self.terminal_user_token.take() { + if token != 0 { + unsafe { + hbb_common::allow_err!(CloseHandle(HANDLE(token as _))); + }; + } + } } } diff --git a/src/server/terminal_service.rs b/src/server/terminal_service.rs index d709454c9..23340e5e9 100644 --- a/src/server/terminal_service.rs +++ b/src/server/terminal_service.rs @@ -7,6 +7,7 @@ use portable_pty::{Child, CommandBuilder, PtySize}; use std::{ collections::{HashMap, VecDeque}, io::{Read, Write}, + ops::{Deref, DerefMut}, sync::{ atomic::{AtomicBool, Ordering}, mpsc::{self, Receiver, SyncSender}, @@ -271,17 +272,51 @@ pub fn get_terminal_session_count(include_zombie_tasks: bool) -> usize { c } -pub fn new(service_id: String, is_persistent: bool) -> GenericService { +pub type UserToken = u64; + +#[derive(Clone)] +pub struct TerminalService { + sp: GenericService, + user_token: Option, +} + +impl Deref for TerminalService { + type Target = ServiceTmpl; + + fn deref(&self) -> &Self::Target { + &self.sp + } +} + +impl DerefMut for TerminalService { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.sp + } +} + +pub fn get_service_name(source: VideoSource, idx: usize) -> String { + format!("{}{}", source.service_name_prefix(), idx) +} + +pub fn new( + service_id: String, + is_persistent: bool, + user_token: Option, +) -> GenericService { // Create the service with initial persistence setting allow_err!(get_or_create_service(service_id.clone(), is_persistent)); - let svc = EmptyExtraFieldService::new(service_id.clone(), false); + let svc = TerminalService { + sp: GenericService::new(service_id.clone(), false), + user_token, + }; GenericService::run(&svc.clone(), move |sp| run(sp, service_id.clone())); svc.sp } -fn run(sp: EmptyExtraFieldService, service_id: String) -> ResultType<()> { +fn run(sp: TerminalService, service_id: String) -> ResultType<()> { while sp.ok() { - let responses = TerminalServiceProxy::new(service_id.clone(), None).read_outputs(); + let responses = TerminalServiceProxy::new(service_id.clone(), None, sp.user_token.clone()) + .read_outputs(); for response in responses { let mut msg_out = Message::new(); msg_out.set_terminal_response(response); @@ -451,6 +486,7 @@ impl TerminalSession { } drop(input_tx); } + self.output_rx = None; // Wait for threads to finish // The reader thread should join before the writer thread on Windows. @@ -544,6 +580,8 @@ impl PersistentTerminalService { pub struct TerminalServiceProxy { service_id: String, is_persistent: bool, + #[cfg(target_os = "windows")] + user_token: Option, } pub fn set_persistent(service_id: &str, is_persistent: bool) -> Result<()> { @@ -556,7 +594,11 @@ pub fn set_persistent(service_id: &str, is_persistent: bool) -> Result<()> { } impl TerminalServiceProxy { - pub fn new(service_id: String, is_persistent: Option) -> Self { + pub fn new( + service_id: String, + is_persistent: Option, + _user_token: Option, + ) -> Self { // Get persistence from the service if it exists let is_persistent = is_persistent.unwrap_or(if let Some(service) = get_service(&service_id) { @@ -567,6 +609,8 @@ impl TerminalServiceProxy { TerminalServiceProxy { service_id, is_persistent, + #[cfg(target_os = "windows")] + user_token: _user_token, } } @@ -670,7 +714,14 @@ impl TerminalServiceProxy { // Use default shell for the platform let shell = get_default_shell(); log::debug!("Using shell: {}", shell); - let cmd = CommandBuilder::new(&shell); + + #[allow(unused_mut)] + let mut cmd = CommandBuilder::new(&shell); + + #[cfg(target_os = "windows")] + if let Some(token) = &self.user_token { + cmd.set_user_token(*token as _); + } log::debug!("Spawning shell process..."); let child = pty_pair