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:
|
def _proxy_loopback_port(proxy_url: str) -> int | None:
|
||||||
"""Return listen port if proxy_url is a loopback HTTP proxy."""
|
"""Return listen port if proxy_url is a loopback HTTP proxy."""
|
||||||
try:
|
try:
|
||||||
@@ -108,6 +120,27 @@ def _parallels_host_ips() -> list[str]:
|
|||||||
return ips
|
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:
|
def _port_listening_on(host: str, port: int) -> bool:
|
||||||
return bool(_pids_listening_on(host, port))
|
return bool(_pids_listening_on(host, port))
|
||||||
|
|
||||||
@@ -318,6 +351,32 @@ def _clear_state() -> None:
|
|||||||
pass
|
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]:
|
def _require_proxy(name: str) -> tuple[str, str]:
|
||||||
"""Return (name, url) or exit."""
|
"""Return (name, url) or exit."""
|
||||||
proxies = _get_proxies()
|
proxies = _get_proxies()
|
||||||
@@ -411,6 +470,8 @@ def _collect_forward_pids(port: int) -> list[int]:
|
|||||||
|
|
||||||
|
|
||||||
def _start_daemon(name: str, *, set_git: bool) -> None:
|
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)
|
_, proxy_url = _require_proxy(name)
|
||||||
port = _proxy_loopback_port(proxy_url)
|
port = _proxy_loopback_port(proxy_url)
|
||||||
|
|
||||||
@@ -443,11 +504,19 @@ def _start_daemon(name: str, *, set_git: bool) -> None:
|
|||||||
return
|
return
|
||||||
|
|
||||||
if _port_listening_on("127.0.0.1", port):
|
if _port_listening_on("127.0.0.1", port):
|
||||||
|
pids = _procs_matching(_pids_listening_on("127.0.0.1", port), name)
|
||||||
|
if pids:
|
||||||
click.secho(
|
click.secho(
|
||||||
f"{name} already listening on 127.0.0.1:{port}", fg="cyan"
|
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",
|
||||||
)
|
)
|
||||||
pids = _pids_listening_on("127.0.0.1", port)
|
|
||||||
proxy_pid = pids[0] if pids else None
|
|
||||||
else:
|
else:
|
||||||
binary = shutil.which(name)
|
binary = shutil.which(name)
|
||||||
if not binary:
|
if not binary:
|
||||||
@@ -525,7 +594,7 @@ def _stop_daemon(name: str | None) -> None:
|
|||||||
|
|
||||||
if port is not None:
|
if port is not None:
|
||||||
forward_pids = _collect_forward_pids(port)
|
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 []:
|
for pid in state.get("forward_pids") or []:
|
||||||
if isinstance(pid, int) and pid not in forward_pids:
|
if isinstance(pid, int) and pid not in forward_pids:
|
||||||
forward_pids.append(pid)
|
forward_pids.append(pid)
|
||||||
@@ -534,17 +603,19 @@ def _stop_daemon(name: str | None) -> None:
|
|||||||
else:
|
else:
|
||||||
click.secho(f"{name} socat: nothing listening on guest IPs", fg="yellow")
|
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:
|
if proxy_pids:
|
||||||
_kill_pids(proxy_pids, name)
|
_kill_pids(proxy_pids, name)
|
||||||
else:
|
else:
|
||||||
click.secho(
|
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:
|
else:
|
||||||
click.secho(f"{name}: no local loopback daemon to stop", fg="yellow")
|
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_http = state.get("old_http")
|
||||||
old_https = state.get("old_https")
|
old_https = state.get("old_https")
|
||||||
if old_http in (None, "(not set)"):
|
if old_http in (None, "(not set)"):
|
||||||
@@ -672,6 +743,8 @@ def proxy_status(name: str | None) -> None:
|
|||||||
current = _current_daemon_name()
|
current = _current_daemon_name()
|
||||||
state = _read_state()
|
state = _read_state()
|
||||||
|
|
||||||
|
_restore_stale_git_proxy()
|
||||||
|
|
||||||
click.secho("Git proxy daemons:", fg="cyan")
|
click.secho("Git proxy daemons:", fg="cyan")
|
||||||
if not proxies:
|
if not proxies:
|
||||||
click.echo(" (none configured in ~/.mytoolkit/config.json)")
|
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():
|
def test_proxy_loopback_port_rejects_remote():
|
||||||
assert _proxy_loopback_port("http://10.211.55.2:38457") is None
|
assert _proxy_loopback_port("http://10.211.55.2:38457") is None
|
||||||
assert _proxy_loopback_port("socks5://127.0.0.1:1080") 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