mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-11-04 00:33:12 +00:00
Merge branch 'master' into login_method
This commit is contained in:
commit
2fd2d89aa9
69 files changed
+526
-44
No files matched your search
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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: [:])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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():
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"))?,
|
||||
),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ use hbb_common::{
|
||||
use std::fs;
|
||||
|
||||
mod configuration;
|
||||
mod credentials;
|
||||
mod recovery;
|
||||
|
||||
const PASSWORD: &str = "permanent-test-password";
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
Reference in new issue
Block a user