Skip to content

Commit acd895b

Browse files
committed
fix(realtime): 修复前后端分离运行时实时模式校验失败
前后端分离运行时补齐 WCDB sidecar 自动启动链路 在缺少 sidecar 环境变量时自动探测并拉起本地 Electron sidecar 初始化失败时回退到进程内 WCDB 路径,避免直接中断实时模式 服务关闭时回收自动启动的 sidecar 进程,减少残留进程
1 parent ddc5c4a commit acd895b

1 file changed

Lines changed: 231 additions & 23 deletions

File tree

src/wechat_decrypt_tool/wcdb_realtime.py

Lines changed: 231 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import json
55
import os
66
import re
7+
import socket
8+
import subprocess
79
import sys
810
import threading
911
import time
@@ -128,6 +130,10 @@ def _resolve_wcdb_api_dll_path() -> Path:
128130
_preloaded_native_libs: list[ctypes.CDLL] = []
129131
_protection_checked = False
130132
_protection_result: Optional[tuple[int, str]] = None
133+
_AUTO_SIDECAR_LOCK = threading.Lock()
134+
_AUTO_SIDECAR_PROC: Optional[subprocess.Popen] = None
135+
_AUTO_SIDECAR_URL = ""
136+
_AUTO_SIDECAR_TOKEN = ""
131137

132138

133139
def _is_windows() -> bool:
@@ -238,6 +244,197 @@ def _sidecar_enabled() -> bool:
238244
return bool(_sidecar_url())
239245

240246

