refactor windows specific session (#7170)

1. Modify the process to have the control side lead the session switching: After the control side sends a `LoginRequest`, the controlled side will add all session information and the current session ID in the `LoginResponse`. Upon receiving the `LoginResponse`, the control side will check if the current session ID matches the ID in the `LoginConfigHandler`. If they match, the control side will send the current session ID. If they don't match, a session selection dialog will pop up, the selected session id will be sent. Upon receiving this message, the controlled side will restart if different or sub service if same .
2. Always show physical console session on the top
3. Show running session and distinguish sessions with the same name
4. Not sub service until correct session id is ensured
5. Fix switch sides not work for multisession session
6. Remove all session string join/split except get_available_sessions in
   windows.rs
7. Fix prelogin, when share rdp is enabled and there is a rdp session,
   the console is in login screen, get_active_username will be the rdp's
   username and prelogin will be false, cm can't be created an that
   causes disconnection in a loop
8. Rename all user session to windows session

Known issue:
1. Use current process session id for `run_as_user`, sahil says it can
   be wrong but I didn't reproduce.
2. Have not change tray process to current session
3. File transfer doesn't update home directory when session changed
4. When it's in login screen, remote file directory is empty, because cm
   have not start up

Signed-off-by: 21pages <pages21@163.com>
This commit is contained in:
21pages 2024-02-18 22:08:25 +08:00 committed by GitHub
parent 4f1a4dc6a5
commit 0f44de7dc3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 376 additions and 416 deletions

View File

@ -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<String> keys;
late final List<String> 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<String>(
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<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
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);
}

View File

@ -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<String> sessionsList = sessions.split(',');
Map<String, String> sessionMap = {};
for (var session in sessionsList) {
var sessionInfo = session.split('-');
if (sessionInfo.isNotEmpty) {
sessionMap[sessionInfo[0]] = sessionInfo[1];
}
List<dynamic> sessionsList = [];
try {
sessionsList = json.decode(sessions);
} catch (e) {
print(e);
}
String selectedUserValue = sessionMap.keys.first;
List<String> sids = [];
List<String> names = [];
for (var session in sessionsList) {
sids.add(session['sid']);
names.add(session['name']);
}
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<String, String> sessions;
final Function(String) onValueChanged;
SessionsDropdown(
this.peerId, this.sessionId, this.sessions, this.onValueChanged);
@override
_SessionsDropdownState createState() => _SessionsDropdownState();
}
class _SessionsDropdownState extends State<SessionsDropdown> {
late String selectedValue;
@override
void initState() {
super.initState();
selectedValue = widget.sessions.keys.first;
}
@override
Widget build(BuildContext context) {
return Container(
width: 300,
child: DropdownButton<String>(
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,
),
),
);
}
}

View File

@ -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<T>(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<String> keys;
late final List<String> 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<String>(
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<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
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,

View File

@ -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<String, dynamic> 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...'),

View File

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

View File

@ -1149,7 +1149,7 @@ pub struct LoginConfigHandler {
pub custom_fps: Arc<Mutex<Option<usize>>>,
pub adapter_luid: Option<i64>,
pub mark_unsupported: Vec<CodecFormat>,
pub selected_user_session_id: String,
pub selected_windows_session_id: Option<u32>,
}
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<hbb_common::message_proto::RdpUserSession>);
fn set_multiple_windows_session(&self, sessions: Vec<WindowsSession>);
fn on_error(&self, err: &str) {
self.msgbox("error", "Error", err, "");
}

View File

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

View File

@ -826,15 +826,21 @@ impl InvokeUiSession for FlutterHandler {
)
}
fn set_multiple_user_session(&self, sessions: Vec<hbb_common::message_proto::RdpUserSession>) {
let formatted_sessions: Vec<String> = 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<WindowsSession>) {
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()),
)],
);
}

View File

