juc500 정상동작

This commit is contained in:
Macbook
2026-07-21 21:04:25 +09:00
parent a423db0556
commit bb9874b931
2917 changed files with 708025 additions and 668 deletions
@@ -0,0 +1,382 @@
"""Detect and terminate official j5create / OTi Wormhole processes.
The JUC500 virtual CD (WORMHOLE) auto-runs Wormhole.exe / WormholeShell which
exclusively claims USB Interface 5. Our stack needs that interface free.
"""
from __future__ import annotations
import os
import signal
import subprocess
import sys
import time
from dataclasses import asdict, dataclass
from typing import Any
_IS_WIN = sys.platform == "win32"
# Exact / substring match against process image name (case-insensitive)
PROCESS_NAMES = (
"wormhole.exe",
"wormhold.exe", # typo safety
"wormholeld.exe",
"wormholeshell",
"wormhole",
"u3wormhole.exe",
"u3wormholeshell",
"u3wormhole",
"gobridge.exe",
"gobridgedemon.exe",
"gobridgedemon",
"gobridgedemonu3.exe",
"gobridgedemonu3",
"downloadupdatetool.exe",
"downloadupdatetool",
"smartdatalink.exe",
"smartlink.exe",
"oti_kmloader.exe",
"otigoBridge.exe",
"gosuite.exe",
)
# Broader name hints (substring in process name)
NAME_HINTS = (
"wormhole",
"gobridge",
"kaijet",
"j5create",
"smartdatalink",
"smart data link",
"oti_",
"otisync",
)
# Do not kill ourselves
SELF_HINTS = ("juc500", "juc500_xfer", "juc500-portable")
# Other copies of this app also hold Interface 5 exclusively
# Keep markers specific — do not match scripts that merely import juc500_xfer.
SELF_PROCESS_MARKERS = (
"juc500_app.py",
"juc500-portable.exe",
"juc500_portable.exe",
"juc500-portable",
)
@dataclass
class ProcInfo:
pid: int
name: str
path: str = ""
def to_dict(self) -> dict[str, Any]:
return asdict(self)
def _is_self(name: str, path: str = "") -> bool:
blob = f"{name} {path}".lower()
return any(s in blob for s in SELF_HINTS)
def _matches_wormhole(name: str, path: str = "") -> bool:
n = (name or "").lower().strip()
p = (path or "").lower()
if not n:
return False
if _is_self(n, p):
return False
base = n if n.endswith(".exe") else n
# exact-ish
for cand in PROCESS_NAMES:
c = cand.lower()
if base == c or base == c.replace(".exe", "") or n == c:
return True
# path / hint
blob = f"{n} {p}"
return any(h in blob for h in NAME_HINTS)
def list_wormhole_processes() -> list[ProcInfo]:
if _IS_WIN:
return _list_windows()
return _list_posix()
def _list_windows() -> list[ProcInfo]:
# CSV: "name","pid","session","mem"
try:
proc = subprocess.run(
["tasklist", "/FO", "CSV", "/NH"],
capture_output=True,
text=True,
timeout=20,
check=False,
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
)
except Exception:
return []
out: list[ProcInfo] = []
for line in (proc.stdout or "").splitlines():
line = line.strip()
if not line:
continue
# "Wormhole.exe","1234","Console","1","12,345 K"
parts = [x.strip().strip('"') for x in line.split('","')]
if len(parts) < 2:
# fallback split
parts = [x.strip().strip('"') for x in line.split(",")]
if len(parts) < 2:
continue
name, pid_s = parts[0], parts[1]
try:
pid = int(pid_s)
except ValueError:
continue
if _matches_wormhole(name):
out.append(ProcInfo(pid=pid, name=name))
# Enrich path via WMIC / PowerShell (optional)
return _dedupe(out)
def _list_posix() -> list[ProcInfo]:
try:
proc = subprocess.run(
["ps", "-ax", "-o", "pid=,comm=,command="],
capture_output=True,
text=True,
timeout=15,
check=False,
)
except Exception:
return []
out: list[ProcInfo] = []
my_pid = os.getpid()
for line in (proc.stdout or "").splitlines():
line = line.strip()
if not line:
continue
try:
pid_s, rest = line.split(None, 1)
pid = int(pid_s)
except ValueError:
continue
if pid == my_pid:
continue
# comm is first token of rest sometimes; keep full command
tokens = rest.split(None, 1)
comm = tokens[0]
cmd = tokens[1] if len(tokens) > 1 else ""
if _matches_wormhole(comm, cmd) or _matches_wormhole(os.path.basename(comm), cmd):
out.append(ProcInfo(pid=pid, name=os.path.basename(comm) or comm, path=cmd[:240]))
return _dedupe(out)
def _dedupe(items: list[ProcInfo]) -> list[ProcInfo]:
seen: set[int] = set()
out: list[ProcInfo] = []
for p in items:
if p.pid in seen:
continue
seen.add(p.pid)
out.append(p)
return out
def kill_wormhole_processes(*, force: bool = True) -> dict[str, Any]:
"""Terminate official Wormhole processes. Returns status dict."""
before = list_wormhole_processes()
killed: list[dict[str, Any]] = []
errors: list[str] = []
for info in before:
try:
_kill_one(info.pid, force=force)
killed.append(info.to_dict())
except Exception as exc: # noqa: BLE001
errors.append(f"{info.name}({info.pid}): {exc}")
# brief settle so USB handles are released
if killed:
time.sleep(0.8)
after = list_wormhole_processes()
# second pass for stubborn ones
if after and force:
for info in after:
try:
_kill_one(info.pid, force=True)
if info.to_dict() not in killed:
killed.append(info.to_dict())
except Exception as exc: # noqa: BLE001
errors.append(f"retry {info.name}({info.pid}): {exc}")
time.sleep(0.5)
after = list_wormhole_processes()
return {
"ok": len(after) == 0,
"found": [p.to_dict() for p in before],
"killed": killed,
"remaining": [p.to_dict() for p in after],
"errors": errors,
"message": _summary(before, killed, after),
}
def _summary(before: list[ProcInfo], killed: list[dict], after: list[ProcInfo]) -> str:
if not before:
return "실행 중인 Wormhole 관련 프로세스가 없습니다."
if not after:
names = ", ".join(sorted({k.get("name", "?") for k in killed})) or "프로세스"
return f"Wormhole 종료 완료: {names}"
rem = ", ".join(f"{p.name}({p.pid})" for p in after)
return f"일부 종료되지 않음: {rem} (관리자 권한으로 다시 시도하거나 작업 관리자에서 종료)"
def list_other_juc500_processes() -> list[ProcInfo]:
"""Other juc500 GUI/CLI instances (excludes this PID). They hold IF5."""
my_pid = os.getpid()
out: list[ProcInfo] = []
if _IS_WIN:
try:
ps = (
"Get-CimInstance Win32_Process | "
"Where-Object { $_.CommandLine -match 'juc500_app\\.py|juc500-portable|juc500_portable|-m juc500_xfer' } | "
"Select-Object ProcessId, Name, CommandLine | ConvertTo-Json -Compress"
)
proc = subprocess.run(
["powershell", "-NoProfile", "-Command", ps],
capture_output=True,
text=True,
timeout=15,
check=False,
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
)
raw = (proc.stdout or "").strip()
if not raw:
return []
import json
data = json.loads(raw)
if isinstance(data, dict):
data = [data]
for d in data:
pid = int(d.get("ProcessId") or 0)
if not pid or pid == my_pid:
continue
cmd = str(d.get("CommandLine") or "")
name = str(d.get("Name") or "python.exe")
if any(m in cmd.lower() for m in SELF_PROCESS_MARKERS):
out.append(ProcInfo(pid=pid, name=name, path=cmd[:240]))
except Exception:
return []
return _dedupe(out)
# POSIX: scan ps
try:
proc = subprocess.run(
["ps", "-ax", "-o", "pid=,command="],
capture_output=True,
text=True,
timeout=15,
check=False,
)
except Exception:
return []
for line in (proc.stdout or "").splitlines():
line = line.strip()
if not line:
continue
try:
pid_s, cmd = line.split(None, 1)
pid = int(pid_s)
except ValueError:
continue
if pid == my_pid:
continue
low = cmd.lower()
if any(m in low for m in SELF_PROCESS_MARKERS):
out.append(ProcInfo(pid=pid, name=os.path.basename(cmd.split()[0]), path=cmd[:240]))
return _dedupe(out)
def kill_other_juc500_processes(*, force: bool = True) -> dict[str, Any]:
"""Terminate other juc500 instances so Interface 5 can be claimed."""
before = list_other_juc500_processes()
killed: list[dict[str, Any]] = []
errors: list[str] = []
for info in before:
try:
_kill_one(info.pid, force=force)
killed.append(info.to_dict())
except Exception as exc: # noqa: BLE001
errors.append(f"{info.name}({info.pid}): {exc}")
if killed:
time.sleep(0.6)
after = list_other_juc500_processes()
return {
"ok": len(after) == 0,
"found": [p.to_dict() for p in before],
"killed": killed,
"remaining": [p.to_dict() for p in after],
"errors": errors,
"message": (
"다른 JUC500 인스턴스가 없습니다."
if not before
else (
f"다른 JUC500 종료: {', '.join(str(k.get('pid')) for k in killed)}"
if not after
else f"일부 JUC500이 남음: {[p.pid for p in after]}"
)
),
}
def _kill_one(pid: int, *, force: bool) -> None:
if _IS_WIN:
args = ["taskkill", "/PID", str(pid)]
if force:
args.append("/F")
proc = subprocess.run(
args,
capture_output=True,
text=True,
timeout=15,
check=False,
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
)
if proc.returncode != 0:
err = (proc.stderr or proc.stdout or "taskkill failed").strip()
# 128 = not found — treat as OK
if proc.returncode == 128 or "not found" in err.lower():
return
raise RuntimeError(err)
return
sig = signal.SIGKILL if force else signal.SIGTERM
try:
os.kill(pid, sig)
except ProcessLookupError:
return
if not force:
time.sleep(0.4)
try:
os.kill(pid, 0)
except ProcessLookupError:
return
os.kill(pid, signal.SIGKILL)
def wormhole_status() -> dict[str, Any]:
procs = list_wormhole_processes()
return {
"running": len(procs) > 0,
"count": len(procs),
"processes": [p.to_dict() for p in procs],
"message": (
f"Wormhole 관련 프로세스 {len(procs)}개 실행 중 (Interface 5 점유 → 종료 권장)"
if procs
else "Wormhole 프로세스 없음"
),
}