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."""
|
"""Git utilities including proxy management."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import signal
|
import signal
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
|
||||||
from mytoolkit.config import config
|
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]:
|
def _get_proxies() -> dict[str, str]:
|
||||||
"""Get git proxies from config.
|
"""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")
|
@click.group(name="git")
|
||||||
def git_cmd() -> None:
|
def git_cmd() -> None:
|
||||||
"""Git utilities."""
|
"""Git utilities."""
|
||||||
@@ -114,6 +226,12 @@ def proxy_status() -> None:
|
|||||||
click.echo(f" http.proxy: {http_proxy}")
|
click.echo(f" http.proxy: {http_proxy}")
|
||||||
click.echo(f" https.proxy: {https_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")
|
click.secho("\nAvailable proxies:", fg="cyan")
|
||||||
for proxy_name, url in proxies.items():
|
for proxy_name, url in proxies.items():
|
||||||
marker = "*" if proxy_name == current_name else " "
|
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 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.
|
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()
|
proxies = _get_proxies()
|
||||||
if name not in 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")
|
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
|
# 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
|
cmd_to_run = list(command) if command else None
|
||||||
if cmd_to_run is 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]
|
cmd_to_run = [name]
|
||||||
|
|
||||||
def cleanup():
|
def cleanup():
|
||||||
|
_stop_procs(forward_procs)
|
||||||
if old_http == "(not set)":
|
if old_http == "(not set)":
|
||||||
_unset_git_proxy("http.proxy")
|
_unset_git_proxy("http.proxy")
|
||||||
else:
|
else:
|
||||||
@@ -204,7 +334,7 @@ def proxy_run(name: str, command: tuple[str, ...]) -> None:
|
|||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
else:
|
else:
|
||||||
click.secho(
|
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:
|
try:
|
||||||
signal.pause()
|
signal.pause()
|
||||||
|
|||||||
Reference in New Issue
Block a user