Compare commits

..

7 Commits

Author SHA1 Message Date
rustdesk
b0d934318b cjk 2026-06-22 14:42:55 +08:00
RustDesk
2747d3d8b4 Revert "fix(arm64-linux): fix CJK font rendering on flutter-elinux (#15324)" (#15377)
This reverts commit c9391fb894.
2026-06-22 13:40:39 +08:00
fufesou
3c574a4182 fix(wayland): clipboard, support ext-data-control (#15366)
* fix(wayland): clipboard, support ext-data-control

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(clipboard): restart stale listener and log join panics

Signed-off-by: fufesou <linlong1266@gmail.com>

* update clipboard-master

Signed-off-by: fufesou <linlong1266@gmail.com>

* refactor(clipboard): remove redundant stale listener cleanup

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-06-21 17:09:41 +08:00
Tigah
311d4708e5 fix(linux): reap a crashed headless session's leftovers on next start (#15348)
The teardown cleanup added for #15183 only runs on a clean disconnect.
If the service or its --server crashes before then, the headless logind
session scope and the /tmp/.X<n> lock files it created leak the same way
#15183 leaked them, with nothing to reclaim them afterwards.

Record the session scope and display when the headless session starts,
and on the next --server start reap exactly what the previous run
recorded, then drop the marker. It only ever touches the one scope and
display the previous run recorded, never a scan, so unrelated sessions
are untouched; the reap and X cleanup reuse the teardown path.

A logind session id is only unique within a boot: the counter lives in
/run and resets, so a recorded "session-N.scope" can name a different,
live session after a reboot. Tag the marker with the boot id and only
reap the scope when it matches the current boot. A leaked cgroup cannot
outlive a reboot, so nothing legitimate is lost cross-boot; the X lock
cleanup stays pid-guarded and runs either way.

Signed-off-by: TBX3D <88289044+TBX3D@users.noreply.github.com>
2026-06-21 16:56:41 +08:00
Maison da Silva
5cf4323d07 Fix Portuguese translations for consistency (#15354) 2026-06-21 16:52:28 +08:00
bovirus
3976701ac6 Update Italian translations (#15367) 2026-06-21 16:52:13 +08:00
fufesou
9ded8d6ab2 fix(keyboard): win, key, Pause (#15351)
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-06-20 21:11:19 +08:00
12 changed files with 241 additions and 187 deletions

17
Cargo.lock generated
View File

@@ -1324,7 +1324,7 @@ dependencies = [
[[package]]
name = "clipboard-master"
version = "4.0.0-beta.6"
source = "git+https://github.com/rustdesk-org/clipboard-master#ddc39f00a6211959489ae683aa6ae6eedf03a809"
source = "git+https://github.com/rustdesk-org/clipboard-master#7762d74e38db37cfeb6ded88c964b9cdbddfb6db"
dependencies = [
"objc",
"objc-foundation",
@@ -6920,7 +6920,7 @@ dependencies = [
[[package]]
name = "rdev"
version = "0.5.0-2"
source = "git+https://github.com/rustdesk-org/rdev#f9b60b1dd0f3300a1b797d7a74c116683cd232c8"
source = "git+https://github.com/rustdesk-org/rdev#871bf1c856d6a30af2f56ab8848396a025140855"
dependencies = [
"cocoa 0.24.1",
"core-foundation 0.9.4",
@@ -9733,9 +9733,9 @@ dependencies = [
[[package]]
name = "wayland-protocols-wlr"
version = "0.3.3"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd993de54a40a40fbe5601d9f1fbcaef0aebcc5fda447d7dc8f6dcbaae4f8953"
checksum = "efd94963ed43cf9938a090ca4f7da58eb55325ec8200c3848963e98dc25b78ec"
dependencies = [
"bitflags 2.9.1",
"wayland-backend",
@@ -10838,16 +10838,15 @@ dependencies = [
[[package]]
name = "wl-clipboard-rs"
version = "0.9.0"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4de22eebb1d1e2bad2d970086e96da0e12cde0b411321e5b0f7b2a1f876aa26f"
checksum = "e9651471a32e87d96ef3a127715382b2d11cc7c8bb9822ded8a7cc94072eb0a3"
dependencies = [
"libc",
"log",
"os_pipe",
"rustix 0.38.34",
"tempfile",
"thiserror 1.0.61",
"rustix 1.1.2",
"thiserror 2.0.17",
"tree_magic_mini",
"wayland-backend",
"wayland-client",

View File

@@ -598,22 +598,6 @@ class MyTheme {
}
}
/// Applies [fallbacks] as fontFamilyFallback to every text style in both
/// themes. Called once at startup on ARM64 Linux after a CJK font has been
/// loaded via FontLoader (see flutter/flutter#139293).
static void applyFontFallback(List<String> fallbacks) {
lightTheme = lightTheme.copyWith(
textTheme: lightTheme.textTheme.apply(fontFamilyFallback: fallbacks),
primaryTextTheme:
lightTheme.primaryTextTheme.apply(fontFamilyFallback: fallbacks),
);
darkTheme = darkTheme.copyWith(
textTheme: darkTheme.textTheme.apply(fontFamilyFallback: fallbacks),
primaryTextTheme:
darkTheme.primaryTextTheme.apply(fontFamilyFallback: fallbacks),
);
}
static ThemeMode currentThemeMode() {
final preference = getThemeModePreference();
if (preference == ThemeMode.system) {

View File

@@ -29,8 +29,6 @@ import 'mobile/pages/home_page.dart';
import 'mobile/pages/server_page.dart';
import 'mobile/widgets/deploy_dialog.dart';
import 'models/platform_model.dart';
import 'native/font_manager.dart'
if (dart.library.html) 'web/font_manager.dart';
import 'package:flutter_hbb/plugin/handlers.dart'
if (dart.library.html) 'package:flutter_hbb/web/plugin/handlers.dart';
@@ -39,15 +37,10 @@ import 'package:flutter_hbb/plugin/handlers.dart'
int? kWindowId;
WindowType? kWindowType;
late List<String> kBootArgs;
bool _cjkFontLoaded = false;
Future<void> main(List<String> args) async {
earlyAssert();
WidgetsFlutterBinding.ensureInitialized();
_cjkFontLoaded = await loadSystemCJKFonts();
if (_cjkFontLoaded) {
MyTheme.applyFontFallback([kLinuxCjkFontFamily]);
}
debugPrint("launch args: $args");
kBootArgs = List.from(args);
@@ -390,7 +383,6 @@ void _runApp(
builder: (context, child) {
child = _keepScaleBuilder(context, child);
child = botToastBuilder(context, child);
if (_cjkFontLoaded) child = _mergeCjkFallback(context, child);
return child;
},
),
@@ -541,7 +533,6 @@ class _AppState extends State<App> with WidgetsBindingObserver {
: (context, child) {
child = _keepScaleBuilder(context, child);
child = botToastBuilder(context, child);
if (_cjkFontLoaded) child = _mergeCjkFallback(context, child);
if ((isDesktop && desktopType == DesktopType.main) ||
isWebDesktop) {
child = keyListenerBuilder(context, child);
@@ -595,19 +586,6 @@ _registerEventHandler() {
}
}
/// Merges the theme's fontFamilyFallback into [DefaultTextStyle] so that
/// bare [Text] widgets (and those with inherit:true styles) also pick up the
/// CJK fallback font loaded on ARM64 Linux.
Widget _mergeCjkFallback(BuildContext context, Widget? child) {
final result = child ?? Container();
final fallback = Theme.of(context).textTheme.bodyMedium?.fontFamilyFallback;
if (fallback == null || fallback.isEmpty) return result;
return DefaultTextStyle.merge(
style: TextStyle(fontFamilyFallback: fallback),
child: result,
);
}
Widget keyListenerBuilder(BuildContext context, Widget? child) {
return RawKeyboardListener(
focusNode: FocusNode(),

View File

@@ -1,109 +0,0 @@
import 'dart:ffi' show Abi;
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
/// Font family name registered with [FontLoader] when a system CJK font is
/// successfully loaded on ARM64 Linux.
const kLinuxCjkFontFamily = 'SystemCJK';
const _kFontSearchPaths = [
// Debian / Ubuntu (noto-fonts / fonts-noto-cjk)
'/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc',
'/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc',
'/usr/share/fonts/opentype/noto/NotoSansCJKsc-Regular.otf',
// Fedora / RHEL / Rocky (google-noto-sans-cjk-fonts)
'/usr/share/fonts/google-noto-cjk/NotoSansCJK-Regular.ttc',
'/usr/share/fonts/google-noto-sans-cjk-fonts/NotoSansCJK-Regular.ttc',
// Arch Linux (noto-fonts-cjk)
'/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc',
'/usr/share/fonts/noto-cjk/NotoSansCJKsc-Regular.otf',
// Generic fallback paths
'/usr/share/fonts/noto/NotoSansCJK-Regular.ttc',
'/usr/share/fonts/noto/NotoSansCJKsc-Regular.otf',
// WenQuanYi — commonly pre-installed on CJK-locale systems
'/usr/share/fonts/truetype/wqy/wqy-microhei.ttc',
'/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc',
'/usr/share/fonts/wqy-microhei/wqy-microhei.ttc',
'/usr/share/fonts/wqy-zenhei/wqy-zenhei.ttc',
];
/// Loads a system CJK font on ARM64 Linux into Flutter's font registry via
/// [FontLoader], working around the missing fontconfig support in the
/// flutter-elinux engine (https://github.com/flutter/flutter/issues/139293).
///
/// Returns true if a CJK font was successfully loaded; false otherwise.
/// On all other platforms this is a no-op and returns false immediately.
Future<bool> loadSystemCJKFonts() async {
if (Abi.current() != Abi.linuxArm64) return false;
final path = await _findCjkFontPath();
if (path == null) {
debugPrint('ARM64 Linux: no CJK font found; CJK text may not render');
return false;
}
try {
final loader = FontLoader(kLinuxCjkFontFamily);
final bytes = await File(path).readAsBytes();
loader.addFont(Future.value(ByteData.view(bytes.buffer, bytes.offsetInBytes, bytes.lengthInBytes)));
await loader.load();
debugPrint('ARM64 Linux: loaded CJK font from $path');
return true;
} catch (e) {
debugPrint('ARM64 Linux: failed to load CJK font: $e');
return false;
}
}
Future<String?> _findCjkFontPath() async {
// Query fc-list for each CJK script separately. Fonts present in all three
// sets (zh ∩ ja ∩ ko) are true pan-CJK fonts; prefer them so we don't
// accidentally pick a Chinese-only font that lacks Japanese kana or Korean
// hangul glyphs. fc-list is a fontconfig CLI tool available on most Linux
// systems independent of whether the Flutter engine was built with fontconfig.
final byLang = <String, Set<String>>{};
for (final lang in const ['zh', 'ja', 'ko']) {
final paths = <String>{};
try {
final r =
await Process.run('fc-list', [':lang=$lang', '--format=%{file}\n']);
if (r.exitCode == 0) {
for (final line in r.stdout.toString().split('\n')) {
final p = line.trim();
if (p.isNotEmpty && File(p).existsSync()) paths.add(p);
}
}
} catch (e) {
debugPrint('ARM64 Linux: fc-list failed for lang=$lang: $e');
}
byLang[lang] = paths;
}
final panCjk = byLang['zh']!
.intersection(byLang['ja']!)
.intersection(byLang['ko']!);
final anyCjk =
byLang.values.fold(<String>{}, (acc, s) => acc..addAll(s));
// Among candidates, prefer well-known pan-CJK font families.
String? pick(Iterable<String> pool) {
const preferred = ['notosanscjk', 'sourcehansans', 'sourcehanserif'];
for (final name in preferred) {
for (final p in pool) {
if (p.toLowerCase().contains(name)) return p;
}
}
return pool.isNotEmpty ? pool.first : null;
}
final found = pick(panCjk) ?? pick(anyCjk);
if (found != null) return found;
for (final p in _kFontSearchPaths) {
if (File(p).existsSync()) return p;
}
return null;
}

View File

@@ -1,8 +0,0 @@
/// Web stub for `native/font_manager.dart`.
///
/// The native implementation depends on `dart:io` (Process/File/Platform) to
/// load a system CJK font on ARM64 Linux, which cannot compile for the web
/// target. The web build has no such fontconfig limitation, so this is a no-op.
const kLinuxCjkFontFamily = 'SystemCJK';
Future<bool> loadSystemCJKFonts() async => false;

View File

@@ -868,6 +868,7 @@ pub mod clipboard_listener {
.unwrap()
.insert(name.clone(), tx);
cleanup_stale_listener(&mut listener_lock);
if listener_lock.handle.is_none() {
log::info!("Start clipboard listener thread");
let handler = Handler {
@@ -893,6 +894,24 @@ pub mod clipboard_listener {
Ok(())
}
fn cleanup_stale_listener(listener: &mut ClipboardListener) {
if !listener
.handle
.as_ref()
.map(|(_, h)| h.is_finished())
.unwrap_or(false)
{
return;
}
if let Some((shutdown, h)) = listener.handle.take() {
log::warn!("Cleaning up stale clipboard listener handle");
if let Err(e) = h.join() {
log::error!("Clipboard listener thread panicked during stale cleanup: {:?}", e);
}
drop(shutdown);
}
}
pub fn unsubscribe(name: &str) {
log::info!("Unsubscribe clipboard listener: {}", name);
let mut listener_lock = CLIPBOARD_LISTENER.lock().unwrap();

View File

@@ -1245,11 +1245,49 @@ pub fn legacy_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Vec<KeyEv
#[inline]
pub fn map_keyboard_mode(_peer: &str, event: &Event, key_event: KeyEvent) -> Vec<KeyEvent> {
if let Some(evt) = windows_peer_special_key(_peer, event) {
return vec![evt];
}
_map_keyboard_mode(_peer, event, key_event)
.map(|e| vec![e])
.unwrap_or_default()
}
fn windows_peer_special_key(peer: &str, event: &Event) -> Option<KeyEvent> {
if peer != OS_LOWER_WINDOWS {
return None;
}
let (key, down) = match event.event_type {
EventType::KeyPress(key) => (key, true),
EventType::KeyRelease(key) => (key, false),
_ => return None,
};
// Handle only `Pause` for Windows peers for now.
// Windows has no normal scan code for `Pause`, so send it as a legacy control key.
#[cfg(target_os = "windows")]
let is_pause = {
// The Windows scan code can look like `NumLock`; VK_PAUSE distinguishes it.
let pause_vk_code = rdev::win_code_from_key(Key::Pause);
key == Key::Pause || pause_vk_code == Some(event.platform_code as _)
};
#[cfg(not(target_os = "windows"))]
let is_pause = key == Key::Pause;
if !is_pause {
return None;
}
let mut key_event = KeyEvent::new();
key_event.mode = KeyboardMode::Legacy.into();
key_event.down = down;
key_event.set_control_key(ControlKey::Pause);
let (alt, ctrl, shift, command) = client::get_modifiers_state(false, false, false, false);
client::legacy_modifiers(&mut key_event, alt, ctrl, shift, command);
Some(key_event)
}
fn _map_keyboard_mode(_peer: &str, event: &Event, mut key_event: KeyEvent) -> Option<KeyEvent> {
match event.event_type {
EventType::KeyPress(..) => {
@@ -1421,6 +1459,11 @@ fn is_press(event: &Event) -> bool {
pub fn translate_keyboard_mode(peer: &str, event: &Event, key_event: KeyEvent) -> Vec<KeyEvent> {
let mut events: Vec<KeyEvent> = Vec::new();
if let Some(evt) = windows_peer_special_key(peer, event) {
events.push(evt);
return events;
}
if let Some(unicode_info) = &event.unicode {
if unicode_info.is_dead {
#[cfg(target_os = "macos")]

View File

@@ -103,15 +103,29 @@ pub const LANGS: &[(&str, &str)] = &[
("gu", "ગુજરાતી"),
];
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub fn translate(name: String) -> String {
let locale = sys_locale::get_locale().unwrap_or_default();
translate_locale(name, &locale)
pub(crate) fn cjk_ui_unavailable() -> bool {
cfg!(all(
target_os = "linux",
target_arch = "aarch64",
feature = "flutter"
))
}
pub fn translate_locale(name: String, locale: &str) -> String {
pub(crate) fn is_cjk_lang(lang_or_locale: &str) -> bool {
let lang = lang_or_locale
.split(|c| c == '-' || c == '_')
.next()
.unwrap_or_default()
.to_lowercase();
matches!(lang.as_str(), "zh" | "ja" | "ko")
}
fn resolve_lang(saved_lang: &str, locale: &str, cjk_fallback: bool) -> String {
let locale = locale.to_lowercase();
let mut lang = hbb_common::config::LocalConfig::get_option("lang").to_lowercase();
let mut lang = saved_lang.to_lowercase();
if cjk_fallback && is_cjk_lang(&lang) {
return "en".to_owned();
}
if lang.is_empty() {
// zh_CN on Linux, zh-Hans-CN on mac, zh_CN_#Hans on Android
if locale.starts_with("zh") {
@@ -131,7 +145,25 @@ pub fn translate_locale(name: String, locale: &str) -> String {
.unwrap_or_default()
.to_owned();
}
let lang = lang.to_lowercase();
if cjk_fallback && is_cjk_lang(&lang) {
"en".to_owned()
} else {
lang
}
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub fn translate(name: String) -> String {
let locale = sys_locale::get_locale().unwrap_or_default();
translate_locale(name, &locale)
}
pub fn translate_locale(name: String, locale: &str) -> String {
let lang = resolve_lang(
&hbb_common::config::LocalConfig::get_option("lang"),
locale,
cjk_ui_unavailable(),
);
let m = match lang.as_str() {
"fr" => fr::T.deref(),
"zh-cn" => cn::T.deref(),
@@ -275,4 +307,32 @@ mod test {
("{} times {4} makes {8}".to_string(), Some("2".to_string()))
);
}
#[test]
fn test_resolve_lang_forces_english_for_saved_cjk_when_target_disables_cjk() {
use super::resolve_lang as f;
assert_eq!(f("zh-cn", "en-US", true), "en");
assert_eq!(f("zh-tw", "en-US", true), "en");
assert_eq!(f("ja", "en-US", true), "en");
assert_eq!(f("ko", "en-US", true), "en");
}
#[test]
fn test_resolve_lang_forces_english_for_cjk_locale_when_target_disables_cjk() {
use super::resolve_lang as f;
assert_eq!(f("", "zh_CN", true), "en");
assert_eq!(f("", "ja-JP", true), "en");
assert_eq!(f("", "ko_KR", true), "en");
}
#[test]
fn test_resolve_lang_preserves_cjk_when_target_allows_cjk() {
use super::resolve_lang as f;
assert_eq!(f("zh-cn", "en-US", false), "zh-cn");
assert_eq!(f("", "zh_TW", false), "zh-tw");
assert_eq!(f("", "ja-JP", false), "ja");
}
}

View File

@@ -759,7 +759,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("remember-wayland-keyboard-choice-tip", "Non chiedere più per questo computer remoto"),
("Why this happens", "Perché accade questo"),
("Switch display", "Cambia schermo"),
("Show monitor switch button on the main toolbar", "Mostra il pulsante di cambio monitor nella barra degli strumenti principale"),
("Show on the minimized toolbar", "Mostra nella barra degli strumenti ridotta a icona"),
("Show monitor switch button on the main toolbar", "Visualizza nella barra strumenti principale il pulsante per il cambio schermo"),
("Show on the minimized toolbar", "Visualizza nella barra strumenti ridotta a icona"),
].iter().cloned().collect();
}

View File

@@ -16,18 +16,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Control Remote Desktop", "Controle um Computador Remoto"),
("Transfer file", "Transferir arquivos"),
("Connect", "Conectar"),
("Recent sessions", "Sessões Recentes"),
("Address book", "Lista de Endereços"),
("Recent sessions", "Sessões recentes"),
("Address book", "Lista de endereços"),
("Confirmation", "Confirmação"),
("TCP tunneling", "Tunelamento TCP"),
("Remove", "Remover"),
("Refresh random password", "Atualizar senha aleatória"),
("Set your own password", "Configure sua própria senha"),
("Refresh random password", "Gerar nova senha aleatória"),
("Set your own password", "Definir sua própria senha"),
("Enable keyboard/mouse", "Habilitar teclado/mouse"),
("Enable clipboard", "Habilitar área de transferência"),
("Enable file transfer", "Habilitar transferência de arquivos"),
("Enable TCP tunneling", "Habilitar tunelamento TCP"),
("IP Whitelisting", "Lista de IPs Confiáveis"),
("IP Whitelisting", "Lista de IPs Permitidos"),
("ID/Relay Server", "Servidor ID/Relay"),
("Import server config", "Importar Configuração do Servidor"),
("Export Server Config", "Exportar Configuração do Servidor"),
@@ -320,12 +320,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Exit Fullscreen", "Sair da Tela Cheia"),
("Fullscreen", "Tela Cheia"),
("Mobile Actions", "Ações móveis"),
("Select Monitor", "Selecionar monitor"),
("Select Monitor", "Selecionar tela"),
("Control Actions", "Controlar ações"),
("Display Settings", "Configurações de exibição"),
("Ratio", "Proporção"),
("Image Quality", "Qualidade de imagem"),
("Scroll Style", "Estilo de Rolagem"),
("Scroll Style", "Estilo de rolagem"),
("Show Toolbar", "Mostrar barra de ferramentas"),
("Hide Toolbar", "Ocultar barra de ferramentas"),
("Direct Connection", "Conexão Direta"),
@@ -353,7 +353,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Disconnect all devices?", "Desconectar todos os dispositivos?"),
("Clear", "Limpar"),
("Audio Input Device", "Dispositivo de entrada de áudio"),
("Use IP Whitelisting", "Utilizar lista de IPs confiáveis"),
("Use IP Whitelisting", "Utilizar lista de IPs permitidos"),
("Network", "Rede"),
("Pin Toolbar", "Fixar barra de ferramentas"),
("Unpin Toolbar", "Desafixar barra de ferramentas"),
@@ -463,7 +463,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Empty Password", "Senha Vazia"),
("Me", "Eu"),
("identical_file_tip", "Este arquivo é idêntico ao do parceiro."),
("show_monitors_tip", "Mostrar monitores na barra de ferramentas"),
("show_monitors_tip", "Mostrar telas na barra de ferramentas"),
("View Mode", "Modo de visualização"),
("login_linux_tip", "Você precisa fazer login na conta Linux remota para habilitar uma sessão de desktop X"),
("verify_rustdesk_password_tip", "Verifique a senha do RustDesk"),
@@ -674,7 +674,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("dont-show-again-tip", "Não mostrar novamente"),
("Take screenshot", "Capturar tela"),
("Taking screenshot", "Capturando tela"),
("screenshot-merged-screen-not-supported-tip", "Mesclar a captura de tela de múltiplos monitores não é suportada no momento. Por favor, alterne para um único monitor e tente novamente."),
("screenshot-merged-screen-not-supported-tip", "A captura de tela de múltiplas telas não é suportada no momento. Por favor, alterne para uma única tela e tente novamente."),
("screenshot-action-tip", "Por favor, selecione como deseja continuar com a captura de tela."),
("Save as", "Salvar como"),
("Copy to clipboard", "Copiar para área de transferência"),
@@ -694,7 +694,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable UDP hole punching", "Habilitar UDP hole punching"),
("View camera", "Visualizar câmera"),
("Enable camera", "Habilitar câmera"),
("No cameras", "Nenhuma câmeras"),
("No cameras", "Nenhuma câmera"),
("view_camera_unsupported_tip", "O dispositivo remoto não suporta visualização da câmera."),
("Terminal", "Terminal"),
("Enable terminal", "Habilitar terminal"),
@@ -759,7 +759,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("remember-wayland-keyboard-choice-tip", "Não perguntar novamente para este computador remoto"),
("Why this happens", "Por que isso acontece"),
("Switch display", "Trocar de tela"),
("Show monitor switch button on the main toolbar", "Mostrar o botão de troca de monitor na barra de ferramentas principal"),
("Show monitor switch button on the main toolbar", "Mostrar botão de troca de tela na barra de ferramentas"),
("Show on the minimized toolbar", "Mostrar na barra de ferramentas minimizada"),
].iter().cloned().collect();
}

View File

@@ -51,6 +51,7 @@ fn check_desktop_manager() {
pub fn start_xdesktop() {
debug_assert!(crate::is_server());
std::thread::spawn(|| {
DesktopManager::recover_orphaned_session();
*DESKTOP_MANAGER.lock().unwrap() = Some(DesktopManager::new());
let interval = time::Duration::from_millis(super::SERVICE_INTERVAL);
@@ -462,9 +463,10 @@ impl DesktopManager {
let (child_xorg, child_wm) = Self::start_x11(uid, gid, username, display_num, &envs)?;
is_child_running.store(true, Ordering::SeqCst);
// capture the logind session scope (from a live child) for teardown, see
// reap_session_scope.
// capture the logind session scope (from a live child) for teardown and crash
// recovery, see reap_session_scope and recover_orphaned_session.
let scope_dir = Self::session_scope_dir(child_xorg.id());
Self::save_orphaned_marker(&scope_dir, display_num);
log::info!("Start xorg and wm done, notify and wait xtop x11");
allow_err!(tx_res.send("".to_owned()));
@@ -879,6 +881,66 @@ impl DesktopManager {
std::io::Error::last_os_error().raw_os_error() == Some(hbb_common::libc::EPERM)
}
const ORPHANED_SESSION_KEY: &'static str = "headless-orphaned-session";
fn save_orphaned_marker(scope_dir: &str, display_num: u32) {
// tag the marker with this boot's id: a logind session id is only unique within a
// boot (the counter lives in /run and resets), so recovery must not reap a recorded
// scope path after a reboot, when it may name a different live session.
let boot_id = Self::current_boot_id().unwrap_or_default();
hbb_common::config::LocalConfig::set_option(
Self::ORPHANED_SESSION_KEY.to_owned(),
format!("{};{};{}", scope_dir, display_num, boot_id),
);
}
fn current_boot_id() -> Option<String> {
std::fs::read_to_string("/proc/sys/kernel/random/boot_id")
.ok()
.map(|s| s.trim().to_owned())
}
fn clear_orphaned_marker() {
hbb_common::config::LocalConfig::set_option(
Self::ORPHANED_SESSION_KEY.to_owned(),
String::new(),
);
}
fn parse_orphaned_marker(marker: &str) -> Option<(&str, u32, &str)> {
let (rest, boot_id) = marker.rsplit_once(';')?;
let (scope_dir, display) = rest.rsplit_once(';')?;
Some((scope_dir, display.trim().parse::<u32>().ok()?, boot_id))
}
// a run that dies before wait_stop_x11 (service or --server crash) leaks the headless
// session scope + X lock files, the same as a missed teardown (rustdesk/rustdesk#15183).
// reap exactly what the dead run recorded - never a scan, so unrelated sessions are safe.
fn recover_orphaned_session() {
let marker = hbb_common::config::LocalConfig::get_option(Self::ORPHANED_SESSION_KEY);
if marker.is_empty() {
return;
}
if let Some((scope_dir, display_num, boot_id)) = Self::parse_orphaned_marker(&marker) {
// only reap the recorded scope when the marker is from this same boot: a leaked
// cgroup cannot outlive a reboot, so cross-boot there is nothing legitimate to
// reap, and the recorded "session-N.scope" may by then name a different live
// session. the X lock cleanup is pid-guarded, so run it either way.
let same_boot = Self::current_boot_id().map_or(false, |b| b == boot_id);
log::info!(
"Recovering leaked headless session from a previous run: scope {}, display {} (same boot: {})",
scope_dir,
display_num,
same_boot
);
if same_boot {
Self::reap_session_scope(scope_dir);
}
Self::cleanup_x_display_files(display_num);
}
Self::clear_orphaned_marker();
}
fn try_wait_stop_x11(
child_xorg: &mut Child,
child_wm: &mut Child,
@@ -898,6 +960,7 @@ impl DesktopManager {
Self::wait_x11_children_exit(child_xorg, child_wm);
Self::reap_session_scope(scope_dir);
Self::cleanup_x_display_files(display_num);
Self::clear_orphaned_marker();
desktop_manager
.is_child_running
.store(false, Ordering::SeqCst);
@@ -1082,4 +1145,27 @@ mod tests {
assert_eq!(pids, vec![100, 101, 200, 300]);
}
#[test]
fn parses_orphaned_session_marker() {
assert_eq!(
DesktopManager::parse_orphaned_marker(
"/sys/fs/cgroup/user.slice/user-1000.slice/session-3.scope;7;abc-123"
),
Some((
"/sys/fs/cgroup/user.slice/user-1000.slice/session-3.scope",
7,
"abc-123"
))
);
// an empty scope still carries the display so its stale X lock can be cleaned
assert_eq!(DesktopManager::parse_orphaned_marker(";5;abc-123"), Some(("", 5, "abc-123")));
// an empty boot id never matches the live one, so the scope reap is skipped
assert_eq!(DesktopManager::parse_orphaned_marker("/scope;5;"), Some(("/scope", 5, "")));
assert_eq!(DesktopManager::parse_orphaned_marker(""), None);
assert_eq!(DesktopManager::parse_orphaned_marker("garbage"), None);
// the pre-boot-id two-field format no longer parses, recovery just skips it
assert_eq!(DesktopManager::parse_orphaned_marker("/scope;7"), None);
assert_eq!(DesktopManager::parse_orphaned_marker("/scope;notnum;abc"), None);
}
}

View File

@@ -902,8 +902,10 @@ pub fn get_async_job_status() -> String {
#[inline]
pub fn get_langs() -> String {
use serde_json::json;
let hide_cjk = crate::lang::cjk_ui_unavailable();
let mut x: Vec<(&str, String)> = crate::lang::LANGS
.iter()
.filter(|a| !hide_cjk || !crate::lang::is_cjk_lang(a.0))
.map(|a| (a.0, format!("{} ({})", a.1, a.0)))
.collect();
x.sort_by(|a, b| a.0.cmp(b.0));