diff --git a/mytoolkit/commands/git.py b/mytoolkit/commands/git.py index 613c3ba..248bc53 100644 --- a/mytoolkit/commands/git.py +++ b/mytoolkit/commands/git.py @@ -112,6 +112,24 @@ def _restore_baseline(proxy: str, port: int | None) -> str: return proxy +def _primary_lan_ip() -> str | None: + """Wi-Fi/Ethernet IPv4 (en0). Bridged Parallels guests use this, not *.*.*.2.""" + if sys.platform != "darwin": + return None + try: + out = subprocess.check_output( + ["ipconfig", "getifaddr", "en0"], + text=True, + stderr=subprocess.DEVNULL, + ) + except (OSError, subprocess.CalledProcessError): + return None + ip = out.strip() + if not ip or ip.startswith("127."): + return None + return ip + + def _parallels_host_ips() -> list[str]: """Mac IPs that Parallels guests use to reach this host (auto from ifconfig).""" if sys.platform != "darwin": @@ -130,6 +148,9 @@ def _parallels_host_ips() -> list[str]: ip = match.group(1) if ip not in ips: ips.append(ip) + lan = _primary_lan_ip() + if lan and lan not in ips: + ips.append(lan) return ips @@ -483,6 +504,20 @@ def _collect_forward_pids(port: int) -> list[int]: return pids +def _should_kill_proxy_process(state: dict[str, Any] | None, name: str) -> bool: + """Kill FastGitHub only if this CLI spawned it. + + Native `fastgithub start` / CI `ensure-fastgithub.sh` / a leftover instance + must keep running when `mytoolkit git proxy stop` only tears down socat + and git config. + """ + if not state or state.get("name") != name: + return False + if state.get("adopted"): + return False + return True + + def _start_daemon(name: str, *, set_git: bool) -> None: # Clear a proxy left pointing at a dead daemon by an earlier aborted run. _restore_stale_git_proxy() @@ -498,6 +533,7 @@ def _start_daemon(name: str, *, set_git: bool) -> None: old_https = _restore_baseline(old_https, port) proxy_pid: int | None = None forward_pids: list[int] = [] + adopted = False if port is None: click.secho( @@ -526,10 +562,12 @@ def _start_daemon(name: str, *, set_git: bool) -> None: pids = _procs_matching(_pids_listening_on("127.0.0.1", port), name) if pids: click.secho( - f"{name} already listening on 127.0.0.1:{port} (pid={pids[0]})", + f"{name} already listening on 127.0.0.1:{port} (pid={pids[0]}) " + "— adopting, stop will not kill it", fg="cyan", ) proxy_pid = pids[0] + adopted = True else: click.secho( f"WARN: 127.0.0.1:{port} is held by an unknown process — " @@ -585,6 +623,7 @@ def _start_daemon(name: str, *, set_git: bool) -> None: "port": port, "proxy_pid": proxy_pid, "forward_pids": forward_pids, + "adopted": adopted, "set_git": set_git, "old_http": old_http if set_git else None, "old_https": old_https if set_git else None, @@ -623,8 +662,21 @@ def _stop_daemon(name: str | None) -> None: click.secho(f"{name} socat: nothing listening on guest IPs", fg="yellow") proxy_pids = _procs_matching(_pids_listening_on("127.0.0.1", port), name) - if proxy_pids: - _kill_pids(proxy_pids, name) + if _should_kill_proxy_process(state if state_matches else None, name): + if proxy_pids: + _kill_pids(proxy_pids, name) + else: + click.secho( + f"{name} is not listening on 127.0.0.1:{port} " + "(or port is held by an unrelated process — left untouched)", + fg="yellow", + ) + elif proxy_pids: + click.secho( + f"Leaving {name} pid={proxy_pids[0]} running " + "(started outside mytoolkit; stop only tears down socat/git)", + fg="cyan", + ) else: click.secho( f"{name} is not listening on 127.0.0.1:{port} " @@ -930,6 +982,7 @@ def proxy_run(name: str, command: tuple[str, ...]) -> None: (_pids_listening_on("127.0.0.1", port) or [None])[0] ), "forward_pids": [p.pid for p in forward_procs if p.pid], + "adopted": proxy_proc is None, "set_git": True, "old_http": old_http, "old_https": old_https, diff --git a/tests/test_git_proxy.py b/tests/test_git_proxy.py index 1a22f24..b3eff65 100644 --- a/tests/test_git_proxy.py +++ b/tests/test_git_proxy.py @@ -85,3 +85,31 @@ def test_restore_stale_git_proxy_noop_when_alive(monkeypatch): ) assert _restore_stale_git_proxy() is False assert restored == {} + + +def test_parallels_host_ips_includes_lan(monkeypatch): + from mytoolkit.commands.git import _parallels_host_ips + + monkeypatch.setattr("mytoolkit.commands.git.sys.platform", "darwin") + monkeypatch.delenv("XIAOHE_CI_PARALLELS_HOST_IP", raising=False) + monkeypatch.setattr( + "mytoolkit.commands.git.subprocess.check_output", + lambda *a, **k: "inet 10.211.55.2 netmask\n", + ) + monkeypatch.setattr("mytoolkit.commands.git._primary_lan_ip", lambda: "192.168.1.168") + assert _parallels_host_ips() == ["10.211.55.2", "192.168.1.168"] + + +def test_should_kill_proxy_only_if_we_spawned(): + from mytoolkit.commands.git import _should_kill_proxy_process + + assert _should_kill_proxy_process(None, "fastgithub") is False + assert _should_kill_proxy_process({"name": "other"}, "fastgithub") is False + assert _should_kill_proxy_process( + {"name": "fastgithub", "adopted": True}, "fastgithub" + ) is False + assert _should_kill_proxy_process( + {"name": "fastgithub", "adopted": False}, "fastgithub" + ) is True + # Old state files without adopted: keep previous stop-kills-daemon behavior + assert _should_kill_proxy_process({"name": "fastgithub"}, "fastgithub") is True