247+
def _repo_root() -> Path:
248+
return Path(__file__).resolve().parents[2]
249+
250+
251+
def _source_sidecar_assets() -> tuple[Path, Path, Path] | None:
252+
if getattr(sys, "frozen", False):
253+
return None
254+
255+
repo_root = _repo_root()
256+
electron_exe = repo_root / "desktop" / "node_modules" / "electron" / "dist" / "electron.exe"
257+
sidecar_script = repo_root / "desktop" / "src" / "wcdb-sidecar.cjs"
258+
koffi_dir = repo_root / "desktop" / "vendor" / "koffi"
259+
260+
try:
261+
if electron_exe.is_file() and sidecar_script.is_file() and koffi_dir.exists():
262+
return electron_exe, sidecar_script, koffi_dir
263+
except Exception:
264+
return None
265+
return None
266+
267+
268+
def _auto_sidecar_started_here() -> bool:
269+
with _AUTO_SIDECAR_LOCK:
270+
return bool(_AUTO_SIDECAR_URL and _AUTO_SIDECAR_TOKEN)
271+
272+
273+
def _parse_port(value: object) -> Optional[int]:
274+
try:
275+
raw = str(value or "").strip()
276+
if not raw:
277+
return None
278+
port = int(raw, 10)
279+
except Exception:
280+
return None
281+
if 1 <= port <= 65535:
282+
return port
283+
return None
284+
285+
286+
def _pick_free_port() -> int:
287+
requested = _parse_port(os.environ.get("WECHAT_TOOL_WCDB_SIDECAR_PORT"))
288+
if requested is not None:
289+
return requested
290+
291+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
292+
sock.bind(("127.0.0.1", 0))
293+
sock.listen(1)
294+
return int(sock.getsockname()[1])
295+
296+
297+
def _build_auto_sidecar_resource_paths(wcdb_api_dll: Path) -> list[str]:
298+
items: list[str] = []
299+
seen: set[str] = set()
300+
301+
def add(path: str | Path | None) -> None:
302+
if not path:
303+
return
304+
try:
305+
resolved = Path(path).resolve()
306+
except Exception:
307+
resolved = Path(path)
308+
key = str(resolved).replace("/", "\\").rstrip("\\").lower()
309+
if not key or key in seen:
310+
return
311+
seen.add(key)
312+
items.append(str(resolved))
313+
314+
repo_root = _repo_root()
315+
dll_dir = wcdb_api_dll.parent
316+
add(dll_dir)
317+
add(dll_dir.parent)
318+
add(repo_root)
319+
add(repo_root / "resources")
320+
321+
data_dir = str(os.environ.get("WECHAT_TOOL_DATA_DIR", "") or "").strip()
322+
if data_dir:
323+
add(data_dir)
324+
add(Path(data_dir) / "resources")
325+
else:
326+
add(Path.cwd())
327+
add(Path.cwd() / "resources")
328+
329+
return items
330+
331+
332+
def _stop_auto_sidecar() -> None:
333+
global _AUTO_SIDECAR_PROC, _AUTO_SIDECAR_URL, _AUTO_SIDECAR_TOKEN
334+
335+
with _AUTO_SIDECAR_LOCK:
336+
proc = _AUTO_SIDECAR_PROC
337+
owned_url = _AUTO_SIDECAR_URL
338+
owned_token = _AUTO_SIDECAR_TOKEN
339+
_AUTO_SIDECAR_PROC = None
340+
_AUTO_SIDECAR_URL = ""
341+
_AUTO_SIDECAR_TOKEN = ""
342+
343+
if owned_url and os.environ.get("WECHAT_TOOL_WCDB_SIDECAR_URL") == owned_url:
344+
os.environ.pop("WECHAT_TOOL_WCDB_SIDECAR_URL", None)
345+
if owned_token and os.environ.get("WECHAT_TOOL_WCDB_SIDECAR_TOKEN") == owned_token:
346+
os.environ.pop("WECHAT_TOOL_WCDB_SIDECAR_TOKEN", None)
347+
348+
if proc is None:
349+
return
350+
351+
try:
352+
if proc.poll() is None:
353+
proc.terminate()
354+
try:
355+
proc.wait(timeout=5.0)
356+
except Exception:
357+
proc.kill()
358+
except Exception:
359+
pass
360+
361+
362+
def _maybe_start_auto_sidecar() -> bool:
363+
global _AUTO_SIDECAR_PROC, _AUTO_SIDECAR_URL, _AUTO_SIDECAR_TOKEN
364+
365+
if _sidecar_enabled() or not _is_windows():
366+
return False
367+
368+
assets = _source_sidecar_assets()
369+
if not assets:
370+
return False
371+
372+
wcdb_api_dll = _resolve_wcdb_api_dll_path()
373+
try:
374+
if not wcdb_api_dll.exists():
375+
return False
376+
except Exception:
377+
return False
378+
379+
electron_exe, sidecar_script, koffi_dir = assets
380+
repo_root = _repo_root()
381+
382+
with _AUTO_SIDECAR_LOCK:
383+
proc = _AUTO_SIDECAR_PROC
384+
if proc is not None and proc.poll() is None and _AUTO_SIDECAR_URL and _AUTO_SIDECAR_TOKEN:
385+
os.environ["WECHAT_TOOL_WCDB_SIDECAR_URL"] = _AUTO_SIDECAR_URL
386+
os.environ["WECHAT_TOOL_WCDB_SIDECAR_TOKEN"] = _AUTO_SIDECAR_TOKEN
387+
return True
388+
389+
if proc is not None and proc.poll() is not None:
390+
_AUTO_SIDECAR_PROC = None
391+
_AUTO_SIDECAR_URL = ""
392+
_AUTO_SIDECAR_TOKEN = ""
393+
394+
port = _pick_free_port()
395+
token = os.urandom(24).hex()
396+
url = f"http://127.0.0.1:{port}"
397+
env = os.environ.copy()
398+
env.update(
399+
{
400+
"ELECTRON_RUN_AS_NODE": "1",
401+
"WECHAT_TOOL_WCDB_SIDECAR_HOST": "127.0.0.1",
402+
"WECHAT_TOOL_WCDB_SIDECAR_PORT": str(port),
403+
"WECHAT_TOOL_WCDB_SIDECAR_TOKEN": token,
404+
"WECHAT_TOOL_WCDB_API_DLL_PATH": str(wcdb_api_dll),
405+
"WECHAT_TOOL_WCDB_DLL_DIR": str(wcdb_api_dll.parent),
406+
"WECHAT_TOOL_WCDB_RESOURCE_PATHS": json.dumps(
407+
_build_auto_sidecar_resource_paths(wcdb_api_dll), ensure_ascii=False
408+
),
409+
"WECHAT_TOOL_KOFFI_DIR": str(koffi_dir),
410+
}
411+
)
412+
413+
creationflags = int(getattr(subprocess, "CREATE_NO_WINDOW", 0) or 0)
414+
try:
415+
proc = subprocess.Popen(
416+
[str(electron_exe), str(sidecar_script)],
417+
cwd=str(repo_root),
418+
env=env,
419+
stdin=subprocess.DEVNULL,
420+
stdout=subprocess.DEVNULL,
421+
stderr=subprocess.DEVNULL,
422+
creationflags=creationflags,
423+
)
424+
except Exception as exc:
425+
logger.warning("[wcdb] auto sidecar start failed: %s", exc)
426+
return False
427+
428+
_AUTO_SIDECAR_PROC = proc
429+
_AUTO_SIDECAR_URL = url
430+
_AUTO_SIDECAR_TOKEN = token
431+
os.environ["WECHAT_TOOL_WCDB_SIDECAR_URL"] = url
432+
os.environ["WECHAT_TOOL_WCDB_SIDECAR_TOKEN"] = token
433+
434+
logger.info("[wcdb] auto-started electron sidecar url=%s dll=%s", _AUTO_SIDECAR_URL, wcdb_api_dll)
435+
return True
436+
437+
241438
def _sidecar_call(action: str, payload: Optional[dict[str, Any]] = None, *, timeout: float = 30.0) -> dict[str, Any]:
242439
base_url = _sidecar_url()
243440
if not base_url:
@@ -476,30 +673,37 @@ def _load_wcdb_lib() -> ctypes.CDLL:
476673

