mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-04-15 20:21:27 +03:00
Compare commits
21 Commits
82a9fd1540
...
1.4.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1abc897c45 | ||
|
|
ab64a32f30 | ||
|
|
52b66e71d1 | ||
|
|
41ab5bbdd8 | ||
|
|
732b250815 | ||
|
|
157dbdc543 | ||
|
|
6ba23683d5 | ||
|
|
80a5865db3 | ||
|
|
9cb6f38aea | ||
|
|
cd7e3e4505 | ||
|
|
1833cb0655 | ||
|
|
e4208aa9cf | ||
|
|
bb3501a4f9 | ||
|
|
4abdb2e08b | ||
|
|
d49ae493b2 | ||
|
|
394079833e | ||
|
|
12d6789c2e | ||
|
|
34803f8e9b | ||
|
|
fd43184406 | ||
|
|
3cc3315081 | ||
|
|
6aee70fa18 |
2
.github/workflows/flutter-build.yml
vendored
2
.github/workflows/flutter-build.yml
vendored
@@ -39,7 +39,7 @@ env:
|
||||
# 2. Update the `VCPKG_COMMIT_ID` in `ci.yml` and `playground.yml`.
|
||||
VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b"
|
||||
ARMV7_VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836" # 2025.01.13, got "/opt/artifacts/vcpkg/vcpkg: No such file or directory" with latest version
|
||||
VERSION: "1.4.5"
|
||||
VERSION: "1.4.6"
|
||||
NDK_VERSION: "r27c"
|
||||
#signing keys env variable checks
|
||||
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
||||
|
||||
2
.github/workflows/playground.yml
vendored
2
.github/workflows/playground.yml
vendored
@@ -17,7 +17,7 @@ env:
|
||||
TAG_NAME: "nightly"
|
||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||
VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b"
|
||||
VERSION: "1.4.5"
|
||||
VERSION: "1.4.6"
|
||||
NDK_VERSION: "r26d"
|
||||
#signing keys env variable checks
|
||||
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
||||
|
||||
4
.github/workflows/winget.yml
vendored
4
.github/workflows/winget.yml
vendored
@@ -10,6 +10,6 @@ jobs:
|
||||
- uses: vedantmgoyal9/winget-releaser@main
|
||||
with:
|
||||
identifier: RustDesk.RustDesk
|
||||
version: "1.4.5"
|
||||
release-tag: "1.4.5"
|
||||
version: "1.4.6"
|
||||
release-tag: "1.4.6"
|
||||
token: ${{ secrets.WINGET_TOKEN }}
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,6 +3,7 @@
|
||||
.vscode
|
||||
.idea
|
||||
.DS_Store
|
||||
.env
|
||||
libsciter-gtk.so
|
||||
src/ui/inline.rs
|
||||
extractor
|
||||
|
||||
4
Cargo.lock
generated
4
Cargo.lock
generated
@@ -7134,7 +7134,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustdesk"
|
||||
version = "1.4.5"
|
||||
version = "1.4.6"
|
||||
dependencies = [
|
||||
"android-wakelock",
|
||||
"android_logger",
|
||||
@@ -7249,7 +7249,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustdesk-portable-packer"
|
||||
version = "1.4.5"
|
||||
version = "1.4.6"
|
||||
dependencies = [
|
||||
"brotli",
|
||||
"dirs 5.0.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rustdesk"
|
||||
version = "1.4.5"
|
||||
version = "1.4.6"
|
||||
authors = ["rustdesk <info@rustdesk.com>"]
|
||||
edition = "2021"
|
||||
build= "build.rs"
|
||||
|
||||
@@ -18,7 +18,7 @@ AppDir:
|
||||
id: rustdesk
|
||||
name: rustdesk
|
||||
icon: rustdesk
|
||||
version: 1.4.5
|
||||
version: 1.4.6
|
||||
exec: usr/share/rustdesk/rustdesk
|
||||
exec_args: $@
|
||||
apt:
|
||||
|
||||
@@ -18,7 +18,7 @@ AppDir:
|
||||
id: rustdesk
|
||||
name: rustdesk
|
||||
icon: rustdesk
|
||||
version: 1.4.5
|
||||
version: 1.4.6
|
||||
exec: usr/share/rustdesk/rustdesk
|
||||
exec_args: $@
|
||||
apt:
|
||||
|
||||
@@ -4118,3 +4118,43 @@ String mouseButtonsToPeer(int buttons) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/// Build an avatar widget from an avatar URL or data URI string.
|
||||
/// Returns [fallback] if avatar is empty or cannot be decoded.
|
||||
/// [borderRadius] defaults to [size]/2 (circle).
|
||||
Widget? buildAvatarWidget({
|
||||
required String avatar,
|
||||
required double size,
|
||||
double? borderRadius,
|
||||
Widget? fallback,
|
||||
}) {
|
||||
final trimmed = avatar.trim();
|
||||
if (trimmed.isEmpty) return fallback;
|
||||
|
||||
ImageProvider? imageProvider;
|
||||
if (trimmed.startsWith('data:image/')) {
|
||||
final comma = trimmed.indexOf(',');
|
||||
if (comma > 0) {
|
||||
try {
|
||||
imageProvider = MemoryImage(base64Decode(trimmed.substring(comma + 1)));
|
||||
} catch (_) {}
|
||||
}
|
||||
} else if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
|
||||
imageProvider = NetworkImage(trimmed);
|
||||
}
|
||||
|
||||
if (imageProvider == null) return fallback;
|
||||
|
||||
final radius = borderRadius ?? size / 2;
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(radius),
|
||||
child: Image(
|
||||
image: imageProvider,
|
||||
width: size,
|
||||
height: size,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) =>
|
||||
fallback ?? SizedBox.shrink(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ enum UserStatus { kDisabled, kNormal, kUnverified }
|
||||
class UserPayload {
|
||||
String name = '';
|
||||
String displayName = '';
|
||||
String avatar = '';
|
||||
String email = '';
|
||||
String note = '';
|
||||
String? verifier;
|
||||
@@ -35,6 +36,7 @@ class UserPayload {
|
||||
UserPayload.fromJson(Map<String, dynamic> json)
|
||||
: name = json['name'] ?? '',
|
||||
displayName = json['display_name'] ?? '',
|
||||
avatar = json['avatar'] ?? '',
|
||||
email = json['email'] ?? '',
|
||||
note = json['note'] ?? '',
|
||||
verifier = json['verifier'],
|
||||
@@ -49,6 +51,7 @@ class UserPayload {
|
||||
final Map<String, dynamic> map = {
|
||||
'name': name,
|
||||
'display_name': displayName,
|
||||
'avatar': avatar,
|
||||
'status': status == UserStatus.kDisabled
|
||||
? 0
|
||||
: status == UserStatus.kUnverified
|
||||
|
||||
@@ -473,8 +473,7 @@ class _GeneralState extends State<_General> {
|
||||
}
|
||||
|
||||
Widget other() {
|
||||
final showAutoUpdate =
|
||||
isWindows && bind.mainIsInstalled() && !bind.isCustomClient();
|
||||
final showAutoUpdate = isWindows && bind.mainIsInstalled();
|
||||
final children = <Widget>[
|
||||
if (!isWeb && !bind.isIncomingOnly())
|
||||
_OptionCheckBox(context, 'Confirm before closing multiple tabs',
|
||||
@@ -2027,28 +2026,65 @@ class _AccountState extends State<_Account> {
|
||||
}
|
||||
|
||||
Widget useInfo() {
|
||||
text(String key, String value) {
|
||||
return Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: SelectionArea(child: Text('${translate(key)}: $value'))
|
||||
.marginSymmetric(vertical: 4),
|
||||
);
|
||||
}
|
||||
|
||||
return Obx(() => Offstage(
|
||||
offstage: gFFI.userModel.userName.value.isEmpty,
|
||||
child: Column(
|
||||
children: [
|
||||
if (gFFI.userModel.displayName.value.trim().isNotEmpty &&
|
||||
gFFI.userModel.displayName.value.trim() !=
|
||||
gFFI.userModel.userName.value.trim())
|
||||
text('Display Name', gFFI.userModel.displayName.value.trim()),
|
||||
text('Username', gFFI.userModel.userName.value),
|
||||
// text('Group', gFFI.groupModel.groupName.value),
|
||||
],
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Builder(builder: (context) {
|
||||
final avatarWidget = _buildUserAvatar();
|
||||
return Row(
|
||||
children: [
|
||||
if (avatarWidget != null) avatarWidget,
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
gFFI.userModel.displayNameOrUserName,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
SelectionArea(
|
||||
child: Text(
|
||||
'@${gFFI.userModel.userName.value}',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color:
|
||||
Theme.of(context).textTheme.bodySmall?.color,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
)).marginOnly(left: 18, top: 16);
|
||||
}
|
||||
|
||||
Widget? _buildUserAvatar() {
|
||||
// Resolve relative avatar path at display time
|
||||
final avatar =
|
||||
bind.mainResolveAvatarUrl(avatar: gFFI.userModel.avatar.value);
|
||||
return buildAvatarWidget(
|
||||
avatar: avatar,
|
||||
size: 44,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Checkbox extends StatefulWidget {
|
||||
|
||||
@@ -462,23 +462,7 @@ class _CmHeaderState extends State<_CmHeader>
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 70,
|
||||
height: 70,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: str2color(client.name),
|
||||
borderRadius: BorderRadius.circular(15.0),
|
||||
),
|
||||
child: Text(
|
||||
client.name[0],
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
fontSize: 55,
|
||||
),
|
||||
),
|
||||
).marginOnly(right: 10.0),
|
||||
_buildClientAvatar().marginOnly(right: 10.0),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
@@ -582,6 +566,36 @@ class _CmHeaderState extends State<_CmHeader>
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
Widget _buildClientAvatar() {
|
||||
return buildAvatarWidget(
|
||||
avatar: client.avatar,
|
||||
size: 70,
|
||||
borderRadius: 15,
|
||||
fallback: _buildInitialAvatar(),
|
||||
) ??
|
||||
_buildInitialAvatar();
|
||||
}
|
||||
|
||||
Widget _buildInitialAvatar() {
|
||||
return Container(
|
||||
width: 70,
|
||||
height: 70,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: str2color(client.name),
|
||||
borderRadius: BorderRadius.circular(15.0),
|
||||
),
|
||||
child: Text(
|
||||
client.name.isNotEmpty ? client.name[0] : '?',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
fontSize: 55,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PrivilegeBoard extends StatefulWidget {
|
||||
|
||||
@@ -841,13 +841,7 @@ class ClientInfo extends StatelessWidget {
|
||||
flex: -1,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 12),
|
||||
child: CircleAvatar(
|
||||
backgroundColor: str2color(
|
||||
client.name,
|
||||
Theme.of(context).brightness == Brightness.light
|
||||
? 255
|
||||
: 150),
|
||||
child: Text(client.name[0])))),
|
||||
child: _buildAvatar(context))),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -860,6 +854,20 @@ class ClientInfo extends StatelessWidget {
|
||||
),
|
||||
]));
|
||||
}
|
||||
|
||||
Widget _buildAvatar(BuildContext context) {
|
||||
final fallback = CircleAvatar(
|
||||
backgroundColor: str2color(client.name,
|
||||
Theme.of(context).brightness == Brightness.light ? 255 : 150),
|
||||
child: Text(client.name.isNotEmpty ? client.name[0] : '?'),
|
||||
);
|
||||
return buildAvatarWidget(
|
||||
avatar: client.avatar,
|
||||
size: 40,
|
||||
fallback: fallback,
|
||||
) ??
|
||||
fallback;
|
||||
}
|
||||
}
|
||||
|
||||
void androidChannelInit() {
|
||||
|
||||
@@ -617,7 +617,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
onToggle: (bool v) async {
|
||||
await mainSetLocalBoolOption(kOptionEnableShowTerminalExtraKeys, v);
|
||||
final newValue =
|
||||
mainGetLocalBoolOptionSync(kOptionEnableShowTerminalExtraKeys);
|
||||
mainGetLocalBoolOptionSync(kOptionEnableShowTerminalExtraKeys);
|
||||
setState(() {
|
||||
_showTerminalExtraKeys = newValue;
|
||||
});
|
||||
@@ -689,7 +689,17 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
title: Obx(() => Text(gFFI.userModel.userName.value.isEmpty
|
||||
? translate('Login')
|
||||
: '${translate('Logout')} (${gFFI.userModel.accountLabelWithHandle})')),
|
||||
leading: Icon(Icons.person),
|
||||
leading: Obx(() {
|
||||
final avatar = bind.mainResolveAvatarUrl(
|
||||
avatar: gFFI.userModel.avatar.value);
|
||||
return buildAvatarWidget(
|
||||
avatar: avatar,
|
||||
size: 28,
|
||||
borderRadius: null,
|
||||
fallback: Icon(Icons.person),
|
||||
) ??
|
||||
Icon(Icons.person);
|
||||
}),
|
||||
onPressed: (context) {
|
||||
if (gFFI.userModel.userName.value.isEmpty) {
|
||||
loginDialog();
|
||||
@@ -829,10 +839,12 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
||||
),
|
||||
if (!incomingOnly)
|
||||
SettingsTile.switchTile(
|
||||
title: Text(translate('keep-awake-during-outgoing-sessions-label')),
|
||||
title:
|
||||
Text(translate('keep-awake-during-outgoing-sessions-label')),
|
||||
initialValue: _preventSleepWhileConnected,
|
||||
onToggle: (v) async {
|
||||
await mainSetLocalBoolOption(kOptionKeepAwakeDuringOutgoingSessions, v);
|
||||
await mainSetLocalBoolOption(
|
||||
kOptionKeepAwakeDuringOutgoingSessions, v);
|
||||
setState(() {
|
||||
_preventSleepWhileConnected = v;
|
||||
});
|
||||
|
||||
@@ -365,6 +365,16 @@ class InputModel {
|
||||
final isPhysicalMouse = false.obs;
|
||||
int _lastButtons = 0;
|
||||
Offset lastMousePos = Offset.zero;
|
||||
int _lastWheelTsUs = 0;
|
||||
|
||||
// Wheel acceleration thresholds.
|
||||
static const int _wheelAccelFastThresholdUs = 40000; // 40ms
|
||||
static const int _wheelAccelMediumThresholdUs = 80000; // 80ms
|
||||
static const double _wheelBurstVelocityThreshold =
|
||||
0.002; // delta units per microsecond
|
||||
// Wheel burst acceleration (empirical tuning).
|
||||
// Applies only to fast, non-smooth bursts to preserve single-step scrolling.
|
||||
// Flutter uses microseconds for dt, so velocity is in delta/us.
|
||||
|
||||
// Relative mouse mode (for games/3D apps).
|
||||
final relativeMouseMode = false.obs;
|
||||
@@ -964,6 +974,7 @@ class InputModel {
|
||||
toReleaseRawKeys.release(handleRawKeyEvent);
|
||||
_pointerMovedAfterEnter = false;
|
||||
_pointerInsideImage = enter;
|
||||
_lastWheelTsUs = 0;
|
||||
|
||||
// Fix status
|
||||
if (!enter) {
|
||||
@@ -1407,17 +1418,44 @@ class InputModel {
|
||||
if (isViewOnly) return;
|
||||
if (isViewCamera) return;
|
||||
if (e is PointerScrollEvent) {
|
||||
var dx = e.scrollDelta.dx.toInt();
|
||||
var dy = e.scrollDelta.dy.toInt();
|
||||
final rawDx = e.scrollDelta.dx;
|
||||
final rawDy = e.scrollDelta.dy;
|
||||
final dominantDelta = rawDx.abs() > rawDy.abs() ? rawDx.abs() : rawDy.abs();
|
||||
final isSmooth = dominantDelta < 1;
|
||||
final nowUs = DateTime.now().microsecondsSinceEpoch;
|
||||
final dtUs = _lastWheelTsUs == 0 ? 0 : nowUs - _lastWheelTsUs;
|
||||
_lastWheelTsUs = nowUs;
|
||||
int accel = 1;
|
||||
if (!isSmooth &&
|
||||
dtUs > 0 &&
|
||||
dtUs <= _wheelAccelMediumThresholdUs &&
|
||||
(isWindows || isLinux) &&
|
||||
peerPlatform == kPeerPlatformMacOS) {
|
||||
final velocity = dominantDelta / dtUs;
|
||||
if (velocity >= _wheelBurstVelocityThreshold) {
|
||||
if (dtUs < _wheelAccelFastThresholdUs) {
|
||||
accel = 3;
|
||||
} else {
|
||||
accel = 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
var dx = rawDx.toInt();
|
||||
var dy = rawDy.toInt();
|
||||
if (rawDx.abs() > rawDy.abs()) {
|
||||
dy = 0;
|
||||
} else {
|
||||
dx = 0;
|
||||
}
|
||||
if (dx > 0) {
|
||||
dx = -1;
|
||||
dx = -accel;
|
||||
} else if (dx < 0) {
|
||||
dx = 1;
|
||||
dx = accel;
|
||||
}
|
||||
if (dy > 0) {
|
||||
dy = -1;
|
||||
dy = -accel;
|
||||
} else if (dy < 0) {
|
||||
dy = 1;
|
||||
dy = accel;
|
||||
}
|
||||
bind.sessionSendMouse(
|
||||
sessionId: sessionId,
|
||||
|
||||
@@ -820,6 +820,7 @@ class Client {
|
||||
bool isTerminal = false;
|
||||
String portForward = "";
|
||||
String name = "";
|
||||
String avatar = "";
|
||||
String peerId = ""; // peer user's id,show at app
|
||||
bool keyboard = false;
|
||||
bool clipboard = false;
|
||||
@@ -847,6 +848,7 @@ class Client {
|
||||
isTerminal = json['is_terminal'] ?? false;
|
||||
portForward = json['port_forward'];
|
||||
name = json['name'];
|
||||
avatar = json['avatar'] ?? '';
|
||||
peerId = json['peer_id'];
|
||||
keyboard = json['keyboard'];
|
||||
clipboard = json['clipboard'];
|
||||
@@ -870,6 +872,7 @@ class Client {
|
||||
data['is_terminal'] = isTerminal;
|
||||
data['port_forward'] = portForward;
|
||||
data['name'] = name;
|
||||
data['avatar'] = avatar;
|
||||
data['peer_id'] = peerId;
|
||||
data['keyboard'] = keyboard;
|
||||
data['clipboard'] = clipboard;
|
||||
|
||||
@@ -17,6 +17,7 @@ bool refreshingUser = false;
|
||||
class UserModel {
|
||||
final RxString userName = ''.obs;
|
||||
final RxString displayName = ''.obs;
|
||||
final RxString avatar = ''.obs;
|
||||
final RxBool isAdmin = false.obs;
|
||||
final RxString networkError = ''.obs;
|
||||
bool get isLogin => userName.isNotEmpty;
|
||||
@@ -33,6 +34,7 @@ class UserModel {
|
||||
}
|
||||
return '$preferred (@$username)';
|
||||
}
|
||||
|
||||
WeakReference<FFI> parent;
|
||||
|
||||
UserModel(this.parent) {
|
||||
@@ -114,6 +116,7 @@ class UserModel {
|
||||
if (userInfo != null) {
|
||||
userName.value = (userInfo['name'] ?? '').toString();
|
||||
displayName.value = (userInfo['display_name'] ?? '').toString();
|
||||
avatar.value = (userInfo['avatar'] ?? '').toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,11 +129,13 @@ class UserModel {
|
||||
}
|
||||
userName.value = '';
|
||||
displayName.value = '';
|
||||
avatar.value = '';
|
||||
}
|
||||
|
||||
_parseAndUpdateUser(UserPayload user) {
|
||||
userName.value = user.name;
|
||||
displayName.value = user.displayName;
|
||||
avatar.value = user.avatar;
|
||||
isAdmin.value = user.isAdmin;
|
||||
bind.mainSetLocalOption(key: 'user_info', value: jsonEncode(user));
|
||||
if (isWeb) {
|
||||
|
||||
@@ -2034,5 +2034,9 @@ class RustdeskImpl {
|
||||
return false;
|
||||
}
|
||||
|
||||
String mainResolveAvatarUrl({required String avatar, dynamic hint}) {
|
||||
return js.context.callMethod('getByName', ['resolve_avatar_url', avatar])?.toString() ?? avatar;
|
||||
}
|
||||
|
||||
void dispose() {}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers
|
||||
version: 1.4.5+63
|
||||
version: 1.4.6+64
|
||||
|
||||
environment:
|
||||
sdk: '^3.1.0'
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
#include <cstdlib> // for getenv and _putenv
|
||||
#include <cstring> // for strcmp
|
||||
#include <string> // for std::wstring
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -15,6 +16,43 @@ constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW";
|
||||
// The number of Win32Window objects that currently exist.
|
||||
static int g_active_window_count = 0;
|
||||
|
||||
// Static variable to hold the custom icon (needs cleanup on exit)
|
||||
static HICON g_custom_icon_ = nullptr;
|
||||
|
||||
// Try to load icon from data\flutter_assets\assets\icon.ico if it exists.
|
||||
// Returns nullptr if the file doesn't exist or can't be loaded.
|
||||
HICON LoadCustomIcon() {
|
||||
if (g_custom_icon_ != nullptr) {
|
||||
return g_custom_icon_;
|
||||
}
|
||||
wchar_t exe_path[MAX_PATH];
|
||||
if (!GetModuleFileNameW(nullptr, exe_path, MAX_PATH)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::wstring icon_path = exe_path;
|
||||
size_t last_slash = icon_path.find_last_of(L"\\/");
|
||||
if (last_slash == std::wstring::npos) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
icon_path = icon_path.substr(0, last_slash + 1);
|
||||
icon_path += L"data\\flutter_assets\\assets\\icon.ico";
|
||||
|
||||
// Check file attributes - reject if missing, directory, or reparse point (symlink/junction)
|
||||
DWORD file_attr = GetFileAttributesW(icon_path.c_str());
|
||||
if (file_attr == INVALID_FILE_ATTRIBUTES ||
|
||||
(file_attr & FILE_ATTRIBUTE_DIRECTORY) ||
|
||||
(file_attr & FILE_ATTRIBUTE_REPARSE_POINT)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
g_custom_icon_ = (HICON)LoadImageW(
|
||||
nullptr, icon_path.c_str(), IMAGE_ICON, 0, 0,
|
||||
LR_LOADFROMFILE | LR_DEFAULTSIZE);
|
||||
return g_custom_icon_;
|
||||
}
|
||||
|
||||
using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd);
|
||||
|
||||
// Scale helper to convert logical scaler values to physical using passed in
|
||||
@@ -81,8 +119,16 @@ const wchar_t* WindowClassRegistrar::GetWindowClass() {
|
||||
window_class.cbClsExtra = 0;
|
||||
window_class.cbWndExtra = 0;
|
||||
window_class.hInstance = GetModuleHandle(nullptr);
|
||||
window_class.hIcon =
|
||||
LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON));
|
||||
|
||||
// Try to load icon from data\flutter_assets\assets\icon.ico if it exists
|
||||
HICON custom_icon = LoadCustomIcon();
|
||||
if (custom_icon != nullptr) {
|
||||
window_class.hIcon = custom_icon;
|
||||
} else {
|
||||
window_class.hIcon =
|
||||
LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON));
|
||||
}
|
||||
|
||||
window_class.hbrBackground = 0;
|
||||
window_class.lpszMenuName = nullptr;
|
||||
window_class.lpfnWndProc = Win32Window::WndProc;
|
||||
@@ -95,6 +141,12 @@ const wchar_t* WindowClassRegistrar::GetWindowClass() {
|
||||
void WindowClassRegistrar::UnregisterWindowClass() {
|
||||
UnregisterClass(kWindowClassName, nullptr);
|
||||
class_registered_ = false;
|
||||
|
||||
// Clean up the custom icon if it was loaded
|
||||
if (g_custom_icon_ != nullptr) {
|
||||
DestroyIcon(g_custom_icon_);
|
||||
g_custom_icon_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
Win32Window::Win32Window() {
|
||||
|
||||
@@ -269,7 +269,7 @@ impl KeyboardControllable for Enigo {
|
||||
for pos in 0..mod_len {
|
||||
let rpos = mod_len - 1 - pos;
|
||||
if flag & (0x0001 << rpos) != 0 {
|
||||
self.key_up(modifiers[pos]);
|
||||
self.key_up(modifiers[rpos]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,7 +298,18 @@ impl KeyboardControllable for Enigo {
|
||||
}
|
||||
|
||||
fn key_up(&mut self, key: Key) {
|
||||
keybd_event(KEYEVENTF_KEYUP, self.key_to_keycode(key), 0);
|
||||
match key {
|
||||
Key::Layout(c) => {
|
||||
let code = self.get_layoutdependent_keycode(c);
|
||||
if code as u16 != 0xFFFF {
|
||||
let vk = code & 0x00FF;
|
||||
keybd_event(KEYEVENTF_KEYUP, vk, 0);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
keybd_event(KEYEVENTF_KEYUP, self.key_to_keycode(key), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_key_state(&mut self, key: Key) -> bool {
|
||||
|
||||
Submodule libs/hbb_common updated: 0b60b9ffa0...48c37de3e6
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rustdesk-portable-packer"
|
||||
version = "1.4.5"
|
||||
version = "1.4.6"
|
||||
edition = "2021"
|
||||
description = "RustDesk Remote Desktop"
|
||||
|
||||
|
||||
@@ -6,15 +6,13 @@ if [ "$1" = configure ]; then
|
||||
|
||||
INITSYS=$(ls -al /proc/1/exe | awk -F' ' '{print $NF}' | awk -F'/' '{print $NF}')
|
||||
ln -f -s /usr/share/rustdesk/rustdesk /usr/bin/rustdesk
|
||||
|
||||
|
||||
if [ "systemd" == "$INITSYS" ]; then
|
||||
|
||||
if [ -e /etc/systemd/system/rustdesk.service ]; then
|
||||
rm /etc/systemd/system/rustdesk.service /usr/lib/systemd/system/rustdesk.service /usr/lib/systemd/user/rustdesk.service >/dev/null 2>&1
|
||||
fi
|
||||
version=$(python3 -V 2>&1 | grep -Po '(?<=Python )(.+)')
|
||||
parsedVersion=$(echo "${version//./}")
|
||||
mkdir -p /usr/lib/systemd/system/
|
||||
mkdir -p /usr/lib/systemd/system/
|
||||
cp /usr/share/rustdesk/files/systemd/rustdesk.service /usr/lib/systemd/system/rustdesk.service
|
||||
# try fix error in Ubuntu 18.04
|
||||
# Failed to reload rustdesk.service: Unit rustdesk.service is not loaded properly: Exec format error.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
pkgname=rustdesk
|
||||
pkgver=1.4.5
|
||||
pkgver=1.4.6
|
||||
pkgrel=0
|
||||
epoch=
|
||||
pkgdesc=""
|
||||
|
||||
@@ -31,22 +31,168 @@ LExit:
|
||||
return WcaFinalize(er);
|
||||
}
|
||||
|
||||
// CAUTION: We can't simply remove the install folder here, because silent repair/upgrade will fail.
|
||||
// `RemoveInstallFolder()` is a deferred custom action, it will be executed after the files are copied.
|
||||
// `msiexec /i package.msi /qn`
|
||||
// Helper function to safely delete a file or directory using handle-based deletion.
|
||||
// This avoids TOCTOU (Time-Of-Check-Time-Of-Use) race conditions.
|
||||
BOOL SafeDeleteItem(LPCWSTR fullPath)
|
||||
{
|
||||
// Open the file/directory with DELETE access and FILE_FLAG_OPEN_REPARSE_POINT
|
||||
// to prevent following symlinks.
|
||||
// Use shared access to allow deletion even when other processes have the file open.
|
||||
DWORD flags = FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT;
|
||||
HANDLE hFile = CreateFileW(
|
||||
fullPath,
|
||||
DELETE,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, // Allow shared access
|
||||
NULL,
|
||||
OPEN_EXISTING,
|
||||
flags,
|
||||
NULL
|
||||
);
|
||||
|
||||
if (hFile == INVALID_HANDLE_VALUE)
|
||||
{
|
||||
WcaLog(LOGMSG_STANDARD, "SafeDeleteItem: Failed to open '%ls'. Error: %lu", fullPath, GetLastError());
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
// Use SetFileInformationByHandle to mark for deletion.
|
||||
// The file will be deleted when the handle is closed.
|
||||
FILE_DISPOSITION_INFO dispInfo;
|
||||
dispInfo.DeleteFile = TRUE;
|
||||
|
||||
BOOL result = SetFileInformationByHandle(
|
||||
hFile,
|
||||
FileDispositionInfo,
|
||||
&dispInfo,
|
||||
sizeof(dispInfo)
|
||||
);
|
||||
|
||||
if (!result)
|
||||
{
|
||||
DWORD error = GetLastError();
|
||||
WcaLog(LOGMSG_STANDARD, "SafeDeleteItem: Failed to mark '%ls' for deletion. Error: %lu", fullPath, error);
|
||||
}
|
||||
|
||||
CloseHandle(hFile);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Helper function to recursively delete a directory's contents with detailed logging.
|
||||
void RecursiveDelete(LPCWSTR path)
|
||||
{
|
||||
// Ensure the path is not empty or null.
|
||||
if (path == NULL || path[0] == L'\0')
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Extra safety: never operate directly on a root path.
|
||||
if (PathIsRootW(path))
|
||||
{
|
||||
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: refusing to operate on root path '%ls'.", path);
|
||||
return;
|
||||
}
|
||||
|
||||
// MAX_PATH is enough here since the installer should not be using longer paths.
|
||||
// No need to handle extended-length paths (\\?\) in this context.
|
||||
WCHAR searchPath[MAX_PATH];
|
||||
HRESULT hr = StringCchPrintfW(searchPath, MAX_PATH, L"%s\\*", path);
|
||||
if (FAILED(hr)) {
|
||||
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Path too long to enumerate: %ls", path);
|
||||
return;
|
||||
}
|
||||
|
||||
WIN32_FIND_DATAW findData;
|
||||
HANDLE hFind = FindFirstFileW(searchPath, &findData);
|
||||
|
||||
if (hFind == INVALID_HANDLE_VALUE)
|
||||
{
|
||||
// This can happen if the directory is empty or doesn't exist, which is not an error in our case.
|
||||
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Failed to enumerate directory '%ls'. It may be missing or inaccessible. Error: %lu", path, GetLastError());
|
||||
return;
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
// Skip '.' and '..' directories.
|
||||
if (wcscmp(findData.cFileName, L".") == 0 || wcscmp(findData.cFileName, L"..") == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// MAX_PATH is enough here since the installer should not be using longer paths.
|
||||
// No need to handle extended-length paths (\\?\) in this context.
|
||||
WCHAR fullPath[MAX_PATH];
|
||||
hr = StringCchPrintfW(fullPath, MAX_PATH, L"%s\\%s", path, findData.cFileName);
|
||||
if (FAILED(hr)) {
|
||||
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Path too long for item '%ls' in '%ls', skipping.", findData.cFileName, path);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Before acting, ensure the read-only attribute is not set.
|
||||
if (findData.dwFileAttributes & FILE_ATTRIBUTE_READONLY)
|
||||
{
|
||||
if (FALSE == SetFileAttributesW(fullPath, findData.dwFileAttributes & ~FILE_ATTRIBUTE_READONLY))
|
||||
{
|
||||
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Failed to remove read-only attribute. Error: %lu", GetLastError());
|
||||
}
|
||||
}
|
||||
|
||||
if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
|
||||
{
|
||||
// Check for reparse points (symlinks/junctions) to prevent directory traversal attacks.
|
||||
// Do not follow reparse points, only remove the link itself.
|
||||
if (findData.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT)
|
||||
{
|
||||
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Not recursing into reparse point (symlink/junction), deleting link itself: %ls", fullPath);
|
||||
SafeDeleteItem(fullPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Recursively delete directory contents first
|
||||
RecursiveDelete(fullPath);
|
||||
// Then delete the directory itself
|
||||
SafeDeleteItem(fullPath);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Delete file using safe handle-based deletion
|
||||
SafeDeleteItem(fullPath);
|
||||
}
|
||||
} while (FindNextFileW(hFind, &findData) != 0);
|
||||
|
||||
DWORD lastError = GetLastError();
|
||||
if (lastError != ERROR_NO_MORE_FILES)
|
||||
{
|
||||
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: FindNextFileW failed with error %lu", lastError);
|
||||
}
|
||||
|
||||
FindClose(hFind);
|
||||
}
|
||||
|
||||
// See `Package.wxs` for the sequence of this custom action.
|
||||
//
|
||||
// So we need to delete the files separately in install folder.
|
||||
// Upgrade/uninstall sequence:
|
||||
// 1. InstallInitialize
|
||||
// 2. RemoveExistingProducts
|
||||
// ├─ TerminateProcesses
|
||||
// ├─ TryStopDeleteService
|
||||
// ├─ RemoveInstallFolder - <-- Here
|
||||
// └─ RemoveFiles
|
||||
// 3. InstallValidate
|
||||
// 4. InstallFiles
|
||||
// 5. InstallExecute
|
||||
// 6. InstallFinalize
|
||||
UINT __stdcall RemoveInstallFolder(
|
||||
__in MSIHANDLE hInstall)
|
||||
{
|
||||
HRESULT hr = S_OK;
|
||||
DWORD er = ERROR_SUCCESS;
|
||||
|
||||
int nResult = 0;
|
||||
LPWSTR installFolder = NULL;
|
||||
LPWSTR pwz = NULL;
|
||||
LPWSTR pwzData = NULL;
|
||||
WCHAR runtimeBroker[1024] = { 0, };
|
||||
|
||||
hr = WcaInitialize(hInstall, "RemoveInstallFolder");
|
||||
ExitOnFailure(hr, "Failed to initialize");
|
||||
@@ -58,24 +204,23 @@ UINT __stdcall RemoveInstallFolder(
|
||||
hr = WcaReadStringFromCaData(&pwz, &installFolder);
|
||||
ExitOnFailure(hr, "failed to read database key from custom action data: %ls", pwz);
|
||||
|
||||
StringCchPrintfW(runtimeBroker, sizeof(runtimeBroker) / sizeof(runtimeBroker[0]), L"%ls\\RuntimeBroker_rustdesk.exe", installFolder);
|
||||
|
||||
SHFILEOPSTRUCTW fileOp;
|
||||
ZeroMemory(&fileOp, sizeof(SHFILEOPSTRUCT));
|
||||
fileOp.wFunc = FO_DELETE;
|
||||
fileOp.pFrom = runtimeBroker;
|
||||
fileOp.fFlags = FOF_NOCONFIRMATION | FOF_SILENT;
|
||||
|
||||
nResult = SHFileOperationW(&fileOp);
|
||||
if (nResult == 0)
|
||||
{
|
||||
WcaLog(LOGMSG_STANDARD, "The external file \"%ls\" has been deleted.", runtimeBroker);
|
||||
if (installFolder == NULL || installFolder[0] == L'\0') {
|
||||
WcaLog(LOGMSG_STANDARD, "Install folder path is empty, skipping recursive delete.");
|
||||
goto LExit;
|
||||
}
|
||||
else
|
||||
{
|
||||
WcaLog(LOGMSG_STANDARD, "The external file \"%ls\" has not been deleted, error code: 0x%02X. Please refer to https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-shfileoperationa for the error codes.", runtimeBroker, nResult);
|
||||
|
||||
if (PathIsRootW(installFolder)) {
|
||||
WcaLog(LOGMSG_STANDARD, "Refusing to recursively delete root folder '%ls'.", installFolder);
|
||||
goto LExit;
|
||||
}
|
||||
|
||||
WcaLog(LOGMSG_STANDARD, "Attempting to recursively delete contents of install folder: %ls", installFolder);
|
||||
|
||||
RecursiveDelete(installFolder);
|
||||
|
||||
// The standard MSI 'RemoveFolders' action will take care of removing the (now empty) directories.
|
||||
// We don't need to call RemoveDirectoryW on installFolder itself, as it might still be in use by the installer.
|
||||
|
||||
LExit:
|
||||
ReleaseStr(pwzData);
|
||||
|
||||
@@ -109,9 +254,12 @@ bool TerminateProcessIfNotContainsParam(pfnNtQueryInformationProcess NtQueryInfo
|
||||
{
|
||||
if (pebUpp.CommandLine.Length > 0)
|
||||
{
|
||||
WCHAR *commandLine = (WCHAR *)malloc(pebUpp.CommandLine.Length);
|
||||
// Allocate extra space for null terminator
|
||||
WCHAR *commandLine = (WCHAR *)malloc(pebUpp.CommandLine.Length + sizeof(WCHAR));
|
||||
if (commandLine != NULL)
|
||||
{
|
||||
// Initialize all bytes to zero for safety
|
||||
memset(commandLine, 0, pebUpp.CommandLine.Length + sizeof(WCHAR));
|
||||
if (ReadProcessMemory(process, pebUpp.CommandLine.Buffer,
|
||||
commandLine, pebUpp.CommandLine.Length, &dwBytesRead))
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Name: rustdesk
|
||||
Version: 1.4.5
|
||||
Version: 1.4.6
|
||||
Release: 0
|
||||
Summary: RPM package
|
||||
License: GPL-3.0
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Name: rustdesk
|
||||
Version: 1.4.5
|
||||
Version: 1.4.6
|
||||
Release: 0
|
||||
Summary: RPM package
|
||||
License: GPL-3.0
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Name: rustdesk
|
||||
Version: 1.4.5
|
||||
Version: 1.4.6
|
||||
Release: 0
|
||||
Summary: RPM package
|
||||
License: GPL-3.0
|
||||
|
||||
@@ -33,7 +33,7 @@ use crate::{
|
||||
create_symmetric_key_msg, decode_id_pk, get_rs_pk, is_keyboard_mode_supported,
|
||||
kcp_stream::KcpStream,
|
||||
secure_tcp,
|
||||
ui_interface::{get_builtin_option, use_texture_render},
|
||||
ui_interface::{get_builtin_option, resolve_avatar_url, use_texture_render},
|
||||
ui_session_interface::{InvokeUiSession, Session},
|
||||
};
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
@@ -2625,6 +2625,20 @@ impl LoginConfigHandler {
|
||||
} else {
|
||||
(my_id, self.id.clone())
|
||||
};
|
||||
let mut avatar = get_builtin_option(keys::OPTION_AVATAR);
|
||||
if avatar.is_empty() {
|
||||
avatar = serde_json::from_str::<serde_json::Value>(&LocalConfig::get_option(
|
||||
"user_info",
|
||||
))
|
||||
.ok()
|
||||
.and_then(|x| {
|
||||
x.get("avatar")
|
||||
.and_then(|x| x.as_str())
|
||||
.map(|x| x.trim().to_owned())
|
||||
})
|
||||
.unwrap_or_default();
|
||||
}
|
||||
avatar = resolve_avatar_url(avatar);
|
||||
let mut display_name = get_builtin_option(keys::OPTION_DISPLAY_NAME);
|
||||
if display_name.is_empty() {
|
||||
display_name =
|
||||
@@ -2684,6 +2698,7 @@ impl LoginConfigHandler {
|
||||
})
|
||||
.into(),
|
||||
hwid,
|
||||
avatar,
|
||||
..Default::default()
|
||||
};
|
||||
match self.conn_type {
|
||||
|
||||
@@ -39,7 +39,7 @@ use hbb_common::{
|
||||
|
||||
use crate::{
|
||||
hbbs_http::{create_http_client_async, get_url_for_tls},
|
||||
ui_interface::{get_option, set_option},
|
||||
ui_interface::{get_option, is_installed, set_option},
|
||||
};
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
|
||||
@@ -187,7 +187,10 @@ pub fn core_main() -> Option<Vec<String>> {
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
hbb_common::config::PeerConfig::preload_peers();
|
||||
{
|
||||
crate::platform::try_remove_temp_update_files();
|
||||
hbb_common::config::PeerConfig::preload_peers();
|
||||
}
|
||||
std::thread::spawn(move || crate::start_server(false, no_server));
|
||||
} else {
|
||||
#[cfg(windows)]
|
||||
@@ -202,17 +205,24 @@ pub fn core_main() -> Option<Vec<String>> {
|
||||
if config::is_disable_installation() {
|
||||
return None;
|
||||
}
|
||||
let res = platform::update_me(false);
|
||||
let text = match res {
|
||||
Ok(_) => translate("Update successfully!".to_string()),
|
||||
Err(err) => {
|
||||
log::error!("Failed with error: {err}");
|
||||
translate("Update failed!".to_string())
|
||||
|
||||
let text = match crate::platform::prepare_custom_client_update() {
|
||||
Err(e) => {
|
||||
log::error!("Error preparing custom client update: {}", e);
|
||||
"Update failed!".to_string()
|
||||
}
|
||||
Ok(false) => "Update failed!".to_string(),
|
||||
Ok(true) => match platform::update_me(false) {
|
||||
Ok(_) => "Update successfully!".to_string(),
|
||||
Err(err) => {
|
||||
log::error!("Failed with error: {err}");
|
||||
"Update failed!".to_string()
|
||||
}
|
||||
},
|
||||
};
|
||||
Toast::new(Toast::POWERSHELL_APP_ID)
|
||||
.title(&config::APP_NAME.read().unwrap())
|
||||
.text1(&text)
|
||||
.text1(&translate(text))
|
||||
.sound(Some(Sound::Default))
|
||||
.duration(Duration::Short)
|
||||
.show()
|
||||
|
||||
@@ -1101,6 +1101,10 @@ pub fn main_get_api_server() -> String {
|
||||
get_api_server()
|
||||
}
|
||||
|
||||
pub fn main_resolve_avatar_url(avatar: String) -> SyncReturn<String> {
|
||||
SyncReturn(resolve_avatar_url(avatar))
|
||||
}
|
||||
|
||||
pub fn main_http_request(url: String, method: String, body: Option<String>, header: String) {
|
||||
http_request(url, method, body, header)
|
||||
}
|
||||
@@ -2776,10 +2780,13 @@ pub fn main_get_common(key: String) -> String {
|
||||
} else if key.starts_with("download-file-") {
|
||||
let _version = key.replace("download-file-", "");
|
||||
#[cfg(target_os = "windows")]
|
||||
return match crate::platform::windows::is_msi_installed() {
|
||||
Ok(true) => format!("rustdesk-{_version}-x86_64.msi"),
|
||||
Ok(false) => format!("rustdesk-{_version}-x86_64.exe"),
|
||||
Err(e) => {
|
||||
return match (
|
||||
crate::platform::windows::is_msi_installed(),
|
||||
crate::common::is_custom_client(),
|
||||
) {
|
||||
(Ok(true), false) => format!("rustdesk-{_version}-x86_64.msi"),
|
||||
(Ok(true), true) | (Ok(false), _) => format!("rustdesk-{_version}-x86_64.exe"),
|
||||
(Err(e), _) => {
|
||||
log::error!("Failed to check if is msi: {}", e);
|
||||
format!("error:update-failed-check-msi-tip")
|
||||
}
|
||||
@@ -2876,30 +2883,17 @@ pub fn main_set_common(_key: String, _value: String) {
|
||||
if let Some(f) = new_version_file.to_str() {
|
||||
// 1.4.0 does not support "--update"
|
||||
// But we can assume that the new version supports it.
|
||||
#[cfg(target_os = "windows")]
|
||||
if f.ends_with(".exe") {
|
||||
if let Err(e) =
|
||||
crate::platform::run_exe_in_cur_session(f, vec!["--update"], false)
|
||||
{
|
||||
log::error!("Failed to run the update exe: {}", e);
|
||||
}
|
||||
} else if f.ends_with(".msi") {
|
||||
if let Err(e) = crate::platform::update_me_msi(f, false) {
|
||||
log::error!("Failed to run the update msi: {}", e);
|
||||
}
|
||||
} else {
|
||||
// unreachable!()
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
match crate::platform::update_to(f) {
|
||||
Ok(_) => {
|
||||
log::info!("Update successfully!");
|
||||
log::info!("Update process is launched successfully!");
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to update to new version, {}", e);
|
||||
fs::remove_file(f).ok();
|
||||
}
|
||||
}
|
||||
fs::remove_file(f).ok();
|
||||
}
|
||||
}
|
||||
} else if _key == "extract-update-dmg" {
|
||||
|
||||
@@ -17,6 +17,7 @@ lazy_static::lazy_static! {
|
||||
|
||||
const QUERY_INTERVAL_SECS: f32 = 1.0;
|
||||
const QUERY_TIMEOUT_SECS: u64 = 60 * 3;
|
||||
|
||||
const REQUESTING_ACCOUNT_AUTH: &str = "Requesting account auth";
|
||||
const WAITING_ACCOUNT_AUTH: &str = "Waiting account auth";
|
||||
const LOGIN_ACCOUNT_AUTH: &str = "Login account auth";
|
||||
@@ -82,6 +83,8 @@ pub struct UserPayload {
|
||||
#[serde(default)]
|
||||
pub display_name: Option<String>,
|
||||
#[serde(default)]
|
||||
pub avatar: Option<String>,
|
||||
#[serde(default)]
|
||||
pub email: Option<String>,
|
||||
#[serde(default)]
|
||||
pub note: Option<String>,
|
||||
@@ -273,6 +276,7 @@ impl OidcSession {
|
||||
serde_json::json!({
|
||||
"name": auth_body.user.name,
|
||||
"display_name": auth_body.user.display_name,
|
||||
"avatar": auth_body.user.avatar,
|
||||
"status": auth_body.user.status
|
||||
})
|
||||
.to_string(),
|
||||
|
||||
@@ -53,8 +53,25 @@ pub fn download_file(
|
||||
auto_del_dur: Option<Duration>,
|
||||
) -> ResultType<String> {
|
||||
let id = url.clone();
|
||||
if DOWNLOADERS.lock().unwrap().contains_key(&id) {
|
||||
return Ok(id);
|
||||
// First pass: if a non-error downloader exists for this URL, reuse it.
|
||||
// If an errored downloader exists, remove it so this call can retry.
|
||||
let mut stale_path = None;
|
||||
{
|
||||
let mut downloaders = DOWNLOADERS.lock().unwrap();
|
||||
if let Some(downloader) = downloaders.get(&id) {
|
||||
if downloader.error.is_none() {
|
||||
return Ok(id);
|
||||
}
|
||||
stale_path = downloader.path.clone();
|
||||
downloaders.remove(&id);
|
||||
}
|
||||
}
|
||||
if let Some(p) = stale_path {
|
||||
if p.exists() {
|
||||
if let Err(e) = std::fs::remove_file(&p) {
|
||||
log::warn!("Failed to remove stale download file {}: {}", p.display(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(path) = path.as_ref() {
|
||||
@@ -75,8 +92,26 @@ pub fn download_file(
|
||||
tx_cancel: tx,
|
||||
finished: false,
|
||||
};
|
||||
let mut downloaders = DOWNLOADERS.lock().unwrap();
|
||||
downloaders.insert(id.clone(), downloader);
|
||||
// Second pass (atomic with insert) to avoid race with another concurrent caller.
|
||||
let mut stale_path_after_check = None;
|
||||
{
|
||||
let mut downloaders = DOWNLOADERS.lock().unwrap();
|
||||
if let Some(existing) = downloaders.get(&id) {
|
||||
if existing.error.is_none() {
|
||||
return Ok(id);
|
||||
}
|
||||
stale_path_after_check = existing.path.clone();
|
||||
downloaders.remove(&id);
|
||||
}
|
||||
downloaders.insert(id.clone(), downloader);
|
||||
}
|
||||
if let Some(p) = stale_path_after_check {
|
||||
if p.exists() {
|
||||
if let Err(e) = std::fs::remove_file(&p) {
|
||||
log::warn!("Failed to remove stale download file {}: {}", p.display(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let id2 = id.clone();
|
||||
std::thread::spawn(
|
||||
|
||||
@@ -226,6 +226,7 @@ pub enum Data {
|
||||
is_terminal: bool,
|
||||
peer_id: String,
|
||||
name: String,
|
||||
avatar: String,
|
||||
authorized: bool,
|
||||
port_forward: String,
|
||||
keyboard: bool,
|
||||
@@ -1583,6 +1584,6 @@ mod test {
|
||||
#[test]
|
||||
fn verify_ffi_enum_data_size() {
|
||||
println!("{}", std::mem::size_of::<Data>());
|
||||
assert!(std::mem::size_of::<Data>() <= 96);
|
||||
assert!(std::mem::size_of::<Data>() <= 120);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -736,8 +736,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("rel-mouse-exit-{}-tip", "按下 {} 退出"),
|
||||
("rel-mouse-permission-lost-tip", "键盘权限被撤销。相对鼠标模式已被禁用。"),
|
||||
("Changelog", "更新日志"),
|
||||
("keep-awake-during-outgoing-sessions-label", ""),
|
||||
("keep-awake-during-incoming-sessions-label", ""),
|
||||
("keep-awake-during-outgoing-sessions-label", "传出会话期间保持屏幕常亮"),
|
||||
("keep-awake-during-incoming-sessions-label", "传入会话期间保持屏幕常亮"),
|
||||
("Continue with {}", "使用 {} 登录"),
|
||||
("Display Name", "显示名称"),
|
||||
].iter().cloned().collect();
|
||||
|
||||
@@ -739,6 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("keep-awake-during-outgoing-sessions-label", "Bildschirm während ausgehender Sitzungen aktiv halten"),
|
||||
("keep-awake-during-incoming-sessions-label", "Bildschirm während eingehender Sitzungen aktiv halten"),
|
||||
("Continue with {}", "Fortfahren mit {}"),
|
||||
("Display Name", ""),
|
||||
("Display Name", "Anzeigename"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -739,6 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("keep-awake-during-outgoing-sessions-label", "발신 세션 중 화면 켜짐 유지"),
|
||||
("keep-awake-during-incoming-sessions-label", "수신 세션 중 화면 켜짐 유지"),
|
||||
("Continue with {}", "{}(으)로 계속"),
|
||||
("Display Name", ""),
|
||||
("Display Name", "표시 이름"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -739,6 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("keep-awake-during-outgoing-sessions-label", "Houd het scherm open tijdens de uitgaande sessies."),
|
||||
("keep-awake-during-incoming-sessions-label", "Houd het scherm open tijdens de inkomende sessies."),
|
||||
("Continue with {}", "Ga verder met {}"),
|
||||
("Display Name", ""),
|
||||
("Display Name", "Naam Weergeven"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -739,6 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("keep-awake-during-outgoing-sessions-label", "Не отключать экран во время исходящих сеансов"),
|
||||
("keep-awake-during-incoming-sessions-label", "Не отключать экран во время входящих сеансов"),
|
||||
("Continue with {}", "Продолжить с {}"),
|
||||
("Display Name", ""),
|
||||
("Display Name", "Отображаемое имя"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -42,9 +42,16 @@ static PRIVILEGES_SCRIPTS_DIR: Dir =
|
||||
include_dir!("$CARGO_MANIFEST_DIR/src/platform/privileges_scripts");
|
||||
static mut LATEST_SEED: i32 = 0;
|
||||
|
||||
// Using a fixed temporary directory for updates is preferable to
|
||||
// using one that includes the custom client name.
|
||||
const UPDATE_TEMP_DIR: &str = "/tmp/.rustdeskupdate";
|
||||
#[inline]
|
||||
fn get_update_temp_dir() -> PathBuf {
|
||||
let euid = unsafe { hbb_common::libc::geteuid() };
|
||||
Path::new("/tmp").join(format!(".rustdeskupdate-{}", euid))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get_update_temp_dir_string() -> String {
|
||||
get_update_temp_dir().to_string_lossy().into_owned()
|
||||
}
|
||||
|
||||
/// Global mutex to serialize CoreGraphics cursor operations.
|
||||
/// This prevents race conditions between cursor visibility (hide depth tracking)
|
||||
@@ -279,24 +286,12 @@ fn update_daemon_agent(agent_plist_file: String, update_source_dir: String, sync
|
||||
Err(e) => {
|
||||
log::error!("run osascript failed: {}", e);
|
||||
}
|
||||
Ok(status) if !status.success() => {
|
||||
log::warn!("run osascript failed with status: {}", status);
|
||||
}
|
||||
_ => {
|
||||
let installed = std::path::Path::new(&agent_plist_file).exists();
|
||||
log::info!("Agent file {} installed: {}", &agent_plist_file, installed);
|
||||
if installed {
|
||||
// Unload first, or load may not work if already loaded.
|
||||
// We hope that the load operation can immediately trigger a start.
|
||||
std::process::Command::new("launchctl")
|
||||
.args(&["unload", "-w", &agent_plist_file])
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.status()
|
||||
.ok();
|
||||
let status = std::process::Command::new("launchctl")
|
||||
.args(&["load", "-w", &agent_plist_file])
|
||||
.status();
|
||||
log::info!("launch server, status: {:?}", &status);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -415,7 +410,9 @@ pub fn set_cursor_pos(x: i32, y: i32) -> bool {
|
||||
let _guard = match CG_CURSOR_MUTEX.try_lock() {
|
||||
Ok(guard) => guard,
|
||||
Err(std::sync::TryLockError::WouldBlock) => {
|
||||
log::error!("[BUG] set_cursor_pos: CG_CURSOR_MUTEX is already held - potential deadlock!");
|
||||
log::error!(
|
||||
"[BUG] set_cursor_pos: CG_CURSOR_MUTEX is already held - potential deadlock!"
|
||||
);
|
||||
debug_assert!(false, "Re-entrant call to set_cursor_pos detected");
|
||||
return false;
|
||||
}
|
||||
@@ -822,7 +819,8 @@ pub fn quit_gui() {
|
||||
|
||||
#[inline]
|
||||
pub fn try_remove_temp_update_dir(dir: Option<&str>) {
|
||||
let target_path = Path::new(dir.unwrap_or(UPDATE_TEMP_DIR));
|
||||
let target_path_buf = dir.map(PathBuf::from).unwrap_or_else(get_update_temp_dir);
|
||||
let target_path = target_path_buf.as_path();
|
||||
if target_path.exists() {
|
||||
std::fs::remove_dir_all(target_path).ok();
|
||||
}
|
||||
@@ -851,32 +849,34 @@ pub fn update_me() -> ResultType<()> {
|
||||
if is_installed_daemon && !is_service_stopped {
|
||||
let agent = format!("{}_server.plist", crate::get_full_name());
|
||||
let agent_plist_file = format!("/Library/LaunchAgents/{}", agent);
|
||||
std::process::Command::new("launchctl")
|
||||
.args(&["unload", "-w", &agent_plist_file])
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.status()
|
||||
.ok();
|
||||
update_daemon_agent(agent_plist_file, app_dir, true);
|
||||
} else {
|
||||
// `kill -9` may not work without "administrator privileges"
|
||||
let update_body = format!(
|
||||
r#"
|
||||
do shell script "
|
||||
pgrep -x '{app_name}' | grep -v {pid} | xargs kill -9 && rm -rf '/Applications/{app_name}.app' && ditto '{app_dir}' '/Applications/{app_name}.app' && chown -R {user}:staff '/Applications/{app_name}.app' && xattr -r -d com.apple.quarantine '/Applications/{app_name}.app'
|
||||
" with prompt "{app_name} wants to update itself" with administrator privileges
|
||||
"#,
|
||||
app_name = app_name,
|
||||
pid = std::process::id(),
|
||||
app_dir = app_dir,
|
||||
user = get_active_username()
|
||||
);
|
||||
match Command::new("osascript")
|
||||
let update_body = r#"
|
||||
on run {app_name, cur_pid, app_dir, user_name}
|
||||
set app_bundle to "/Applications/" & app_name & ".app"
|
||||
set app_bundle_q to quoted form of app_bundle
|
||||
set app_dir_q to quoted form of app_dir
|
||||
set user_name_q to quoted form of user_name
|
||||
|
||||
set check_source to "test -d " & app_dir_q & " || exit 1;"
|
||||
set kill_others to "pids=$(pgrep -x '" & app_name & "' | grep -vx " & cur_pid & " || true); if [ -n \"$pids\" ]; then echo \"$pids\" | xargs kill -9 || true; fi;"
|
||||
set copy_files to "rm -rf " & app_bundle_q & " && ditto " & app_dir_q & " " & app_bundle_q & " && chown -R " & user_name_q & ":staff " & app_bundle_q & " && (xattr -r -d com.apple.quarantine " & app_bundle_q & " || true);"
|
||||
set sh to "set -e;" & check_source & kill_others & copy_files
|
||||
|
||||
do shell script sh with prompt app_name & " wants to update itself" with administrator privileges
|
||||
end run
|
||||
"#;
|
||||
let active_user = get_active_username();
|
||||
let status = Command::new("osascript")
|
||||
.arg("-e")
|
||||
.arg(update_body)
|
||||
.status()
|
||||
{
|
||||
.arg(app_name.to_string())
|
||||
.arg(std::process::id().to_string())
|
||||
.arg(app_dir)
|
||||
.arg(active_user)
|
||||
.status();
|
||||
match status {
|
||||
Ok(status) if !status.success() => {
|
||||
log::error!("osascript execution failed with status: {}", status);
|
||||
}
|
||||
@@ -897,25 +897,28 @@ pgrep -x '{app_name}' | grep -v {pid} | xargs kill -9 && rm -rf '/Applications/{
|
||||
}
|
||||
|
||||
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);
|
||||
extract_dmg(dmg_path, UPDATE_TEMP_DIR)?;
|
||||
extract_dmg(dmg_path, &update_temp_dir)?;
|
||||
println!("DMG extracted");
|
||||
update_extracted(UPDATE_TEMP_DIR)?;
|
||||
update_extracted(&update_temp_dir)?;
|
||||
println!("Update process started");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update_to(_file: &str) -> ResultType<()> {
|
||||
update_extracted(UPDATE_TEMP_DIR)?;
|
||||
let update_temp_dir = get_update_temp_dir_string();
|
||||
update_extracted(&update_temp_dir)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn extract_update_dmg(file: &str) {
|
||||
let update_temp_dir = get_update_temp_dir_string();
|
||||
let mut evt: HashMap<&str, String> =
|
||||
HashMap::from([("name", "extract-update-dmg".to_string())]);
|
||||
match extract_dmg(file, UPDATE_TEMP_DIR) {
|
||||
match extract_dmg(file, &update_temp_dir) {
|
||||
Ok(_) => {
|
||||
log::info!("Extracted dmg file to {}", UPDATE_TEMP_DIR);
|
||||
log::info!("Extracted dmg file to {}", update_temp_dir);
|
||||
}
|
||||
Err(e) => {
|
||||
evt.insert("err", e.to_string());
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
on run {daemon_file, agent_file, user, cur_pid, source_dir}
|
||||
|
||||
set unload_service to "launchctl unload -w /Library/LaunchDaemons/com.carriez.RustDesk_service.plist || true;"
|
||||
set agent_plist to "/Library/LaunchAgents/com.carriez.RustDesk_server.plist"
|
||||
set daemon_plist to "/Library/LaunchDaemons/com.carriez.RustDesk_service.plist"
|
||||
set app_bundle to "/Applications/RustDesk.app"
|
||||
|
||||
set kill_others to "pgrep -x 'RustDesk' | grep -v " & cur_pid & " | xargs kill -9;"
|
||||
set check_source to "test -d " & quoted form of source_dir & " || exit 1;"
|
||||
set resolve_uid to "uid=$(id -u " & quoted form of user & " 2>/dev/null || true);"
|
||||
set unload_agent to "if [ -n \"$uid\" ]; then launchctl bootout gui/$uid " & quoted form of agent_plist & " 2>/dev/null || launchctl bootout user/$uid " & quoted form of agent_plist & " 2>/dev/null || launchctl unload -w " & quoted form of agent_plist & " || true; else launchctl unload -w " & quoted form of agent_plist & " || true; fi;"
|
||||
set unload_service to "launchctl unload -w " & daemon_plist & " || true;"
|
||||
set kill_others to "pids=$(pgrep -x 'RustDesk' | grep -vx " & cur_pid & " || true); if [ -n \"$pids\" ]; then echo \"$pids\" | xargs kill -9 || true; fi;"
|
||||
|
||||
set copy_files to "rm -rf /Applications/RustDesk.app && ditto " & source_dir & " /Applications/RustDesk.app && chown -R " & quoted form of user & ":staff /Applications/RustDesk.app && xattr -r -d com.apple.quarantine /Applications/RustDesk.app;"
|
||||
set copy_files to "(rm -rf " & quoted form of app_bundle & " && ditto " & quoted form of source_dir & " " & quoted form of app_bundle & " && chown -R " & quoted form of user & ":staff " & quoted form of app_bundle & " && (xattr -r -d com.apple.quarantine " & quoted form of app_bundle & " || true)) || exit 1;"
|
||||
|
||||
set sh1 to "echo " & quoted form of daemon_file & " > /Library/LaunchDaemons/com.carriez.RustDesk_service.plist && chown root:wheel /Library/LaunchDaemons/com.carriez.RustDesk_service.plist;"
|
||||
set write_daemon_plist to "echo " & quoted form of daemon_file & " > " & daemon_plist & " && chown root:wheel " & daemon_plist & ";"
|
||||
set write_agent_plist to "echo " & quoted form of agent_file & " > " & agent_plist & " && chown root:wheel " & agent_plist & ";"
|
||||
set load_service to "launchctl load -w " & daemon_plist & ";"
|
||||
set agent_label_cmd to "agent_label=$(basename " & quoted form of agent_plist & " .plist);"
|
||||
set bootstrap_agent to "if [ -n \"$uid\" ]; then launchctl bootstrap gui/$uid " & quoted form of agent_plist & " 2>/dev/null || launchctl bootstrap user/$uid " & quoted form of agent_plist & " 2>/dev/null || launchctl load -w " & quoted form of agent_plist & " || true; else launchctl load -w " & quoted form of agent_plist & " || true; fi;"
|
||||
set kickstart_agent to "if [ -n \"$uid\" ]; then launchctl kickstart -k gui/$uid/$agent_label 2>/dev/null || launchctl kickstart -k user/$uid/$agent_label 2>/dev/null || true; fi;"
|
||||
set load_agent to agent_label_cmd & bootstrap_agent & kickstart_agent
|
||||
|
||||
set sh2 to "echo " & quoted form of agent_file & " > /Library/LaunchAgents/com.carriez.RustDesk_server.plist && chown root:wheel /Library/LaunchAgents/com.carriez.RustDesk_server.plist;"
|
||||
|
||||
set sh3 to "launchctl load -w /Library/LaunchDaemons/com.carriez.RustDesk_service.plist;"
|
||||
|
||||
set sh to unload_service & kill_others & copy_files & sh1 & sh2 & sh3
|
||||
set sh to "set -e;" & check_source & resolve_uid & unload_agent & unload_service & kill_others & copy_files & write_daemon_plist & write_agent_plist & load_service & load_agent
|
||||
|
||||
do shell script sh with prompt "RustDesk wants to update itself" with administrator privileges
|
||||
end run
|
||||
|
||||
@@ -153,11 +153,7 @@ pub fn clip_cursor(rect: Option<(i32, i32, i32, i32)>) -> bool {
|
||||
};
|
||||
if result == FALSE {
|
||||
let err = GetLastError();
|
||||
log::warn!(
|
||||
"ClipCursor failed: rect={:?}, error_code={}",
|
||||
rect,
|
||||
err
|
||||
);
|
||||
log::warn!("ClipCursor failed: rect={:?}, error_code={}", rect, err);
|
||||
return false;
|
||||
}
|
||||
true
|
||||
@@ -757,15 +753,37 @@ pub fn run_as_user(arg: Vec<&str>) -> ResultType<Option<std::process::Child>> {
|
||||
run_exe_in_cur_session(std::env::current_exe()?.to_str().unwrap_or(""), arg, false)
|
||||
}
|
||||
|
||||
pub fn run_exe_direct(
|
||||
exe: &str,
|
||||
arg: Vec<&str>,
|
||||
show: bool,
|
||||
) -> ResultType<Option<std::process::Child>> {
|
||||
let mut cmd = std::process::Command::new(exe);
|
||||
for a in arg {
|
||||
cmd.arg(a);
|
||||
}
|
||||
if !show {
|
||||
cmd.creation_flags(CREATE_NO_WINDOW);
|
||||
}
|
||||
match cmd.spawn() {
|
||||
Ok(child) => Ok(Some(child)),
|
||||
Err(e) => bail!("Failed to start process: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_exe_in_cur_session(
|
||||
exe: &str,
|
||||
arg: Vec<&str>,
|
||||
show: bool,
|
||||
) -> ResultType<Option<std::process::Child>> {
|
||||
let Some(session_id) = get_current_process_session_id() else {
|
||||
bail!("Failed to get current process session id");
|
||||
};
|
||||
run_exe_in_session(exe, arg, session_id, show)
|
||||
if is_root() {
|
||||
let Some(session_id) = get_current_process_session_id() else {
|
||||
bail!("Failed to get current process session id");
|
||||
};
|
||||
run_exe_in_session(exe, arg, session_id, show)
|
||||
} else {
|
||||
run_exe_direct(exe, arg, show)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_exe_in_session(
|
||||
@@ -1331,6 +1349,38 @@ pub fn copy_exe_cmd(src_exe: &str, exe: &str, path: &str) -> ResultType<String>
|
||||
))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn rename_exe_cmd(src_exe: &str, path: &str) -> ResultType<String> {
|
||||
let src_exe_filename = PathBuf::from(src_exe)
|
||||
.file_name()
|
||||
.ok_or(anyhow!("Can't get file name of {src_exe}"))?
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
let app_name = crate::get_app_name().to_lowercase();
|
||||
if src_exe_filename.to_lowercase() == format!("{app_name}.exe") {
|
||||
Ok("".to_owned())
|
||||
} else {
|
||||
Ok(format!(
|
||||
"
|
||||
move /Y \"{path}\\{src_exe_filename}\" \"{path}\\{app_name}.exe\"
|
||||
",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn remove_meta_toml_cmd(is_msi: bool, path: &str) -> String {
|
||||
if is_msi && crate::is_custom_client() {
|
||||
format!(
|
||||
"
|
||||
del /F /Q \"{path}\\meta.toml\"
|
||||
",
|
||||
)
|
||||
} else {
|
||||
"".to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
fn get_after_install(
|
||||
exe: &str,
|
||||
reg_value_start_menu_shortcuts: Option<String>,
|
||||
@@ -1417,7 +1467,11 @@ pub fn install_me(options: &str, path: String, silent: bool, debug: bool) -> Res
|
||||
}
|
||||
let app_name = crate::get_app_name();
|
||||
|
||||
let current_exe = std::env::current_exe()?;
|
||||
|
||||
let tmp_path = std::env::temp_dir().to_string_lossy().to_string();
|
||||
let cur_exe = current_exe.to_str().unwrap_or("").to_owned();
|
||||
let shortcut_icon_location = get_shortcut_icon_location(&cur_exe);
|
||||
let mk_shortcut = write_cmds(
|
||||
format!(
|
||||
"
|
||||
@@ -1426,6 +1480,7 @@ sLinkFile = \"{tmp_path}\\{app_name}.lnk\"
|
||||
|
||||
Set oLink = oWS.CreateShortcut(sLinkFile)
|
||||
oLink.TargetPath = \"{exe}\"
|
||||
{shortcut_icon_location}
|
||||
oLink.Save
|
||||
"
|
||||
),
|
||||
@@ -1482,8 +1537,13 @@ copy /Y \"{tmp_path}\\Uninstall {app_name}.lnk\" \"{start_menu}\\\"
|
||||
reg_value_printer = "1".to_owned();
|
||||
}
|
||||
|
||||
let meta = std::fs::symlink_metadata(std::env::current_exe()?)?;
|
||||
let size = meta.len() / 1024;
|
||||
let meta = std::fs::symlink_metadata(¤t_exe)?;
|
||||
let mut size = meta.len() / 1024;
|
||||
if let Some(parent_dir) = current_exe.parent() {
|
||||
if let Some(d) = parent_dir.to_str() {
|
||||
size = get_directory_size_kb(d);
|
||||
}
|
||||
}
|
||||
// https://docs.microsoft.com/zh-cn/windows/win32/msi/uninstall-registry-key?redirectedfrom=MSDNa
|
||||
// https://www.windowscentral.com/how-edit-registry-using-command-prompt-windows-10
|
||||
// https://www.tenforums.com/tutorials/70903-add-remove-allowed-apps-through-windows-firewall-windows-10-a.html
|
||||
@@ -1536,7 +1596,7 @@ chcp 65001
|
||||
md \"{path}\"
|
||||
{copy_exe}
|
||||
reg add {subkey} /f
|
||||
reg add {subkey} /f /v DisplayIcon /t REG_SZ /d \"{exe}\"
|
||||
reg add {subkey} /f /v DisplayIcon /t REG_SZ /d \"{display_icon}\"
|
||||
reg add {subkey} /f /v DisplayName /t REG_SZ /d \"{app_name}\"
|
||||
reg add {subkey} /f /v DisplayVersion /t REG_SZ /d \"{version}\"
|
||||
reg add {subkey} /f /v Version /t REG_SZ /d \"{version}\"
|
||||
@@ -1560,6 +1620,7 @@ copy /Y \"{tmp_path}\\Uninstall {app_name}.lnk\" \"{path}\\\"
|
||||
{install_remote_printer}
|
||||
{sleep}
|
||||
",
|
||||
display_icon = get_custom_icon(&cur_exe).unwrap_or(exe.to_string()),
|
||||
version = crate::VERSION.replace("-", "."),
|
||||
build_date = crate::BUILD_DATE,
|
||||
after_install = get_after_install(
|
||||
@@ -1795,6 +1856,163 @@ fn get_reg_of(subkey: &str, name: &str) -> String {
|
||||
"".to_owned()
|
||||
}
|
||||
|
||||
fn get_public_base_dir() -> PathBuf {
|
||||
if let Ok(allusersprofile) = std::env::var("ALLUSERSPROFILE") {
|
||||
let path = PathBuf::from(&allusersprofile);
|
||||
if path.exists() {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
if let Ok(public) = std::env::var("PUBLIC") {
|
||||
let path = PathBuf::from(public).join("Documents");
|
||||
if path.exists() {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
let program_data_dir = PathBuf::from("C:\\ProgramData");
|
||||
if program_data_dir.exists() {
|
||||
return program_data_dir;
|
||||
}
|
||||
std::env::temp_dir()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_custom_client_staging_dir() -> PathBuf {
|
||||
get_public_base_dir()
|
||||
.join("RustDesk")
|
||||
.join("RustDeskCustomClientStaging")
|
||||
}
|
||||
|
||||
/// Removes the custom client staging directory.
|
||||
///
|
||||
/// Current behavior: intentionally a no-op (does not delete).
|
||||
///
|
||||
/// Rationale
|
||||
/// - The staging directory only contains a small `custom.txt`, leaving it is harmless.
|
||||
/// - Deleting directories under a public location (e.g., C:\\ProgramData\\RustDesk) is
|
||||
/// susceptible to TOCTOU attacks if an unprivileged user can replace the path with a
|
||||
/// symlink/junction between checks and deletion.
|
||||
///
|
||||
/// Future work:
|
||||
/// - Use the files (if needed) in the installation directory instead of a public location.
|
||||
/// This directory only contains a small `custom.txt` file.
|
||||
/// - Pass the custom client name directly via command line
|
||||
/// or environment variable during update installation. Then no staging directory is needed.
|
||||
#[inline]
|
||||
pub fn remove_custom_client_staging_dir(staging_dir: &Path) -> ResultType<bool> {
|
||||
if !staging_dir.exists() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// First explicitly removes `custom.txt` to ensure stale config is never replayed,
|
||||
// even if the subsequent directory removal fails.
|
||||
//
|
||||
// `std::fs::remove_file` on a symlink removes the symlink itself, not the target,
|
||||
// so this is safe even in a TOCTOU race.
|
||||
let custom_txt_path = staging_dir.join("custom.txt");
|
||||
if custom_txt_path.exists() {
|
||||
allow_err!(std::fs::remove_file(&custom_txt_path));
|
||||
}
|
||||
|
||||
// Intentionally not deleting. See the function docs for rationale.
|
||||
log::debug!(
|
||||
"Skip deleting staging directory {:?} (intentional to avoid TOCTOU)",
|
||||
staging_dir
|
||||
);
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
// Prepare custom client update by copying staged custom.txt to current directory and loading it.
|
||||
// Returns:
|
||||
// 1. Ok(true) if preparation was successful or no staging directory exists.
|
||||
// 2. Ok(false) if custom.txt file exists but has invalid contents or fails security checks
|
||||
// (e.g., is a symlink or has invalid contents).
|
||||
// 3. Err if any unexpected error occurs during file operations.
|
||||
pub fn prepare_custom_client_update() -> ResultType<bool> {
|
||||
let custom_client_staging_dir = get_custom_client_staging_dir();
|
||||
let current_exe = std::env::current_exe()?;
|
||||
let current_exe_dir = current_exe
|
||||
.parent()
|
||||
.ok_or(anyhow!("Cannot get parent directory of current exe"))?;
|
||||
|
||||
let staging_dir = custom_client_staging_dir.clone();
|
||||
let clear_staging_on_exit = crate::SimpleCallOnReturn {
|
||||
b: true,
|
||||
f: Box::new(
|
||||
move || match remove_custom_client_staging_dir(&staging_dir) {
|
||||
Ok(existed) => {
|
||||
if existed {
|
||||
log::info!("Custom client staging directory removed successfully.");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!(
|
||||
"Failed to remove custom client staging directory {:?}: {}",
|
||||
staging_dir,
|
||||
e
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
};
|
||||
|
||||
if custom_client_staging_dir.exists() {
|
||||
let custom_txt_path = custom_client_staging_dir.join("custom.txt");
|
||||
if !custom_txt_path.exists() {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
let metadata = std::fs::symlink_metadata(&custom_txt_path)?;
|
||||
if metadata.is_symlink() {
|
||||
log::error!(
|
||||
"custom.txt is a symlink. Refusing to load custom client for security reasons."
|
||||
);
|
||||
drop(clear_staging_on_exit);
|
||||
return Ok(false);
|
||||
}
|
||||
if metadata.is_file() {
|
||||
// Copy custom.txt to current directory
|
||||
let local_custom_file_path = current_exe_dir.join("custom.txt");
|
||||
log::debug!(
|
||||
"Copying staged custom file from {:?} to {:?}",
|
||||
custom_txt_path,
|
||||
local_custom_file_path
|
||||
);
|
||||
|
||||
// No need to check symlink before copying.
|
||||
// `load_custom_client()` will fail if the file is not valid.
|
||||
fs::copy(&custom_txt_path, &local_custom_file_path)?;
|
||||
log::info!("Staged custom client file copied to current directory.");
|
||||
|
||||
// Load custom client
|
||||
let is_custom_file_exists =
|
||||
local_custom_file_path.exists() && local_custom_file_path.is_file();
|
||||
crate::load_custom_client();
|
||||
|
||||
// Remove the copied custom.txt file
|
||||
allow_err!(fs::remove_file(&local_custom_file_path));
|
||||
|
||||
// Check if loaded successfully
|
||||
if is_custom_file_exists && !crate::common::is_custom_client() {
|
||||
// The custom.txt file existed, but its contents are invalid.
|
||||
log::error!("Failed to load custom client from custom.txt.");
|
||||
drop(clear_staging_on_exit);
|
||||
// ERROR_INVALID_DATA
|
||||
return Ok(false);
|
||||
}
|
||||
} else {
|
||||
log::info!("No custom client files found in staging directory.");
|
||||
}
|
||||
} else {
|
||||
log::info!(
|
||||
"Custom client staging directory {:?} does not exist.",
|
||||
custom_client_staging_dir
|
||||
);
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub fn get_license_from_exe_name() -> ResultType<CustomServer> {
|
||||
let mut exe = std::env::current_exe()?.to_str().unwrap_or("").to_owned();
|
||||
// if defined portable appname entry, replace original executable name with it.
|
||||
@@ -1903,12 +2121,48 @@ unsafe fn set_default_dll_directories() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn get_custom_icon(exe: &str) -> Option<String> {
|
||||
if crate::is_custom_client() {
|
||||
if let Some(p) = PathBuf::from(exe).parent() {
|
||||
let alter_icon_path = p.join("data\\flutter_assets\\assets\\icon.ico");
|
||||
if alter_icon_path.exists() {
|
||||
// Verify that the icon is not a symlink for security
|
||||
if let Ok(metadata) = std::fs::symlink_metadata(&alter_icon_path) {
|
||||
if metadata.is_symlink() {
|
||||
log::warn!(
|
||||
"Custom icon at {:?} is a symlink, refusing to use it.",
|
||||
alter_icon_path
|
||||
);
|
||||
return None;
|
||||
}
|
||||
if metadata.is_file() {
|
||||
return Some(alter_icon_path.to_string_lossy().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get_shortcut_icon_location(exe: &str) -> String {
|
||||
if exe.is_empty() {
|
||||
return "".to_owned();
|
||||
}
|
||||
|
||||
get_custom_icon(exe)
|
||||
.map(|p| format!("oLink.IconLocation = \"{}\"", p))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn create_shortcut(id: &str) -> ResultType<()> {
|
||||
let exe = std::env::current_exe()?.to_str().unwrap_or("").to_owned();
|
||||
// https://github.com/rustdesk/rustdesk/issues/13735
|
||||
// Replace ':' with '_' for filename since ':' is not allowed in Windows filenames
|
||||
// https://github.com/rustdesk/hbb_common/blob/8b0e25867375ba9e6bff548acf44fe6d6ffa7c0e/src/config.rs#L1384
|
||||
let filename = id.replace(':', "_");
|
||||
let shortcut_icon_location = get_shortcut_icon_location(&exe);
|
||||
let shortcut = write_cmds(
|
||||
format!(
|
||||
"
|
||||
@@ -1919,6 +2173,7 @@ sLinkFile = objFSO.BuildPath(strDesktop, \"{filename}.lnk\")
|
||||
Set oLink = oWS.CreateShortcut(sLinkFile)
|
||||
oLink.TargetPath = \"{exe}\"
|
||||
oLink.Arguments = \"--connect {id}\"
|
||||
{shortcut_icon_location}
|
||||
oLink.Save
|
||||
"
|
||||
),
|
||||
@@ -2724,6 +2979,44 @@ if exist \"{tray_shortcut}\" del /f /q \"{tray_shortcut}\"
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
/// Calculate the total size of a directory in KB
|
||||
/// Does not follow symlinks to prevent directory traversal attacks.
|
||||
fn get_directory_size_kb(path: &str) -> u64 {
|
||||
let mut total_size = 0u64;
|
||||
let mut stack = vec![PathBuf::from(path)];
|
||||
|
||||
while let Some(current_path) = stack.pop() {
|
||||
let entries = match std::fs::read_dir(¤t_path) {
|
||||
Ok(entries) => entries,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
for entry in entries {
|
||||
let entry = match entry {
|
||||
Ok(entry) => entry,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
let metadata = match std::fs::symlink_metadata(entry.path()) {
|
||||
Ok(metadata) => metadata,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
if metadata.is_symlink() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if metadata.is_dir() {
|
||||
stack.push(entry.path());
|
||||
} else {
|
||||
total_size = total_size.saturating_add(metadata.len());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
total_size / 1024
|
||||
}
|
||||
|
||||
pub fn update_me(debug: bool) -> ResultType<()> {
|
||||
let app_name = crate::get_app_name();
|
||||
let src_exe = std::env::current_exe()?.to_string_lossy().to_string();
|
||||
@@ -2764,12 +3057,35 @@ pub fn update_me(debug: bool) -> ResultType<()> {
|
||||
if versions.len() > 2 {
|
||||
version_build = versions[2];
|
||||
}
|
||||
let meta = std::fs::symlink_metadata(std::env::current_exe()?)?;
|
||||
let size = meta.len() / 1024;
|
||||
let version = crate::VERSION.replace("-", ".");
|
||||
let size = get_directory_size_kb(&path);
|
||||
let build_date = crate::BUILD_DATE;
|
||||
let display_icon = get_custom_icon(&exe).unwrap_or(exe.to_string());
|
||||
|
||||
let reg_cmd = format!(
|
||||
"
|
||||
reg add {subkey} /f /v DisplayIcon /t REG_SZ /d \"{exe}\"
|
||||
let is_msi = is_msi_installed().ok();
|
||||
|
||||
fn get_reg_cmd(
|
||||
subkey: &str,
|
||||
is_msi: Option<bool>,
|
||||
display_icon: &str,
|
||||
version: &str,
|
||||
build_date: &str,
|
||||
version_major: &str,
|
||||
version_minor: &str,
|
||||
version_build: &str,
|
||||
size: u64,
|
||||
) -> String {
|
||||
let reg_display_icon = if is_msi.unwrap_or(false) {
|
||||
"".to_string()
|
||||
} else {
|
||||
format!(
|
||||
"reg add {} /f /v DisplayIcon /t REG_SZ /d \"{}\"",
|
||||
subkey, display_icon
|
||||
)
|
||||
};
|
||||
format!(
|
||||
"
|
||||
{reg_display_icon}
|
||||
reg add {subkey} /f /v DisplayVersion /t REG_SZ /d \"{version}\"
|
||||
reg add {subkey} /f /v Version /t REG_SZ /d \"{version}\"
|
||||
reg add {subkey} /f /v BuildDate /t REG_SZ /d \"{build_date}\"
|
||||
@@ -2777,10 +3093,39 @@ reg add {subkey} /f /v VersionMajor /t REG_DWORD /d {version_major}
|
||||
reg add {subkey} /f /v VersionMinor /t REG_DWORD /d {version_minor}
|
||||
reg add {subkey} /f /v VersionBuild /t REG_DWORD /d {version_build}
|
||||
reg add {subkey} /f /v EstimatedSize /t REG_DWORD /d {size}
|
||||
",
|
||||
version = crate::VERSION.replace("-", "."),
|
||||
build_date = crate::BUILD_DATE,
|
||||
);
|
||||
"
|
||||
)
|
||||
}
|
||||
|
||||
let reg_cmd = {
|
||||
let reg_cmd_main = get_reg_cmd(
|
||||
&subkey,
|
||||
is_msi,
|
||||
&display_icon,
|
||||
&version,
|
||||
&build_date,
|
||||
&version_major,
|
||||
&version_minor,
|
||||
&version_build,
|
||||
size,
|
||||
);
|
||||
let reg_cmd_msi = if let Some(reg_msi_key) = get_reg_msi_key(&subkey, is_msi) {
|
||||
get_reg_cmd(
|
||||
®_msi_key,
|
||||
is_msi,
|
||||
&display_icon,
|
||||
&version,
|
||||
&build_date,
|
||||
&version_major,
|
||||
&version_minor,
|
||||
&version_build,
|
||||
size,
|
||||
)
|
||||
} else {
|
||||
"".to_owned()
|
||||
};
|
||||
format!("{}{}", reg_cmd_main, reg_cmd_msi)
|
||||
};
|
||||
|
||||
let filter = format!(" /FI \"PID ne {}\"", get_current_pid());
|
||||
let restore_service_cmd = if is_service_running {
|
||||
@@ -2820,6 +3165,8 @@ sc stop {app_name}
|
||||
taskkill /F /IM {app_name}.exe{filter}
|
||||
{reg_cmd}
|
||||
{copy_exe}
|
||||
{rename_exe}
|
||||
{remove_meta_toml}
|
||||
{restore_service_cmd}
|
||||
{uninstall_printer_cmd}
|
||||
{install_printer_cmd}
|
||||
@@ -2827,43 +3174,106 @@ taskkill /F /IM {app_name}.exe{filter}
|
||||
",
|
||||
app_name = app_name,
|
||||
copy_exe = copy_exe_cmd(&src_exe, &exe, &path)?,
|
||||
rename_exe = rename_exe_cmd(&src_exe, &path)?,
|
||||
remove_meta_toml = remove_meta_toml_cmd(is_msi.unwrap_or(true), &path),
|
||||
sleep = if debug { "timeout 300" } else { "" },
|
||||
);
|
||||
|
||||
let _restore_session_guard = crate::common::SimpleCallOnReturn {
|
||||
b: true,
|
||||
f: Box::new(move || {
|
||||
let is_root = is_root();
|
||||
if tray_sessions.is_empty() {
|
||||
log::info!("No tray process found.");
|
||||
} else {
|
||||
log::info!(
|
||||
"Try to restore the tray process..., sessions: {:?}",
|
||||
&tray_sessions
|
||||
);
|
||||
// When not running as root, only spawn once since run_exe_direct
|
||||
// doesn't target specific sessions.
|
||||
let mut spawned_non_root_tray = false;
|
||||
for s in tray_sessions.clone().into_iter() {
|
||||
if s != 0 {
|
||||
// We need to check if is_root here because if `update_me()` is called from
|
||||
// the main window running with administrator permission,
|
||||
// `run_exe_in_session()` will fail with error 1314 ("A required privilege is
|
||||
// not held by the client").
|
||||
//
|
||||
// This issue primarily affects the MSI-installed version running in Administrator
|
||||
// session during testing, but we check permissions here to be safe.
|
||||
if is_root {
|
||||
allow_err!(run_exe_in_session(&exe, vec!["--tray"], s, true));
|
||||
} else if !spawned_non_root_tray {
|
||||
// Only spawn once for non-root since run_exe_direct doesn't take session parameter
|
||||
allow_err!(run_exe_direct(&exe, vec!["--tray"], false));
|
||||
spawned_non_root_tray = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if main_window_sessions.is_empty() {
|
||||
log::info!("No main window process found.");
|
||||
} else {
|
||||
log::info!("Try to restore the main window process...");
|
||||
std::thread::sleep(std::time::Duration::from_millis(2000));
|
||||
// When not running as root, only spawn once since run_exe_direct
|
||||
// doesn't target specific sessions.
|
||||
let mut spawned_non_root_main = false;
|
||||
for s in main_window_sessions.clone().into_iter() {
|
||||
if s != 0 {
|
||||
if is_root {
|
||||
allow_err!(run_exe_in_session(&exe, vec![], s, true));
|
||||
} else if !spawned_non_root_main {
|
||||
// Only spawn once for non-root since run_exe_direct doesn't take session parameter
|
||||
allow_err!(run_exe_direct(&exe, vec![], false));
|
||||
spawned_non_root_main = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(300));
|
||||
}),
|
||||
};
|
||||
|
||||
run_cmds(cmds, debug, "update")?;
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_millis(2000));
|
||||
if tray_sessions.is_empty() {
|
||||
log::info!("No tray process found.");
|
||||
} else {
|
||||
log::info!("Try to restore the tray process...");
|
||||
log::info!(
|
||||
"Try to restore the tray process..., sessions: {:?}",
|
||||
&tray_sessions
|
||||
);
|
||||
for s in tray_sessions {
|
||||
if s != 0 {
|
||||
allow_err!(run_exe_in_session(&exe, vec!["--tray"], s, true));
|
||||
}
|
||||
}
|
||||
}
|
||||
if main_window_sessions.is_empty() {
|
||||
log::info!("No main window process found.");
|
||||
} else {
|
||||
log::info!("Try to restore the main window process...");
|
||||
std::thread::sleep(std::time::Duration::from_millis(2000));
|
||||
for s in main_window_sessions {
|
||||
if s != 0 {
|
||||
allow_err!(run_exe_in_session(&exe, vec![], s, true));
|
||||
}
|
||||
}
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(300));
|
||||
log::info!("Update completed.");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_reg_msi_key(subkey: &str, is_msi: Option<bool>) -> Option<String> {
|
||||
// Only proceed if it's a custom client and MSI is installed.
|
||||
// `is_msi.unwrap_or(true)` is intentional: subsequent code validates the registry,
|
||||
// hence no early return is required upon MSI detection failure.
|
||||
if !(crate::common::is_custom_client() && is_msi.unwrap_or(true)) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Get the uninstall string from registry
|
||||
let uninstall_string = get_reg_of(subkey, "UninstallString");
|
||||
if uninstall_string.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Find the product code (GUID) in the uninstall string
|
||||
// Handle both quoted and unquoted GUIDs: /X {GUID} or /X "{GUID}"
|
||||
let start = uninstall_string.rfind('{')?;
|
||||
let end = uninstall_string.rfind('}')?;
|
||||
if start >= end {
|
||||
return None;
|
||||
}
|
||||
let product_code = &uninstall_string[start..=end];
|
||||
|
||||
// Build the MSI registry key path
|
||||
let pos = subkey.rfind('\\')?;
|
||||
let reg_msi_key = format!("{}{}", &subkey[..=pos], product_code);
|
||||
|
||||
Some(reg_msi_key)
|
||||
}
|
||||
|
||||
// Double confirm the process name
|
||||
fn kill_process_by_pids(name: &str, pids: Vec<Pid>) -> ResultType<()> {
|
||||
let name = name.to_lowercase();
|
||||
@@ -2885,6 +3295,109 @@ fn kill_process_by_pids(name: &str, pids: Vec<Pid>) -> ResultType<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn handle_custom_client_staging_dir_before_update(
|
||||
custom_client_staging_dir: &PathBuf,
|
||||
) -> ResultType<()> {
|
||||
let Some(current_exe_dir) = std::env::current_exe()
|
||||
.ok()
|
||||
.and_then(|p| p.parent().map(|p| p.to_path_buf()))
|
||||
else {
|
||||
bail!("Failed to get current exe directory");
|
||||
};
|
||||
|
||||
// Clean up existing staging directory
|
||||
if custom_client_staging_dir.exists() {
|
||||
log::debug!(
|
||||
"Removing existing custom client staging directory: {:?}",
|
||||
custom_client_staging_dir
|
||||
);
|
||||
if let Err(e) = remove_custom_client_staging_dir(custom_client_staging_dir) {
|
||||
bail!(
|
||||
"Failed to remove existing custom client staging directory {:?}: {}",
|
||||
custom_client_staging_dir,
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let src_path = current_exe_dir.join("custom.txt");
|
||||
if src_path.exists() {
|
||||
// Verify that custom.txt is not a symlink before copying
|
||||
let metadata = match std::fs::symlink_metadata(&src_path) {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
bail!(
|
||||
"Failed to read metadata for custom.txt at {:?}: {}",
|
||||
src_path,
|
||||
e
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if metadata.is_symlink() {
|
||||
allow_err!(remove_custom_client_staging_dir(&custom_client_staging_dir));
|
||||
bail!(
|
||||
"custom.txt at {:?} is a symlink, refusing to stage for security reasons.",
|
||||
src_path
|
||||
);
|
||||
}
|
||||
|
||||
if metadata.is_file() {
|
||||
if !custom_client_staging_dir.exists() {
|
||||
if let Err(e) = std::fs::create_dir_all(custom_client_staging_dir) {
|
||||
bail!("Failed to create parent directory {:?} when staging custom client files: {}", custom_client_staging_dir, e);
|
||||
}
|
||||
}
|
||||
let dst_path = custom_client_staging_dir.join("custom.txt");
|
||||
if let Err(e) = std::fs::copy(&src_path, &dst_path) {
|
||||
allow_err!(remove_custom_client_staging_dir(&custom_client_staging_dir));
|
||||
bail!(
|
||||
"Failed to copy custom txt from {:?} to {:?}: {}",
|
||||
src_path,
|
||||
dst_path,
|
||||
e
|
||||
);
|
||||
}
|
||||
} else {
|
||||
log::warn!(
|
||||
"custom.txt at {:?} is not a regular file, skipping.",
|
||||
src_path
|
||||
);
|
||||
}
|
||||
} else {
|
||||
log::info!("No custom txt found to stage for update.");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Used for auto update and manual update in the main window.
|
||||
pub fn update_to(file: &str) -> ResultType<()> {
|
||||
if file.ends_with(".exe") {
|
||||
let custom_client_staging_dir = get_custom_client_staging_dir();
|
||||
if crate::is_custom_client() {
|
||||
handle_custom_client_staging_dir_before_update(&custom_client_staging_dir)?;
|
||||
} else {
|
||||
// Clean up any residual staging directory from previous custom client
|
||||
allow_err!(remove_custom_client_staging_dir(&custom_client_staging_dir));
|
||||
}
|
||||
if !run_uac(file, "--update")? {
|
||||
bail!(
|
||||
"Failed to run the update exe with UAC, error: {:?}",
|
||||
std::io::Error::last_os_error()
|
||||
);
|
||||
}
|
||||
} else if file.ends_with(".msi") {
|
||||
if let Err(e) = update_me_msi(file, false) {
|
||||
bail!("Failed to run the update msi: {}", e);
|
||||
}
|
||||
} else {
|
||||
// unreachable!()
|
||||
bail!("Unsupported update file format: {}", file);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Don't launch tray app when running with `\qn`.
|
||||
// 1. Because `/qn` requires administrator permission and the tray app should be launched with user permission.
|
||||
// Or launching the main window from the tray app will cause the main window to be launched with administrator permission.
|
||||
@@ -2905,6 +3418,7 @@ pub fn update_me_msi(msi: &str, quiet: bool) -> ResultType<()> {
|
||||
}
|
||||
|
||||
pub fn get_tray_shortcut(exe: &str, tmp_path: &str) -> ResultType<String> {
|
||||
let shortcut_icon_location = get_shortcut_icon_location(exe);
|
||||
Ok(write_cmds(
|
||||
format!(
|
||||
"
|
||||
@@ -2914,6 +3428,7 @@ sLinkFile = \"{tmp_path}\\{app_name} Tray.lnk\"
|
||||
Set oLink = oWS.CreateShortcut(sLinkFile)
|
||||
oLink.TargetPath = \"{exe}\"
|
||||
oLink.Arguments = \"--tray\"
|
||||
{shortcut_icon_location}
|
||||
oLink.Save
|
||||
",
|
||||
app_name = crate::get_app_name(),
|
||||
@@ -2976,6 +3491,44 @@ fn run_after_run_cmds(silent: bool) {
|
||||
std::thread::sleep(std::time::Duration::from_millis(300));
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn try_remove_temp_update_files() {
|
||||
let temp_dir = std::env::temp_dir();
|
||||
let Ok(entries) = std::fs::read_dir(&temp_dir) else {
|
||||
log::debug!("Failed to read temp directory: {:?}", temp_dir);
|
||||
return;
|
||||
};
|
||||
|
||||
let one_hour = std::time::Duration::from_secs(60 * 60);
|
||||
for entry in entries {
|
||||
if let Ok(entry) = entry {
|
||||
let path = entry.path();
|
||||
if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
|
||||
// Match files like rustdesk-*.msi or rustdesk-*.exe
|
||||
if file_name.starts_with("rustdesk-")
|
||||
&& (file_name.ends_with(".msi") || file_name.ends_with(".exe"))
|
||||
{
|
||||
// Skip files modified within the last hour to avoid deleting files being downloaded
|
||||
if let Ok(metadata) = std::fs::metadata(&path) {
|
||||
if let Ok(modified) = metadata.modified() {
|
||||
if let Ok(elapsed) = modified.elapsed() {
|
||||
if elapsed < one_hour {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Err(e) = std::fs::remove_file(&path) {
|
||||
log::debug!("Failed to remove temp update file {:?}: {}", path, e);
|
||||
} else {
|
||||
log::info!("Removed temp update file: {:?}", path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn try_kill_broker() {
|
||||
allow_err!(std::process::Command::new("cmd")
|
||||
@@ -3151,7 +3704,8 @@ pub fn is_x64() -> bool {
|
||||
pub fn try_kill_rustdesk_main_window_process() -> ResultType<()> {
|
||||
// Kill rustdesk.exe without extra arg, should only be called by --server
|
||||
// We can find the exact process which occupies the ipc, see more from https://github.com/winsiderss/systeminformer
|
||||
log::info!("try kill rustdesk main window process");
|
||||
let app_name = crate::get_app_name().to_lowercase();
|
||||
log::info!("try kill main window process");
|
||||
use hbb_common::sysinfo::System;
|
||||
let mut sys = System::new();
|
||||
sys.refresh_processes();
|
||||
@@ -3160,7 +3714,6 @@ pub fn try_kill_rustdesk_main_window_process() -> ResultType<()> {
|
||||
.map(|x| x.user_id())
|
||||
.unwrap_or_default();
|
||||
let my_pid = std::process::id();
|
||||
let app_name = crate::get_app_name().to_lowercase();
|
||||
if app_name.is_empty() {
|
||||
bail!("app name is empty");
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ impl RendezvousMediator {
|
||||
}
|
||||
crate::hbbs_http::sync::start();
|
||||
#[cfg(target_os = "windows")]
|
||||
if crate::platform::is_installed() && crate::is_server() && !crate::is_custom_client() {
|
||||
if crate::platform::is_installed() && crate::is_server() {
|
||||
crate::updater::start_auto_update();
|
||||
}
|
||||
check_zombie();
|
||||
|
||||
@@ -560,7 +560,9 @@ impl Connection {
|
||||
match data {
|
||||
ipc::Data::Authorize => {
|
||||
conn.require_2fa.take();
|
||||
conn.send_logon_response().await;
|
||||
if !conn.send_logon_response_and_keep_alive().await {
|
||||
break;
|
||||
}
|
||||
if conn.port_forward_socket.is_some() {
|
||||
break;
|
||||
}
|
||||
@@ -1338,9 +1340,66 @@ impl Connection {
|
||||
crate::post_request(url, v.to_string(), "").await
|
||||
}
|
||||
|
||||
async fn send_logon_response(&mut self) {
|
||||
fn normalize_port_forward_target(pf: &mut PortForward) -> (String, bool) {
|
||||
let mut is_rdp = false;
|
||||
if pf.host == "RDP" && pf.port == 0 {
|
||||
pf.host = "localhost".to_owned();
|
||||
pf.port = 3389;
|
||||
is_rdp = true;
|
||||
}
|
||||
if pf.host.is_empty() {
|
||||
pf.host = "localhost".to_owned();
|
||||
}
|
||||
(format!("{}:{}", pf.host, pf.port), is_rdp)
|
||||
}
|
||||
|
||||
async fn connect_port_forward_if_needed(&mut self) -> bool {
|
||||
if self.port_forward_socket.is_some() {
|
||||
return true;
|
||||
}
|
||||
let Some(login_request::Union::PortForward(pf)) = self.lr.union.as_ref() else {
|
||||
return true;
|
||||
};
|
||||
let mut pf = pf.clone();
|
||||
let (mut addr, is_rdp) = Self::normalize_port_forward_target(&mut pf);
|
||||
self.port_forward_address = addr.clone();
|
||||
match timeout(3000, TcpStream::connect(&addr)).await {
|
||||
Ok(Ok(sock)) => {
|
||||
self.port_forward_socket = Some(Framed::new(sock, BytesCodec::new()));
|
||||
true
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
log::warn!("Port forward connect failed for {}: {}", addr, e);
|
||||
if is_rdp {
|
||||
addr = "RDP".to_owned();
|
||||
}
|
||||
self.send_login_error(format!(
|
||||
"Failed to access remote {}. Please make sure it is reachable/open.",
|
||||
addr
|
||||
))
|
||||
.await;
|
||||
false
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("Port forward connect timed out for {}: {}", addr, e);
|
||||
if is_rdp {
|
||||
addr = "RDP".to_owned();
|
||||
}
|
||||
self.send_login_error(format!(
|
||||
"Failed to access remote {}. Please make sure it is reachable/open.",
|
||||
addr
|
||||
))
|
||||
.await;
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Returns whether this connection should be kept alive.
|
||||
// `true` does not necessarily mean authorization succeeded (e.g. REQUIRE_2FA case).
|
||||
async fn send_logon_response_and_keep_alive(&mut self) -> bool {
|
||||
if self.authorized {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
if self.require_2fa.is_some() && !self.is_recent_session(true) && !self.from_switch {
|
||||
self.require_2fa.as_ref().map(|totp| {
|
||||
@@ -1371,7 +1430,11 @@ impl Connection {
|
||||
}
|
||||
});
|
||||
self.send_login_error(crate::client::REQUIRE_2FA).await;
|
||||
return;
|
||||
// Keep the connection alive so the client can continue with 2FA.
|
||||
return true;
|
||||
}
|
||||
if !self.connect_port_forward_if_needed().await {
|
||||
return false;
|
||||
}
|
||||
self.authorized = true;
|
||||
let (conn_type, auth_conn_type) = if self.file_transfer.is_some() {
|
||||
@@ -1494,7 +1557,7 @@ impl Connection {
|
||||
res.set_peer_info(pi);
|
||||
msg_out.set_login_response(res);
|
||||
self.send(msg_out).await;
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
if self.is_remote() {
|
||||
@@ -1517,7 +1580,7 @@ impl Connection {
|
||||
let mut msg_out = Message::new();
|
||||
msg_out.set_login_response(res);
|
||||
self.send(msg_out).await;
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
#[allow(unused_mut)]
|
||||
@@ -1671,6 +1734,7 @@ impl Connection {
|
||||
self.try_sub_monitor_services();
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn try_sub_camera_displays(&mut self) {
|
||||
@@ -1813,6 +1877,7 @@ impl Connection {
|
||||
port_forward: self.port_forward_address.clone(),
|
||||
peer_id,
|
||||
name,
|
||||
avatar: self.lr.avatar.clone(),
|
||||
authorized,
|
||||
keyboard: self.keyboard,
|
||||
clipboard: self.clipboard,
|
||||
@@ -2178,33 +2243,8 @@ impl Connection {
|
||||
sleep(1.).await;
|
||||
return false;
|
||||
}
|
||||
let mut is_rdp = false;
|
||||
if pf.host == "RDP" && pf.port == 0 {
|
||||
pf.host = "localhost".to_owned();
|
||||
pf.port = 3389;
|
||||
is_rdp = true;
|
||||
}
|
||||
if pf.host.is_empty() {
|
||||
pf.host = "localhost".to_owned();
|
||||
}
|
||||
let mut addr = format!("{}:{}", pf.host, pf.port);
|
||||
self.port_forward_address = addr.clone();
|
||||
match timeout(3000, TcpStream::connect(&addr)).await {
|
||||
Ok(Ok(sock)) => {
|
||||
self.port_forward_socket = Some(Framed::new(sock, BytesCodec::new()));
|
||||
}
|
||||
_ => {
|
||||
if is_rdp {
|
||||
addr = "RDP".to_owned();
|
||||
}
|
||||
self.send_login_error(format!(
|
||||
"Failed to access remote {}, please make sure if it is open",
|
||||
addr
|
||||
))
|
||||
.await;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
let (addr, _is_rdp) = Self::normalize_port_forward_target(&mut pf);
|
||||
self.port_forward_address = addr;
|
||||
}
|
||||
_ => {
|
||||
if !self.check_privacy_mode_on().await {
|
||||
@@ -2235,9 +2275,7 @@ impl Connection {
|
||||
// `is_logon_ui()` is a fallback for logon UI detection on Windows.
|
||||
#[cfg(target_os = "windows")]
|
||||
let is_logon = || {
|
||||
crate::platform::is_prelogin()
|
||||
|| crate::platform::is_locked()
|
||||
|| {
|
||||
crate::platform::is_prelogin() || crate::platform::is_locked() || {
|
||||
match crate::platform::is_logon_ui() {
|
||||
Ok(result) => result,
|
||||
Err(e) => {
|
||||
@@ -2276,7 +2314,9 @@ impl Connection {
|
||||
if err_msg.is_empty() {
|
||||
#[cfg(target_os = "linux")]
|
||||
self.linux_headless_handle.wait_desktop_cm_ready().await;
|
||||
self.send_logon_response().await;
|
||||
if !self.send_logon_response_and_keep_alive().await {
|
||||
return false;
|
||||
}
|
||||
self.try_start_cm(lr.my_id.clone(), lr.my_name.clone(), self.authorized);
|
||||
} else {
|
||||
self.send_login_error(err_msg).await;
|
||||
@@ -2312,7 +2352,9 @@ impl Connection {
|
||||
if err_msg.is_empty() {
|
||||
#[cfg(target_os = "linux")]
|
||||
self.linux_headless_handle.wait_desktop_cm_ready().await;
|
||||
self.send_logon_response().await;
|
||||
if !self.send_logon_response_and_keep_alive().await {
|
||||
return false;
|
||||
}
|
||||
self.try_start_cm(lr.my_id, lr.my_name, self.authorized);
|
||||
} else {
|
||||
self.send_login_error(err_msg).await;
|
||||
@@ -2330,7 +2372,9 @@ impl Connection {
|
||||
self.update_failure(failure, true, 1);
|
||||
self.require_2fa.take();
|
||||
raii::AuthedConnID::set_session_2fa(self.session_key());
|
||||
self.send_logon_response().await;
|
||||
if !self.send_logon_response_and_keep_alive().await {
|
||||
return false;
|
||||
}
|
||||
self.try_start_cm(
|
||||
self.lr.my_id.to_owned(),
|
||||
self.lr.my_name.to_owned(),
|
||||
@@ -2381,7 +2425,9 @@ impl Connection {
|
||||
if let Some((_instant, uuid_old)) = uuid_old {
|
||||
if uuid == uuid_old {
|
||||
self.from_switch = true;
|
||||
self.send_logon_response().await;
|
||||
if !self.send_logon_response_and_keep_alive().await {
|
||||
return false;
|
||||
}
|
||||
self.try_start_cm(
|
||||
lr.my_id.clone(),
|
||||
lr.my_name.clone(),
|
||||
@@ -5347,9 +5393,8 @@ mod raii {
|
||||
}
|
||||
|
||||
pub fn check_wake_lock_on_setting_changed() {
|
||||
let current = config::Config::get_bool_option(
|
||||
keys::OPTION_KEEP_AWAKE_DURING_INCOMING_SESSIONS,
|
||||
);
|
||||
let current =
|
||||
config::Config::get_bool_option(keys::OPTION_KEEP_AWAKE_DURING_INCOMING_SESSIONS);
|
||||
let cached = *WAKELOCK_KEEP_AWAKE_OPTION.lock().unwrap();
|
||||
if cached != Some(current) {
|
||||
Self::check_wake_lock();
|
||||
|
||||
@@ -809,7 +809,7 @@ fn record_key_is_control_key(record_key: u64) -> bool {
|
||||
|
||||
#[inline]
|
||||
fn record_key_is_chr(record_key: u64) -> bool {
|
||||
record_key < KEY_CHAR_START
|
||||
record_key >= KEY_CHAR_START
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -1513,6 +1513,27 @@ fn get_control_key_value(key_event: &KeyEvent) -> i32 {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn has_hotkey_modifiers(key_event: &KeyEvent) -> bool {
|
||||
key_event.modifiers.iter().any(|ck| {
|
||||
let v = ck.value();
|
||||
v == ControlKey::Control.value()
|
||||
|| v == ControlKey::RControl.value()
|
||||
|| v == ControlKey::Meta.value()
|
||||
|| v == ControlKey::RWin.value()
|
||||
|| {
|
||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||
{
|
||||
v == ControlKey::Alt.value() || v == ControlKey::RAlt.value()
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn release_unpressed_modifiers(en: &mut Enigo, key_event: &KeyEvent) {
|
||||
let ck_value = get_control_key_value(key_event);
|
||||
fix_modifiers(&key_event.modifiers[..], en, ck_value);
|
||||
@@ -1572,7 +1593,7 @@ fn need_to_uppercase(en: &mut Enigo) -> bool {
|
||||
get_modifier_state(Key::Shift, en) || get_modifier_state(Key::CapsLock, en)
|
||||
}
|
||||
|
||||
fn process_chr(en: &mut Enigo, chr: u32, down: bool) {
|
||||
fn process_chr(en: &mut Enigo, chr: u32, down: bool, _hotkey: bool) {
|
||||
// On Wayland with uinput mode, use clipboard for character input
|
||||
#[cfg(target_os = "linux")]
|
||||
if !crate::platform::linux::is_x11() && wayland_use_uinput() {
|
||||
@@ -1587,6 +1608,16 @@ fn process_chr(en: &mut Enigo, chr: u32, down: bool) {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "macos", target_os = "windows"))]
|
||||
if !_hotkey {
|
||||
if down {
|
||||
if let Ok(chr) = char::try_from(chr) {
|
||||
en.key_sequence(&chr.to_string());
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let key = char_value_to_key(chr);
|
||||
|
||||
if down {
|
||||
@@ -1856,7 +1887,7 @@ fn legacy_keyboard_mode(evt: &KeyEvent) {
|
||||
|
||||
let record_key = chr as u64 + KEY_CHAR_START;
|
||||
record_pressed_key(KeysDown::EnigoKey(record_key), down);
|
||||
process_chr(&mut en, chr, down)
|
||||
process_chr(&mut en, chr, down, has_hotkey_modifiers(evt))
|
||||
}
|
||||
Some(key_event::Union::Unicode(chr)) => {
|
||||
// Same as Chr: release Shift for Unicode input
|
||||
|
||||
@@ -12,7 +12,11 @@
|
||||
include "common.tis";
|
||||
var p = view.parameters;
|
||||
view.refresh = function() {
|
||||
var draft_input = $(input);
|
||||
var draft = draft_input ? (draft_input.value || "") : "";
|
||||
$(body).content(<ChatBox msgs={p.msgs} callback={p.callback} />);
|
||||
var next_input = $(input);
|
||||
if (next_input) next_input.value = draft;
|
||||
view.focus = $(input);
|
||||
}
|
||||
function self.closing() {
|
||||
|
||||
@@ -57,6 +57,11 @@ div.icon {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
img.icon {
|
||||
size: 96px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
div.id {
|
||||
@ELLIPSIS;
|
||||
color: color(green-blue);
|
||||
|
||||
@@ -28,6 +28,7 @@ impl InvokeUiCM for SciterHandler {
|
||||
client.port_forward.clone(),
|
||||
client.peer_id.clone(),
|
||||
client.name.clone(),
|
||||
client.avatar.clone(),
|
||||
client.authorized,
|
||||
client.keyboard,
|
||||
client.clipboard,
|
||||
|
||||
@@ -42,9 +42,11 @@ class Body: Reactor.Component
|
||||
return <div .content style="size:*">
|
||||
<div .left-panel>
|
||||
<div .icon-and-id>
|
||||
{c.avatar ?
|
||||
<img .icon src={c.avatar} /> :
|
||||
<div .icon style={"background: " + string2RGB(c.name, 1)}>
|
||||
{c.name[0].toUpperCase()}
|
||||
</div>
|
||||
</div>}
|
||||
<div>
|
||||
<div .id style="font-weight: bold; font-size: 1.2em;">{c.name}</div>
|
||||
<div .id>({c.peer_id})</div>
|
||||
@@ -366,7 +368,7 @@ function bring_to_top(idx=-1) {
|
||||
}
|
||||
}
|
||||
|
||||
handler.addConnection = function(id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, recording, block_input) {
|
||||
handler.addConnection = function(id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, avatar, authorized, keyboard, clipboard, audio, file, restart, recording, block_input) {
|
||||
stdout.println("new connection #" + id + ": " + peer_id);
|
||||
var conn;
|
||||
connections.map(function(c) {
|
||||
@@ -385,6 +387,7 @@ handler.addConnection = function(id, is_file_transfer, is_view_camera, is_termin
|
||||
conn = {
|
||||
id: id, is_file_transfer: is_file_transfer, is_view_camera: is_view_camera, is_terminal: is_terminal, peer_id: peer_id,
|
||||
port_forward: port_forward,
|
||||
avatar: avatar,
|
||||
name: name, authorized: authorized, time: new Date(), now: new Date(),
|
||||
keyboard: keyboard, clipboard: clipboard, msgs: [], unreaded: 0,
|
||||
audio: audio, file: file, restart: restart, recording: recording,
|
||||
|
||||
@@ -824,7 +824,9 @@ class UpdateMe: Reactor.Component {
|
||||
return <div .install-me>
|
||||
<div>{translate('Status')}</div>
|
||||
<div>There is a newer version of {handler.get_app_name()} ({handler.get_new_version()}) available.</div>
|
||||
<div #install-me.link>{translate('Click to ' + update_or_download)}</div>
|
||||
{is_custom_client
|
||||
? <div style="font-size: 1em; font-weight: normal; text-align: left; padding-top: 1em;">{translate('Enable \"Auto update\" or contact your administrator for the latest version.')}</div>
|
||||
: <div #install-me.link>{translate('Click to ' + update_or_download)}</div>}
|
||||
<div #download-percent style="display:hidden; padding-top: 1em;" />
|
||||
</div>;
|
||||
}
|
||||
@@ -1449,6 +1451,9 @@ function set_local_user_info(user) {
|
||||
if (user.display_name) {
|
||||
user_info.display_name = user.display_name;
|
||||
}
|
||||
if (user.avatar) {
|
||||
user_info.avatar = user.avatar;
|
||||
}
|
||||
if (user.status) {
|
||||
user_info.status = user.status;
|
||||
}
|
||||
|
||||
@@ -142,6 +142,14 @@ function resetWheel() {
|
||||
}
|
||||
|
||||
var INERTIA_ACCELERATION = 30;
|
||||
var WHEEL_ACCEL_VELOCITY_THRESHOLD = 5000;
|
||||
var WHEEL_ACCEL_DT_FAST = 0.04;
|
||||
var WHEEL_ACCEL_DT_MEDIUM = 0.08;
|
||||
var WHEEL_ACCEL_VALUE_FAST = 3;
|
||||
var WHEEL_ACCEL_VALUE_MEDIUM = 2;
|
||||
// Wheel burst acceleration (empirical tuning).
|
||||
// Applies only on fast, non-smooth wheel bursts to keep single-step scroll unchanged.
|
||||
// Sciter uses seconds for dt, so velocity is in delta/sec.
|
||||
|
||||
// not good, precision not enough to simulate acceleration effect,
|
||||
// seems have to use pixel based rather line based delta
|
||||
@@ -237,12 +245,28 @@ function handler.onMouse(evt)
|
||||
// mouseWheelDistance = 8 * [currentUserDefs floatForKey:@"com.apple.scrollwheel.scaling"];
|
||||
mask = 3;
|
||||
{
|
||||
var (dx, dy) = evt.wheelDeltas;
|
||||
if (dx > 0) dx = 1;
|
||||
else if (dx < 0) dx = -1;
|
||||
if (dy > 0) dy = 1;
|
||||
else if (dy < 0) dy = -1;
|
||||
if (Math.abs(dx) > Math.abs(dy)) {
|
||||
var now = getTime();
|
||||
var dt = last_wheel_time > 0 ? (now - last_wheel_time) / 1000 : 0;
|
||||
var (raw_dx, raw_dy) = evt.wheelDeltas;
|
||||
var dx = 0;
|
||||
var dy = 0;
|
||||
var abs_dx = Math.abs(raw_dx);
|
||||
var abs_dy = Math.abs(raw_dy);
|
||||
var dominant = abs_dx > abs_dy ? abs_dx : abs_dy;
|
||||
var is_smooth = dominant < 1;
|
||||
var accel = 1;
|
||||
if (!is_smooth && dt > 0 && (is_win || is_linux) && get_peer_platform() == "Mac OS") {
|
||||
var velocity = dominant / dt;
|
||||
if (velocity >= WHEEL_ACCEL_VELOCITY_THRESHOLD) {
|
||||
if (dt < WHEEL_ACCEL_DT_FAST) accel = WHEEL_ACCEL_VALUE_FAST;
|
||||
else if (dt < WHEEL_ACCEL_DT_MEDIUM) accel = WHEEL_ACCEL_VALUE_MEDIUM;
|
||||
}
|
||||
}
|
||||
if (raw_dx > 0) dx = accel;
|
||||
else if (raw_dx < 0) dx = -accel;
|
||||
if (raw_dy > 0) dy = accel;
|
||||
else if (raw_dy < 0) dy = -accel;
|
||||
if (abs_dx > abs_dy) {
|
||||
dy = 0;
|
||||
} else {
|
||||
dx = 0;
|
||||
@@ -253,8 +277,6 @@ function handler.onMouse(evt)
|
||||
wheel_delta_y = acc_wheel_delta_y.toInteger();
|
||||
acc_wheel_delta_x -= wheel_delta_x;
|
||||
acc_wheel_delta_y -= wheel_delta_y;
|
||||
var now = getTime();
|
||||
var dt = last_wheel_time > 0 ? (now - last_wheel_time) / 1000 : 0;
|
||||
if (dt > 0) {
|
||||
var vx = dx / dt;
|
||||
var vy = dy / dt;
|
||||
@@ -297,11 +319,13 @@ function handler.onMouse(evt)
|
||||
entered = true;
|
||||
stdout.println("enter");
|
||||
handler.enter(handler.get_keyboard_mode());
|
||||
last_wheel_time = 0;
|
||||
return keyboard_enabled;
|
||||
case Event.MOUSE_LEAVE:
|
||||
entered = false;
|
||||
stdout.println("leave");
|
||||
handler.leave(handler.get_keyboard_mode());
|
||||
last_wheel_time = 0;
|
||||
if (is_left_down && get_peer_platform() == "Android") {
|
||||
is_left_down = false;
|
||||
handler.send_mouse((1 << 3) | 2, 0, 0, evt.altKey,
|
||||
|
||||
@@ -134,6 +134,7 @@ pub struct Client {
|
||||
pub is_terminal: bool,
|
||||
pub port_forward: String,
|
||||
pub name: String,
|
||||
pub avatar: String,
|
||||
pub peer_id: String,
|
||||
pub keyboard: bool,
|
||||
pub clipboard: bool,
|
||||
@@ -220,6 +221,7 @@ impl<T: InvokeUiCM> ConnectionManager<T> {
|
||||
port_forward: String,
|
||||
peer_id: String,
|
||||
name: String,
|
||||
avatar: String,
|
||||
authorized: bool,
|
||||
keyboard: bool,
|
||||
clipboard: bool,
|
||||
@@ -240,6 +242,7 @@ impl<T: InvokeUiCM> ConnectionManager<T> {
|
||||
is_terminal,
|
||||
port_forward,
|
||||
name: name.clone(),
|
||||
avatar,
|
||||
peer_id: peer_id.clone(),
|
||||
keyboard,
|
||||
clipboard,
|
||||
@@ -500,9 +503,9 @@ impl<T: InvokeUiCM> IpcTaskRunner<T> {
|
||||
}
|
||||
Ok(Some(data)) => {
|
||||
match data {
|
||||
Data::Login{id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, file_transfer_enabled: _file_transfer_enabled, restart, recording, block_input, from_switch} => {
|
||||
Data::Login{id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, avatar, authorized, keyboard, clipboard, audio, file, file_transfer_enabled: _file_transfer_enabled, restart, recording, block_input, from_switch} => {
|
||||
log::debug!("conn_id: {}", id);
|
||||
self.cm.add_connection(id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, recording, block_input, from_switch, self.tx.clone());
|
||||
self.cm.add_connection(id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, avatar, authorized, keyboard, clipboard, audio, file, restart, recording, block_input, from_switch, self.tx.clone());
|
||||
self.conn_id = id;
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
@@ -823,6 +826,7 @@ pub async fn start_listen<T: InvokeUiCM>(
|
||||
port_forward,
|
||||
peer_id,
|
||||
name,
|
||||
avatar,
|
||||
authorized,
|
||||
keyboard,
|
||||
clipboard,
|
||||
@@ -843,6 +847,7 @@ pub async fn start_listen<T: InvokeUiCM>(
|
||||
port_forward,
|
||||
peer_id,
|
||||
name,
|
||||
avatar,
|
||||
authorized,
|
||||
keyboard,
|
||||
clipboard,
|
||||
|
||||
@@ -245,7 +245,20 @@ pub fn get_builtin_option(key: &str) -> String {
|
||||
|
||||
#[inline]
|
||||
pub fn set_local_option(key: String, value: String) {
|
||||
LocalConfig::set_option(key.clone(), value.clone());
|
||||
LocalConfig::set_option(key.clone(), value);
|
||||
}
|
||||
|
||||
/// Resolve relative avatar path (e.g. "/avatar/xxx") to absolute URL
|
||||
/// by prepending the API server address.
|
||||
pub fn resolve_avatar_url(avatar: String) -> String {
|
||||
let avatar = avatar.trim().to_owned();
|
||||
if avatar.starts_with('/') {
|
||||
let api_server = get_api_server();
|
||||
if !api_server.is_empty() {
|
||||
return format!("{}{}", api_server.trim_end_matches('/'), avatar);
|
||||
}
|
||||
}
|
||||
avatar
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))]
|
||||
|
||||
@@ -119,7 +119,7 @@ fn start_auto_update_check_(rx_msg: Receiver<UpdateMsg>) {
|
||||
|
||||
fn check_update(manually: bool) -> ResultType<()> {
|
||||
#[cfg(target_os = "windows")]
|
||||
let is_msi = crate::platform::is_msi_installed()?;
|
||||
let update_msi = crate::platform::is_msi_installed()? && !crate::is_custom_client();
|
||||
if !(manually || config::Config::get_bool_option(config::keys::OPTION_ALLOW_AUTO_UPDATE)) {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -140,7 +140,7 @@ fn check_update(manually: bool) -> ResultType<()> {
|
||||
"{}/rustdesk-{}-x86_64.{}",
|
||||
download_url,
|
||||
version,
|
||||
if is_msi { "msi" } else { "exe" }
|
||||
if update_msi { "msi" } else { "exe" }
|
||||
)
|
||||
} else {
|
||||
format!("{}/rustdesk-{}-x86-sciter.exe", download_url, version)
|
||||
@@ -190,21 +190,21 @@ fn check_update(manually: bool) -> ResultType<()> {
|
||||
// before the download, but not empty after the download.
|
||||
if has_no_active_conns() {
|
||||
#[cfg(target_os = "windows")]
|
||||
update_new_version(is_msi, &version, &file_path);
|
||||
update_new_version(update_msi, &version, &file_path);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn update_new_version(is_msi: bool, version: &str, file_path: &PathBuf) {
|
||||
fn update_new_version(update_msi: bool, version: &str, file_path: &PathBuf) {
|
||||
log::debug!(
|
||||
"New version is downloaded, update begin, is msi: {is_msi}, version: {version}, file: {:?}",
|
||||
"New version is downloaded, update begin, update msi: {update_msi}, version: {version}, file: {:?}",
|
||||
file_path.to_str()
|
||||
);
|
||||
if let Some(p) = file_path.to_str() {
|
||||
if let Some(session_id) = crate::platform::get_current_process_session_id() {
|
||||
if is_msi {
|
||||
if update_msi {
|
||||
match crate::platform::update_me_msi(p, true) {
|
||||
Ok(_) => {
|
||||
log::debug!("New version \"{}\" updated.", version);
|
||||
@@ -215,21 +215,57 @@ fn update_new_version(is_msi: bool, version: &str, file_path: &PathBuf) {
|
||||
version,
|
||||
e
|
||||
);
|
||||
std::fs::remove_file(&file_path).ok();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
match crate::platform::launch_privileged_process(
|
||||
let custom_client_staging_dir = if crate::is_custom_client() {
|
||||
let custom_client_staging_dir =
|
||||
crate::platform::get_custom_client_staging_dir();
|
||||
if let Err(e) = crate::platform::handle_custom_client_staging_dir_before_update(
|
||||
&custom_client_staging_dir,
|
||||
) {
|
||||
log::error!(
|
||||
"Failed to handle custom client staging dir before update: {}",
|
||||
e
|
||||
);
|
||||
std::fs::remove_file(&file_path).ok();
|
||||
return;
|
||||
}
|
||||
Some(custom_client_staging_dir)
|
||||
} else {
|
||||
// Clean up any residual staging directory from previous custom client
|
||||
let staging_dir = crate::platform::get_custom_client_staging_dir();
|
||||
hbb_common::allow_err!(crate::platform::remove_custom_client_staging_dir(
|
||||
&staging_dir
|
||||
));
|
||||
None
|
||||
};
|
||||
let update_launched = match crate::platform::launch_privileged_process(
|
||||
session_id,
|
||||
&format!("{} --update", p),
|
||||
) {
|
||||
Ok(h) => {
|
||||
if h.is_null() {
|
||||
log::error!("Failed to update to the new version: {}", version);
|
||||
false
|
||||
} else {
|
||||
log::debug!("New version \"{}\" is launched.", version);
|
||||
true
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to run the new version: {}", e);
|
||||
false
|
||||
}
|
||||
};
|
||||
if !update_launched {
|
||||
if let Some(dir) = custom_client_staging_dir {
|
||||
hbb_common::allow_err!(crate::platform::remove_custom_client_staging_dir(
|
||||
&dir
|
||||
));
|
||||
}
|
||||
std::fs::remove_file(&file_path).ok();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -237,6 +273,7 @@ fn update_new_version(is_msi: bool, version: &str, file_path: &PathBuf) {
|
||||
"Failed to get the current process session id, Error {}",
|
||||
std::io::Error::last_os_error()
|
||||
);
|
||||
std::fs::remove_file(&file_path).ok();
|
||||
}
|
||||
} else {
|
||||
// unreachable!()
|
||||
|
||||
Reference in New Issue
Block a user