From 765332793cac7af0f45785e8d65fb7f248b8757d Mon Sep 17 00:00:00 2001 From: Alexey Date: Wed, 12 Aug 2026 11:00:10 +0300 Subject: [PATCH] alpha v0.0976 --- CLIENT_AGENT_V2_INSTALLER.md | 7 +- client_agent_v2.py | 73 ++++++++---- installer/configure_rustdesk.ps1 | 195 ++++++++++++++++++++++++------- 3 files changed, 203 insertions(+), 72 deletions(-) diff --git a/CLIENT_AGENT_V2_INSTALLER.md b/CLIENT_AGENT_V2_INSTALLER.md index 72882ab..0d6a9ed 100644 --- a/CLIENT_AGENT_V2_INSTALLER.md +++ b/CLIENT_AGENT_V2_INSTALLER.md @@ -82,6 +82,7 @@ Setup. Это исключает распаковку Tcl/Tk во временн ``` Перед обновлением сценарий автоматически закрывает уже открытое пользовательское -окно RustDesk. После тихой установки он ожидает завершения только процесса -установщика, поэтому запущенное окно RustDesk не задерживает завершение установки -клиента. +окно RustDesk, не затрагивая процесс службы. Он ожидает полного завершения тихой +установки, но самостоятельно закрывает появившееся окно RustDesk, поэтому оно не +задерживает установщик клиента. Для службы используется только системная копия из +`Program Files`; служба с устаревшим или некорректным путём пересоздаётся. diff --git a/client_agent_v2.py b/client_agent_v2.py index 7c89b36..055694a 100644 --- a/client_agent_v2.py +++ b/client_agent_v2.py @@ -918,7 +918,7 @@ class ClientV2App: self.position_var = tk.StringVar(value=config.get("position", "")) self.full_name_var = tk.StringVar(value=config.get("full_name", "")) self.status_var = tk.StringVar(value="Готов к подключению") - self.connection_state_var = tk.StringVar(value="Не подключено") + self.connection_state_var = tk.StringVar(value="Проверка подключения к серверу...") self.hostname_var = tk.StringVar(value=self.detected_payload["hostname"]) self.cpu_var = tk.StringVar(value=self.detected_payload["cpu"]) @@ -1160,7 +1160,7 @@ class ClientV2App: anchor="w", padx=0, ) - self.connection_state_label.place(x=16, y=0) + self.connection_state_label.grid(row=0, column=1, sticky="w", padx=(3, 0)) tk.Label( connection_indicator, textvariable=self.last_update_var, @@ -1169,7 +1169,7 @@ class ClientV2App: font=("MS Sans Serif", 8), anchor="w", ).grid(row=1, column=0, columnspan=2, sticky="w", pady=(2, 0)) - self.set_connection_state(False) + self.set_connection_state(None) self.button(controls, "Обновить", self.refresh_detected_data, width=11).grid(row=0, column=2, padx=(0, 10)) self.register_button = self.button(controls, "Отправить и привязать", self.on_register_clicked, width=23, primary=True) self.unregister_button = self.button(controls, "Отвязать", self.on_unregister_clicked, width=13) @@ -1209,13 +1209,23 @@ class ClientV2App: pass def set_connection_state(self, connected, update_time=False): - self.connection_state_var.set("Подключено" if connected else "Не подключено") + if connected is None: + message = "Проверка подключения к серверу..." + color = TEXT_MUTED + elif connected: + message = "Подключение к серверу успешно" + color = SUCCESS + else: + message = "Не удалось подключиться к серверу" + color = DANGER + + self.connection_state_var.set(message) if update_time: self.last_update_var.set(f"Последнее обновление: {time.strftime('%H:%M')}") if hasattr(self, "connection_state_label"): - self.connection_state_label.configure(fg=SUCCESS if connected else DANGER) + self.connection_state_label.configure(fg=color) if hasattr(self, "connection_icon_label"): - image = self.connection_icon_images.get(bool(connected)) + image = self.connection_icon_images.get(bool(connected)) if connected is not None else None self.connection_icon_label.configure(image=image or "") self.connection_icon_label.image = image @@ -1512,8 +1522,10 @@ class ClientV2App: server = normalize_server_url(self.server_var.get()) token = self.token_var.get().strip() if not server or not token: - self.status_var.set("Укажите адрес сервера и токен") + self.set_connection_state(False) + self.status_var.set("Не настроен адрес сервера") return + self.set_connection_state(None) self.status_var.set("Загружаю здания и кабинеты...") threading.Thread(target=self._connect_worker, args=(server, token), daemon=True).start() @@ -1700,7 +1712,8 @@ class ClientV2App: server = normalize_server_url(self.server_var.get()) token = self.token_var.get().strip() if not server or not token: - self.status_var.set("Укажите адрес сервера и токен") + self.set_connection_state(False) + self.status_var.set("Не настроен адрес сервера") return payload = self.build_register_payload() self.save_manual_config() @@ -1739,7 +1752,8 @@ class ClientV2App: token = self.token_var.get().strip() inventory_number = self.inventory_var.get().strip() or "-" if not server or not token: - self.status_var.set("Укажите адрес сервера и токен") + self.set_connection_state(False) + self.status_var.set("Не настроен адрес сервера") return confirmed = messagebox.askyesno( "Отвязать устройство", @@ -1770,12 +1784,13 @@ class ClientV2App: def apply_unregister_success(self, message="Устройство отвязано"): self.stop_heartbeat() - self.set_connection_state(False) + self.set_connection_state(True, update_time=True) self.is_bound_to_server = False self.bound_inventory_number = "" self.config.pop("bound_inventory_number", None) save_config(self.config) self.sync_binding_buttons() + self.start_heartbeat() try: disable_windows_autostart() except OSError as error: @@ -1792,7 +1807,8 @@ class ClientV2App: if not server or not token: self.stop_heartbeat() return - context = (server, token, inventory_number, self.client_uid) + is_bound = bool(self.is_bound_to_server and self.bound_inventory_number) + context = (server, token, inventory_number, self.client_uid, is_bound) if self.heartbeat_thread and self.heartbeat_thread.is_alive() and self.heartbeat_context == context: return self.stop_heartbeat() @@ -1920,29 +1936,32 @@ class ClientV2App: if self.tray_available and not self.is_shutting_down and self.root.state() == "iconic": self.hide_window() - def _heartbeat_loop(self, server, token, inventory_number, client_uid, stop_event): + def _heartbeat_loop(self, server, token, inventory_number, client_uid, is_bound, stop_event): last_error = "" while not stop_event.is_set(): try: - response = send_client_test_heartbeat( - server, - token, - inventory_number, - client_uid, - is_online=True, - ) - if not response.get("ok") or not response.get("is_online"): - raise RuntimeError("Сервер не подтвердил статус online") + if is_bound: + response = send_client_test_heartbeat( + server, + token, + inventory_number, + client_uid, + is_online=True, + ) + if not response.get("ok") or not response.get("is_online"): + raise RuntimeError("Сервер не подтвердил статус online") + else: + fetch_locations(server, token) self.tray_actions.put(("connection_state", True)) if last_error: - self.tray_actions.put(("heartbeat_status", "✓ Статус online восстановлен")) + self.tray_actions.put(("heartbeat_status", "✓ Подключение к серверу восстановлено")) last_error = "" except Exception as error: message = format_error(error) self.tray_actions.put(("connection_state", False)) if message != last_error: self.tray_actions.put( - ("heartbeat_status", f"Ошибка отправки статуса online: {message}") + ("heartbeat_status", f"Ошибка подключения к серверу: {message}") ) last_error = message stop_event.wait(HEARTBEAT_INTERVAL_SECONDS) @@ -1964,10 +1983,14 @@ class ClientV2App: stop_event.set() if heartbeat_thread and heartbeat_thread.is_alive(): heartbeat_thread.join(timeout=HEARTBEAT_TIMEOUT_SECONDS + 1) - if context: + if context and context[4]: try: + server, token, inventory_number, client_uid, _is_bound = context response = send_client_test_heartbeat( - *context, + server, + token, + inventory_number, + client_uid, is_online=False, timeout=HEARTBEAT_TIMEOUT_SECONDS, ) diff --git a/installer/configure_rustdesk.ps1 b/installer/configure_rustdesk.ps1 index b7d3ee4..ed85b73 100644 --- a/installer/configure_rustdesk.ps1 +++ b/installer/configure_rustdesk.ps1 @@ -76,17 +76,13 @@ if ($Mode -notin @('automatic', 'manual')) { Write-InstallLog '------------------------------------------------------------' Write-InstallLog "Запуск установки RustDesk. Режим: $Mode" -function Find-RustDeskExecutable { +function Find-SystemRustDeskExecutable { $candidates = @() foreach ($basePath in @($env:ProgramFiles, ${env:ProgramFiles(x86)})) { if ($basePath) { $candidates += Join-Path $basePath 'RustDesk\rustdesk.exe' } } - if ($env:LOCALAPPDATA) { - $candidates += Join-Path $env:LOCALAPPDATA 'RustDesk\rustdesk.exe' - $candidates += Join-Path $env:LOCALAPPDATA 'Programs\RustDesk\rustdesk.exe' - } foreach ($candidate in $candidates) { if ($candidate -and (Test-Path -LiteralPath $candidate)) { @@ -112,13 +108,34 @@ function Stop-RustDeskDesktopProcesses { Write-InstallLog "Не удалось определить PID службы RustDesk: $($_.Exception.Message)" } - $desktopProcesses = @( - Get-Process -Name 'RustDesk' -ErrorAction SilentlyContinue | - Where-Object { ($serviceProcessId -le 0) -or ($_.Id -ne $serviceProcessId) } - ) - foreach ($process in $desktopProcesses) { - Write-InstallLog "Закрывается пользовательский процесс RustDesk, PID: $($process.Id)." - Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue + try { + $rustDeskProcesses = @( + Get-CimInstance ` + -ClassName Win32_Process ` + -Filter "Name = 'rustdesk.exe'" ` + -ErrorAction SilentlyContinue + ) + foreach ($process in $rustDeskProcesses) { + if ([int]$process.SessionId -eq 0) { + continue + } + + if (($serviceProcessId -gt 0) -and + ([int]$process.ProcessId -eq $serviceProcessId)) { + continue + } + + $commandLine = [string]$process.CommandLine + if ($commandLine -match '(?i)--[^\s"]*install[^\s"]*|--(?:option|password)') { + continue + } + + Write-InstallLog "Закрывается пользовательский процесс RustDesk, PID: $($process.ProcessId)." + Stop-Process -Id $process.ProcessId -Force -ErrorAction SilentlyContinue + } + } + catch { + Write-InstallLog "Не удалось закрыть пользовательское окно RustDesk: $($_.Exception.Message)" } } @@ -129,32 +146,62 @@ if (-not (Test-Path -LiteralPath $InstallerPath)) { Stop-RustDeskDesktopProcesses Write-InstallLog 'Запускается встроенный установщик RustDesk.' -$installProcess = Start-Process -FilePath $InstallerPath -ArgumentList '--silent-install' -PassThru -WindowStyle Hidden +$installJob = Start-Job -ScriptBlock { + param([string]$Path) -# Start-Process -Wait waits for the complete process tree. RustDesk starts its -# desktop window as a child process after installation, so -Wait blocks until -# that window is closed manually. Process.WaitForExit waits only for the -# installer process itself and lets setup continue while RustDesk is running. -$installerExited = $installProcess.WaitForExit(60000) -if ($installerExited) { - $installProcess.Refresh() - $installExitCode = $installProcess.ExitCode - Write-InstallLog "Процесс установщика RustDesk завершён с кодом: $installExitCode" -} -else { - $installedExecutable = Find-RustDeskExecutable - if (-not $installedExecutable) { - throw 'Установщик RustDesk не завершился за 60 секунд, установленный rustdesk.exe не найден.' + $process = Start-Process ` + -FilePath $Path ` + -ArgumentList '--silent-install' ` + -Wait ` + -PassThru ` + -WindowStyle Hidden + [int]$process.ExitCode +} -ArgumentList $InstallerPath + +try { + for ($attempt = 0; $attempt -lt 120; $attempt++) { + $installJob = Get-Job -Id $installJob.Id + if ($installJob.State -ne 'Running') { + break + } + + # RustDesk opens its desktop window as a child of the installer. + # Start-Process -Wait also waits for that window, so close it here. + Stop-RustDeskDesktopProcesses + Start-Sleep -Seconds 1 } - Write-InstallLog 'Процесс установки не завершился за 60 секунд, но RustDesk уже установлен. Установка продолжается.' - Stop-Process -Id $installProcess.Id -Force -ErrorAction SilentlyContinue - $installExitCode = 0 + $installJob = Get-Job -Id $installJob.Id + if ($installJob.State -eq 'Running') { + throw 'Тихая установка RustDesk не завершилась за 120 секунд.' + } + if ($installJob.State -ne 'Completed') { + $jobError = ($installJob.ChildJobs[0].JobStateInfo.Reason | Out-String).Trim() + throw "Процесс установки RustDesk завершился с ошибкой: $jobError" + } + + $installJobOutput = @(Receive-Job -Job $installJob -ErrorAction Stop) + if ($installJobOutput.Count -eq 0) { + throw 'Установщик RustDesk завершился без кода возврата.' + } + $installExitCode = [int]$installJobOutput[-1] +} +finally { + if ($installJob) { + if ($installJob.State -eq 'Running') { + Stop-Job -Job $installJob -ErrorAction SilentlyContinue + } + Remove-Job -Job $installJob -Force -ErrorAction SilentlyContinue + } +} +Write-InstallLog "Установщик RustDesk завершён с кодом: $installExitCode" +if ($installExitCode -ne 0) { + throw "Установщик RustDesk завершился с кодом ошибки: $installExitCode." } $rustDeskExecutable = $null -for ($attempt = 0; $attempt -lt 45; $attempt++) { - $rustDeskExecutable = Find-RustDeskExecutable +for ($attempt = 0; $attempt -lt 60; $attempt++) { + $rustDeskExecutable = Find-SystemRustDeskExecutable if ($rustDeskExecutable) { break } @@ -162,7 +209,7 @@ for ($attempt = 0; $attempt -lt 45; $attempt++) { } if (-not $rustDeskExecutable) { - throw "После установки не найден rustdesk.exe. Код установщика RustDesk: $installExitCode." + throw "После установки не найден системный rustdesk.exe в Program Files. Код установщика RustDesk: $installExitCode." } Write-InstallLog "Найден RustDesk: $rustDeskExecutable" @@ -216,26 +263,86 @@ function Set-RustDeskOption { throw "Параметр RustDesk '$Name' не был применён после 5 попыток. Код записи: $lastSetExitCode; код чтения: $lastReadExitCode; результат проверки: $readState." } -$service = Get-Service -Name 'RustDesk' -ErrorAction SilentlyContinue -if (-not $service) { - Write-InstallLog 'Устанавливается служба RustDesk.' - $null = Invoke-RustDeskCommand -Arguments @('--install-service') - for ($attempt = 0; $attempt -lt 30; $attempt++) { - $service = Get-Service -Name 'RustDesk' -ErrorAction SilentlyContinue - if ($service) { - break +function Remove-RustDeskService { + $existingService = Get-Service -Name 'RustDesk' -ErrorAction SilentlyContinue + if (-not $existingService) { + return + } + + if ($existingService.Status -ne 'Stopped') { + Stop-Service -Name 'RustDesk' -Force -ErrorAction SilentlyContinue + try { + $existingService.WaitForStatus('Stopped', [TimeSpan]::FromSeconds(20)) } - Start-Sleep -Seconds 2 + catch { + Write-InstallLog 'Служба RustDesk не остановилась штатно; выполняется её пересоздание.' + } + } + + $existingService.Dispose() + $null = & sc.exe delete 'RustDesk' 2>&1 + for ($attempt = 0; $attempt -lt 30; $attempt++) { + if (-not (Get-Service -Name 'RustDesk' -ErrorAction SilentlyContinue)) { + return + } + Start-Sleep -Seconds 1 + } + + throw 'Не удалось удалить некорректную службу RustDesk.' +} + +function Install-RustDeskService { + Write-InstallLog 'Устанавливается служба RustDesk.' + $installServiceResult = Invoke-RustDeskCommand -Arguments @('--install-service') + Write-InstallLog "Команда установки службы завершена с кодом: $($installServiceResult.ExitCode)" + + for ($attempt = 0; $attempt -lt 30; $attempt++) { + $installedService = Get-Service -Name 'RustDesk' -ErrorAction SilentlyContinue + if ($installedService) { + return $installedService + } + Start-Sleep -Seconds 1 + } + + throw 'Служба RustDesk не была установлена.' +} + +$serviceInstance = Get-CimInstance ` + -ClassName Win32_Service ` + -Filter "Name = 'RustDesk'" ` + -ErrorAction SilentlyContinue +if ($serviceInstance) { + $servicePath = [Environment]::ExpandEnvironmentVariables( + [string]$serviceInstance.PathName + ) + if ($servicePath.IndexOf( + $rustDeskExecutable, + [StringComparison]::OrdinalIgnoreCase + ) -lt 0) { + Write-InstallLog "Обнаружена служба RustDesk с некорректным путём: $servicePath" + Remove-RustDeskService } } +$service = Get-Service -Name 'RustDesk' -ErrorAction SilentlyContinue if (-not $service) { - throw 'Служба RustDesk не была установлена.' + $service = Install-RustDeskService } if ($service.Status -ne 'Running') { Write-InstallLog 'Запускается служба RustDesk.' - Start-Service -Name 'RustDesk' + try { + Start-Service -Name 'RustDesk' + } + catch { + Write-InstallLog "Первый запуск службы завершился ошибкой: $($_.Exception.Message)" + Write-InstallLog 'Служба RustDesk будет пересоздана из системного rustdesk.exe.' + $service.Dispose() + Remove-RustDeskService + $service = Install-RustDeskService + Start-Service -Name 'RustDesk' + } + $service = Get-Service -Name 'RustDesk' $service.WaitForStatus('Running', [TimeSpan]::FromSeconds(30)) }