@ -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<String> {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
return get_sound_inputs();

View File

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

View File

@ -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<OsString>) -> 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<HANDL
Ok(h)
}
pub fn run_as_user(arg: Vec<&str>, usid: Option<u32>) -> ResultType<Option<std::process::Child>> {
pub fn run_as_user(arg: Vec<&str>) -> ResultType<Option<std::process::Child>> {
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<u16> = 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<u32> {
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<Vec<String>> {
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::<Vec<_>>()[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<u16> = 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::<u32>().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<WindowsSession> {
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<wchar_t> = 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<WindowsSession> = 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::<u32>() {
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::<Vec<_>>()
.join(",");
out.trim_matches(char::from(0)).trim().to_string()
if name {
let mut name_count: HashMap<String, usize> = 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<PathBuf> {
@ -845,7 +867,11 @@ pub fn get_active_user_home() -> Option<PathBuf> {
}
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"
}

View File

@ -237,8 +237,8 @@ pub struct Connection {
file_remove_log_control: FileRemoveLogControl,
#[cfg(feature = "gpucodec")]
supported_encoding_flag: (bool, Option<bool>),
user_session_id: Option<u32>,
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<u32>) -> 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::<u32>() {
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<ipc::Data>,
mut _rx_desktop_ready: mpsc::Receiver<()>,
tx_stream_ready: mpsc::Sender<()>,
user_session_id: Option<u32>,
) -> 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 {

View File

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

View File

@ -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 = <MultipleSessionComponent sessions={sessionData} />;
callback = function () {
retryConnect();
return;
};
height += 50;
}
}
last_msgbox_tag = type + "-" + title + "-" + content + "-" + link;
$(#msgbox).content(<MsgboxComponent width={width} height={height} autoLogin={autoLogin} type={type} title={title} content={content} link={link} remember={remember} callback={callback} contentStyle={contentStyle} hasRetry={hasRetry} />);
}
@ -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 {
<div .title>{this.messageText}</div>
<select>
{this.sessions.map(session =>
<option value={session.id}>{session.name}</option>
<option value={session.sid}>{session.name}</option>
)}
</select>
</div>;
}
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);
}
}

View File

@ -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"), <MultipleSessionComponent sessions={sessions} />, "", function(res) {
if (res && res.sid) {
handler.set_selected_windows_session_id("" + res.sid);
}
}, 230);
});
}
function updatePrivacyMode() {

View File

@ -312,6 +312,9 @@ class MsgboxComponent: Reactor.Component {
return;
}
}
if (this.type == "multiple-sessions-nocancel") {
values.sid = (this.$$(select))[0].value;
}
return values;
}

View File

@ -259,14 +259,17 @@ impl InvokeUiSession for SciterHandler {
// Ignore for sciter version.
}
fn set_multiple_user_session(&self, sessions: Vec<hbb_common::message_proto::RdpUserSession>) {
let formatted_sessions: Vec<String> = 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::<Vec<String>>().join(",");
let u_names:String = formatted_sessions.iter().map(|s| s.split("-").nth(1).unwrap().to_string()).collect::<Vec<String>>().join(",");
self.call("setMultipleUserSession", &make_args!(u_sids, u_names));
}
fn set_multiple_windows_session(&self, sessions: Vec<WindowsSession>) {
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 {

View File

@ -1003,7 +1003,7 @@ impl<T: InvokeUiSession> Session<T> {
}
}
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<T: InvokeUiSession> Session<T> {
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<T: InvokeUiSession> Session<T> {
pub fn close_voice_call(&self) {
self.send(Data::CloseVoiceCall);
}
pub fn send_selected_session_id(&self, sid: String) {
if let Ok(sid) = sid.parse::<u32>() {
self.lc.write().unwrap().selected_windows_session_id = Some(sid);
let mut misc = Misc::new();
misc.set_selected_sid(sid);
let mut msg = Message::new();
msg.set_misc(misc);
self.send(Data::Message(msg));
} 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<hbb_common::message_proto::RdpUserSession>);
fn set_multiple_windows_session(&self, sessions: Vec<WindowsSession>);
}
impl<T: InvokeUiSession> Deref for Session<T> {
@ -1355,8 +1365,8 @@ impl<T: InvokeUiSession> Interface for Session<T> {
handle_login_error(self.lc.clone(), err, self)
}
fn set_multiple_user_sessions(&self, sessions: Vec<hbb_common::message_proto::RdpUserSession>) {
self.ui_handler.set_multiple_user_session(sessions);
fn set_multiple_windows_session(&self, sessions: Vec<WindowsSession>) {
self.ui_handler.set_multiple_windows_session(sessions);
}
fn handle_peer_info(&self, mut pi: PeerInfo) {
@ -1419,6 +1429,19 @@ impl<T: InvokeUiSession> Interface for Session<T> {
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) {