44import json
55import os
66import re
7+ import socket
8+ import subprocess
79import sys
810import threading
911import 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
133139def _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+
241438def _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
477674def _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
12101418def _resolve_session_db_path (db_storage_dir : Path ) -> Path :
0 commit comments