|
|
|
@ -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 |