477674
def _ensure_initialized() -> None:
478675
global _initialized, _loaded_wcdb_api_dll, _protection_result
676+
_maybe_start_auto_sidecar()
479677
if _sidecar_enabled():
480678
with _lib_lock:
481679
if _initialized:
482680
return
483-
result = _sidecar_call("init", timeout=30.0)
484-
dll_path = str(result.get("dllPath") or "").strip()
485-
if dll_path:
486-
try:
487-
_loaded_wcdb_api_dll = Path(dll_path)
488-
except Exception:
489-
pass
490-
protection = result.get("protection")
491-
if isinstance(protection, list):
492-
for item in protection:
493-
if isinstance(item, dict) and "rc" in item:
494-
try:
495-
_protection_result = (int(item.get("rc")), str(item.get("path") or ""))
496-
if int(item.get("rc")) == 0:
497-
break
498-
except Exception:
499-
continue
500-
with _lib_lock:
501-
_initialized = True
502-
return
681+
try:
682+
result = _sidecar_call("init", timeout=30.0)
683+
dll_path = str(result.get("dllPath") or "").strip()
684+
if dll_path:
685+
try:
686+
_loaded_wcdb_api_dll = Path(dll_path)
687+
except Exception:
688+
pass
689+
protection = result.get("protection")
690+
if isinstance(protection, list):
691+
for item in protection:
692+
if isinstance(item, dict) and "rc" in item:
693+
try:
694+
_protection_result = (int(item.get("rc")), str(item.get("path") or ""))
695+
if int(item.get("rc")) == 0:
696+
break
697+
except Exception:
698+
continue
699+
with _lib_lock:
700+
_initialized = True
701+
return
702+
except Exception:
703+
if not _auto_sidecar_started_here():
704+
raise
705+
logger.warning("[wcdb] auto sidecar init failed, fallback to in-process wcdb")
706+
_stop_auto_sidecar()
503707

504708
lib = _load_wcdb_lib()
505709
with _lib_lock:
@@ -1188,13 +1392,15 @@ def shutdown() -> None:
11881392
global _initialized
11891393
if _sidecar_enabled():
11901394
with _lib_lock:
1191-
if not _initialized:
1192-
return
1395+
should_shutdown = bool(_initialized)
11931396
try:
1194-
_sidecar_call("shutdown", timeout=5.0)
1397+
if should_shutdown:
1398+
_sidecar_call("shutdown", timeout=5.0)
11951399
finally:
11961400
with _lib_lock:
11971401
_initialized = False
1402+
if _auto_sidecar_started_here():
1403+
_stop_auto_sidecar()
11981404
return
11991405

12001406
lib = _load_wcdb_lib()
@@ -1205,6 +1411,8 @@ def shutdown() -> None:
12051411
lib.wcdb_shutdown()
12061412
finally:
12071413
_initialized = False
1414+
if _auto_sidecar_started_here():
1415+
_stop_auto_sidecar()
12081416

12091417

12101418
def _resolve_session_db_path(db_storage_dir: Path) -> Path:

0 commit comments

Comments
 (0)