diff --git a/__pycache__/client_agent_v2.cpython-314.pyc b/__pycache__/client_agent_v2.cpython-314.pyc index 4b5e453..bb1066a 100644 Binary files a/__pycache__/client_agent_v2.cpython-314.pyc and b/__pycache__/client_agent_v2.cpython-314.pyc differ diff --git a/client_agent_v2.py b/client_agent_v2.py index cdfe1ce..cfc22e5 100644 --- a/client_agent_v2.py +++ b/client_agent_v2.py @@ -28,7 +28,14 @@ DEFAULT_TOKEN = "change-me-client-token" CONNECTION_TIMEOUT_SECONDS = 10 HEARTBEAT_INTERVAL_SECONDS = 10 HEARTBEAT_TIMEOUT_SECONDS = 5 -DEVICE_TYPES = ("Компьютер", "Ноутбук", "Моноблок", "Тонкий клиент", "Сервер") +DEVICE_TYPES = ( + "Компьютер", + "Ноутбук", + "Моноблок", + "Тонкий клиент", + "Сервер", + "Виртуальная машина", +) CLIENT_ICON_RELATIVE = Path("picture") / "ICON" / "icon_client.png" BG = "#c0c0c0" @@ -506,6 +513,106 @@ def detect_os_info(): return f"{platform.system()} {platform.release()} {platform.version()} {platform.machine()}".strip() +def contains_virtual_machine_signature(value): + text = str(value or "").lower() + signatures = ( + "vmware", + "virtualbox", + "virtual machine", + "virtual pc", + "virtio", + "qemu", + "kvm", + "xen", + "parallels", + "bhyve", + "bochs", + ) + return any(signature in text for signature in signatures) + + +def contains_server_os_signature(value): + text = str(value or "").lower() + return any( + signature in text + for signature in ( + "windows server", + "server edition", + "ubuntu server", + "red hat enterprise linux server", + "suse linux enterprise server", + "oracle linux server", + ) + ) + + +def detect_windows_device_type(os_info): + evidence = run_powershell( + "$cs=Get-CimInstance Win32_ComputerSystem; " + "$battery=@(Get-CimInstance Win32_Battery -ErrorAction SilentlyContinue).Count; " + "$vmDrivers=(Get-CimInstance Win32_PnPSignedDriver -ErrorAction SilentlyContinue | " + "Where-Object { ($_.DeviceName + ' ' + $_.Manufacturer) -match " + "'VMware|VirtualBox|VirtIO|QEMU|Xen|Parallels' } | " + "Select-Object -ExpandProperty DeviceName) -join ', '; " + '"SYSTEM=$($cs.Manufacturer) $($cs.Model)`nBATTERY=$battery`nDRIVERS=$vmDrivers"', + timeout=15, + ) + if contains_virtual_machine_signature(evidence): + return "Виртуальная машина" + if contains_server_os_signature(os_info): + return "Сервер" + battery_match = re.search(r"(?im)^BATTERY=(\d+)\s*$", evidence) + if battery_match and int(battery_match.group(1)) > 0: + return "Ноутбук" + return "Компьютер" + + +def detect_macos_device_type(os_info): + hardware = run_command(["system_profiler", "SPHardwareDataType"], timeout=15) + if contains_virtual_machine_signature(hardware): + return "Виртуальная машина" + if contains_server_os_signature(os_info): + return "Сервер" + battery = run_command(["pmset", "-g", "batt"]) + if "InternalBattery" in battery and "No batteries" not in battery: + return "Ноутбук" + return "Компьютер" + + +def detect_linux_device_type(os_info): + evidence_parts = [run_command(["systemd-detect-virt"])] + for path in ( + Path("/sys/class/dmi/id/sys_vendor"), + Path("/sys/class/dmi/id/product_name"), + Path("/sys/class/dmi/id/board_vendor"), + ): + try: + evidence_parts.append(path.read_text(encoding="utf-8", errors="ignore")) + except OSError: + pass + if contains_virtual_machine_signature(" ".join(evidence_parts)): + return "Виртуальная машина" + if contains_server_os_signature(os_info): + return "Сервер" + power_supply = Path("/sys/class/power_supply") + if power_supply.exists(): + for type_path in power_supply.glob("*/type"): + try: + if type_path.read_text(encoding="utf-8").strip().lower() == "battery": + return "Ноутбук" + except OSError: + pass + return "Компьютер" + + +def detect_device_type(os_info=""): + if is_windows(): + return detect_windows_device_type(os_info) + if is_macos(): + return detect_macos_device_type(os_info) + return detect_linux_device_type(os_info) + + def rustdesk_config_paths(): paths = [] if is_windows(): @@ -753,11 +860,19 @@ class ClientV2App: self.cabinet_id_by_label = {} self.saved_cabinet_id = str(config.get("cabinet_id") or "").strip() self.detected_payload = build_detected_payload() + self.detected_device_type = detect_device_type(self.detected_payload.get("os_info", "")) + self.device_type_is_manual = bool(config.get("device_type_manual", False)) + configured_device_type = str(config.get("device_type") or "").strip() + initial_device_type = ( + configured_device_type + if self.device_type_is_manual and configured_device_type + else self.detected_device_type + ) self.server_var = tk.StringVar(value=config.get("server", DEFAULT_SERVER)) self.token_var = tk.StringVar(value=config.get("token", DEFAULT_TOKEN)) self.inventory_var = tk.StringVar(value=config.get("inventory_number", "")) - self.device_type_var = tk.StringVar(value=config.get("device_type", DEVICE_TYPES[0])) + self.device_type_var = tk.StringVar(value=initial_device_type) self.model_var = tk.StringVar(value=config.get("model", "")) self.serial_var = tk.StringVar(value=config.get("serial_number", "")) self.building_var = tk.StringVar(value=config.get("building", "")) @@ -885,8 +1000,11 @@ class ClientV2App: manual.columnconfigure(3, weight=1, uniform="manual_field") self.add_grid_control(manual, "Инв. №", self.entry(manual, self.inventory_var), 0, 0, 1, entry=True) self.device_type_box = self.combobox(manual, self.device_type_var, DEVICE_TYPES) + self.device_type_box.configure(state="normal") self.add_grid_control(manual, "Тип", self.device_type_box, 0, 2, 3) self.device_type_box.bind("<>", self.on_device_type_changed) + self.device_type_box.bind("", self.on_device_type_changed) + self.device_type_box.bind("", self.on_device_type_changed) self.add_grid_control(manual, "Модель", self.entry(manual, self.model_var), 1, 0, 1, entry=True) self.add_grid_control(manual, "Серийный №", self.entry(manual, self.serial_var), 1, 2, 3, entry=True) self.building_box = self.combobox(manual, self.building_var, ()) @@ -1132,6 +1250,7 @@ class ClientV2App: ) def on_device_type_changed(self, _event=None): + self.device_type_is_manual = True self.motherboard_var.set(self.motherboard_display_value()) def refresh_detected_data(self): @@ -1288,6 +1407,7 @@ class ClientV2App: "token": self.token_var.get().strip(), "inventory_number": self.inventory_var.get().strip(), "device_type": self.device_type_var.get().strip(), + "device_type_manual": self.device_type_is_manual, "model": self.model_var.get().strip(), "serial_number": self.serial_var.get().strip(), "building": self.building_var.get().strip(), @@ -1417,6 +1537,9 @@ class ClientV2App: self.heartbeat_context = None def start_tray(self): + if not is_windows(): + self.tray_available = False + return False try: import pystray from PIL import Image @@ -1588,8 +1711,9 @@ def run_gui(args): config["server"] = normalize_server_url(args.server) if args.token: config["token"] = args.token.strip() + windows_startup = bool(args.startup and is_windows()) root = tk.Tk() - if args.startup: + if windows_startup: root.withdraw() app = ClientV2App(root, config) if app.is_bound_to_server: @@ -1597,12 +1721,12 @@ def run_gui(args): enable_windows_autostart() except OSError as error: app.status_var.set(f"Не удалось обновить автозагрузку: {error}") - tray_started = app.start_tray() - if args.startup and tray_started: + tray_started = app.start_tray() if is_windows() else False + if windows_startup and tray_started: app.hide_window() - elif not tray_started: + elif is_windows() and not tray_started: app.status_var.set("Трей недоступен: установите pystray и Pillow, затем пересоберите клиент") - if args.startup: + if windows_startup: app.show_window() root.mainloop() @@ -1617,7 +1741,7 @@ def run_once(args): payload.update( { "inventory_number": (args.inventory_number or "").strip(), - "device_type": args.device_type or DEVICE_TYPES[0], + "device_type": args.device_type or detect_device_type(payload.get("os_info", "")), "model": args.model or "", "serial_number": args.serial_number or "", "building": args.building or "",