Compare commits

...

9 Commits

Author SHA1 Message Date
fufesou
80a5865db3 macOS update: restore LaunchAgent in GUI session and isolate temp update dir by euid (#14434)
* fix(update): macos, load agent

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

* fix(update): macos, isolate temp update dir by euid

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

* refact(update): macos script

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-03-01 20:06:04 +08:00
MichaIng
9cb6f38aea packaging: deb: remove obsolete Python version check (#14429)
It was used to conditionally install a Python module in the past. But that is not the case anymore since https://github.com/rustdesk/rustdesk/commit/37dbfcc. Now the check is obsolete.

Due to `set -e`, the check leads to a package configuration failure if Python is not installed, which however otherwise is not needed for RustDesk.

The commit includes an indentation fix and trailing space removal.

Signed-off-by: MichaIng <micha@dietpi.com>
2026-03-01 18:05:19 +08:00
fufesou
cd7e3e4505 fix(update): macos, input password (#14430)
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-03-01 15:19:07 +08:00
fufesou
1833cb0655 fix(update): revert check (#14424)
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-02-28 18:17:26 +08:00
fufesou
e4208aa9cf fix(update): revert check (#14423)
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-02-28 16:33:54 +08:00
Amirhosein Akhlaghpoor
bb3501a4f9 ui: scale wheel lines on Windows/Linux to Mac (#14395)
* input: accelerate wheel bursts on Windows->Mac

- boost fast wheel bursts without affecting single-step scrolls\n- use dominant-axis smooth detection and velocity gate\n- reset wheel timestamp on enter/leave\n- enforce single-axis scrolling\n- extract/tune Sciter wheel accel thresholds

Signed-off-by: Amirhossein Akhlaghpour <m9.akhlaghpoor@gmail.com>

* input: clarify wheel burst tuning

- add comments on acceleration rules and units\n- apply burst accel on Windows/Linux to macOS\n- reset wheel timing on enter/leave

Signed-off-by: Amirhossein Akhlaghpour <m9.akhlaghpoor@gmail.com>

* input: align wheel burst velocity thresholds

- match Flutter velocity gate with Sciter

Signed-off-by: Amirhossein Akhlaghpour <m9.akhlaghpoor@gmail.com>

* input: restore flutter wheel velocity threshold

- keep burst threshold at 0.002 delta/us

Signed-off-by: Amirhossein Akhlaghpour <m9.akhlaghpoor@gmail.com>

---------

Signed-off-by: Amirhossein Akhlaghpour <m9.akhlaghpoor@gmail.com>
2026-02-28 10:56:25 +08:00
fufesou
4abdb2e08b feat: windows, custom client, update (#13687)
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-02-27 21:50:20 +08:00
rustdesk
d49ae493b2 bump to 1.4.6 2026-02-27 20:53:40 +08:00
VenusGirl❤
394079833e Update ko.rs (#14418) 2026-02-27 20:14:14 +08:00
32 changed files with 1104 additions and 208 deletions

View File

@@ -39,7 +39,7 @@ env:
# 2. Update the `VCPKG_COMMIT_ID` in `ci.yml` and `playground.yml`. # 2. Update the `VCPKG_COMMIT_ID` in `ci.yml` and `playground.yml`.
VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b" 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 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" NDK_VERSION: "r27c"
#signing keys env variable checks #signing keys env variable checks
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"

View File

@@ -17,7 +17,7 @@ env:
TAG_NAME: "nightly" TAG_NAME: "nightly"
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b" VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b"
VERSION: "1.4.5" VERSION: "1.4.6"
NDK_VERSION: "r26d" NDK_VERSION: "r26d"
#signing keys env variable checks #signing keys env variable checks
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"

View File

@@ -10,6 +10,6 @@ jobs:
- uses: vedantmgoyal9/winget-releaser@main - uses: vedantmgoyal9/winget-releaser@main
with: with:
identifier: RustDesk.RustDesk identifier: RustDesk.RustDesk
version: "1.4.5" version: "1.4.6"
release-tag: "1.4.5" release-tag: "1.4.6"
token: ${{ secrets.WINGET_TOKEN }} token: ${{ secrets.WINGET_TOKEN }}

1
.gitignore vendored
View File

@@ -3,6 +3,7 @@
.vscode .vscode
.idea .idea
.DS_Store .DS_Store
.env
libsciter-gtk.so libsciter-gtk.so
src/ui/inline.rs src/ui/inline.rs
extractor extractor

4
Cargo.lock generated
View File

@@ -7134,7 +7134,7 @@ dependencies = [
[[package]] [[package]]
name = "rustdesk" name = "rustdesk"
version = "1.4.5" version = "1.4.6"
dependencies = [ dependencies = [
"android-wakelock", "android-wakelock",
"android_logger", "android_logger",
@@ -7249,7 +7249,7 @@ dependencies = [
[[package]] [[package]]
name = "rustdesk-portable-packer" name = "rustdesk-portable-packer"
version = "1.4.5" version = "1.4.6"
dependencies = [ dependencies = [
"brotli", "brotli",
"dirs 5.0.1", "dirs 5.0.1",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "rustdesk" name = "rustdesk"
version = "1.4.5" version = "1.4.6"
authors = ["rustdesk <info@rustdesk.com>"] authors = ["rustdesk <info@rustdesk.com>"]
edition = "2021" edition = "2021"
build= "build.rs" build= "build.rs"

View File

@@ -18,7 +18,7 @@ AppDir:
id: rustdesk id: rustdesk
name: rustdesk name: rustdesk
icon: rustdesk icon: rustdesk
version: 1.4.5 version: 1.4.6
exec: usr/share/rustdesk/rustdesk exec: usr/share/rustdesk/rustdesk
exec_args: $@ exec_args: $@
apt: apt:

View File

@@ -18,7 +18,7 @@ AppDir:
id: rustdesk id: rustdesk
name: rustdesk name: rustdesk
icon: rustdesk icon: rustdesk
version: 1.4.5 version: 1.4.6
exec: usr/share/rustdesk/rustdesk exec: usr/share/rustdesk/rustdesk
exec_args: $@ exec_args: $@
apt: apt:

View File

@@ -473,8 +473,7 @@ class _GeneralState extends State<_General> {
} }
Widget other() { Widget other() {
final showAutoUpdate = final showAutoUpdate = isWindows && bind.mainIsInstalled();
isWindows && bind.mainIsInstalled() && !bind.isCustomClient();
final children = <Widget>[ final children = <Widget>[
if (!isWeb && !bind.isIncomingOnly()) if (!isWeb && !bind.isIncomingOnly())
_OptionCheckBox(context, 'Confirm before closing multiple tabs', _OptionCheckBox(context, 'Confirm before closing multiple tabs',

View File

@@ -365,6 +365,16 @@ class InputModel {
final isPhysicalMouse = false.obs; final isPhysicalMouse = false.obs;
int _lastButtons = 0; int _lastButtons = 0;
Offset lastMousePos = Offset.zero; 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). // Relative mouse mode (for games/3D apps).
final relativeMouseMode = false.obs; final relativeMouseMode = false.obs;
@@ -964,6 +974,7 @@ class InputModel {
toReleaseRawKeys.release(handleRawKeyEvent); toReleaseRawKeys.release(handleRawKeyEvent);
_pointerMovedAfterEnter = false; _pointerMovedAfterEnter = false;
_pointerInsideImage = enter; _pointerInsideImage = enter;
_lastWheelTsUs = 0;
// Fix status // Fix status
if (!enter) { if (!enter) {
@@ -1407,17 +1418,44 @@ class InputModel {
if (isViewOnly) return; if (isViewOnly) return;
if (isViewCamera) return; if (isViewCamera) return;
if (e is PointerScrollEvent) { if (e is PointerScrollEvent) {
var dx = e.scrollDelta.dx.toInt(); final rawDx = e.scrollDelta.dx;
var dy = e.scrollDelta.dy.toInt(); 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) { if (dx > 0) {
dx = -1; dx = -accel;
} else if (dx < 0) { } else if (dx < 0) {
dx = 1; dx = accel;
} }
if (dy > 0) { if (dy > 0) {
dy = -1; dy = -accel;
} else if (dy < 0) { } else if (dy < 0) {
dy = 1; dy = accel;
} }
bind.sessionSendMouse( bind.sessionSendMouse(
sessionId: sessionId, sessionId: sessionId,

View File

@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# Read more about iOS versioning at # Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # 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 # 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: environment:
sdk: '^3.1.0' sdk: '^3.1.0'

View File

@@ -7,6 +7,7 @@
#include <cstdlib> // for getenv and _putenv #include <cstdlib> // for getenv and _putenv
#include <cstring> // for strcmp #include <cstring> // for strcmp
#include <string> // for std::wstring
namespace { namespace {
@@ -15,6 +16,43 @@ constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW";
// The number of Win32Window objects that currently exist. // The number of Win32Window objects that currently exist.
static int g_active_window_count = 0; 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); using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd);
// Scale helper to convert logical scaler values to physical using passed in // 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.cbClsExtra = 0;
window_class.cbWndExtra = 0; window_class.cbWndExtra = 0;
window_class.hInstance = GetModuleHandle(nullptr); 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.hbrBackground = 0;
window_class.lpszMenuName = nullptr; window_class.lpszMenuName = nullptr;
window_class.lpfnWndProc = Win32Window::WndProc; window_class.lpfnWndProc = Win32Window::WndProc;
@@ -95,6 +141,12 @@ const wchar_t* WindowClassRegistrar::GetWindowClass() {
void WindowClassRegistrar::UnregisterWindowClass() { void WindowClassRegistrar::UnregisterWindowClass() {
UnregisterClass(kWindowClassName, nullptr); UnregisterClass(kWindowClassName, nullptr);
class_registered_ = false; 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() { Win32Window::Win32Window() {

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "rustdesk-portable-packer" name = "rustdesk-portable-packer"
version = "1.4.5" version = "1.4.6"
edition = "2021" edition = "2021"
description = "RustDesk Remote Desktop" description = "RustDesk Remote Desktop"

View File

@@ -6,15 +6,13 @@ if [ "$1" = configure ]; then
INITSYS=$(ls -al /proc/1/exe | awk -F' ' '{print $NF}' | awk -F'/' '{print $NF}') INITSYS=$(ls -al /proc/1/exe | awk -F' ' '{print $NF}' | awk -F'/' '{print $NF}')
ln -f -s /usr/share/rustdesk/rustdesk /usr/bin/rustdesk ln -f -s /usr/share/rustdesk/rustdesk /usr/bin/rustdesk
if [ "systemd" == "$INITSYS" ]; then if [ "systemd" == "$INITSYS" ]; then
if [ -e /etc/systemd/system/rustdesk.service ]; 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 rm /etc/systemd/system/rustdesk.service /usr/lib/systemd/system/rustdesk.service /usr/lib/systemd/user/rustdesk.service >/dev/null 2>&1
fi fi
version=$(python3 -V 2>&1 | grep -Po '(?<=Python )(.+)') mkdir -p /usr/lib/systemd/system/
parsedVersion=$(echo "${version//./}")
mkdir -p /usr/lib/systemd/system/
cp /usr/share/rustdesk/files/systemd/rustdesk.service /usr/lib/systemd/system/rustdesk.service cp /usr/share/rustdesk/files/systemd/rustdesk.service /usr/lib/systemd/system/rustdesk.service
# try fix error in Ubuntu 18.04 # try fix error in Ubuntu 18.04
# Failed to reload rustdesk.service: Unit rustdesk.service is not loaded properly: Exec format error. # Failed to reload rustdesk.service: Unit rustdesk.service is not loaded properly: Exec format error.

View File

@@ -1,5 +1,5 @@
pkgname=rustdesk pkgname=rustdesk
pkgver=1.4.5 pkgver=1.4.6
pkgrel=0 pkgrel=0
epoch= epoch=
pkgdesc="" pkgdesc=""

View File

@@ -31,22 +31,168 @@ LExit:
return WcaFinalize(er); return WcaFinalize(er);
} }
// CAUTION: We can't simply remove the install folder here, because silent repair/upgrade will fail. // Helper function to safely delete a file or directory using handle-based deletion.
// `RemoveInstallFolder()` is a deferred custom action, it will be executed after the files are copied. // This avoids TOCTOU (Time-Of-Check-Time-Of-Use) race conditions.
// `msiexec /i package.msi /qn` 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( UINT __stdcall RemoveInstallFolder(
__in MSIHANDLE hInstall) __in MSIHANDLE hInstall)
{ {
HRESULT hr = S_OK; HRESULT hr = S_OK;
DWORD er = ERROR_SUCCESS; DWORD er = ERROR_SUCCESS;
int nResult = 0;
LPWSTR installFolder = NULL; LPWSTR installFolder = NULL;
LPWSTR pwz = NULL; LPWSTR pwz = NULL;
LPWSTR pwzData = NULL; LPWSTR pwzData = NULL;
WCHAR runtimeBroker[1024] = { 0, };
hr = WcaInitialize(hInstall, "RemoveInstallFolder"); hr = WcaInitialize(hInstall, "RemoveInstallFolder");
ExitOnFailure(hr, "Failed to initialize"); ExitOnFailure(hr, "Failed to initialize");
@@ -58,24 +204,23 @@ UINT __stdcall RemoveInstallFolder(
hr = WcaReadStringFromCaData(&pwz, &installFolder); hr = WcaReadStringFromCaData(&pwz, &installFolder);
ExitOnFailure(hr, "failed to read database key from custom action data: %ls", pwz); 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); if (installFolder == NULL || installFolder[0] == L'\0') {
WcaLog(LOGMSG_STANDARD, "Install folder path is empty, skipping recursive delete.");
SHFILEOPSTRUCTW fileOp; goto LExit;
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);
} }
else
{ if (PathIsRootW(installFolder)) {
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); 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: LExit:
ReleaseStr(pwzData); ReleaseStr(pwzData);
@@ -109,9 +254,12 @@ bool TerminateProcessIfNotContainsParam(pfnNtQueryInformationProcess NtQueryInfo
{ {
if (pebUpp.CommandLine.Length > 0) 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) if (commandLine != NULL)
{ {
// Initialize all bytes to zero for safety
memset(commandLine, 0, pebUpp.CommandLine.Length + sizeof(WCHAR));
if (ReadProcessMemory(process, pebUpp.CommandLine.Buffer, if (ReadProcessMemory(process, pebUpp.CommandLine.Buffer,
commandLine, pebUpp.CommandLine.Length, &dwBytesRead)) commandLine, pebUpp.CommandLine.Length, &dwBytesRead))
{ {

View File

@@ -1,5 +1,5 @@
Name: rustdesk Name: rustdesk
Version: 1.4.5 Version: 1.4.6
Release: 0 Release: 0
Summary: RPM package Summary: RPM package
License: GPL-3.0 License: GPL-3.0

View File

@@ -1,5 +1,5 @@
Name: rustdesk Name: rustdesk
Version: 1.4.5 Version: 1.4.6
Release: 0 Release: 0
Summary: RPM package Summary: RPM package
License: GPL-3.0 License: GPL-3.0

View File

@@ -1,5 +1,5 @@
Name: rustdesk Name: rustdesk
Version: 1.4.5 Version: 1.4.6
Release: 0 Release: 0
Summary: RPM package Summary: RPM package
License: GPL-3.0 License: GPL-3.0

View File

@@ -39,7 +39,7 @@ use hbb_common::{
use crate::{ use crate::{
hbbs_http::{create_http_client_async, get_url_for_tls}, 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)] #[derive(Debug, Eq, PartialEq)]

View File

@@ -187,7 +187,10 @@ pub fn core_main() -> Option<Vec<String>> {
} }
#[cfg(windows)] #[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)); std::thread::spawn(move || crate::start_server(false, no_server));
} else { } else {
#[cfg(windows)] #[cfg(windows)]
@@ -202,17 +205,24 @@ pub fn core_main() -> Option<Vec<String>> {
if config::is_disable_installation() { if config::is_disable_installation() {
return None; return None;
} }
let res = platform::update_me(false);
let text = match res { let text = match crate::platform::prepare_custom_client_update() {
Ok(_) => translate("Update successfully!".to_string()), Err(e) => {
Err(err) => { log::error!("Error preparing custom client update: {}", e);
log::error!("Failed with error: {err}"); "Update failed!".to_string()
translate("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) Toast::new(Toast::POWERSHELL_APP_ID)
.title(&config::APP_NAME.read().unwrap()) .title(&config::APP_NAME.read().unwrap())
.text1(&text) .text1(&translate(text))
.sound(Some(Sound::Default)) .sound(Some(Sound::Default))
.duration(Duration::Short) .duration(Duration::Short)
.show() .show()

View File

@@ -2776,10 +2776,13 @@ pub fn main_get_common(key: String) -> String {
} else if key.starts_with("download-file-") { } else if key.starts_with("download-file-") {
let _version = key.replace("download-file-", ""); let _version = key.replace("download-file-", "");
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
return match crate::platform::windows::is_msi_installed() { return match (
Ok(true) => format!("rustdesk-{_version}-x86_64.msi"), crate::platform::windows::is_msi_installed(),
Ok(false) => format!("rustdesk-{_version}-x86_64.exe"), crate::common::is_custom_client(),
Err(e) => { ) {
(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); log::error!("Failed to check if is msi: {}", e);
format!("error:update-failed-check-msi-tip") format!("error:update-failed-check-msi-tip")
} }
@@ -2876,30 +2879,17 @@ pub fn main_set_common(_key: String, _value: String) {
if let Some(f) = new_version_file.to_str() { if let Some(f) = new_version_file.to_str() {
// 1.4.0 does not support "--update" // 1.4.0 does not support "--update"
// But we can assume that the new version supports it. // But we can assume that the new version supports it.
#[cfg(target_os = "windows")]
if f.ends_with(".exe") { #[cfg(any(target_os = "windows", target_os = "macos"))]
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")]
match crate::platform::update_to(f) { match crate::platform::update_to(f) {
Ok(_) => { Ok(_) => {
log::info!("Update successfully!"); log::info!("Update process is launched successfully!");
} }
Err(e) => { Err(e) => {
log::error!("Failed to update to new version, {}", 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" { } else if _key == "extract-update-dmg" {

View File

@@ -53,8 +53,25 @@ pub fn download_file(
auto_del_dur: Option<Duration>, auto_del_dur: Option<Duration>,
) -> ResultType<String> { ) -> ResultType<String> {
let id = url.clone(); let id = url.clone();
if DOWNLOADERS.lock().unwrap().contains_key(&id) { // First pass: if a non-error downloader exists for this URL, reuse it.
return Ok(id); // 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() { if let Some(path) = path.as_ref() {
@@ -75,8 +92,26 @@ pub fn download_file(
tx_cancel: tx, tx_cancel: tx,
finished: false, finished: false,
}; };
let mut downloaders = DOWNLOADERS.lock().unwrap(); // Second pass (atomic with insert) to avoid race with another concurrent caller.
downloaders.insert(id.clone(), downloader); 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(); let id2 = id.clone();
std::thread::spawn( std::thread::spawn(

View File

@@ -739,6 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("keep-awake-during-outgoing-sessions-label", "발신 세션 중 화면 켜짐 유지"), ("keep-awake-during-outgoing-sessions-label", "발신 세션 중 화면 켜짐 유지"),
("keep-awake-during-incoming-sessions-label", "수신 세션 중 화면 켜짐 유지"), ("keep-awake-during-incoming-sessions-label", "수신 세션 중 화면 켜짐 유지"),
("Continue with {}", "{}(으)로 계속"), ("Continue with {}", "{}(으)로 계속"),
("Display Name", ""), ("Display Name", "표시 이름"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -42,9 +42,16 @@ static PRIVILEGES_SCRIPTS_DIR: Dir =
include_dir!("$CARGO_MANIFEST_DIR/src/platform/privileges_scripts"); include_dir!("$CARGO_MANIFEST_DIR/src/platform/privileges_scripts");
static mut LATEST_SEED: i32 = 0; static mut LATEST_SEED: i32 = 0;
// Using a fixed temporary directory for updates is preferable to #[inline]
// using one that includes the custom client name. fn get_update_temp_dir() -> PathBuf {
const UPDATE_TEMP_DIR: &str = "/tmp/.rustdeskupdate"; 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. /// Global mutex to serialize CoreGraphics cursor operations.
/// This prevents race conditions between cursor visibility (hide depth tracking) /// 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) => { Err(e) => {
log::error!("run osascript failed: {}", 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(); let installed = std::path::Path::new(&agent_plist_file).exists();
log::info!("Agent file {} installed: {}", &agent_plist_file, installed); 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() { let _guard = match CG_CURSOR_MUTEX.try_lock() {
Ok(guard) => guard, Ok(guard) => guard,
Err(std::sync::TryLockError::WouldBlock) => { 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"); debug_assert!(false, "Re-entrant call to set_cursor_pos detected");
return false; return false;
} }
@@ -822,7 +819,8 @@ pub fn quit_gui() {
#[inline] #[inline]
pub fn try_remove_temp_update_dir(dir: Option<&str>) { 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() { if target_path.exists() {
std::fs::remove_dir_all(target_path).ok(); std::fs::remove_dir_all(target_path).ok();
} }
@@ -851,32 +849,33 @@ pub fn update_me() -> ResultType<()> {
if is_installed_daemon && !is_service_stopped { if is_installed_daemon && !is_service_stopped {
let agent = format!("{}_server.plist", crate::get_full_name()); let agent = format!("{}_server.plist", crate::get_full_name());
let agent_plist_file = format!("/Library/LaunchAgents/{}", agent); 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); update_daemon_agent(agent_plist_file, app_dir, true);
} else { } else {
// `kill -9` may not work without "administrator privileges" // `kill -9` may not work without "administrator privileges"
let update_body = format!( let update_body = r#"
r#" on run {app_name, cur_pid, app_dir, user_name}
do shell script " set app_bundle to "/Applications/" & app_name & ".app"
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' set app_bundle_q to quoted form of app_bundle
" with prompt "{app_name} wants to update itself" with administrator privileges set app_dir_q to quoted form of app_dir
"#, set user_name_q to quoted form of user_name
app_name = app_name,
pid = std::process::id(), 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;"
app_dir = app_dir, 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);"
user = get_active_username() set sh to "set -e;" & kill_others & copy_files
);
match Command::new("osascript") 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("-e")
.arg(update_body) .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() => { Ok(status) if !status.success() => {
log::error!("osascript execution failed with status: {}", status); log::error!("osascript execution failed with status: {}", status);
} }
@@ -897,25 +896,28 @@ pgrep -x '{app_name}' | grep -v {pid} | xargs kill -9 && rm -rf '/Applications/{
} }
pub fn update_from_dmg(dmg_path: &str) -> ResultType<()> { 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); println!("Starting update from DMG: {}", dmg_path);
extract_dmg(dmg_path, UPDATE_TEMP_DIR)?; extract_dmg(dmg_path, &update_temp_dir)?;
println!("DMG extracted"); println!("DMG extracted");
update_extracted(UPDATE_TEMP_DIR)?; update_extracted(&update_temp_dir)?;
println!("Update process started"); println!("Update process started");
Ok(()) Ok(())
} }
pub fn update_to(_file: &str) -> ResultType<()> { 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(()) Ok(())
} }
pub fn extract_update_dmg(file: &str) { pub fn extract_update_dmg(file: &str) {
let update_temp_dir = get_update_temp_dir_string();
let mut evt: HashMap<&str, String> = let mut evt: HashMap<&str, String> =
HashMap::from([("name", "extract-update-dmg".to_string())]); HashMap::from([("name", "extract-update-dmg".to_string())]);
match extract_dmg(file, UPDATE_TEMP_DIR) { match extract_dmg(file, &update_temp_dir) {
Ok(_) => { Ok(_) => {
log::info!("Extracted dmg file to {}", UPDATE_TEMP_DIR); log::info!("Extracted dmg file to {}", update_temp_dir);
} }
Err(e) => { Err(e) => {
evt.insert("err", e.to_string()); evt.insert("err", e.to_string());

View File

@@ -1,18 +1,25 @@
on run {daemon_file, agent_file, user, cur_pid, source_dir} 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 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 sh to "set -e;" & resolve_uid & unload_agent & unload_service & kill_others & copy_files & write_daemon_plist & write_agent_plist & load_service & load_agent
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
do shell script sh with prompt "RustDesk wants to update itself" with administrator privileges do shell script sh with prompt "RustDesk wants to update itself" with administrator privileges
end run end run

View File

@@ -153,11 +153,7 @@ pub fn clip_cursor(rect: Option<(i32, i32, i32, i32)>) -> bool {
}; };
if result == FALSE { if result == FALSE {
let err = GetLastError(); let err = GetLastError();
log::warn!( log::warn!("ClipCursor failed: rect={:?}, error_code={}", rect, err);
"ClipCursor failed: rect={:?}, error_code={}",
rect,
err
);
return false; return false;
} }
true 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) 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( pub fn run_exe_in_cur_session(
exe: &str, exe: &str,
arg: Vec<&str>, arg: Vec<&str>,
show: bool, show: bool,
) -> ResultType<Option<std::process::Child>> { ) -> ResultType<Option<std::process::Child>> {
let Some(session_id) = get_current_process_session_id() else { if is_root() {
bail!("Failed to get current process session id"); 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) };
run_exe_in_session(exe, arg, session_id, show)
} else {
run_exe_direct(exe, arg, show)
}
} }
pub fn run_exe_in_session( 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( fn get_after_install(
exe: &str, exe: &str,
reg_value_start_menu_shortcuts: Option<String>, 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 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 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( let mk_shortcut = write_cmds(
format!( format!(
" "
@@ -1426,6 +1480,7 @@ sLinkFile = \"{tmp_path}\\{app_name}.lnk\"
Set oLink = oWS.CreateShortcut(sLinkFile) Set oLink = oWS.CreateShortcut(sLinkFile)
oLink.TargetPath = \"{exe}\" oLink.TargetPath = \"{exe}\"
{shortcut_icon_location}
oLink.Save oLink.Save
" "
), ),
@@ -1482,8 +1537,13 @@ copy /Y \"{tmp_path}\\Uninstall {app_name}.lnk\" \"{start_menu}\\\"
reg_value_printer = "1".to_owned(); reg_value_printer = "1".to_owned();
} }
let meta = std::fs::symlink_metadata(std::env::current_exe()?)?; let meta = std::fs::symlink_metadata(&current_exe)?;
let size = meta.len() / 1024; 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://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.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 // 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}\" md \"{path}\"
{copy_exe} {copy_exe}
reg add {subkey} /f 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 DisplayName /t REG_SZ /d \"{app_name}\"
reg add {subkey} /f /v DisplayVersion /t REG_SZ /d \"{version}\" 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 Version /t REG_SZ /d \"{version}\"
@@ -1560,6 +1620,7 @@ copy /Y \"{tmp_path}\\Uninstall {app_name}.lnk\" \"{path}\\\"
{install_remote_printer} {install_remote_printer}
{sleep} {sleep}
", ",
display_icon = get_custom_icon(&cur_exe).unwrap_or(exe.to_string()),
version = crate::VERSION.replace("-", "."), version = crate::VERSION.replace("-", "."),
build_date = crate::BUILD_DATE, build_date = crate::BUILD_DATE,
after_install = get_after_install( after_install = get_after_install(
@@ -1795,6 +1856,163 @@ fn get_reg_of(subkey: &str, name: &str) -> String {
"".to_owned() "".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> { pub fn get_license_from_exe_name() -> ResultType<CustomServer> {
let mut exe = std::env::current_exe()?.to_str().unwrap_or("").to_owned(); let mut exe = std::env::current_exe()?.to_str().unwrap_or("").to_owned();
// if defined portable appname entry, replace original executable name with it. // if defined portable appname entry, replace original executable name with it.
@@ -1903,12 +2121,48 @@ unsafe fn set_default_dll_directories() -> bool {
true 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<()> { pub fn create_shortcut(id: &str) -> ResultType<()> {
let exe = std::env::current_exe()?.to_str().unwrap_or("").to_owned(); let exe = std::env::current_exe()?.to_str().unwrap_or("").to_owned();
// https://github.com/rustdesk/rustdesk/issues/13735 // https://github.com/rustdesk/rustdesk/issues/13735
// Replace ':' with '_' for filename since ':' is not allowed in Windows filenames // Replace ':' with '_' for filename since ':' is not allowed in Windows filenames
// https://github.com/rustdesk/hbb_common/blob/8b0e25867375ba9e6bff548acf44fe6d6ffa7c0e/src/config.rs#L1384 // https://github.com/rustdesk/hbb_common/blob/8b0e25867375ba9e6bff548acf44fe6d6ffa7c0e/src/config.rs#L1384
let filename = id.replace(':', "_"); let filename = id.replace(':', "_");
let shortcut_icon_location = get_shortcut_icon_location(&exe);
let shortcut = write_cmds( let shortcut = write_cmds(
format!( format!(
" "
@@ -1919,6 +2173,7 @@ sLinkFile = objFSO.BuildPath(strDesktop, \"{filename}.lnk\")
Set oLink = oWS.CreateShortcut(sLinkFile) Set oLink = oWS.CreateShortcut(sLinkFile)
oLink.TargetPath = \"{exe}\" oLink.TargetPath = \"{exe}\"
oLink.Arguments = \"--connect {id}\" oLink.Arguments = \"--connect {id}\"
{shortcut_icon_location}
oLink.Save oLink.Save
" "
), ),
@@ -2724,6 +2979,44 @@ if exist \"{tray_shortcut}\" del /f /q \"{tray_shortcut}\"
std::process::exit(0); 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(&current_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<()> { pub fn update_me(debug: bool) -> ResultType<()> {
let app_name = crate::get_app_name(); let app_name = crate::get_app_name();
let src_exe = std::env::current_exe()?.to_string_lossy().to_string(); 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 { if versions.len() > 2 {
version_build = versions[2]; version_build = versions[2];
} }
let meta = std::fs::symlink_metadata(std::env::current_exe()?)?; let version = crate::VERSION.replace("-", ".");
let size = meta.len() / 1024; 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!( let is_msi = is_msi_installed().ok();
"
reg add {subkey} /f /v DisplayIcon /t REG_SZ /d \"{exe}\" 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 DisplayVersion /t REG_SZ /d \"{version}\"
reg add {subkey} /f /v Version /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}\" 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 VersionMinor /t REG_DWORD /d {version_minor}
reg add {subkey} /f /v VersionBuild /t REG_DWORD /d {version_build} reg add {subkey} /f /v VersionBuild /t REG_DWORD /d {version_build}
reg add {subkey} /f /v EstimatedSize /t REG_DWORD /d {size} 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(
&reg_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 filter = format!(" /FI \"PID ne {}\"", get_current_pid());
let restore_service_cmd = if is_service_running { let restore_service_cmd = if is_service_running {
@@ -2820,6 +3165,8 @@ sc stop {app_name}
taskkill /F /IM {app_name}.exe{filter} taskkill /F /IM {app_name}.exe{filter}
{reg_cmd} {reg_cmd}
{copy_exe} {copy_exe}
{rename_exe}
{remove_meta_toml}
{restore_service_cmd} {restore_service_cmd}
{uninstall_printer_cmd} {uninstall_printer_cmd}
{install_printer_cmd} {install_printer_cmd}
@@ -2827,43 +3174,106 @@ taskkill /F /IM {app_name}.exe{filter}
", ",
app_name = app_name, app_name = app_name,
copy_exe = copy_exe_cmd(&src_exe, &exe, &path)?, 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 { "" }, 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")?; run_cmds(cmds, debug, "update")?;
std::thread::sleep(std::time::Duration::from_millis(2000)); 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."); log::info!("Update completed.");
Ok(()) 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 // Double confirm the process name
fn kill_process_by_pids(name: &str, pids: Vec<Pid>) -> ResultType<()> { fn kill_process_by_pids(name: &str, pids: Vec<Pid>) -> ResultType<()> {
let name = name.to_lowercase(); let name = name.to_lowercase();
@@ -2885,6 +3295,109 @@ fn kill_process_by_pids(name: &str, pids: Vec<Pid>) -> ResultType<()> {
Ok(()) 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`. // 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. // 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. // 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> { pub fn get_tray_shortcut(exe: &str, tmp_path: &str) -> ResultType<String> {
let shortcut_icon_location = get_shortcut_icon_location(exe);
Ok(write_cmds( Ok(write_cmds(
format!( format!(
" "
@@ -2914,6 +3428,7 @@ sLinkFile = \"{tmp_path}\\{app_name} Tray.lnk\"
Set oLink = oWS.CreateShortcut(sLinkFile) Set oLink = oWS.CreateShortcut(sLinkFile)
oLink.TargetPath = \"{exe}\" oLink.TargetPath = \"{exe}\"
oLink.Arguments = \"--tray\" oLink.Arguments = \"--tray\"
{shortcut_icon_location}
oLink.Save oLink.Save
", ",
app_name = crate::get_app_name(), 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)); 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] #[inline]
pub fn try_kill_broker() { pub fn try_kill_broker() {
allow_err!(std::process::Command::new("cmd") 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<()> { pub fn try_kill_rustdesk_main_window_process() -> ResultType<()> {
// Kill rustdesk.exe without extra arg, should only be called by --server // 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 // 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; use hbb_common::sysinfo::System;
let mut sys = System::new(); let mut sys = System::new();
sys.refresh_processes(); sys.refresh_processes();
@@ -3160,7 +3714,6 @@ pub fn try_kill_rustdesk_main_window_process() -> ResultType<()> {
.map(|x| x.user_id()) .map(|x| x.user_id())
.unwrap_or_default(); .unwrap_or_default();
let my_pid = std::process::id(); let my_pid = std::process::id();
let app_name = crate::get_app_name().to_lowercase();
if app_name.is_empty() { if app_name.is_empty() {
bail!("app name is empty"); bail!("app name is empty");
} }

View File

@@ -66,7 +66,7 @@ impl RendezvousMediator {
} }
crate::hbbs_http::sync::start(); crate::hbbs_http::sync::start();
#[cfg(target_os = "windows")] #[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(); crate::updater::start_auto_update();
} }
check_zombie(); check_zombie();

View File

@@ -824,7 +824,9 @@ class UpdateMe: Reactor.Component {
return <div .install-me> return <div .install-me>
<div>{translate('Status')}</div> <div>{translate('Status')}</div>
<div>There is a newer version of {handler.get_app_name()} ({handler.get_new_version()}) available.</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 #download-percent style="display:hidden; padding-top: 1em;" />
</div>; </div>;
} }

View File

@@ -142,6 +142,14 @@ function resetWheel() {
} }
var INERTIA_ACCELERATION = 30; 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, // not good, precision not enough to simulate acceleration effect,
// seems have to use pixel based rather line based delta // 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"]; // mouseWheelDistance = 8 * [currentUserDefs floatForKey:@"com.apple.scrollwheel.scaling"];
mask = 3; mask = 3;
{ {
var (dx, dy) = evt.wheelDeltas; var now = getTime();
if (dx > 0) dx = 1; var dt = last_wheel_time > 0 ? (now - last_wheel_time) / 1000 : 0;
else if (dx < 0) dx = -1; var (raw_dx, raw_dy) = evt.wheelDeltas;
if (dy > 0) dy = 1; var dx = 0;
else if (dy < 0) dy = -1; var dy = 0;
if (Math.abs(dx) > Math.abs(dy)) { 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; dy = 0;
} else { } else {
dx = 0; dx = 0;
@@ -253,8 +277,6 @@ function handler.onMouse(evt)
wheel_delta_y = acc_wheel_delta_y.toInteger(); wheel_delta_y = acc_wheel_delta_y.toInteger();
acc_wheel_delta_x -= wheel_delta_x; acc_wheel_delta_x -= wheel_delta_x;
acc_wheel_delta_y -= wheel_delta_y; 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) { if (dt > 0) {
var vx = dx / dt; var vx = dx / dt;
var vy = dy / dt; var vy = dy / dt;
@@ -297,11 +319,13 @@ function handler.onMouse(evt)
entered = true; entered = true;
stdout.println("enter"); stdout.println("enter");
handler.enter(handler.get_keyboard_mode()); handler.enter(handler.get_keyboard_mode());
last_wheel_time = 0;
return keyboard_enabled; return keyboard_enabled;
case Event.MOUSE_LEAVE: case Event.MOUSE_LEAVE:
entered = false; entered = false;
stdout.println("leave"); stdout.println("leave");
handler.leave(handler.get_keyboard_mode()); handler.leave(handler.get_keyboard_mode());
last_wheel_time = 0;
if (is_left_down && get_peer_platform() == "Android") { if (is_left_down && get_peer_platform() == "Android") {
is_left_down = false; is_left_down = false;
handler.send_mouse((1 << 3) | 2, 0, 0, evt.altKey, handler.send_mouse((1 << 3) | 2, 0, 0, evt.altKey,

View File

@@ -119,7 +119,7 @@ fn start_auto_update_check_(rx_msg: Receiver<UpdateMsg>) {
fn check_update(manually: bool) -> ResultType<()> { fn check_update(manually: bool) -> ResultType<()> {
#[cfg(target_os = "windows")] #[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)) { if !(manually || config::Config::get_bool_option(config::keys::OPTION_ALLOW_AUTO_UPDATE)) {
return Ok(()); return Ok(());
} }
@@ -140,7 +140,7 @@ fn check_update(manually: bool) -> ResultType<()> {
"{}/rustdesk-{}-x86_64.{}", "{}/rustdesk-{}-x86_64.{}",
download_url, download_url,
version, version,
if is_msi { "msi" } else { "exe" } if update_msi { "msi" } else { "exe" }
) )
} else { } else {
format!("{}/rustdesk-{}-x86-sciter.exe", download_url, version) 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. // before the download, but not empty after the download.
if has_no_active_conns() { if has_no_active_conns() {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
update_new_version(is_msi, &version, &file_path); update_new_version(update_msi, &version, &file_path);
} }
} }
Ok(()) Ok(())
} }
#[cfg(target_os = "windows")] #[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!( 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() file_path.to_str()
); );
if let Some(p) = 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 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) { match crate::platform::update_me_msi(p, true) {
Ok(_) => { Ok(_) => {
log::debug!("New version \"{}\" updated.", version); log::debug!("New version \"{}\" updated.", version);
@@ -215,21 +215,57 @@ fn update_new_version(is_msi: bool, version: &str, file_path: &PathBuf) {
version, version,
e e
); );
std::fs::remove_file(&file_path).ok();
} }
} }
} else { } 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, session_id,
&format!("{} --update", p), &format!("{} --update", p),
) { ) {
Ok(h) => { Ok(h) => {
if h.is_null() { if h.is_null() {
log::error!("Failed to update to the new version: {}", version); log::error!("Failed to update to the new version: {}", version);
false
} else {
log::debug!("New version \"{}\" is launched.", version);
true
} }
} }
Err(e) => { Err(e) => {
log::error!("Failed to run the new version: {}", 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 { } 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 {}", "Failed to get the current process session id, Error {}",
std::io::Error::last_os_error() std::io::Error::last_os_error()
); );
std::fs::remove_file(&file_path).ok();
} }
} else { } else {
// unreachable!() // unreachable!()