mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-08-28 00:56:46 +00:00
fix(linux): a session logout should hand the peer to the login screen (#15905)
* fix(linux): a session logout should hand the peer to the login screen Logging out closes every window in the session, the connection manager's included, and its close handler kicks every peer with the reason a person gets when they disconnect one by hand. That reason is the one thing the client never retries on, so the remote session dies on a frozen frame instead of reconnecting to the greeter that is already there. The close carries nothing to tell the two apart: measured on KDE, the CM receives no signal and logind still reports the session active at that instant, and the server is killed within a few hundred ms either way, so neither a state check nor a grace period can decide it. What is distinguishable is the ACTION: disconnecting a peer is not the same event as this window going away. So the window-close path now says so, and the server ends the session without poisoning the retry; the Disconnect button and the app's own close control keep kicking exactly as before. Linux only, since that is where a logout closes the window. Verified on plasma/sddm with a client attached: a logout now reconnects to the greeter with no dialog, while closing the manager window still shows Closed manually by the peer. * fix(linux): close the tunnel too, and keep the web build compiling Three seams the first pass missed. The web bridge is hand written, not generated, so the new call needs its stub there or flutter build web stops compiling - and that job is disabled in CI, so it would have gone green. try_port_forward_loop is a second consumer of the same channel and only knew Close, so a forwarded tunnel outlived the window it was supposed to die with. And the variant had landed inside the DRM section, whose comment says everything below it is drm-gated.
This commit is contained in:
@@ -22,6 +22,14 @@ import '../../models/file_model.dart';
|
||||
import '../../models/platform_model.dart';
|
||||
import '../../models/server_model.dart';
|
||||
|
||||
/// Set only by this window's own close control, and only once the user has confirmed. Any other
|
||||
/// way the window can go - a session logout closing every window, the window manager, a native
|
||||
/// title-bar button this app does not draw - leaves it false, which is the honest answer:
|
||||
/// nothing in that close says who asked for it. It lives at file scope because the control that
|
||||
/// sets it (`ConnectionManagerState`) and the handler that reads it (`_DesktopServerPageState`)
|
||||
/// are different widgets.
|
||||
bool _cmClosedByOperator = false;
|
||||
|
||||
class DesktopServerPage extends StatefulWidget {
|
||||
const DesktopServerPage({Key? key}) : super(key: key);
|
||||
|
||||
@@ -55,7 +63,10 @@ class _DesktopServerPageState extends State<DesktopServerPage>
|
||||
|
||||
@override
|
||||
void onWindowClose() {
|
||||
Future.wait([gFFI.serverModel.closeAll(), gFFI.close()]).then((_) {
|
||||
// Other platforms keep the old behaviour exactly: the ambiguity this guards against is a
|
||||
// Linux session logout, which closes every window in the session.
|
||||
final byOperator = _cmClosedByOperator || !isLinux;
|
||||
Future.wait([gFFI.serverModel.closeAll(byOperator: byOperator), gFFI.close()]).then((_) {
|
||||
if (isMacOS) {
|
||||
RdPlatformChannel.instance.terminate();
|
||||
} else {
|
||||
@@ -327,6 +338,7 @@ class ConnectionManagerState extends State<ConnectionManager>
|
||||
var tabController = gFFI.serverModel.tabController;
|
||||
final connLength = tabController.length;
|
||||
if (connLength <= 1) {
|
||||
_cmClosedByOperator = true;
|
||||
windowManager.close();
|
||||
return true;
|
||||
} else {
|
||||
@@ -338,6 +350,9 @@ class ConnectionManagerState extends State<ConnectionManager>
|
||||
res = await closeConfirmDialog();
|
||||
}
|
||||
if (res) {
|
||||
// After the dialog, never before it: an external close while it is open must not
|
||||
// inherit an intent the user had not expressed yet.
|
||||
_cmClosedByOperator = true;
|
||||
windowManager.close();
|
||||
}
|
||||
return res;
|
||||
|
||||
@@ -738,9 +738,13 @@ class ServerModel with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> closeAll() async {
|
||||
await Future.wait(
|
||||
_clients.map((client) => bind.cmCloseConnection(connId: client.id)));
|
||||
/// `byOperator` false means the CM's window went away rather than a person asking for the
|
||||
/// peers to go. The sessions end either way; only the close reason differs, and with it
|
||||
/// whether the peer is allowed to reconnect. See `ipc::Data::CmWindowClosed`.
|
||||
Future<void> closeAll({bool byOperator = true}) async {
|
||||
await Future.wait(_clients.map((client) => byOperator
|
||||
? bind.cmCloseConnection(connId: client.id)
|
||||
: bind.cmCloseConnectionWindow(connId: client.id)));
|
||||
_clients.clear();
|
||||
tabController.state.value.tabs.clear();
|
||||
if (isAndroid) androidUpdatekeepScreenOn();
|
||||
|
||||
@@ -1373,6 +1373,10 @@ class RustdeskImpl {
|
||||
throw UnimplementedError("cmLoginRes");
|
||||
}
|
||||
|
||||
Future<void> cmCloseConnectionWindow({required int connId, dynamic hint}) {
|
||||
throw UnimplementedError("cmCloseConnectionWindow");
|
||||
}
|
||||
|
||||
Future<void> cmCloseConnection({required int connId, dynamic hint}) {
|
||||
throw UnimplementedError("cmCloseConnection");
|
||||
}
|
||||
|
||||
@@ -2197,6 +2197,15 @@ pub fn cm_close_connection(conn_id: i32) {
|
||||
crate::ui_cm_interface::close(conn_id);
|
||||
}
|
||||
|
||||
/// The CM window closed. On Linux that is ambiguous - a logout closes it the same way a person
|
||||
/// does - so it ends the session without the no-retry reason; elsewhere it is a plain close.
|
||||
pub fn cm_close_connection_window(conn_id: i32) {
|
||||
#[cfg(target_os = "linux")]
|
||||
crate::ui_cm_interface::close_window(conn_id);
|
||||
#[cfg(all(not(target_os = "linux"), not(target_os = "ios")))]
|
||||
crate::ui_cm_interface::close(conn_id);
|
||||
}
|
||||
|
||||
pub fn cm_remove_disconnected_connection(conn_id: i32) {
|
||||
#[cfg(not(any(target_os = "ios")))]
|
||||
crate::ui_cm_interface::remove(conn_id);
|
||||
|
||||
@@ -501,6 +501,15 @@ pub enum Data {
|
||||
ControlPermissionsRemoteModify(Option<bool>),
|
||||
#[cfg(target_os = "windows")]
|
||||
FileTransferEnabledState(Option<bool>),
|
||||
/// CM -> server: the connection manager's WINDOW went away, which is not the same event
|
||||
/// as the operator disconnecting a peer. Linux only, and deliberately: there a session
|
||||
/// logout closes every window, and the close arrives at the CM indistinguishable from a
|
||||
/// person clicking it - measured on KDE, the CM gets no signal and logind still reports the
|
||||
/// session active. So the ambiguous case ends the session WITHOUT the no-retry reason and
|
||||
/// the peer is allowed to reconnect (landing on the greeter after a logout), while the
|
||||
/// explicit Disconnect button keeps sending `Close` and kicking for good.
|
||||
#[cfg(target_os = "linux")]
|
||||
CmWindowClosed,
|
||||
// --- DRM/KMS capture (opt-in `drm` feature) over the `_drm` service-scoped channel ---
|
||||
// All of the following are `cfg(all(linux, drm))`, so the drm-off IPC wire is byte-identical
|
||||
// to upstream. Protocol on `_drm`: on connect the root service sends `DrmDisplayList`, the
|
||||
|
||||
@@ -642,6 +642,18 @@ impl Connection {
|
||||
conn.on_close("connection manager", true).await;
|
||||
break;
|
||||
}
|
||||
// The connection manager's window went away rather than a person
|
||||
// disconnecting this peer. End the session exactly as above, but do not
|
||||
// send the manual close reason: it is the one thing that stops the peer
|
||||
// from retrying, and on a logout the retry is the whole point - it is
|
||||
// what puts the peer back on the login screen a moment later.
|
||||
#[cfg(target_os = "linux")]
|
||||
ipc::Data::CmWindowClosed => {
|
||||
conn.chat_unanswered = false; // seen
|
||||
conn.file_transferred = false; //seen
|
||||
conn.on_close("connection manager window closed", true).await;
|
||||
break;
|
||||
}
|
||||
ipc::Data::CmErr(e) => {
|
||||
if e != "expected" {
|
||||
// cm closed before connection
|
||||
@@ -1201,6 +1213,13 @@ impl Connection {
|
||||
ipc::Data::Close => {
|
||||
bail!("Close requested from connection manager");
|
||||
}
|
||||
// Same end as above: a tunnel must not outlive the window either.
|
||||
// Only the reason differs, and a port forward carries none - the
|
||||
// peer sees the tunnel drop and decides for itself.
|
||||
#[cfg(target_os = "linux")]
|
||||
ipc::Data::CmWindowClosed => {
|
||||
bail!("Connection manager window closed");
|
||||
}
|
||||
ipc::Data::CmErr(e) => {
|
||||
log::error!("Connection manager error: {e}");
|
||||
bail!("{e}");
|
||||
|
||||
@@ -377,6 +377,15 @@ pub fn close(id: i32) {
|
||||
};
|
||||
}
|
||||
|
||||
/// Like `close`, but says the CM's WINDOW closed rather than a person disconnecting this peer.
|
||||
/// See `ipc::Data::CmWindowClosed`.
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn close_window(id: i32) {
|
||||
if let Some(client) = CLIENTS.read().unwrap().get(&id) {
|
||||
allow_err!(client.tx.send(Data::CmWindowClosed));
|
||||
};
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn remove(id: i32) {
|
||||
CLIENTS.write().unwrap().remove(&id);
|
||||
|
||||
Reference in New Issue
Block a user