diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 44d197e763eb..2758ce5b2878 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -2990,3 +2990,83 @@ ColorFilter? svgColor(Color? color) { return ColorFilter.mode(color, BlendMode.srcIn); } } + +// ignore: must_be_immutable +class ComboBox extends StatelessWidget { + late final List keys; + late final List values; + late final String initialKey; + late final Function(String key) onChanged; + late final bool enabled; + late String current; + + ComboBox({ + Key? key, + required this.keys, + required this.values, + required this.initialKey, + required this.onChanged, + this.enabled = true, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + var index = keys.indexOf(initialKey); + if (index < 0) { + index = 0; + } + var ref = values[index].obs; + current = keys[index]; + return Container( + decoration: BoxDecoration( + border: Border.all( + color: enabled + ? MyTheme.color(context).border2 ?? MyTheme.border + : MyTheme.border, + ), + borderRadius: + BorderRadius.circular(8), //border raiuds of dropdown button + ), + height: 42, // should be the height of a TextField + child: Obx(() => DropdownButton( + isExpanded: true, + value: ref.value, + elevation: 16, + underline: Container(), + style: TextStyle( + color: enabled + ? Theme.of(context).textTheme.titleMedium?.color + : disabledTextColor(context, enabled)), + icon: const Icon( + Icons.expand_more_sharp, + size: 20, + ).marginOnly(right: 15), + onChanged: enabled + ? (String? newValue) { + if (newValue != null && newValue != ref.value) { + ref.value = newValue; + current = newValue; + onChanged(keys[values.indexOf(newValue)]); + } + } + : null, + items: values.map>((String value) { + return DropdownMenuItem( + value: value, + child: Text( + value, + style: const TextStyle(fontSize: 15), + overflow: TextOverflow.ellipsis, + ).marginOnly(left: 15), + ); + }).toList(), + )), + ).marginOnly(bottom: 5); + } +} + +Color? disabledTextColor(BuildContext context, bool enabled) { + return enabled + ? null + : Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6); +} diff --git a/flutter/lib/common/widgets/dialog.dart b/flutter/lib/common/widgets/dialog.dart index e7a720c8a052..6cfa31af9f77 100644 --- a/flutter/lib/common/widgets/dialog.dart +++ b/flutter/lib/common/widgets/dialog.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; @@ -1862,6 +1863,7 @@ void enter2FaDialog( }); } +// This dialog should not be dismissed, otherwise it will be black screen, have not reproduced this. void showWindowsSessionsDialog( String type, String title, @@ -1870,97 +1872,40 @@ void showWindowsSessionsDialog( SessionID sessionId, String peerId, String sessions) { - List sessionsList = sessions.split(','); - Map sessionMap = {}; + List sessionsList = []; + try { + sessionsList = json.decode(sessions); + } catch (e) { + print(e); + } + List sids = []; + List names = []; for (var session in sessionsList) { - var sessionInfo = session.split('-'); - if (sessionInfo.isNotEmpty) { - sessionMap[sessionInfo[0]] = sessionInfo[1]; - } + sids.add(session['sid']); + names.add(session['name']); } - String selectedUserValue = sessionMap.keys.first; + String selectedUserValue = sids.first; dialogManager.dismissAll(); dialogManager.show((setState, close, context) { - onConnect() { - bind.sessionReconnect( - sessionId: sessionId, - forceRelay: false, - userSessionId: selectedUserValue); - dialogManager.dismissAll(); - dialogManager.showLoading(translate('Connecting...'), - onCancel: closeConnection); + submit() { + bind.sessionSendSelectedSessionId( + sessionId: sessionId, sid: selectedUserValue); + close(); } return CustomAlertDialog( title: null, content: msgboxContent(type, title, text), actions: [ - SessionsDropdown(peerId, sessionId, sessionMap, (value) { - setState(() { - selectedUserValue = value; - }); - }), - dialogButton('Connect', onPressed: onConnect, isOutline: false), + ComboBox( + keys: sids, + values: names, + initialKey: selectedUserValue, + onChanged: (value) { + selectedUserValue = value; + }), + dialogButton('Connect', onPressed: submit, isOutline: false), ], ); }); } - -class SessionsDropdown extends StatefulWidget { - final String peerId; - final SessionID sessionId; - final Map sessions; - final Function(String) onValueChanged; - - SessionsDropdown( - this.peerId, this.sessionId, this.sessions, this.onValueChanged); - - @override - _SessionsDropdownState createState() => _SessionsDropdownState(); -} - -class _SessionsDropdownState extends State { - late String selectedValue; - @override - void initState() { - super.initState(); - selectedValue = widget.sessions.keys.first; - } - - @override - Widget build(BuildContext context) { - return Container( - width: 300, - child: DropdownButton( - value: selectedValue, - isExpanded: true, - borderRadius: BorderRadius.circular(8), - padding: EdgeInsets.symmetric(horizontal: 10, vertical: 5), - items: widget.sessions.entries.map((entry) { - return DropdownMenuItem( - value: entry.key, - child: Text( - entry.value, - style: TextStyle( - color: MyTheme.currentThemeMode() == ThemeMode.dark - ? Colors.white - : MyTheme.dark, - ), - ), - ); - }).toList(), - onChanged: (value) { - if (value != null) { - setState(() { - selectedValue = value; - }); - widget.onValueChanged(value); - } - }, - style: TextStyle( - fontSize: 16.0, - ), - ), - ); - } -} diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 7e82dd359efe..1abea9f5e2fd 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -514,7 +514,7 @@ class _GeneralState extends State<_General> { if (!keys.contains(currentKey)) { currentKey = ''; } - return _ComboBox( + return ComboBox( keys: keys, values: values, initialKey: currentKey, @@ -600,7 +600,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { child: Text( translate('enable-2fa-title'), style: - TextStyle(color: _disabledTextColor(context, enabled)), + TextStyle(color: disabledTextColor(context, enabled)), )) ], )), @@ -654,7 +654,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { } return _Card(title: 'Permissions', children: [ - _ComboBox( + ComboBox( keys: [ '', 'full', @@ -761,7 +761,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { Text( value, style: TextStyle( - color: _disabledTextColor( + color: disabledTextColor( context, onChanged != null)), ), ], @@ -781,7 +781,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { final usePassword = model.approveMode != 'click'; return _Card(title: 'Password', children: [ - _ComboBox( + ComboBox( enabled: !locked, keys: modeKeys, values: modeValues, @@ -841,7 +841,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { Expanded( child: Text(translate('Enable RDP session sharing'), style: - TextStyle(color: _disabledTextColor(context, enabled))), + TextStyle(color: disabledTextColor(context, enabled))), ) ], ).marginOnly(left: _kCheckBoxLeftMargin), @@ -944,7 +944,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { child: Text( translate('Use IP Whitelisting'), style: - TextStyle(color: _disabledTextColor(context, enabled)), + TextStyle(color: disabledTextColor(context, enabled)), )) ], )), @@ -988,7 +988,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { child: Text( translate('Hide connection management window'), style: TextStyle( - color: _disabledTextColor( + color: disabledTextColor( context, enabled && enableHideCm)), ), ), @@ -1686,12 +1686,6 @@ Widget _Card( ); } -Color? _disabledTextColor(BuildContext context, bool enabled) { - return enabled - ? null - : Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6); -} - // ignore: non_constant_identifier_names Widget _OptionCheckBox(BuildContext context, String label, String key, {Function()? update, @@ -1740,7 +1734,7 @@ Widget _OptionCheckBox(BuildContext context, String label, String key, Expanded( child: Text( translate(label), - style: TextStyle(color: _disabledTextColor(context, enabled)), + style: TextStyle(color: disabledTextColor(context, enabled)), )) ], ), @@ -1777,7 +1771,7 @@ Widget _Radio(BuildContext context, overflow: autoNewLine ? null : TextOverflow.ellipsis, style: TextStyle( fontSize: _kContentFontSize, - color: _disabledTextColor(context, enabled))) + color: disabledTextColor(context, enabled))) .marginOnly(left: 5), ), ], @@ -1827,7 +1821,7 @@ Widget _SubLabeledWidget(BuildContext context, String label, Widget child, children: [ Text( '${translate(label)}: ', - style: TextStyle(color: _disabledTextColor(context, enabled)), + style: TextStyle(color: disabledTextColor(context, enabled)), ), SizedBox( width: 10, @@ -1891,7 +1885,7 @@ _LabeledTextField( '${translate(label)}:', textAlign: TextAlign.right, style: TextStyle( - fontSize: 16, color: _disabledTextColor(context, enabled)), + fontSize: 16, color: disabledTextColor(context, enabled)), ).marginOnly(right: 10)), Expanded( child: TextField( @@ -1901,87 +1895,13 @@ _LabeledTextField( decoration: InputDecoration( errorText: errorText.isNotEmpty ? errorText : null), style: TextStyle( - color: _disabledTextColor(context, enabled), + color: disabledTextColor(context, enabled), )), ), ], ).marginOnly(bottom: 8); } -// ignore: must_be_immutable -class _ComboBox extends StatelessWidget { - late final List keys; - late final List values; - late final String initialKey; - late final Function(String key) onChanged; - late final bool enabled; - late String current; - - _ComboBox({ - Key? key, - required this.keys, - required this.values, - required this.initialKey, - required this.onChanged, - this.enabled = true, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - var index = keys.indexOf(initialKey); - if (index < 0) { - index = 0; - } - var ref = values[index].obs; - current = keys[index]; - return Container( - decoration: BoxDecoration( - border: Border.all( - color: enabled - ? MyTheme.color(context).border2 ?? MyTheme.border - : MyTheme.border, - ), - borderRadius: - BorderRadius.circular(8), //border raiuds of dropdown button - ), - height: 42, // should be the height of a TextField - child: Obx(() => DropdownButton( - isExpanded: true, - value: ref.value, - elevation: 16, - underline: Container(), - style: TextStyle( - color: enabled - ? Theme.of(context).textTheme.titleMedium?.color - : _disabledTextColor(context, enabled)), - icon: const Icon( - Icons.expand_more_sharp, - size: 20, - ).marginOnly(right: 15), - onChanged: enabled - ? (String? newValue) { - if (newValue != null && newValue != ref.value) { - ref.value = newValue; - current = newValue; - onChanged(keys[values.indexOf(newValue)]); - } - } - : null, - items: values.map>((String value) { - return DropdownMenuItem( - value: value, - child: Text( - value, - style: const TextStyle(fontSize: _kContentFontSize), - overflow: TextOverflow.ellipsis, - ).marginOnly(left: 15), - ); - }).toList(), - )), - ).marginOnly(bottom: 5); - } -} - class _CountDownButton extends StatefulWidget { _CountDownButton({ Key? key, diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 6a0c8d6d31e0..80fc0677a2bc 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -245,8 +245,8 @@ class FfiModel with ChangeNotifier { var name = evt['name']; if (name == 'msgbox') { handleMsgBox(evt, sessionId, peerId); - } else if (name == 'set_multiple_user_session') { - handleMultipleUserSession(evt, sessionId, peerId); + } else if (name == 'set_multiple_windows_session') { + handleMultipleWindowsSession(evt, sessionId, peerId); } else if (name == 'peer_info') { handlePeerInfo(evt, peerId, false); } else if (name == 'sync_peer_info') { @@ -490,7 +490,7 @@ class FfiModel with ChangeNotifier { dialogManager.dismissByTag(tag); } - handleMultipleUserSession( + handleMultipleWindowsSession( Map evt, SessionID sessionId, String peerId) { if (parent.target == null) return; final dialogManager = parent.target!.dialogManager; @@ -564,8 +564,7 @@ class FfiModel with ChangeNotifier { void reconnect(OverlayDialogManager dialogManager, SessionID sessionId, bool forceRelay) { - bind.sessionReconnect( - sessionId: sessionId, forceRelay: forceRelay, userSessionId: ""); + bind.sessionReconnect(sessionId: sessionId, forceRelay: forceRelay); clearPermissions(); dialogManager.dismissAll(); dialogManager.showLoading(translate('Connecting...'), diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index 66df3a656da3..f816e3d6260b 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -122,11 +122,12 @@ message PeerInfo { // Use JSON's key-value format which is friendly for peer to handle. // NOTE: Only support one-level dictionaries (for peer to update), and the key is of type string. string platform_additions = 12; + WindowsSessions windows_sessions = 13; } -message RdpUserSession { - string user_session_id = 1; - string user_name = 2; +message WindowsSession { + uint32 sid = 1; + string name = 2; } message LoginResponse { @@ -594,7 +595,7 @@ message OptionMessage { BoolOption disable_keyboard = 12; // Position 13 is used for Resolution. Remove later. // Resolution custom_resolution = 13; - string user_session = 14; + BoolOption support_windows_specific_session = 14; } message TestDelay { @@ -709,8 +710,9 @@ message PluginFailure { string msg = 3; } -message RdpUserSessions { - repeated RdpUserSession rdp_user_sessions = 1; +message WindowsSessions { + repeated WindowsSession sessions = 1; + uint32 current_sid = 2; } message Misc { @@ -744,7 +746,7 @@ message Misc { ToggleVirtualDisplay toggle_virtual_display = 32; TogglePrivacyMode toggle_privacy_mode = 33; SupportedEncoding supported_encoding = 34; - RdpUserSessions rdp_user_sessions = 35; + uint32 selected_sid = 35; } } diff --git a/src/client.rs b/src/client.rs index 45eac5abd715..c3f240fb58fd 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1149,7 +1149,7 @@ pub struct LoginConfigHandler { pub custom_fps: Arc>>, pub adapter_luid: Option, pub mark_unsupported: Vec, - pub selected_user_session_id: String, + pub selected_windows_session_id: Option, } impl Deref for LoginConfigHandler { @@ -1236,7 +1236,7 @@ impl LoginConfigHandler { self.received = false; self.switch_uuid = switch_uuid; self.adapter_luid = adapter_luid; - self.selected_user_session_id = "".to_owned(); + self.selected_windows_session_id = None; } /// Check if the client should auto login. @@ -1518,7 +1518,7 @@ impl LoginConfigHandler { } let mut n = 0; let mut msg = OptionMessage::new(); - msg.user_session = self.selected_user_session_id.clone(); + msg.support_windows_specific_session = BoolOption::Yes.into(); n += 1; if self.conn_type.eq(&ConnType::FILE_TRANSFER) { @@ -2595,7 +2595,7 @@ pub async fn handle_hash( peer: &mut Stream, ) { lc.write().unwrap().hash = hash.clone(); - let uuid = lc.read().unwrap().switch_uuid.clone(); + let uuid = lc.write().unwrap().switch_uuid.take(); if let Some(uuid) = uuid { if let Ok(uuid) = uuid::Uuid::from_str(&uuid) { send_switch_login_request(lc.clone(), peer, uuid).await; @@ -2744,7 +2744,7 @@ pub trait Interface: Send + Clone + 'static + Sized { fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str); fn handle_login_error(&self, err: &str) -> bool; fn handle_peer_info(&self, pi: PeerInfo); - fn set_multiple_user_sessions(&self, sessions: Vec); + fn set_multiple_windows_session(&self, sessions: Vec); fn on_error(&self, err: &str) { self.msgbox("error", "Error", err, ""); } diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 82b87a690c0a..ec2fdd3e7422 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1314,13 +1314,6 @@ impl Remote { } } Some(message::Union::Misc(misc)) => match misc.union { - Some(misc::Union::RdpUserSessions(sessions)) => { - if !sessions.rdp_user_sessions.is_empty() { - self.handler - .set_multiple_user_session(sessions.rdp_user_sessions); - return false; - } - } Some(misc::Union::AudioFormat(f)) => { self.audio_sender.send(MediaData::AudioFormat(f)).ok(); } diff --git a/src/flutter.rs b/src/flutter.rs index 160e40c3b27b..b6b0b70ddbc2 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -826,15 +826,21 @@ impl InvokeUiSession for FlutterHandler { ) } - fn set_multiple_user_session(&self, sessions: Vec) { - let formatted_sessions: Vec = sessions - .iter() - .map(|session| format!("{}-{}", session.user_session_id, session.user_name)) - .collect(); - let sessions = formatted_sessions.join(","); + fn set_multiple_windows_session(&self, sessions: Vec) { + let mut msg_vec = Vec::new(); + let mut sessions = sessions; + for d in sessions.drain(..) { + let mut h: HashMap<&str, String> = Default::default(); + h.insert("sid", d.sid.to_string()); + h.insert("name", d.name); + msg_vec.push(h); + } self.push_event( - "set_multiple_user_session", - vec![("user_sessions", &sessions)], + "set_multiple_windows_session", + vec![( + "user_sessions", + &serde_json::ser::to_string(&msg_vec).unwrap_or("".to_owned()), + )], ); } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index cb68a8480e5c..0a9b6183c6ef 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -217,9 +217,9 @@ pub fn session_record_status(session_id: SessionID, status: bool) { } } -pub fn session_reconnect(session_id: SessionID, force_relay: bool, user_session_id: String) { +pub fn session_reconnect(session_id: SessionID, force_relay: bool) { if let Some(session) = sessions::get_session_by_session_id(&session_id) { - session.reconnect(force_relay, user_session_id); + session.reconnect(force_relay); } session_on_waiting_for_image_dialog_show(session_id); } @@ -701,6 +701,12 @@ pub fn session_set_size(_session_id: SessionID, _display: usize, _width: usize, super::flutter::session_set_size(_session_id, _display, _width, _height) } +pub fn session_send_selected_session_id(session_id: SessionID, sid: String) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.send_selected_session_id(sid); + } +} + pub fn main_get_sound_inputs() -> Vec { #[cfg(not(any(target_os = "android", target_os = "ios")))] return get_sound_inputs(); diff --git a/src/platform/windows.cc b/src/platform/windows.cc index 68d194c5028e..e6f3c4fc0378 100644 --- a/src/platform/windows.cc +++ b/src/platform/windows.cc @@ -434,17 +434,6 @@ extern "C" return nout; } - uint32_t get_current_process_session_id() - { - DWORD sessionId = 0; - HANDLE hProcess = GetCurrentProcess(); - if (hProcess) { - ProcessIdToSessionId(GetCurrentProcessId(), &sessionId); - CloseHandle(hProcess); - } - return sessionId; - } - uint32_t get_session_user_info(PWSTR bufin, uint32_t nin, BOOL rdp, uint32_t id) { uint32_t nout = 0; diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 59e155dbc0d5..fac8ffa62db3 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -12,9 +12,10 @@ use hbb_common::{ bail, config::{self, Config}, log, - message_proto::Resolution, + message_proto::{Resolution, WindowsSession}, sleep, timeout, tokio, }; +use sha2::digest::generic_array::functional::FunctionalSequence; use std::process::{Command, Stdio}; use std::{ collections::HashMap, @@ -38,7 +39,7 @@ use winapi::{ minwinbase::STILL_ACTIVE, processthreadsapi::{ GetCurrentProcess, GetCurrentProcessId, GetExitCodeProcess, OpenProcess, - OpenProcessToken, PROCESS_INFORMATION, STARTUPINFOW, + OpenProcessToken, ProcessIdToSessionId, PROCESS_INFORMATION, STARTUPINFOW, }, securitybaseapi::GetTokenInformation, shellapi::ShellExecuteW, @@ -511,8 +512,11 @@ async fn run_service(_arguments: Vec) -> ResultType<()> { let mut incoming = ipc::new_listener(crate::POSTFIX_SERVICE).await?; let mut stored_usid = None; loop { - let sids = get_all_active_session_ids(); - if !sids.contains(&format!("{}", session_id)) || !is_share_rdp() { + let sids: Vec<_> = get_available_sessions(false) + .iter() + .map(|e| e.sid) + .collect(); + if !sids.contains(&session_id) || !is_share_rdp() { let current_active_session = unsafe { get_current_session(share_rdp()) }; if session_id != current_active_session { session_id = current_active_session; @@ -628,16 +632,15 @@ async fn launch_server(session_id: DWORD, close_first: bool) -> ResultType, usid: Option) -> ResultType> { +pub fn run_as_user(arg: Vec<&str>) -> ResultType> { let cmd = format!( "\"{}\" {}", std::env::current_exe()?.to_str().unwrap_or(""), arg.join(" "), ); - let mut session_id = get_current_process_session_id(); - if let Some(usid) = usid { - session_id = usid; - } + let Some(session_id) = get_current_process_session_id() else { + bail!("Failed to get current process session id"); + }; use std::os::windows::ffi::OsStrExt; let wstr: Vec = std::ffi::OsStr::new(&cmd) .encode_wide() @@ -733,11 +736,13 @@ pub fn set_share_rdp(enable: bool) { run_cmds(cmd, false, "share_rdp").ok(); } -pub fn get_current_process_session_id() -> u32 { - extern "C" { - fn get_current_process_session_id() -> u32; +pub fn get_current_process_session_id() -> Option { + let mut sid = 0; + if unsafe { ProcessIdToSessionId(GetCurrentProcessId(), &mut sid) == TRUE } { + Some(sid) + } else { + None } - unsafe { get_current_process_session_id() } } pub fn get_active_username() -> String { @@ -762,74 +767,91 @@ pub fn get_active_username() -> String { .to_owned() } -pub fn get_all_active_sessions() -> Vec> { - let sids = get_all_active_session_ids_with_station(); - let mut out = Vec::new(); - for sid in sids.split(',') { - let username = get_session_username(sid.to_owned()); - if !username.is_empty() { - let sid_split = sid.split(':').collect::>()[1]; - let v = vec![sid_split.to_owned(), username]; - out.push(v); - } - } - out -} - -pub fn get_session_username(session_id_with_station_name: String) -> String { - let mut session_id = session_id_with_station_name.split(':'); - let station = session_id.next().unwrap_or(""); - let session_id = session_id.next().unwrap_or(""); - if session_id == "" { - return "".to_owned(); - } - +fn get_session_username(session_id: u32) -> String { extern "C" { fn get_session_user_info(path: *mut u16, n: u32, rdp: bool, session_id: u32) -> u32; } let buff_size = 256; let mut buff: Vec = Vec::with_capacity(buff_size); buff.resize(buff_size, 0); - let n = unsafe { - get_session_user_info( - buff.as_mut_ptr(), - buff_size as _, - true, - session_id.parse::().unwrap(), - ) - }; + let n = unsafe { get_session_user_info(buff.as_mut_ptr(), buff_size as _, true, session_id) }; if n == 0 { return "".to_owned(); } let sl = unsafe { std::slice::from_raw_parts(buff.as_ptr(), n as _) }; - let out = String::from_utf16(sl) + String::from_utf16(sl) .unwrap_or("".to_owned()) .trim_end_matches('\0') - .to_owned(); - station.to_owned() + ": " + &out + .to_owned() } -pub fn get_all_active_session_ids_with_station() -> String { +pub fn get_available_sessions(name: bool) -> Vec { extern "C" { fn get_available_session_ids(buf: *mut wchar_t, buf_size: c_int, include_rdp: bool); } const BUF_SIZE: c_int = 1024; let mut buf: Vec = vec![0; BUF_SIZE as usize]; - unsafe { + let station_session_id_array = unsafe { get_available_session_ids(buf.as_mut_ptr(), BUF_SIZE, true); let session_ids = String::from_utf16_lossy(&buf); session_ids.trim_matches(char::from(0)).trim().to_string() + }; + let mut v: Vec = vec![]; + // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-wtsgetactiveconsolesessionid + let physical_console_session_id = unsafe { get_current_session(FALSE) }; + let physical_console_username = get_session_username(physical_console_session_id); + let physical_console_name = if name { + if physical_console_username.is_empty() { + "Console".to_owned() + } else { + format!("Console:{physical_console_username}") + } + } else { + "".to_owned() + }; + v.push(WindowsSession { + sid: physical_console_session_id, + name: physical_console_name, + ..Default::default() + }); + // https://learn.microsoft.com/en-us/previous-versions//cc722458(v=technet.10)?redirectedfrom=MSDN + for type_session_id in station_session_id_array.split(",") { + let split: Vec<_> = type_session_id.split(":").collect(); + if split.len() == 2 { + if let Ok(sid) = split[1].parse::() { + if !v.iter().any(|e| (*e).sid == sid) { + let name = if name { + format!("{}:{}", split[0], get_session_username(sid)) + } else { + "".to_owned() + }; + v.push(WindowsSession { + sid, + name, + ..Default::default() + }); + } + } + } } -} - -pub fn get_all_active_session_ids() -> String { - let out = get_all_active_session_ids_with_station() - .split(',') - .map(|x| x.split(':').nth(1).unwrap_or("")) - .collect::>() - .join(","); - out.trim_matches(char::from(0)).trim().to_string() + if name { + let mut name_count: HashMap = HashMap::new(); + for session in &v { + *name_count.entry(session.name.clone()).or_insert(0) += 1; + } + let current_sid = get_current_process_session_id().unwrap_or_default(); + for e in v.iter_mut() { + let running = e.sid == current_sid && current_sid != 0; + if name_count.get(&e.name).map(|v| *v).unwrap_or_default() > 1 { + e.name = format!("{} (sid = {})", e.name, e.sid); + } + if running { + e.name = format!("{} (running)", e.name); + } + } + } + v } pub fn get_active_user_home() -> Option { @@ -845,7 +867,11 @@ pub fn get_active_user_home() -> Option { } pub fn is_prelogin() -> bool { - let username = get_active_username(); + let Some(sid) = get_current_process_session_id() else { + log::error!("get_current_process_session_id failed"); + return false; + }; + let username = get_session_username(sid); username.is_empty() || username == "SYSTEM" } diff --git a/src/server/connection.rs b/src/server/connection.rs index 8d0f1caf7b64..8433b7c3fe44 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -237,8 +237,8 @@ pub struct Connection { file_remove_log_control: FileRemoveLogControl, #[cfg(feature = "gpucodec")] supported_encoding_flag: (bool, Option), - user_session_id: Option, - checked_multiple_session: bool, + need_sub_remote_service: bool, + remote_service_subed: bool, } impl ConnInner { @@ -386,8 +386,8 @@ impl Connection { file_remove_log_control: FileRemoveLogControl::new(id), #[cfg(feature = "gpucodec")] supported_encoding_flag: (false, None), - user_session_id: None, - checked_multiple_session: false, + need_sub_remote_service: false, + remote_service_subed: false, }; let addr = hbb_common::try_into_v4(addr); if !conn.on_open(addr).await { @@ -1194,6 +1194,9 @@ impl Connection { .into(); let mut sub_service = false; + let mut delay_sub_service = false; + #[cfg(windows)] + self.handle_windows_specific_session(&mut pi, &mut delay_sub_service); if self.file_transfer.is_some() { res.set_peer_info(pi); } else { @@ -1255,6 +1258,16 @@ impl Connection { }; self.read_dir(dir, show_hidden); } else if sub_service { + self.need_sub_remote_service = true; + if !delay_sub_service { + self.check_sub_remote_services(); + } + } + } + + fn check_sub_remote_services(&mut self) { + if self.need_sub_remote_service && !self.remote_service_subed { + self.remote_service_subed = true; if let Some(s) = self.server.upgrade() { let mut noperms = Vec::new(); if !self.peer_keyboard_enabled() && !self.show_remote_cursor { @@ -1279,6 +1292,27 @@ impl Connection { } } + #[cfg(windows)] + fn handle_windows_specific_session(&mut self, pi: &mut PeerInfo, delay_sub_service: &mut bool) { + let sessions = crate::platform::get_available_sessions(true); + let current_sid = crate::platform::get_current_process_session_id().unwrap_or_default(); + if crate::platform::is_installed() + && crate::platform::is_share_rdp() + && raii::AuthedConnID::remote_and_file_conn_count() == 1 + && sessions.len() > 1 + && current_sid != 0 + && self.lr.option.support_windows_specific_session == BoolOption::Yes.into() + { + pi.windows_sessions = Some(WindowsSessions { + sessions, + current_sid, + ..Default::default() + }) + .into(); + *delay_sub_service = true; + } + } + fn on_remote_authorized(&self) { self.update_codec_on_login(); #[cfg(any(target_os = "windows", target_os = "linux"))] @@ -1495,50 +1529,8 @@ impl Connection { self.video_ack_required = lr.video_ack_required; } - #[cfg(target_os = "windows")] - async fn handle_multiple_user_sessions(&mut self, usid: Option) -> bool { - if self.port_forward_socket.is_some() { - return true; - } else { - let active_sessions = crate::platform::get_all_active_sessions(); - if active_sessions.len() <= 1 { - return true; - } - let current_process_usid = crate::platform::get_current_process_session_id(); - if usid.is_none() { - let mut res = Misc::new(); - let mut rdp = Vec::new(); - for session in active_sessions { - let u_sid = &session[0]; - let u_name = &session[1]; - let mut rdp_session = RdpUserSession::new(); - rdp_session.user_session_id = u_sid.clone(); - rdp_session.user_name = u_name.clone(); - rdp.push(rdp_session); - } - res.set_rdp_user_sessions(RdpUserSessions { - rdp_user_sessions: rdp, - ..Default::default() - }); - let mut msg_out = Message::new(); - msg_out.set_misc(res); - self.send(msg_out).await; - return true; - } - if usid != Some(current_process_usid) { - self.on_close("Reconnecting...", false).await; - std::thread::spawn(move || { - let _ = ipc::connect_to_user_session(usid); - }); - return false; - } - true - } - } - #[cfg(not(any(target_os = "android", target_os = "ios")))] fn try_start_cm_ipc(&mut self) { - let usid = self.user_session_id; if let Some(p) = self.start_cm_ipc_para.take() { tokio::spawn(async move { #[cfg(windows)] @@ -1548,7 +1540,6 @@ impl Connection { p.tx_from_cm, p.rx_desktop_ready, p.tx_cm_stream_ready, - usid.clone(), ) .await { @@ -1562,7 +1553,7 @@ impl Connection { #[cfg(all(windows, feature = "flutter"))] std::thread::spawn(move || { if crate::is_server() && !crate::check_process("--tray", false) { - crate::platform::run_as_user(vec!["--tray"], usid).ok(); + crate::platform::run_as_user(vec!["--tray"]).ok(); } }); } @@ -1570,19 +1561,6 @@ impl Connection { async fn on_message(&mut self, msg: Message) -> bool { if let Some(message::Union::LoginRequest(lr)) = msg.union { - #[cfg(target_os = "windows")] - { - if !self.checked_multiple_session { - let usid; - match lr.option.user_session.parse::() { - Ok(n) => usid = Some(n), - Err(..) => usid = None, - } - if usid.is_some() { - self.user_session_id = usid; - } - } - } self.handle_login_request_without_validation(&lr).await; if self.authorized { return true; @@ -1821,22 +1799,6 @@ impl Connection { } } } else if self.authorized { - #[cfg(target_os = "windows")] - if !self.checked_multiple_session { - self.checked_multiple_session = true; - if crate::platform::is_installed() - && crate::platform::is_share_rdp() - && Self::alive_conns().len() == 1 - && get_version_number(&self.lr.version) >= get_version_number("1.2.4") - { - if !self - .handle_multiple_user_sessions(self.user_session_id) - .await - { - return false; - } - } - } match msg.union { Some(message::Union::MouseEvent(me)) => { #[cfg(any(target_os = "android", target_os = "ios"))] @@ -2304,6 +2266,26 @@ impl Connection { .lock() .unwrap() .user_record(self.inner.id(), status), + #[cfg(windows)] + Some(misc::Union::SelectedSid(sid)) => { + let current_process_usid = + crate::platform::get_current_process_session_id().unwrap_or_default(); + let sessions = crate::platform::get_available_sessions(false); + if crate::platform::is_installed() + && crate::platform::is_share_rdp() + && raii::AuthedConnID::remote_and_file_conn_count() == 1 + && sessions.len() > 1 + && current_process_usid != 0 + && current_process_usid != sid + && sessions.iter().any(|e| e.sid == sid) + { + std::thread::spawn(move || { + let _ = ipc::connect_to_user_session(Some(sid)); + }); + return false; + } + self.check_sub_remote_services(); + } _ => {} }, Some(message::Union::AudioFrame(frame)) => { @@ -3087,7 +3069,6 @@ async fn start_ipc( tx_from_cm: mpsc::UnboundedSender, mut _rx_desktop_ready: mpsc::Receiver<()>, tx_stream_ready: mpsc::Sender<()>, - user_session_id: Option, ) -> ResultType<()> { use hbb_common::anyhow::anyhow; @@ -3135,7 +3116,7 @@ async fn start_ipc( if crate::platform::is_root() { let mut res = Ok(None); for _ in 0..10 { - #[cfg(not(any(target_os = "linux", target_os = "windows")))] + #[cfg(not(any(target_os = "linux")))] { log::debug!("Start cm"); res = crate::platform::run_as_user(args.clone()); @@ -3149,14 +3130,10 @@ async fn start_ipc( None::<(&str, &str)>, ); } - #[cfg(target_os = "windows")] - { - log::debug!("Start cm"); - res = crate::platform::run_as_user(args.clone(), user_session_id); - } if res.is_ok() { break; } + log::error!("Failed to run cm: {res:?}"); sleep(1.).await; } if let Some(task) = res? { @@ -3540,6 +3517,15 @@ mod raii { .unwrap() .send((conn_count, remote_count))); } + + pub fn remote_and_file_conn_count() -> usize { + AUTHED_CONNS + .lock() + .unwrap() + .iter() + .filter(|c| c.1 == AuthConnType::Remote || c.1 == AuthConnType::FileTransfer) + .count() + } } impl Drop for AuthedConnID { diff --git a/src/server/video_service.rs b/src/server/video_service.rs index 567d57c012b6..f4ba3a4e6b63 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -50,7 +50,7 @@ use scrap::hwcodec::{HwEncoder, HwEncoderConfig}; use scrap::Capturer; use scrap::{ aom::AomEncoderConfig, - codec::{Encoder, EncoderCfg, EncodingUpdate, Quality}, + codec::{Encoder, EncoderCfg, Quality}, record::{Recorder, RecorderContext}, vpxcodec::{VpxEncoderConfig, VpxVideoCodecId}, CodecName, Display, Frame, TraitCapturer, @@ -643,7 +643,7 @@ fn get_encoder_config( GpuEncoder::set_not_use(_display_idx, true); } #[cfg(feature = "gpucodec")] - Encoder::update(EncodingUpdate::Check); + Encoder::update(scrap::codec::EncodingUpdate::Check); // https://www.wowza.com/community/t/the-correct-keyframe-interval-in-obs-studio/95162 let keyframe_interval = if record { Some(240) } else { None }; let negotiated_codec = Encoder::negotiated_codec(); diff --git a/src/ui/common.tis b/src/ui/common.tis index e3ed83a6ee2f..d48b8babedf8 100644 --- a/src/ui/common.tis +++ b/src/ui/common.tis @@ -304,21 +304,7 @@ function msgbox(type, title, content, link="", callback=null, height=180, width= return; } }; - } else if (type === "multiple-sessions") { - var parts = content.split("-"); - var ids = parts[0].split(","); - var names = parts[1].split(","); - var sessionData = []; - for (var i = 0; i < ids.length; i++) { - sessionData.push({ id: ids[i], name: names[i] }); - } - content = ; - callback = function () { - retryConnect(); - return; - }; - height += 50; - } + } last_msgbox_tag = type + "-" + title + "-" + content + "-" + link; $(#msgbox).content(); } @@ -353,7 +339,7 @@ handler.msgbox_retry = function(type, title, text, link, hasRetry) { function retryConnect(cancelTimer=false) { if (cancelTimer) self.timer(0, retryConnect); if (!is_port_forward) connecting(); - handler.reconnect(false, ""); + handler.reconnect(false); } /******************** end of msgbox ****************************************/ @@ -474,19 +460,12 @@ function awake() { class MultipleSessionComponent extends Reactor.Component { this var sessions = []; - this var selectedSessionId = null; - this var sessionlength = 0; this var messageText = translate("Please select the user you want to connect to"); function this(params) { if (params && params.sessions) { this.sessions = params.sessions; - this.selectedSessionId = params.sessions[0].id; - this.sessions.map(session => { - this.sessionlength += session.name.length; - }); } - handler.set_selected_user_session_id(this.selectedSessionId); } function render() { @@ -494,15 +473,9 @@ class MultipleSessionComponent extends Reactor.Component {
{this.messageText}
; } - - event change { - var selectedSessionName = this.value.substr(this.messageText.length + this.sessionlength); - this.selectedSessionId = this.sessions.find(session => session.name == selectedSessionName).id; - handler.set_selected_user_session_id(this.selectedSessionId); - } } \ No newline at end of file diff --git a/src/ui/header.tis b/src/ui/header.tis index 69be084b67a9..b859ecb36648 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -527,8 +527,15 @@ handler.updateDisplays = function(v) { } } -handler.setMultipleUserSession = function(usid,uname) { - msgbox("multiple-sessions", translate("Multiple active user sessions found"), usid+"-"+uname, "", function(res) {}); +handler.setMultipleWindowsSession = function(sessions) { + // It will be covered by other message box if the timer is not used, + self.timer(1000ms, function() { + msgbox("multiple-sessions-nocancel", translate("Multiple active user sessions found"), , "", function(res) { + if (res && res.sid) { + handler.set_selected_windows_session_id("" + res.sid); + } + }, 230); + }); } function updatePrivacyMode() { diff --git a/src/ui/msgbox.tis b/src/ui/msgbox.tis index 391a5e7ff8d3..a8fa79ad4a5c 100644 --- a/src/ui/msgbox.tis +++ b/src/ui/msgbox.tis @@ -312,6 +312,9 @@ class MsgboxComponent: Reactor.Component { return; } } + if (this.type == "multiple-sessions-nocancel") { + values.sid = (this.$$(select))[0].value; + } return values; } diff --git a/src/ui/remote.rs b/src/ui/remote.rs index cf3c951839d4..f7b10d2a0d42 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -259,14 +259,17 @@ impl InvokeUiSession for SciterHandler { // Ignore for sciter version. } - fn set_multiple_user_session(&self, sessions: Vec) { - let formatted_sessions: Vec = sessions.iter() - .map(|session| format!("{}-{}", session.user_session_id, session.user_name)) - .collect(); - let u_sids: String = formatted_sessions.iter().map(|s| s.split("-").next().unwrap().to_string()).collect::>().join(","); - let u_names:String = formatted_sessions.iter().map(|s| s.split("-").nth(1).unwrap().to_string()).collect::>().join(","); - self.call("setMultipleUserSession", &make_args!(u_sids, u_names)); - } + fn set_multiple_windows_session(&self, sessions: Vec) { + let mut v = Value::array(0); + let mut sessions = sessions; + for s in sessions.drain(..) { + let mut obj = Value::map(); + obj.set_item("sid", s.sid.to_string()); + obj.set_item("name", s.name); + v.push(obj); + } + self.call("setMultipleWindowsSession", &make_args!(v)); + } fn on_connected(&self, conn_type: ConnType) { match conn_type { @@ -355,7 +358,6 @@ impl sciter::EventHandler for SciterSession { } fn detached(&mut self, _root: HELEMENT) { - self.set_selected_user_session_id("".to_string()); *self.element.lock().unwrap() = None; self.sender.write().unwrap().take().map(|sender| { sender.send(Data::Close).ok(); @@ -386,7 +388,7 @@ impl sciter::EventHandler for SciterSession { let site = AssetPtr::adopt(ptr as *mut video_destination); log::debug!("[video] start video"); *VIDEO.lock().unwrap() = Some(site); - self.reconnect(false, "".to_string()); + self.reconnect(false); } } BEHAVIOR_EVENTS::VIDEO_INITIALIZED => { @@ -436,7 +438,7 @@ impl sciter::EventHandler for SciterSession { fn transfer_file(); fn tunnel(); fn lock_screen(); - fn reconnect(bool, String); + fn reconnect(bool); fn get_chatbox(); fn get_icon(); fn get_home_dir(); @@ -487,7 +489,7 @@ impl sciter::EventHandler for SciterSession { fn request_voice_call(); fn close_voice_call(); fn version_cmp(String, String); - fn set_selected_user_session_id(String); + fn set_selected_windows_session_id(String); } } @@ -591,8 +593,8 @@ impl SciterSession { log::info!("size saved"); } - fn set_selected_user_session_id(&mut self, u_sid: String) { - self.lc.write().unwrap().selected_user_session_id = u_sid; + fn set_selected_windows_session_id(&mut self, u_sid: String) { + self.send_selected_session_id(u_sid); } fn get_port_forwards(&mut self) -> Value { diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 32fd26a38b81..1ca932e20e91 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -1003,7 +1003,7 @@ impl Session { } } - pub fn reconnect(&self, force_relay: bool, user_session_id: String) { + pub fn reconnect(&self, force_relay: bool) { // 1. If current session is connecting, do not reconnect. // 2. If the connection is established, send `Data::Close`. // 3. If the connection is disconnected, do nothing. @@ -1023,9 +1023,6 @@ impl Session { if true == force_relay { self.lc.write().unwrap().force_relay = true; } - if !user_session_id.is_empty() { - self.lc.write().unwrap().selected_user_session_id = user_session_id; - } let mut lock = self.thread.lock().unwrap(); // No need to join the previous thread, because it will exit automatically. // And the previous thread will not change important states. @@ -1254,6 +1251,19 @@ impl Session { pub fn close_voice_call(&self) { self.send(Data::CloseVoiceCall); } + + pub fn send_selected_session_id(&self, sid: String) { + if let Ok(sid) = sid.parse::() { + self.lc.write().unwrap().selected_windows_session_id = Some(sid); + let mut misc = Misc::new(); + misc.set_selected_sid(sid); + let mut msg = Message::new(); + msg.set_misc(misc); + self.send(Data::Message(msg)); + } else { + log::error!("selected invalid sid: {}", sid); + } + } } pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { @@ -1313,7 +1323,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn next_rgba(&self, display: usize); #[cfg(all(feature = "gpucodec", feature = "flutter"))] fn on_texture(&self, display: usize, texture: *mut c_void); - fn set_multiple_user_session(&self, sessions: Vec); + fn set_multiple_windows_session(&self, sessions: Vec); } impl Deref for Session { @@ -1355,8 +1365,8 @@ impl Interface for Session { handle_login_error(self.lc.clone(), err, self) } - fn set_multiple_user_sessions(&self, sessions: Vec) { - self.ui_handler.set_multiple_user_session(sessions); + fn set_multiple_windows_session(&self, sessions: Vec) { + self.ui_handler.set_multiple_windows_session(sessions); } fn handle_peer_info(&self, mut pi: PeerInfo) { @@ -1419,6 +1429,19 @@ impl Interface for Session { crate::platform::windows::add_recent_document(&path); } } + if !pi.windows_sessions.sessions.is_empty() { + let selected = self + .lc + .read() + .unwrap() + .selected_windows_session_id + .to_owned(); + if selected == Some(pi.windows_sessions.current_sid) { + self.send_selected_session_id(pi.windows_sessions.current_sid.to_string()); + } else { + self.set_multiple_windows_session(pi.windows_sessions.sessions.clone()); + } + } } async fn handle_hash(&self, pass: &str, hash: Hash, peer: &mut Stream) {