Merge branch 'master' into login_method

This commit is contained in:
21pages committed 2026-11-01 16:49:04 +08:00
commit 2fd2d89aa9
69 files changed
+526 -44

No files matched your search

+88 -1
View File
@@ -426,6 +426,37 @@ jobs:
throw "RustDesk reports version '$reportedVersion', expected '$env:VERSION'"
}
- name: Verify template payload signatures
if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != '-2'
shell: powershell
run: |
Get-ChildItem ./rustdesk -Recurse -File -ErrorAction Stop |
Where-Object { $_.Extension -eq '.exe' -or ($_.Extension -eq '.dll' -and $_.FullName -notmatch 'RustDeskPrinterDriver|usbmmidd_v2') } |
ForEach-Object {
$signature = Get-AuthenticodeSignature -LiteralPath $_.FullName -ErrorAction Stop
if ($signature.Status -ne 'Valid') { throw "Invalid template payload signature: $($_.FullName) ($($signature.Status))" }
}
- name: Build template packer
if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != '-2'
shell: bash
run: |
sed -i '/dpiAware/d' res/manifest.xml
pushd ./libs/portable
pip3 install -r requirements.txt
python3 ./generate.py -f ../../rustdesk/ -e rustdesk.exe -l 11 -t "${{ matrix.job.target }}" --exclude-exe
popd
mkdir -p ./packer-template
cp ./target/${{ matrix.job.target }}/release/rustdesk-portable-packer.exe ./packer-template/rustdesk-template-${{ matrix.job.arch }}.exe
- name: Upload unsigned template packer
if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != '-2'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: rustdesk-unsigned-packer-template-${{ matrix.job.arch }}
path: ./packer-template
if-no-files-found: error
- name: Build self-extracted executable
shell: bash
if: env.UPLOAD_ARTIFACT == 'true'
@@ -690,6 +721,37 @@ jobs:
throw "RustDesk reports version '$reportedVersion', expected '$env:VERSION'"
}
- name: Verify template payload signatures
if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != '-2'
shell: powershell
run: |
Get-ChildItem ./Release -Recurse -File -ErrorAction Stop |
Where-Object { $_.Extension -eq '.exe' -or ($_.Extension -eq '.dll' -and $_.FullName -notmatch 'RustDeskPrinterDriver|usbmmidd_v2') } |
ForEach-Object {
$signature = Get-AuthenticodeSignature -LiteralPath $_.FullName -ErrorAction Stop
if ($signature.Status -ne 'Valid') { throw "Invalid template payload signature: $($_.FullName) ($($signature.Status))" }
}
- name: Build template packer
if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != '-2'
shell: bash
run: |
sed -i '/dpiAware/d' res/manifest.xml
pushd ./libs/portable
pip3 install -r requirements.txt
python3 ./generate.py -f ../../Release/ -e rustdesk.exe -l 11 -t "${{ matrix.job.target }}"
popd
mkdir -p ./packer-template
cp ./target/${{ matrix.job.target }}/release/rustdesk-portable-packer.exe ./packer-template/rustdesk-template-${{ matrix.job.arch }}.exe
- name: Upload unsigned template packer
if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != '-2'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: rustdesk-unsigned-packer-template-${{ matrix.job.arch }}
path: ./packer-template
if-no-files-found: error
- name: Build self-extracted executable
shell: bash
run: |
@@ -1301,9 +1363,34 @@ jobs:
name: rustdesk-unsigned-msi-template-aarch64
path: ./msi-template/
- name: Download Artifacts
if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != '-2'
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: rustdesk-unsigned-packer-template-x86_64
path: ./packer-template/
- name: Download Artifacts
if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != '-2'
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: rustdesk-unsigned-packer-template-aarch64
path: ./packer-template/
- name: Download Artifacts
if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != '-2'
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: rustdesk-unsigned-packer-template-x86
path: ./packer-template/
- name: Combine unsigned app
run: |
tar czf rustdesk-${{ env.VERSION }}-unsigned.tar.gz *.dmg windows-x86_64 windows-aarch64 windows-x86 msi-template
packer_template=()
if [[ "$SIGN_BASE_URL" != '-2' ]]; then
packer_template=(packer-template)
fi
tar czf rustdesk-${{ env.VERSION }}-unsigned.tar.gz *.dmg windows-x86_64 windows-aarch64 windows-x86 msi-template "${packer_template[@]}"
- name: Publish unsigned app
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
+3 -7
View File
@@ -40,7 +40,7 @@ prepare_flutter() {
# Patch flutter
if dpkg --compare-versions "${VERSION}" ge "3.24.4"; then
if dpkg --compare-versions "${VERSION}" ge "3.24.4" && dpkg --compare-versions "${VERSION}" lt "3.44.0"; then
git apply "${ROOTDIR}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff"
fi
@@ -324,11 +324,7 @@ prebuild)
# Install Flutter bridge version
# Unbraced on purpose: F-Droid recipes rewrite the braced form with sed to
# pin a version, but the bridge generator only works with the Flutter
# version bridge.yml uses
prepare_flutter "$FLUTTER_BRIDGE_VERSION" "${HOME}/flutter"
prepare_flutter "${FLUTTER_BRIDGE_VERSION}" "${HOME}/flutter"
# Save changes
@@ -346,7 +342,7 @@ prebuild)
pushd flutter
flutter clean
flutter packages pub get --enforce-lockfile
flutter packages pub get
popd # flutter
@@ -10,6 +10,7 @@
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
5CE7AD012EB4000000A1B2C3 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CE7AD002EB4000000A1B2C3 /* SceneDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
@@ -40,6 +41,7 @@
4151ACC476A12FDC49BC7860 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
5CE7AD002EB4000000A1B2C3 /* SceneDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
@@ -104,6 +106,7 @@
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
5CE7AD002EB4000000A1B2C3 /* SceneDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
@@ -279,6 +282,7 @@
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
5CE7AD012EB4000000A1B2C3 /* SceneDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
+13 -1
View File
@@ -8,7 +8,6 @@ import AVFoundation
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
do {
// Mix remote audio with other apps while still respecting silent mode.
try AVAudioSession.sharedInstance().setCategory(.ambient)
@@ -18,6 +17,19 @@ import AVFoundation
dummyMethodToEnforceBundling();
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
// With the UIScene life cycle, the FlutterViewController only exists once the scene
// connects, and FlutterAppDelegate reaches its engine through `window`. A launch URL
// also arrives with the scene instead of in launchOptions, so forward it to plugins here.
func sceneWillConnect(window: UIWindow?, launchURL: URL?) {
self.window = window
GeneratedPluginRegistrant.register(with: self)
var launchOptions: [UIApplication.LaunchOptionsKey: Any] = [:]
if let launchURL = launchURL {
launchOptions[.url] = launchURL
}
_ = super.application(UIApplication.shared, didFinishLaunchingWithOptions: launchOptions)
}
public func dummyMethodToEnforceBundling() {
dummy_method_to_enforce_bundling();
+22 -1
View File
@@ -47,6 +47,27 @@
<string>This app needs camera access to scan QR codes</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>This app needs photo library access to get QR codes from image</string>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneConfigurationName</key>
<string>Default Configuration</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIFileSharingEnabled</key>
@@ -71,7 +92,7 @@
<key>UISupportsDocumentBrowser</key>
<true/>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<true/>
<key>io.flutter.embedded_views_preview</key>
<true/>
</dict>
+21
View File
@@ -0,0 +1,21 @@
import UIKit
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(
_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions
) {
(UIApplication.shared.delegate as? AppDelegate)?.sceneWillConnect(
window: window, launchURL: connectionOptions.urlContexts.first?.url)
}
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }
for context in URLContexts {
_ = appDelegate.application(UIApplication.shared, open: context.url, options: [:])
}
}
}
+51 -21
View File
@@ -3,6 +3,7 @@ import 'dart:convert';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/desktop/widgets/install_directory_confirmation.dart';
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
@@ -164,27 +165,46 @@ class _InstallPageBodyState extends State<_InstallPageBody>
children: [
Text(translate('Installation'),
style: Theme.of(context).textTheme.headlineMedium),
Row(
Table(
columnWidths: const {
0: IntrinsicColumnWidth(),
2: IntrinsicColumnWidth(),
},
defaultVerticalAlignment: TableCellVerticalAlignment.middle,
children: [
Text('${translate('Installation Path')}:')
.marginOnly(right: 10),
Expanded(
child: TextField(
controller: controller,
readOnly: true,
decoration: InputDecoration(
contentPadding: EdgeInsets.all(0.75 * em),
TableRow(
children: [
Text('${translate('Installation Path')}:')
.marginOnly(right: 10),
TextField(
controller: controller,
readOnly: true,
decoration: InputDecoration(
contentPadding: EdgeInsets.all(0.75 * em),
),
).workaroundFreezeLinuxMint().marginOnly(right: 10),
Obx(
() => OutlinedButton.icon(
icon: Icon(Icons.folder_outlined, size: 16),
onPressed:
btnEnabled.value ? selectInstallPath : null,
style: buttonStyle,
label: Text(translate('Change Path')),
),
),
).workaroundFreezeLinuxMint().marginOnly(right: 10),
],
),
TableRow(
children: [
const SizedBox.shrink(),
ValueListenableBuilder<TextEditingValue>(
valueListenable: controller,
builder: (context, value, child) =>
InstallDirectoryWarning(directoryPath: value.text),
).marginOnly(right: 10),
const SizedBox.shrink(),
],
),
Obx(
() => OutlinedButton.icon(
icon: Icon(Icons.folder_outlined, size: 16),
onPressed: btnEnabled.value ? selectInstallPath : null,
style: buttonStyle,
label: Text(translate('Change Path')),
),
)
],
).marginSymmetric(vertical: 2 * em),
Option(startmenu, label: 'Create start menu shortcuts')
@@ -277,7 +297,9 @@ class _InstallPageBodyState extends State<_InstallPageBody>
));
}
void install() {
Future<void> install() async {
if (!btnEnabled.value) return;
final installPath = controller.text;
do_install() {
btnEnabled.value = false;
cancelEnabled.value = false;
@@ -286,10 +308,18 @@ class _InstallPageBodyState extends State<_InstallPageBody>
if (startmenu.value) args += ' startmenu';
if (desktopicon.value) args += ' desktopicon';
if (printer.value) args += ' printer';
bind.installInstallMe(options: args, path: controller.text);
bind.installInstallMe(options: args, path: installPath);
}
do_install();
btnEnabled.value = false;
try {
final confirmed =
await confirmInstallDirectory(this.context, targetPath: installPath);
if (!mounted || !confirmed) return;
do_install();
} finally {
if (mounted && !showProgress.value) btnEnabled.value = true;
}
}
void selectInstallPath() async {
@@ -0,0 +1,96 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:path/path.dart' as path;
const _nonemptyDirectoryWarning = 'install-directory-not-empty-tip';
Future<bool> confirmInstallDirectory(BuildContext context,
{required String targetPath}) async {
try {
final needsWarning = await _needsDirectoryWarning(targetPath);
if (!context.mounted) return false;
if (!needsWarning) return true;
return await _showDirectoryDialog(
context, '${translate(_nonemptyDirectoryWarning)}\n$targetPath');
} on FileSystemException catch (error) {
debugPrint('Failed to inspect installation directory: $error');
if (context.mounted) {
await _showDirectoryDialog(context, error.toString(), isError: true);
}
return false;
}
}
Future<bool> _needsDirectoryWarning(String directoryPath) async {
if (bind.mainIsInstalled() &&
path.equals(directoryPath, bind.installInstallPath())) {
return false;
}
final directory = Directory(directoryPath);
return await directory.exists() &&
!await directory.list(followLinks: false).isEmpty;
}
Future<bool> _showDirectoryDialog(BuildContext context, String message,
{bool isError = false}) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (dialogContext) => AlertDialog(
scrollable: true,
title: Text(translate(isError ? 'Error' : 'Warning')),
content: SelectableText(message),
actions: [
dialogButton(isError ? 'OK' : 'Cancel',
isOutline: !isError,
onPressed: () => Navigator.of(dialogContext).pop(false)),
if (!isError)
dialogButton('Continue',
onPressed: () => Navigator.of(dialogContext).pop(true)),
],
),
);
return confirmed == true;
}
class InstallDirectoryWarning extends StatefulWidget {
InstallDirectoryWarning({required this.directoryPath})
: super(key: ValueKey(directoryPath));
final String directoryPath;
@override
State<InstallDirectoryWarning> createState() =>
_InstallDirectoryWarningState();
}
class _InstallDirectoryWarningState extends State<InstallDirectoryWarning> {
static const _spacing = 8.0;
late final Future<bool> _needsWarning =
_needsDirectoryWarning(widget.directoryPath);
@override
Widget build(BuildContext context) {
return FutureBuilder<bool>(
future: _needsWarning,
builder: (context, snapshot) {
if (!snapshot.hasError && snapshot.data != true) {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.symmetric(vertical: _spacing),
child: Text(
snapshot.hasError
? '${translate('Error')}: ${snapshot.error}'
: translate(_nonemptyDirectoryWarning),
style: TextStyle(
color: snapshot.hasError
? Theme.of(context).colorScheme.error
: Colors.orange.shade700)),
);
},
);
}
}
+45 -2
View File
@@ -6,8 +6,48 @@ import time
import argparse
import logging
import shutil
import sys
import zipfile
def _file_stream_positions(files):
if not files:
return []
values = files.values() if hasattr(files, "values") else (item[1] for item in files)
positions = []
for value in values:
stream = value[1] if isinstance(value, tuple) and len(value) > 1 else value
if hasattr(stream, "tell") and hasattr(stream, "seek"):
try:
positions.append((stream, stream.tell()))
except (OSError, ValueError):
pass
return positions
def _with_simple_retries(fn, max_attempts=8, sleep_secs=1.0):
def _wrapper(*args, **kwargs):
file_positions = _file_stream_positions(kwargs.get("files"))
for i in range(max_attempts):
try:
if i:
for stream, position in file_positions:
stream.seek(position)
return fn(*args, **kwargs)
except Exception as e:
logging.warning(f"{fn.__name__} attempt {i+1}/{max_attempts} failed: {e}")
if i == max_attempts - 1:
raise
time.sleep(sleep_secs)
return _wrapper
HTTP_RETRY_MAX_TRIES = int(os.getenv("HTTP_RETRY_MAX_TRIES") or "8")
HTTP_RETRY_SLEEP_SECS = float(os.getenv("HTTP_RETRY_SLEEP_SECS") or "1")
requests.get = _with_simple_retries(requests.get, max_attempts=HTTP_RETRY_MAX_TRIES, sleep_secs=HTTP_RETRY_SLEEP_SECS)
requests.post = _with_simple_retries(requests.post, max_attempts=HTTP_RETRY_MAX_TRIES, sleep_secs=HTTP_RETRY_SLEEP_SECS)
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s [%(filename)s:%(lineno)d]",
@@ -164,10 +204,13 @@ def sign_one_file(file_path):
n += 1
status = get_status(task_id)
if status and status.get("state") == "done":
download_one_file(
downloaded = download_one_file(
task_id, os.path.basename(file_path), os.path.dirname(file_path)
)
delete_task(task_id)
if not downloaded:
logging.error(f"Failed to download signed {file_path}")
return False
logging.info(f"Signed {file_path}")
return True
return False
@@ -217,7 +260,7 @@ def sign_files(dir_path, only_ext=None):
if ext in SIGN_EXTENSIONS:
if not sign_one_file(file_path):
logging.error(f"Failed to sign {file_path}")
break
sys.exit(1)
def main():
+1
View File
@@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Trust duration (days)", "مدة الثقة (بالأيام)"),
("Renew trust", "تجديد الثقة"),
("Request approval", ""),
("install-directory-not-empty-tip", "المجلد المحدد ليس فارغًا. قد يؤدي التثبيت إلى حذف البيانات الموجودة. يُنصح باختيار مجلد آخر."),
].iter().cloned().collect();
}
+1
View File
@@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Trust duration (days)", "Etibar müddəti (gün)"),
("Renew trust", "Etibarı yenilə"),
("Request approval", ""),
("install-directory-not-empty-tip", "Seçilmiş qovluq boş deyil. Quraşdırma mövcud məlumatları silə bilər. Başqa qovluq seçməyiniz tövsiyə olunur."),
].iter().cloned().collect();
}
+1
View File
@@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Trust duration (days)", "Тэрмін даверу (у днях)"),
("Renew trust", "Падоўжыць давер"),
("Request approval", ""),
("install-directory-not-empty-tip", "Выбраная папка не пустая. Усталёўванне можа выдаліць наяўныя даныя. Лепш выберыце іншую папку."),
].iter().cloned().collect();
}
+1
View File
@@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Trust duration (days)", "Срок на доверие (дни)"),
("Renew trust", "Подновяване на доверието"),
("Request approval", ""),
("install-directory-not-empty-tip", "Избраната папка не е празна. Инсталацията може да изтрие съществуващите данни. Препоръчително е да изберете друга папка."),
].iter().cloned().collect();
}
+1
View File
@@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Trust duration (days)", "Durada de la confiança (dies)"),
("Renew trust", "Renova la confiança"),
("Request approval", ""),
("install-directory-not-empty-tip", "La carpeta seleccionada no és buida. La instal·lació pot suprimir les dades existents. Considereu triar una altra carpeta."),
].iter().cloned().collect();
}
+1
View File
@@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Trust duration (days)", "信任有效期(天)"),
("Renew trust", "延长信任有效期"),
("Request approval", "请求批准"),
("install-directory-not-empty-tip", "所选文件夹不为空。安装可能删除现有数据。建议选择其他文件夹。"),
].iter().cloned().collect();
}
+1
View File
@@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Trust duration (days)", "Doba důvěry (dny)"),
("Renew trust", "Obnovit důvěru"),
("Request approval", ""),
("install-directory-not-empty-tip", "Vybraná složka není prázdná. Instalace může odstranit existující data. Zvažte výběr jiné složky."),
].iter().cloned().collect();
}
+1
View File
@@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Trust duration (days)", "Tillidsperiode (dage)"),
("Renew trust", "Forny tillid"),
("Request approval", ""),
("install-directory-not-empty-tip", "Den valgte mappe er ikke tom. Installationen kan slette eksisterende data. Overvej at vælge en anden mappe."),
].iter().cloned().collect();
}
+1
View File
@@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Trust duration (days)", "Vertrauensdauer (Tage)"),
("Renew trust", "Vertrauen erneuern"),
("Request approval", ""),
("install-directory-not-empty-tip", "Der ausgewählte Ordner ist nicht leer. Bei der Installation können vorhandene Daten gelöscht werden. Erwägen Sie, einen anderen Ordner auszuwählen."),
].iter().cloned().collect();
}
+1
View File
@@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Trust duration (days)", "Διάρκεια εμπιστοσύνης (ημέρες)"),
("Renew trust", "Ανανέωση εμπιστοσύνης"),
("Request approval", ""),
("install-directory-not-empty-tip", "Ο επιλεγμένος φάκελος δεν είναι κενός. Η εγκατάσταση ενδέχεται να διαγράψει υπάρχοντα δεδομένα. Εξετάστε το ενδεχόμενο να επιλέξετε διαφορετικό φάκελο."),
].iter().cloned().collect();
}
+1
View File
@@ -291,5 +291,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("api_server_connection_tip", "Your custom server uses unencrypted HTTP. Contact your administrator to enable HTTPS."),
("api_server_empty_tip", "With API Server empty, your custom server defaults to HTTP. Contact your administrator to enable HTTPS."),
("android-logs-folder-tip", "Choose Android → media to store log files."),
("install-directory-not-empty-tip", "The selected folder is not empty. Installation may delete existing data. Consider choosing a different folder."),
].iter().cloned().collect();
}
+1
View File
@@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Trust duration (days)", "Daŭro de fido (tagoj)"),
("Renew trust", "Renovigi fidon"),
("Request approval", ""),
("install-directory-not-empty-tip", "La elektita dosierujo ne estas malplena. La instalado povas forigi ekzistantajn datumojn. Konsideru elekti alian dosierujon."),
].iter().cloned().collect();
}
+1
View File
@@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Trust duration (days)", "Duración de la confianza (días)"),
("Renew trust", "Renovar confianza"),
("Request approval", ""),
("install-directory-not-empty-tip", "La carpeta seleccionada no está vacía. La instalación puede eliminar los datos existentes. Considera elegir otra carpeta."),
].iter().cloned().collect();
}
+1
View File
@@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Trust duration (days)", "Usalduse kestus (päevades)"),
("Renew trust", "Uuenda usaldust"),
("Request approval", ""),
("install-directory-not-empty-tip", "Valitud kaust ei ole tühi. Paigaldamine võib olemasolevad andmed kustutada. Kaalu teise kausta valimist."),
].iter().cloned().collect();
}
+1
View File
@@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Trust duration (days)", "Konfiantzaren iraupena (egunak)"),
("Renew trust", "Berritu konfiantza"),
("Request approval", ""),
("install-directory-not-empty-tip", "Hautatutako karpeta ez dago hutsik. Instalazioak lehendik dauden datuak ezaba ditzake. Beste karpeta bat aukeratzea gomendatzen da."),
].iter().cloned().collect();
}
+1
View File
@@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Trust duration (days)", "مدت اعتماد (روز)"),
("Renew trust", "تمدید اعتماد"),
("Request approval", ""),
("install-directory-not-empty-tip", "پوشه انتخاب‌شده خالی نیست. نصب ممکن است داده‌های موجود را حذف کند. بهتر است پوشه دیگری انتخاب کنید."),
].iter().cloned().collect();
}
+1
View File
@@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Trust duration (days)", "Luottamuksen kesto (päivää)"),
("Renew trust", "Jatka luottamusta"),
("Request approval", ""),
("install-directory-not-empty-tip", "Valittu kansio ei ole tyhjä. Asennus voi poistaa olemassa olevia tietoja. Harkitse toisen kansion valitsemista."),
].iter().cloned().collect();
}
+1
View File
@@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Trust duration (days)", "Durée de confiance (jours)"),
("Renew trust", "Renouveler la confiance"),
("Request approval", ""),
("install-directory-not-empty-tip", "Le dossier sélectionné n’est pas vide. L’installation peut supprimer des données existantes. Envisagez de choisir un autre dossier."),
].iter().cloned().collect();
}
+1
View File
@@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Trust duration (days)", "ნდობის ხანგრძლივობა (დღე)"),
("Renew trust", "ნდობის განახლება"),
("Request approval", ""),
("install-directory-not-empty-tip", "არჩეული საქაღალდე ცარიელი არ არის. ინსტალაციამ შეიძლება წაშალოს არსებული მონაცემები. რეკომენდებულია სხვა საქაღალდის არჩევა."),
].iter().cloned().collect();
}
+1
View File
@@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Trust duration (days)", "Duración da confianza (días)"),
("Renew trust", "Renovar a confianza"),
("Request approval", ""),
("install-directory-not-empty-tip", "O cartafol seleccionado non está baleiro. A instalación pode eliminar os datos existentes. Considera escoller outro cartafol."),
].iter().cloned().collect();
}
+1
View File
@@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Trust duration (days)", "વિશ્વાસ અવધિ (દિવસો)"),
("Renew trust", "વિશ્વાસ નવીકરણ કરો"),
("Request approval", ""),
("install-directory-not-empty-tip", "પસંદ કરેલ ફોલ્ડર ખાલી નથી. ઇન્સ્ટોલેશન હાલનો ડેટા કાઢી નાખી શકે છે. બીજું ફોલ્ડર પસંદ કરવાનું વિચારો."),
].iter().cloned().collect();
}
+1
View File
@@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Trust duration (days)", "משך האמון (ימים)"),
("Renew trust", "חידוש אמון"),
("Request approval", ""),
("install-directory-not-empty-tip", "התיקייה שנבחרה אינה ריקה. ההתקנה עלולה למחוק נתונים קיימים. מומלץ לבחור תיקייה אחרת."),
].iter().cloned().collect();
}
+1
View File
@@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Trust duration (days)", "विश्वास अवधि (दिन)"),
("Renew trust", "विश्वास नवीनीकृत करें"),
("Request approval", ""),
("install-directory-not-empty-tip", "चयनित फ़ोल्डर खाली नहीं है। इंस्टॉलेशन से मौजूदा डेटा हट सकता है। कोई दूसरा फ़ोल्डर चुनने पर विचार करें।"),
].iter().cloned().collect();
}
+1
View File
@@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Trust duration (days)", "Trajanje povjerenja (dani)"),
("Renew trust", "Obnovi povjerenje"),
("Request approval", ""),
("install-directory-not-empty-tip", "Odabrana mapa nije prazna. Instalacija može izbrisati postojeće podatke. Razmislite o odabiru druge mape."),
].iter().cloned().collect();
}
+1
View File
@@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Trust duration (days)", "Megbízhatóság időtartama (nap)"),
("Renew trust", "Megbízhatóság megújítása"),
("Request approval", ""),
("install-directory-not-empty-tip", "A kiválasztott mappa nem üres. A telepítés törölheti a meglévő adatokat. Fontolja meg egy másik mappa kiválasztását."),
].iter().cloned().collect();
}
+1
View File
@@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Trust duration (days)", "Durasi kepercayaan (hari)"),
("Renew trust", "Perbarui kepercayaan"),
("Request approval", ""),
("install-directory-not-empty-tip", "Folder yang dipilih tidak kosong. Instalasi dapat menghapus data yang ada. Pertimbangkan untuk memilih folder lain."),
].iter().cloned().collect();
}
+1
View File
@@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Trust duration (days)", "Durata attendibilità (giorni)"),
("Renew trust", "Rinnova attendibilità"),
("Request approval", ""),
("install-directory-not-empty-tip", "La cartella selezionata non è vuota. L'installazione potrebbe eliminare i dati esistenti. Valuta la possibilità di scegliere una cartella diversa."),
].iter().cloned().collect();
}
+1
View File
@@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Trust duration (days)", "信頼期間 (日)"),
("Renew trust", "信頼を更新"),
("Request approval", ""),
("install-directory-not-empty-tip", "選択したフォルダーは空ではありません。インストールにより既存のデータが削除される可能性があります。別のフォルダーを選択することをおすすめします。"),
].iter().cloned().collect();
}
+1
View File
@@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Trust duration (days)", "신뢰 유지 기간 (일)"),
("Renew trust", "신뢰 갱신"),
("Request approval", ""),
("install-directory-not-empty-tip", "선택한 폴더가 비어 있지 않습니다. 설치 시 기존 데이터가 삭제될 수 있습니다. 다른 폴더를 선택하는 것이 좋습니다."),
].iter().cloned().collect();
}
+1
View File
@@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Trust duration (days)", "Сенім мерзімі (күн)"),
("Renew trust", "Сенімді жаңарту"),
("Request approval", ""),
("install-directory-not-empty-tip", "Таңдалған бума бос емес. Орнату бар деректерді жоюы мүмкін. Басқа буманы таңдаған жөн."),
].iter().cloned().collect();
}
+1
View File
@@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Trust duration (days)", "Pasitikėjimo trukmė (dienomis)"),
("Renew trust", "Atnaujinti pasitikėjimą"),
("Request approval", ""),
("install-directory-not-empty-tip", "Pasirinktas aplankas nėra tuščias. Diegimas gali ištrinti esamus duomenis. Apsvarstykite galimybę pasirinkti kitą aplanką."),
].iter().cloned().collect();
}
+1
View File
@@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Trust duration (days)", "Uzticēšanās ilgums (dienas)"),
("Renew trust", "Atjaunot uzticēšanos"),
("Request approval", ""),
("install-directory-not-empty-tip", "Izvēlētā mape nav tukša. Instalēšana var izdzēst esošos datus. Apsveriet iespēju izvēlēties citu mapi."),
].iter().cloned().collect();
}
+1
View File
@@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Trust duration (days)", "വിശ്വാസ കാലാവധി (ദിവസം)"),
("Renew trust", "വിശ്വാസം പുതുക്കുക"),
("Request approval", ""),
("install-directory-not-empty-tip", "തിരഞ്ഞെടുത്ത ഫോൾഡർ ശൂന്യമല്ല. ഇൻസ്റ്റാളേഷൻ നിലവിലുള്ള ഡാറ്റ ഇല്ലാതാക്കിയേക്കാം. മറ്റൊരു ഫോൾഡർ തിരഞ്ഞെടുക്കുന്നത് പരിഗണിക്കുക."),
].iter().cloned().collect();
}
+1
View File
@@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Trust duration (days)", "Klareringsperiode (dager)"),
("Renew trust", "Forny klarering"),
("Request approval", ""),
("install-directory-not-empty-tip", "Den valgte mappen er ikke tom. Installasjonen kan slette eksisterende data. Vurder å velge en annen mappe."),
].iter().cloned().collect();
}
+1
View File
@@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Trust duration (days)", "Vertrouwensperiode (dagen)"),
("Renew trust", "Vertrouwen hernieuwen"),
("Request approval", ""),
("install-directory-not-empty-tip", "De geselecteerde map is niet leeg. De installatie kan bestaande gegevens verwijderen. Overweeg een andere map te kiezen."),
].iter().cloned().collect();
}
+1
View File
@@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Trust duration (days)", "Okres zaufania (dni)"),
("Renew trust", "Odnów zaufanie"),
("Request approval", ""),
("install-directory-not-empty-tip", "Wybrany folder nie jest pusty. Instalacja może usunąć istniejące dane. Rozważ wybranie innego folderu."),
].iter().cloned().collect();
}
+3 -2
View File
@@ -187,7 +187,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enter your password", "Informe sua senha"),
("Logging in...", "Fazendo Login..."),
("Enable RDP session sharing", "Habilitar compartilhamento de sessão RDP"),
("Auto Login", "Login Automático (Somente válido se você habilitou \"Bloquear após o fim da sessão\")"),
("Auto Login", "Login Automático (Para que o recurso funcione corretamente, é necessário habilitar \"Bloquear após o fim da sessão\" nas configurações do sistema.)"),
("Enable direct IP access", "Habilitar Acesso IP Direto"),
("Rename", "Renomear"),
("Space", "Espaço"),
@@ -686,7 +686,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("New tab", "Nova aba"),
("Keep terminal sessions on disconnect", "Manter sessões de terminal ao desconectar"),
("Terminal (Run as administrator)", "Terminal (Executar como administrador)"),
("terminal-admin-login-tip", "Insira o nome do usuário e senha de administrador do dispositivo controlado."),
("terminal-admin-login-tip", "Insira o nome do usuário e senha de administrador do dispositivo."),
("Failed to get user token.", "Falha ao obter token do usuário."),
("Incorrect username or password.", "Usuário ou senha incorretos"),
("The user is not an administrator.", "O usuário não é administrador"),
@@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Trust duration (days)", "Duração da confiança (dias)"),
("Renew trust", "Renovar confiança"),
("Request approval", ""),
("install-directory-not-empty-tip", "A pasta selecionada não está vazia. A instalação pode excluir dados existentes. Considere escolher outra pasta."),
].iter().cloned().collect();
}
+1
View File
@@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Trust duration (days)", "Duração da confiança (dias)"),
("Renew trust", "Renovar confiança"),
("Request approval", ""),
("install-directory-not-empty-tip", "A pasta selecionada não está vazia. A instalação pode eliminar dados existentes. Considere escolher outra pasta."),
].iter().cloned().collect();
}
+1
View File
@@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Trust duration (days)", "Durata încrederii (zile)"),
("Renew trust", "Reînnoiește încrederea"),
("Request approval", ""),
("install-directory-not-empty-tip", "Folderul selectat nu este gol. Instalarea poate șterge datele existente. Ia în considerare alegerea altui folder."),
].iter().cloned().collect();
}
+1
View File
@@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Trust duration (days)", "Срок доверия (в днях)"),
("Renew trust", "Продлить доверие"),
("Request approval", ""),
("install-directory-not-empty-tip", "Выбранная папка не пуста. При установке существующие данные могут быть удалены. Рекомендуется выбрать другую папку."),
].iter().cloned().collect();
}
+1
View File
@@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Trust duration (days)", "Durada de sa fidùtzia (dies)"),
("Renew trust", "Annoa sa fidùtzia"),
("Request approval", ""),
("install-directory-not-empty-tip", "Sa cartella seletzionada non est bòida. S'installatzione diat pòdere iscantzellare sos datos chi bi sunt. Ti cussigiamus de isseberare un'àtera cartella."),
].iter().cloned().collect();
}
+1
View File
@@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Trust duration (days)", "Doba dôvery (dni)"),
("Renew trust", "Obnoviť dôveru"),
("Request approval", ""),
("install-directory-not-empty-tip", "Vybraný priečinok nie je prázdny. Inštalácia môže odstrániť existujúce údaje. Zvážte výber iného priečinka."),
].iter().cloned().collect();
}
+1
View File
@@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Trust duration (days)", "Trajanje zaupanja (dni)"),
("Renew trust", "Obnovi zaupanje"),
("Request approval", ""),
("install-directory-not-empty-tip", "Izbrana mapa ni prazna. Namestitev lahko izbriše obstoječe podatke. Razmislite o izbiri druge mape."),
].iter().cloned().collect();
}
+1
View File
@@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Trust duration (days)", "Kohëzgjatja e besimit (ditë)"),
("Renew trust", "Rinovo besimin"),
("Request approval", ""),
("install-directory-not-empty-tip", "Dosja e zgjedhur nuk është bosh. Instalimi mund të fshijë të dhënat ekzistuese. Konsideroni të zgjidhni një dosje tjetër."),
].iter().cloned().collect();
}
+1
View File
@@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Trust duration (days)", "Trajanje poverenja (dani)"),
("Renew trust", "Obnovi poverenje"),
("Request approval", ""),
("install-directory-not-empty-tip", "Izabrani direktorijum nije prazan. Instalacija može da obriše postojeće podatke. Razmislite o izboru drugog direktorijuma."),
].iter().cloned().collect();
}
+1
View File
@@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Trust duration (days)", "Förtroendeperiod (dagar)"),
("Renew trust", "Förnya förtroende"),
("Request approval", ""),
("install-directory-not-empty-tip", "Den valda mappen är inte tom. Installationen kan radera befintliga data. Överväg att välja en annan mapp."),
].iter().cloned().collect();
}
+1
View File
@@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Trust duration (days)", "நம்பிக்கைக் காலம் (நாட்கள்)"),
("Renew trust", "நம்பிக்கையைப் புதுப்பி"),
("Request approval", ""),
("install-directory-not-empty-tip", "தேர்ந்தெடுக்கப்பட்ட கோப்புறை காலியாக இல்லை. நிறுவல் ஏற்கனவே உள்ள தரவை நீக்கக்கூடும். வேறு கோப்புறையைத் தேர்ந்தெடுப்பது நல்லது."),
].iter().cloned().collect();
}
+1
View File
@@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Trust duration (days)", ""),
("Renew trust", ""),
("Request approval", ""),
("install-directory-not-empty-tip", ""),
].iter().cloned().collect();
}
+1
View File
@@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Trust duration (days)", "ระยะเวลาการเชื่อถือ (วัน)"),
("Renew trust", "ต่ออายุการเชื่อถือ"),
("Request approval", ""),
("install-directory-not-empty-tip", "โฟลเดอร์ที่เลือกไม่ว่างเปล่า การติดตั้งอาจลบข้อมูลที่มีอยู่ ควรพิจารณาเลือกโฟลเดอร์อื่น"),
].iter().cloned().collect();
}
+1
View File
@@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Trust duration (days)", "Güven süresi (gün)"),
("Renew trust", "Güveni yenile"),
("Request approval", ""),
("install-directory-not-empty-tip", "Seçilen klasör boş değil. Kurulum mevcut verileri silebilir. Farklı bir klasör seçmeyi düşünün."),
].iter().cloned().collect();
}
+1
View File
@@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Trust duration (days)", "信任有效期(天)"),
("Renew trust", "延長信任期限"),
("Request approval", "請求批准"),
("install-directory-not-empty-tip", "所選資料夾不是空的。安裝可能刪除現有資料。建議選擇其他資料夾。"),
].iter().cloned().collect();
}
+1
View File
@@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Trust duration (days)", "Термін довіри (у днях)"),
("Renew trust", "Подовжити довіру"),
("Request approval", ""),
("install-directory-not-empty-tip", "Вибрана тека не порожня. Встановлення може видалити наявні дані. Радимо вибрати іншу теку."),
].iter().cloned().collect();
}
+1
View File
@@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Trust duration (days)", "اعتماد کی مدت (دن)"),
("Renew trust", "اعتماد کی تجدید کریں"),
("Request approval", ""),
("install-directory-not-empty-tip", "منتخب فولڈر خالی نہیں ہے۔ انسٹالیشن موجودہ ڈیٹا کو حذف کر سکتی ہے۔ کوئی دوسرا فولڈر منتخب کرنے پر غور کریں۔"),
].iter().cloned().collect();
}
+1
View File
@@ -824,5 +824,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Trust duration (days)", "Thời hạn tin cậy (ngày)"),
("Renew trust", "Gia hạn tin cậy"),
("Request approval", ""),
("install-directory-not-empty-tip", "Thư mục đã chọn không trống. Quá trình cài đặt có thể xóa dữ liệu hiện có. Hãy cân nhắc chọn một thư mục khác."),
].iter().cloned().collect();
}
+43
View File
@@ -1015,6 +1015,11 @@ fn update_me_from_source(update_source: UpdateSource) -> ResultType<()> {
}
Ok(output) => log_update_cleanup_warning(&output),
}
if crate::common::is_server_running() {
// This process hosts the backend. A new GUI could reuse our IPC and
// skip starting its own backend, leaving it "Not ready" when we exit.
return relaunch_after_update_exit(&app_name);
}
}
match Command::new("open")
.arg("-n")
@@ -1034,6 +1039,44 @@ fn update_me_from_source(update_source: UpdateSource) -> ResultType<()> {
Ok(())
}
fn relaunch_after_update_exit(app_name: &str) -> ResultType<()> {
// The update caller cleans up and quits this GUI after we return. Waiting
// in a separate process lets that caller finish instead of blocking its exit.
// The shell's kill -0 only checks whether our PID exists; it does not kill us.
const RELAUNCH: &str = r#"
poll_interval=0.1
while /bin/kill -0 "$1" 2>/dev/null; do
/bin/sleep "$poll_interval"
done
if ! output=$(/usr/bin/open -n "$2" 2>&1); then
/usr/bin/logger -t "$0" "Failed to relaunch updated app: $output"
exit 1
fi
"#;
Command::new("/bin/sh")
.args(["-c", RELAUNCH, app_name])
.arg(std::process::id().to_string())
.arg(format!("/Applications/{}.app", app_name))
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
// Children normally inherit their parent's process group, allowing a
// supervisor to signal the whole group. If launchd cleans up the old
// app's group on exit, it could kill this helper before it runs `open`.
// Passing 0 creates a new group whose ID is the child's own PID, so
// cleanup of the old group does not affect the helper or its children.
// It remains our child; ordinary parent exit alone does not kill it.
.process_group(0)
.spawn()
.map_err(|err| {
anyhow!(
"The new version is installed, but automatic restart failed. Please quit and reopen the application. Error: {}",
err
)
})?;
Ok(())
}
pub fn update_from_dmg(dmg_path: &str) -> ResultType<()> {
let update_temp_dir = get_update_temp_dir_string();
println!("Starting update from DMG: {}", dmg_path);
+37 -4
View File
@@ -23,7 +23,7 @@ use hbb_common::{
socket_client::{self, connect_tcp, is_ipv4, new_direct_udp_for, new_udp_for},
tokio::{
self, select,
sync::{mpsc, Mutex},
sync::{mpsc, Mutex, Notify},
time::interval,
},
udp::FramedSocket,
@@ -208,6 +208,7 @@ impl IceRoute {
static SHOULD_EXIT: AtomicBool = AtomicBool::new(false);
static MANUAL_RESTARTED: AtomicBool = AtomicBool::new(false);
static MANUAL_RESTART_NOTIFY: Notify = Notify::const_new();
static SENT_REGISTER_PK: AtomicBool = AtomicBool::new(false);
pub(crate) static NEEDS_DEPLOY: AtomicBool = AtomicBool::new(false);
#[cfg(target_os = "android")]
@@ -256,6 +257,13 @@ pub(crate) fn reset_needs_deploy_notification() {
NOTIFIED_NEEDS_DEPLOY.store(false, Ordering::SeqCst);
}
async fn wait_for_retry(delay: f32, restarted: tokio::sync::futures::Notified<'_>) {
select! {
_ = sleep(delay) => {}
_ = restarted => {}
}
}
#[derive(Clone)]
pub struct RendezvousMediator {
addr: TargetAddr<'static>,
@@ -268,6 +276,7 @@ impl RendezvousMediator {
pub fn restart() {
SHOULD_EXIT.store(true, Ordering::SeqCst);
MANUAL_RESTARTED.store(true, Ordering::SeqCst);
MANUAL_RESTART_NOTIFY.notify_waiters();
log::info!("server restart");
}
@@ -339,10 +348,12 @@ impl RendezvousMediator {
}
Config::reset_online();
let timeout = *timeout.read().unwrap();
// Subscribe before checking the flag so a restart cannot miss the wait.
let restarted = MANUAL_RESTART_NOTIFY.notified();
if !MANUAL_RESTARTED.load(Ordering::SeqCst) {
let elapsed = conn_start_time.elapsed().as_millis() as u64;
if elapsed < timeout {
sleep(((timeout - elapsed) / 1000) as _).await;
wait_for_retry(((timeout - elapsed) / 1000) as _, restarted).await;
}
} else {
// https://github.com/rustdesk/rustdesk/issues/12233
@@ -1719,11 +1730,33 @@ impl Drop for CheckIfResendPk {
#[cfg(test)]
mod tests {
use super::{
connection_meta, mpsc, tokio, AnswererSlot, IceRoute, IntoTargetAddr, PunchHoleSent,
RendezvousMediator, RendezvousMessage, MAX_WEBRTC_ANSWERERS, TCP_PUNCHES, UDP_PUNCHES,
connection_meta, mpsc, tokio, wait_for_retry, AnswererSlot, Duration, IceRoute,
IntoTargetAddr, Ordering, PunchHoleSent, RendezvousMediator, RendezvousMessage,
CONNECT_TIMEOUT, MANUAL_RESTARTED, MANUAL_RESTART_NOTIFY, MAX_WEBRTC_ANSWERERS,
SHOULD_EXIT, TCP_PUNCHES, UDP_PUNCHES,
};
use hbb_common::protobuf::Message as _;
#[tokio::test]
async fn manual_restart_interrupts_retry_wait() {
const WAKE_TIMEOUT: Duration = Duration::from_secs(1);
let waiting = wait_for_retry(
(CONNECT_TIMEOUT / 1000) as _,
MANUAL_RESTART_NOTIFY.notified(),
);
tokio::pin!(waiting);
assert!(hbb_common::futures::poll!(waiting.as_mut()).is_pending());
RendezvousMediator::restart();
let result = tokio::time::timeout(WAKE_TIMEOUT, waiting).await;
MANUAL_RESTARTED.store(false, Ordering::SeqCst);
SHOULD_EXIT.store(false, Ordering::SeqCst);
assert!(
result.is_ok(),
"restart must interrupt the pending retry delay"
);
}
// The places are the bound and they come back on drop: a punch past the limit is declined
// while the ones in flight are left alone, and the next one in is admitted only once a
// place has actually been given back.
+1 -1
View File
@@ -9,7 +9,7 @@ use base::message_proto::LoginResponse;
impl Connection {
pub(super) async fn begin_trusted_device(&mut self) -> bool {
if self.conn_audit_primary_auth != ConnAuditPrimaryAuth::PermanentPassword
|| !self.stream.is_secured()
|| (!self.stream.is_secured() && !crate::common::is_direct_ip_access(&self.lr.username))
|| self.require_2fa.is_none()
|| self.from_switch
|| !hbb_common::config::Config::get_bool_option(
+22 -4
View File
@@ -52,6 +52,12 @@ impl Session {
secured: bool,
key: Option<&[u8]>,
) -> ResultType<Self> {
if crate::common::is_direct_ip_access(login.get_id()) {
return Ok(Self {
credentials: Some(Credentials::direct_ip(login.get_id())),
..Default::default()
});
}
let Some(key) = key.filter(|_| secured) else {
return Ok(Self::default());
};
@@ -137,7 +143,7 @@ impl Session {
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
struct Issuer {
namespace: String,
public_key: [u8; TOKEN_BYTES],
public_key: Option<[u8; TOKEN_BYTES]>,
}
#[derive(Serialize, Deserialize)]
@@ -153,14 +159,26 @@ pub(super) struct Credentials {
}
impl Credentials {
fn direct_ip(peer_id: &str) -> Self {
Self {
peer_id: peer_id.to_owned(),
issuer: Issuer {
namespace: format!("direct-ip:{peer_id}"),
public_key: None,
},
}
}
pub fn new(peer_id: String, namespace: String, public_key: &[u8]) -> ResultType<Self> {
Ok(Self {
peer_id,
issuer: Issuer {
namespace,
public_key: public_key
.try_into()
.map_err(|_| anyhow!("Invalid trusted host public key"))?,
public_key: Some(
public_key
.try_into()
.map_err(|_| anyhow!("Invalid trusted host public key"))?,
),
},
})
}
+1
View File
@@ -11,6 +11,7 @@ use hbb_common::{
use std::fs;
mod configuration;
mod credentials;
mod recovery;
const PASSWORD: &str = "permanent-test-password";
+23
View File
@@ -0,0 +1,23 @@
use super::*;
use hbb_common::password_security::symmetric_crypt;
#[test]
fn id_credentials_saved_before_direct_ip_support_remain_readable() {
const NAMESPACE: &str = "[\"rs.example.com\"]";
const PUBLIC_KEY: [u8; TOKEN_BYTES] = [1; TOKEN_BYTES];
initialize_config();
let peer_id = uuid::Uuid::new_v4().to_string();
let token = random_token().unwrap();
let saved = serde_json::json!({
"issuer": {
"namespace": NAMESPACE,
"public_key": PUBLIC_KEY,
},
"ciphertext": symmetric_crypt(&token, true).unwrap(),
});
PeerConfig::set_trusted_device_v2(&peer_id, saved.to_string(), Default::default()).unwrap();
let credentials = Credentials::new(peer_id, NAMESPACE.into(), &PUBLIC_KEY).unwrap();
assert_eq!(credentials.load().unwrap(), Some(token.to_vec()));
}