git proxy run: auto-forward Parallels host IPs to loopback proxy.
Detect Shared/Host-Only addresses from ifconfig and socat-forward them so Windows guests can reach FastGitHub without a hardcoded 10.211.55.2.
This commit is contained in:
+132
-2
@@ -1,14 +1,23 @@
|
||||
"""Git utilities including proxy management."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import click
|
||||
|
||||
from mytoolkit.config import config
|
||||
|
||||
# Parallels Desktop Shared / Host-Only nets on macOS: host is typically *.*.*.2
|
||||
_PARALLELS_NET_RE = re.compile(r"inet (10\.(?:211\.55|37\.129)\.\d+)\b")
|
||||
|
||||
|
||||
def _get_proxies() -> dict[str, str]:
|
||||
"""Get git proxies from config.
|
||||
@@ -50,6 +59,109 @@ def _unset_git_proxy(key: str) -> None:
|
||||
)
|
||||
|
||||
|
||||
def _proxy_loopback_port(proxy_url: str) -> int | None:
|
||||
"""Return listen port if proxy_url is a loopback HTTP proxy."""
|
||||
try:
|
||||
parsed = urlparse(proxy_url)
|
||||
except ValueError:
|
||||
return None
|
||||
if parsed.scheme not in {"http", "https"}:
|
||||
return None
|
||||
host = (parsed.hostname or "").lower()
|
||||
if host not in {"127.0.0.1", "localhost", "::1"}:
|
||||
return None
|
||||
if parsed.port:
|
||||
return int(parsed.port)
|
||||
return 443 if parsed.scheme == "https" else 80
|
||||
|
||||
|
||||
def _parallels_host_ips() -> list[str]:
|
||||
"""Mac IPs that Parallels guests use to reach this host (auto from ifconfig)."""
|
||||
if sys.platform != "darwin":
|
||||
return []
|
||||
override = os.environ.get("XIAOHE_CI_PARALLELS_HOST_IP", "").strip()
|
||||
if override:
|
||||
return [override]
|
||||
try:
|
||||
out = subprocess.check_output(
|
||||
["ifconfig"], text=True, stderr=subprocess.DEVNULL
|
||||
)
|
||||
except (OSError, subprocess.CalledProcessError):
|
||||
return []
|
||||
ips: list[str] = []
|
||||
for match in _PARALLELS_NET_RE.finditer(out):
|
||||
ip = match.group(1)
|
||||
if ip not in ips:
|
||||
ips.append(ip)
|
||||
return ips
|
||||
|
||||
|
||||
def _port_listening_on(host: str, port: int) -> bool:
|
||||
try:
|
||||
out = subprocess.check_output(
|
||||
["lsof", f"-iTCP@{host}:{port}", "-sTCP:LISTEN", "-n", "-P"],
|
||||
text=True,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
return bool(out.strip())
|
||||
except (OSError, subprocess.CalledProcessError):
|
||||
return False
|
||||
|
||||
|
||||
def _start_parallels_forwards(port: int) -> list[subprocess.Popen]:
|
||||
"""Forward Parallels host IPs:port → 127.0.0.1:port via socat (Mac only)."""
|
||||
ips = _parallels_host_ips()
|
||||
if not ips:
|
||||
return []
|
||||
socat = shutil.which("socat")
|
||||
if not socat:
|
||||
click.secho(
|
||||
"WARN: socat missing — Parallels VMs cannot reach loopback proxy "
|
||||
f"on :{port}. brew install socat",
|
||||
fg="yellow",
|
||||
err=True,
|
||||
)
|
||||
return []
|
||||
procs: list[subprocess.Popen] = []
|
||||
for ip in ips:
|
||||
if _port_listening_on(ip, port):
|
||||
click.echo(f"Parallels forward already up: {ip}:{port}")
|
||||
continue
|
||||
proc = subprocess.Popen(
|
||||
[
|
||||
socat,
|
||||
f"TCP-LISTEN:{port},bind={ip},fork,reuseaddr",
|
||||
f"TCP:127.0.0.1:{port}",
|
||||
],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
# Brief wait so bind failures surface.
|
||||
time.sleep(0.3)
|
||||
if proc.poll() is not None:
|
||||
click.secho(
|
||||
f"WARN: socat failed for {ip}:{port} (exit {proc.returncode})",
|
||||
fg="yellow",
|
||||
err=True,
|
||||
)
|
||||
continue
|
||||
procs.append(proc)
|
||||
click.secho(f"Parallels forward {ip}:{port} → 127.0.0.1:{port}", fg="green")
|
||||
return procs
|
||||
|
||||
|
||||
def _stop_procs(procs: list[subprocess.Popen]) -> None:
|
||||
for proc in procs:
|
||||
if proc.poll() is not None:
|
||||
continue
|
||||
proc.terminate()
|
||||
try:
|
||||
proc.wait(timeout=3)
|
||||
except subprocess.TimeoutExpired:
|
||||
proc.kill()
|
||||
proc.wait()
|
||||
|
||||
|
||||
@click.group(name="git")
|
||||
def git_cmd() -> None:
|
||||
"""Git utilities."""
|
||||
@@ -114,6 +226,12 @@ def proxy_status() -> None:
|
||||
click.echo(f" http.proxy: {http_proxy}")
|
||||
click.echo(f" https.proxy: {https_proxy}")
|
||||
|
||||
ips = _parallels_host_ips()
|
||||
if ips:
|
||||
click.secho("\nParallels host IPs (auto):", fg="cyan")
|
||||
for ip in ips:
|
||||
click.echo(f" {ip}")
|
||||
|
||||
click.secho("\nAvailable proxies:", fg="cyan")
|
||||
for proxy_name, url in proxies.items():
|
||||
marker = "*" if proxy_name == current_name else " "
|
||||
@@ -137,6 +255,9 @@ def proxy_run(name: str, command: tuple[str, ...]) -> None:
|
||||
|
||||
If no COMMAND is given, tries to run a program with the same name as the proxy.
|
||||
If that program is not found, sets the proxy and waits for Ctrl-C.
|
||||
|
||||
For loopback proxies (e.g. fastgithub on 127.0.0.1), also opens socat
|
||||
forwards on auto-detected Parallels host IPs so Windows/Linux VMs can connect.
|
||||
"""
|
||||
proxies = _get_proxies()
|
||||
if name not in proxies:
|
||||
@@ -157,13 +278,22 @@ def proxy_run(name: str, command: tuple[str, ...]) -> None:
|
||||
)
|
||||
click.secho(f"Git proxy set to {name}: {proxy_url}", fg="green")
|
||||
|
||||
forward_procs: list[subprocess.Popen] = []
|
||||
port = _proxy_loopback_port(proxy_url)
|
||||
if port is not None:
|
||||
forward_procs = _start_parallels_forwards(port)
|
||||
|
||||
# Determine what to run
|
||||
already_up = port is not None and _port_listening_on("127.0.0.1", port)
|
||||
cmd_to_run = list(command) if command else None
|
||||
if cmd_to_run is None:
|
||||
if shutil.which(name):
|
||||
if already_up:
|
||||
click.secho(f"{name} already listening on :{port}", fg="cyan")
|
||||
elif shutil.which(name):
|
||||
cmd_to_run = [name]
|
||||
|
||||
def cleanup():
|
||||
_stop_procs(forward_procs)
|
||||
if old_http == "(not set)":
|
||||
_unset_git_proxy("http.proxy")
|
||||
else:
|
||||
@@ -204,7 +334,7 @@ def proxy_run(name: str, command: tuple[str, ...]) -> None:
|
||||
sys.exit(0)
|
||||
else:
|
||||
click.secho(
|
||||
f"Proxy active. Press Ctrl-C to stop and auto-unset proxy", fg="cyan"
|
||||
"Proxy active. Press Ctrl-C to stop and auto-unset proxy", fg="cyan"
|
||||
)
|
||||
try:
|
||||
signal.pause()
|
||||
|
||||
Reference in New Issue
Block a user