fix(git proxy): start/stop 只认领匹配进程(防误杀同端口其它进程) + status/start 自动恢复指向死端口的 stale git proxy
This commit is contained in:
@@ -71,6 +71,18 @@ def _set_git_proxies(proxy_url: str) -> None:
|
||||
)
|
||||
|
||||
|
||||
def _restore_proxy_key(key: str, old: object) -> None:
|
||||
"""Restore a git proxy key to its pre-daemon value (or unset it)."""
|
||||
if old in (None, "(not set)"):
|
||||
_unset_git_proxy(key)
|
||||
else:
|
||||
subprocess.run(
|
||||
["git", "config", "--global", key, str(old)],
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
|
||||
def _proxy_loopback_port(proxy_url: str) -> int | None:
|
||||
"""Return listen port if proxy_url is a loopback HTTP proxy."""
|
||||
try:
|
||||
@@ -108,6 +120,27 @@ def _parallels_host_ips() -> list[str]:
|
||||
return ips
|
||||
|
||||
|
||||
def _procs_matching(pids: list[int], name: str) -> list[int]:
|
||||
"""Filter PIDs whose command line mentions the proxy name.
|
||||
|
||||
Guards against adopting/killing an unrelated process that happens to bind
|
||||
the same loopback port (e.g. a leftover socat or another proxy tool).
|
||||
"""
|
||||
matched: list[int] = []
|
||||
for pid in pids:
|
||||
try:
|
||||
out = subprocess.check_output(
|
||||
["ps", "-p", str(pid), "-o", "command="],
|
||||
text=True,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
except (OSError, subprocess.CalledProcessError):
|
||||
continue
|
||||
if name.lower() in out.strip().lower():
|
||||
matched.append(pid)
|
||||
return matched
|
||||
|
||||
|
||||
def _port_listening_on(host: str, port: int) -> bool:
|
||||
return bool(_pids_listening_on(host, port))
|
||||
|
||||
@@ -318,6 +351,32 @@ def _clear_state() -> None:
|
||||
pass
|
||||
|
||||
|
||||
def _restore_stale_git_proxy() -> bool:
|
||||
"""Restore git proxy if it points at a dead loopback port.
|
||||
|
||||
A session that ran `start --set-git` / `run` and exited without `stop`
|
||||
leaves the global git proxy pointing at the daemon port; if the daemon then
|
||||
died, every git call fails behind a dead proxy. Restore the pre-proxy value
|
||||
recorded in the state file (or unset). Returns True if anything changed.
|
||||
"""
|
||||
state = _read_state()
|
||||
if not state:
|
||||
return False
|
||||
http = _get_git_proxy("http.proxy")
|
||||
if not http:
|
||||
return False
|
||||
port = _proxy_loopback_port(http)
|
||||
if port is None or _port_listening_on("127.0.0.1", port):
|
||||
return False
|
||||
_restore_proxy_key("http.proxy", state.get("old_http"))
|
||||
_restore_proxy_key("https.proxy", state.get("old_https"))
|
||||
click.secho(
|
||||
f"Restored stale git proxy (http.proxy pointed at dead {http})",
|
||||
fg="green",
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
def _require_proxy(name: str) -> tuple[str, str]:
|
||||
"""Return (name, url) or exit."""
|
||||
proxies = _get_proxies()
|
||||
@@ -411,6 +470,8 @@ def _collect_forward_pids(port: int) -> list[int]:
|
||||
|
||||
|
||||
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()
|
||||
_, proxy_url = _require_proxy(name)
|
||||
port = _proxy_loopback_port(proxy_url)
|
||||
|
||||
@@ -443,11 +504,19 @@ def _start_daemon(name: str, *, set_git: bool) -> None:
|
||||
return
|
||||
|
||||
if _port_listening_on("127.0.0.1", port):
|
||||
click.secho(
|
||||
f"{name} already listening on 127.0.0.1:{port}", fg="cyan"
|
||||
)
|
||||
pids = _pids_listening_on("127.0.0.1", port)
|
||||
proxy_pid = pids[0] if pids else 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]})",
|
||||
fg="cyan",
|
||||
)
|
||||
proxy_pid = pids[0]
|
||||
else:
|
||||
click.secho(
|
||||
f"WARN: 127.0.0.1:{port} is held by an unknown process — "
|
||||
f"not adopting it (stop will not touch it)",
|
||||
fg="yellow",
|
||||
)
|
||||
else:
|
||||
binary = shutil.which(name)
|
||||
if not binary:
|
||||
@@ -525,7 +594,7 @@ def _stop_daemon(name: str | None) -> None:
|
||||
|
||||
if port is not None:
|
||||
forward_pids = _collect_forward_pids(port)
|
||||
if state_matches:
|
||||
if state_matches and state is not None:
|
||||
for pid in state.get("forward_pids") or []:
|
||||
if isinstance(pid, int) and pid not in forward_pids:
|
||||
forward_pids.append(pid)
|
||||
@@ -534,17 +603,19 @@ def _stop_daemon(name: str | None) -> None:
|
||||
else:
|
||||
click.secho(f"{name} socat: nothing listening on guest IPs", fg="yellow")
|
||||
|
||||
proxy_pids = _pids_listening_on("127.0.0.1", port)
|
||||
proxy_pids = _procs_matching(_pids_listening_on("127.0.0.1", port), name)
|
||||
if proxy_pids:
|
||||
_kill_pids(proxy_pids, name)
|
||||
else:
|
||||
click.secho(
|
||||
f"{name} is not listening on 127.0.0.1:{port}", fg="yellow"
|
||||
f"{name} is not listening on 127.0.0.1:{port} "
|
||||
"(or port is held by an unrelated process — left untouched)",
|
||||
fg="yellow",
|
||||
)
|
||||
else:
|
||||
click.secho(f"{name}: no local loopback daemon to stop", fg="yellow")
|
||||
|
||||
if state_matches and state.get("set_git"):
|
||||
if state_matches and state is not None and state.get("set_git"):
|
||||
old_http = state.get("old_http")
|
||||
old_https = state.get("old_https")
|
||||
if old_http in (None, "(not set)"):
|
||||
@@ -672,6 +743,8 @@ def proxy_status(name: str | None) -> None:
|
||||
current = _current_daemon_name()
|
||||
state = _read_state()
|
||||
|
||||
_restore_stale_git_proxy()
|
||||
|
||||
click.secho("Git proxy daemons:", fg="cyan")
|
||||
if not proxies:
|
||||
click.echo(" (none configured in ~/.mytoolkit/config.json)")
|
||||
|
||||
@@ -11,3 +11,65 @@ def test_proxy_loopback_port_http():
|
||||
def test_proxy_loopback_port_rejects_remote():
|
||||
assert _proxy_loopback_port("http://10.211.55.2:38457") is None
|
||||
assert _proxy_loopback_port("socks5://127.0.0.1:1080") is None
|
||||
|
||||
|
||||
def test_procs_matching_filters_by_command():
|
||||
from mytoolkit.commands.git import _procs_matching
|
||||
import os
|
||||
|
||||
pid = os.getpid()
|
||||
# Current test process command line contains "python".
|
||||
assert pid in _procs_matching([pid], "python")
|
||||
assert _procs_matching([pid], "definitely-not-a-real-proc-xyz") == []
|
||||
|
||||
|
||||
def test_restore_stale_git_proxy_restores_when_port_dead(monkeypatch):
|
||||
from mytoolkit.commands.git import _restore_stale_git_proxy
|
||||
|
||||
restored: dict[str, object] = {}
|
||||
monkeypatch.setattr(
|
||||
"mytoolkit.commands.git._read_state",
|
||||
lambda: {"old_http": "http://old.example:1", "old_https": None},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"mytoolkit.commands.git._get_git_proxy",
|
||||
lambda key: "http://127.0.0.1:59999",
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"mytoolkit.commands.git._proxy_loopback_port", lambda url: 59999
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"mytoolkit.commands.git._port_listening_on", lambda host, port: False
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"mytoolkit.commands.git._restore_proxy_key",
|
||||
lambda key, old: restored.__setitem__(key, old),
|
||||
)
|
||||
assert _restore_stale_git_proxy() is True
|
||||
assert restored == {"http.proxy": "http://old.example:1", "https.proxy": None}
|
||||
|
||||
|
||||
def test_restore_stale_git_proxy_noop_when_alive(monkeypatch):
|
||||
from mytoolkit.commands.git import _restore_stale_git_proxy
|
||||
|
||||
restored: dict[str, object] = {}
|
||||
monkeypatch.setattr(
|
||||
"mytoolkit.commands.git._read_state",
|
||||
lambda: {"old_http": "http://old.example:1"},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"mytoolkit.commands.git._get_git_proxy",
|
||||
lambda key: "http://127.0.0.1:59999",
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"mytoolkit.commands.git._proxy_loopback_port", lambda url: 59999
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"mytoolkit.commands.git._port_listening_on", lambda host, port: True
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"mytoolkit.commands.git._restore_proxy_key",
|
||||
lambda key, old: restored.__setitem__(key, old),
|
||||
)
|
||||
assert _restore_stale_git_proxy() is False
|
||||
assert restored == {}
|
||||
|
||||
Reference in New Issue
Block a user