# -*- coding: utf-8 -*- """单实例控制。 窗口关掉后程序仍在后台跑,用户再次双击 EXE 时应该能**找回窗口**, 而不是又起一个进程(那样端口会冲突,用户会以为坏了)。 做法:主实例占住一个本机回环端口;后启动的实例连上去发个 "show" 就退出。 """ from __future__ import annotations import socket import threading CONTROL_PORT = 8767 _CMD_SHOW = b"SHOW" class InstanceLock: def __init__(self, port: int = CONTROL_PORT): self.port = port self._sock: socket.socket | None = None self._thread: threading.Thread | None = None self._running = False self.on_show = None # 收到"显示窗口"请求时的回调 @property def is_primary(self) -> bool: return self._sock is not None def try_claim(self) -> bool: """尝试成为主实例。失败说明已有实例在运行。""" try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 关键:Windows 上 SO_REUSEADDR 会允许第二个进程绑同一个端口, # 导致单实例判定失效。Windows 必须用 SO_EXCLUSIVEADDRUSE。 if hasattr(socket, "SO_EXCLUSIVEADDRUSE"): try: sock.setsockopt(socket.SOL_SOCKET, socket.SO_EXCLUSIVEADDRUSE, 1) except OSError: pass else: sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind(("127.0.0.1", self.port)) sock.listen(4) sock.settimeout(1.0) except OSError: try: sock.close() except Exception: pass return False self._sock = sock self._running = True self._thread = threading.Thread(target=self._loop, name="instance", daemon=True) self._thread.start() return True def _loop(self) -> None: while self._running: try: conn, _addr = self._sock.accept() except socket.timeout: continue except OSError: break try: conn.settimeout(2.0) data = conn.recv(32) if data.strip() == _CMD_SHOW and self.on_show: try: self.on_show() except Exception: pass conn.sendall(b"OK") except OSError: pass finally: try: conn.close() except OSError: pass def release(self) -> None: self._running = False if self._thread is not None and self._thread.is_alive(): self._thread.join(timeout=2.0) self._thread = None if self._sock is not None: try: self._sock.close() except OSError: pass self._sock = None def signal_show(port: int = CONTROL_PORT, timeout: float = 2.0) -> bool: """通知已在运行的实例把窗口显示出来。""" try: sock = socket.create_connection(("127.0.0.1", port), timeout=timeout) sock.sendall(_CMD_SHOW) try: sock.recv(16) except OSError: pass sock.close() return True except OSError: return False