diff --git a/client-file-collector/client_file_collector.py b/client-file-collector/client_file_collector.py index 9115bcb..a837370 100644 --- a/client-file-collector/client_file_collector.py +++ b/client-file-collector/client_file_collector.py @@ -4,6 +4,7 @@ import json import os import queue import shutil +import socket import sys import threading import time @@ -16,6 +17,39 @@ import requests from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry +CONTROL_HOST = "127.0.0.1" +CONTROL_PORT = 52347 +SINGLE_INSTANCE_MUTEX_NAME = "Global\\QMSFileCollector_SingleInstance" +ERROR_ALREADY_EXISTS = 183 + + +def acquire_single_instance_lock(): + if os.name != "nt": + return None, True + try: + import ctypes + kernel32 = ctypes.windll.kernel32 + handle = kernel32.CreateMutexW(None, False, SINGLE_INSTANCE_MUTEX_NAME) + if not handle: + return None, True + if kernel32.GetLastError() == ERROR_ALREADY_EXISTS: + kernel32.CloseHandle(handle) + return None, False + return handle, True + except Exception: + return None, True + + +def notify_existing_instance_show(retries=8, sleep_seconds=0.25): + for _ in range(max(1, retries)): + try: + with socket.create_connection((CONTROL_HOST, CONTROL_PORT), timeout=1.0) as conn: + conn.sendall(b"SHOW") + return True + except Exception: + time.sleep(sleep_seconds) + return False + def get_runtime_dir(): if getattr(sys, "frozen", False): @@ -51,16 +85,7 @@ def _ensure_windows_run_registry(entry_name, exe_path): return False, "写入系统自启动项失败: %s" % e -def _cleanup_legacy_startup_items(legacy_entry_name, app_data): - if app_data: - startup_dir = Path(app_data) / "Microsoft" / "Windows" / "Start Menu" / "Programs" / "Startup" - legacy_script = startup_dir / (legacy_entry_name + ".vbs") - try: - if legacy_script.exists(): - legacy_script.unlink() - except Exception: - pass - +def _remove_windows_run_entry(entry_name): try: import winreg run_key_path = r"Software\Microsoft\Windows\CurrentVersion\Run" @@ -71,13 +96,26 @@ def _cleanup_legacy_startup_items(legacy_entry_name, app_data): winreg.KEY_SET_VALUE ) as key: try: - winreg.DeleteValue(key, legacy_entry_name) + winreg.DeleteValue(key, entry_name) except FileNotFoundError: pass except Exception: pass +def _cleanup_legacy_startup_items(legacy_entry_name, app_data): + if app_data: + startup_dir = Path(app_data) / "Microsoft" / "Windows" / "Start Menu" / "Programs" / "Startup" + legacy_script = startup_dir / (legacy_entry_name + ".vbs") + try: + if legacy_script.exists(): + legacy_script.unlink() + except Exception: + pass + + _remove_windows_run_entry(legacy_entry_name) + + def ensure_windows_startup(): if os.name != "nt": return False, "当前系统非Windows,跳过自启动注册。" @@ -95,7 +133,7 @@ def ensure_windows_startup(): startup_script = startup_dir / (entry_name + ".vbs") script_content = ( "Set WshShell = CreateObject(\"WScript.Shell\")\n" - "WshShell.Run Chr(34) & \"%s\" & Chr(34), 0\n" + "WshShell.Run Chr(34) & \"%s\" & Chr(34), 7\n" "Set WshShell = Nothing\n" ) % exe_path.replace("\"", "\"\"") @@ -109,10 +147,16 @@ def ensure_windows_startup(): return False, "启动文件夹自启动项已存在,无需重复写入。" startup_script.write_text(script_content, encoding="utf-8") + _remove_windows_run_entry(entry_name) if old_content: return True, "检测到启动文件夹自启动路径变化,已自动更新。" return True, "首次启动已写入启动文件夹自启动项,下次开机会自动运行。" except Exception as startup_error: + try: + if startup_script.exists(): + startup_script.unlink() + except Exception: + pass try: changed, reg_msg = _ensure_windows_run_registry(entry_name, exe_path) return changed, "写入启动文件夹失败,已回退注册表方式: %s" % reg_msg @@ -142,6 +186,7 @@ class CollectorApp: self.notice_ts = {} self.processing_lock = threading.Lock() self.processing_files = set() + self.control_stop_event = threading.Event() self.http_session = self._create_http_session() self.base_url_var = StringVar() @@ -159,11 +204,66 @@ class CollectorApp: self._build_ui() self.root.protocol("WM_DELETE_WINDOW", self._on_window_close) + self._start_control_server() self._init_windows_startup() self._load_config() self.root.after(200, self._flush_logs) self.root.after(3000, self._watch_worker) + def _start_control_server(self): + self.control_thread = threading.Thread(target=self._control_server_loop, daemon=True) + self.control_thread.start() + + def _control_server_loop(self): + server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + try: + server.bind((CONTROL_HOST, CONTROL_PORT)) + server.listen(1) + server.settimeout(1.0) + except Exception: + try: + server.close() + except Exception: + pass + return + + while not self.control_stop_event.is_set(): + try: + conn, _ = server.accept() + except socket.timeout: + continue + except Exception: + break + + try: + payload = conn.recv(16) + if b"SHOW" in payload: + self.root.after(0, self._show_main_window) + except Exception: + pass + finally: + try: + conn.close() + except Exception: + pass + + try: + server.close() + except Exception: + pass + + def _show_main_window(self): + try: + self.root.deiconify() + self.root.state("normal") + self.root.lift() + self.root.attributes("-topmost", True) + self.root.after(300, lambda: self.root.attributes("-topmost", False)) + self.root.focus_force() + except Exception: + pass + def _create_http_session(self): retry = Retry( total=3, @@ -449,6 +549,7 @@ class CollectorApp: def exit_app(self): self.running = False self.stop_event.set() + self.control_stop_event.set() try: self.http_session.close() except Exception: @@ -689,6 +790,18 @@ class CollectorApp: if __name__ == "__main__": + instance_lock_handle, is_primary = acquire_single_instance_lock() + if not is_primary: + notify_existing_instance_show() + sys.exit(0) + app_root = Tk() app = CollectorApp(app_root) app_root.mainloop() + + if instance_lock_handle and os.name == "nt": + try: + import ctypes + ctypes.windll.kernel32.CloseHandle(instance_lock_handle) + except Exception: + pass diff --git a/client-file-collector/dist/QMSFileCollector.exe b/client-file-collector/dist/QMSFileCollector.exe index 15d060e..95ac308 100644 Binary files a/client-file-collector/dist/QMSFileCollector.exe and b/client-file-collector/dist/QMSFileCollector.exe differ