git proxy: add start/stop/restart daemon lifecycle for FastGitHub.
Background daemons with port/PID tracking and Parallels socat, keep FastGithub logs off the TTY, and document install in the skill.
This commit is contained in:
@@ -21,6 +21,9 @@ triggers:
|
|||||||
- bib转markdown
|
- bib转markdown
|
||||||
- avi转mp4
|
- avi转mp4
|
||||||
- 更新mytoolkit
|
- 更新mytoolkit
|
||||||
|
- fastgithub
|
||||||
|
- git proxy
|
||||||
|
- FastGithub
|
||||||
---
|
---
|
||||||
|
|
||||||
# MyToolkit 个人 CLI 工具集
|
# MyToolkit 个人 CLI 工具集
|
||||||
@@ -72,6 +75,7 @@ mytoolkit completion zsh # 打印 zsh 补全脚本
|
|||||||
| LaTeX 编译 | `mytoolkit latex compile paper.tex` |
|
| LaTeX 编译 | `mytoolkit latex compile paper.tex` |
|
||||||
| BibTeX 转 Markdown | `mytoolkit convert refs.bib -o refs.md` |
|
| BibTeX 转 Markdown | `mytoolkit convert refs.bib -o refs.md` |
|
||||||
| AVI 视频转 MP4 | `mytoolkit convert video.avi -o video.mp4` |
|
| AVI 视频转 MP4 | `mytoolkit convert video.avi -o video.mp4` |
|
||||||
|
| FastGitHub / git 代理守护 | `mytoolkit git proxy start fastgithub`(安装见 `references/others.md`) |
|
||||||
| 更新工具本身 | `cd <xiaohe-agent>/packages/mytoolkit && make install` |
|
| 更新工具本身 | `cd <xiaohe-agent>/packages/mytoolkit && make install` |
|
||||||
|
|
||||||
## 安装与更新
|
## 安装与更新
|
||||||
|
|||||||
@@ -4,12 +4,50 @@
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
mytoolkit init paper <dir> # 初始化 LaTeX 论文项目
|
mytoolkit init paper <dir> # 初始化 LaTeX 论文项目
|
||||||
mytoolkit init manual <dir> # 初始化 LaTeX 手册项目(技术与使用文档)
|
|
||||||
mytoolkit env # 环境变量管理
|
mytoolkit env # 环境变量管理
|
||||||
mytoolkit git # Git 辅助工具
|
mytoolkit git # Git 辅助(含 proxy 守护进程)
|
||||||
mytoolkit ssh # SSH 快捷连接
|
mytoolkit ssh # SSH 快捷连接
|
||||||
mytoolkit server # 开发服务器管理
|
mytoolkit server # 开发服务器管理(webpage,非 git proxy)
|
||||||
mytoolkit update # 从本地仓库更新
|
mytoolkit update # 从本地仓库更新
|
||||||
mytoolkit uninstall # 卸载
|
mytoolkit uninstall # 卸载
|
||||||
mytoolkit completion # 安装/查看/卸载 shell 补全(install / uninstall / doctor / zsh / bash)
|
mytoolkit completion # 安装/查看/卸载 shell 补全(install / uninstall / doctor / zsh / bash)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## git proxy(FastGitHub 等)
|
||||||
|
|
||||||
|
配置在 `~/.mytoolkit/config.json` → `connections.proxy.<name>`(例:`fastgithub` → `http://127.0.0.1:38457`)。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mytoolkit git proxy list
|
||||||
|
mytoolkit git proxy status # 全部守护进程 + git config
|
||||||
|
mytoolkit git proxy start fastgithub # 后台起进程 + Parallels socat(默认不改 git config)
|
||||||
|
mytoolkit git proxy stop # 停「当前」
|
||||||
|
mytoolkit git proxy stop fastgithub
|
||||||
|
mytoolkit git proxy restart # 重启「当前」
|
||||||
|
mytoolkit git proxy set fastgithub # 仅写 git http(s).proxy
|
||||||
|
mytoolkit git proxy unset
|
||||||
|
mytoolkit git proxy run fastgithub # 前台(设 git + Ctrl-C 清理);CI 主机请用 start
|
||||||
|
```
|
||||||
|
|
||||||
|
- `start` / `stop` / `restart`:端口 + PID(`lsof`),状态文件 `~/.mytoolkit/run/proxy-current.json`
|
||||||
|
- CI:主机 `start fastgithub`;job 用 `HEYUE_CI_GIT_PROXY=1` / `XIAOHE_CI_GIT_PROXY=1`(勿依赖全局 git proxy)
|
||||||
|
- Parallels guest 需 `brew install socat`;可选 `XIAOHE_CI_PARALLELS_HOST_IP`
|
||||||
|
|
||||||
|
### 新机器安装 FastGitHub
|
||||||
|
|
||||||
|
上游(自用 fork):https://github.com/creazyboyone/FastGithub
|
||||||
|
(原 `dotnetcore/fastgithub` 已不可用。)
|
||||||
|
|
||||||
|
1. 从 [Releases](https://github.com/creazyboyone/FastGithub/releases) 下载 macOS 包,解压到固定目录(例 `~/Documents/myBin/fastgithub-osx/`)。
|
||||||
|
2. 把二进制放到 PATH,且命令名与 config 里的 proxy 名一致:
|
||||||
|
```bash
|
||||||
|
ln -sf ~/Documents/myBin/fastgithub-osx/fastgithub /opt/homebrew/bin/fastgithub
|
||||||
|
```
|
||||||
|
3. 按官方说明信任 `cacert/fastgithub.cer`(macOS 钥匙串);git 走代理时通常还需 `sslVerify=false`(CI 已在 `GIT_CONFIG_PARAMETERS` 里处理)。
|
||||||
|
4. 写入 config(若尚无):
|
||||||
|
```bash
|
||||||
|
mytoolkit env set connections.proxy.fastgithub http://127.0.0.1:38457
|
||||||
|
```
|
||||||
|
5. 启动:`mytoolkit git proxy start fastgithub` → `mytoolkit git proxy status`
|
||||||
|
|
||||||
|
Windows/Linux 也可用官方 `fastgithub start`(系统服务);本 CLI 的 `start` 面向 **Mac 宿主 + Parallels socat** 场景。
|
||||||
|
|||||||
@@ -44,6 +44,19 @@ sync-skill:
|
|||||||
rm -rf "$(HOME)/.agents/skills/mytoolkit"; \
|
rm -rf "$(HOME)/.agents/skills/mytoolkit"; \
|
||||||
cp -R "$(ROOT_DIR)/.agents/skills/mytoolkit" "$(HOME)/.agents/skills/mytoolkit"; \
|
cp -R "$(ROOT_DIR)/.agents/skills/mytoolkit" "$(HOME)/.agents/skills/mytoolkit"; \
|
||||||
fi
|
fi
|
||||||
|
@# Cursor / Claude Code often resolve skills under ~/.claude/skills
|
||||||
|
@if [[ -L "$(HOME)/.claude/skills/mytoolkit" ]]; then \
|
||||||
|
echo " ~/.claude/skills/mytoolkit is symlink — skip copy"; \
|
||||||
|
elif [[ -d "$(HOME)/.claude/skills" ]] || [[ -d "$(HOME)/.claude" ]]; then \
|
||||||
|
echo "Mirroring skill to ~/.claude/skills/mytoolkit/ ..."; \
|
||||||
|
mkdir -p "$(HOME)/.claude/skills"; \
|
||||||
|
if command -v rsync >/dev/null 2>&1; then \
|
||||||
|
rsync -av --delete "$(ROOT_DIR)/.agents/skills/mytoolkit/" "$(HOME)/.claude/skills/mytoolkit/"; \
|
||||||
|
else \
|
||||||
|
rm -rf "$(HOME)/.claude/skills/mytoolkit"; \
|
||||||
|
cp -R "$(ROOT_DIR)/.agents/skills/mytoolkit" "$(HOME)/.claude/skills/mytoolkit"; \
|
||||||
|
fi; \
|
||||||
|
fi
|
||||||
|
|
||||||
uninstall:
|
uninstall:
|
||||||
@mytoolkit uninstall || $(PYTHON) -m pip uninstall mytoolkit -y
|
@mytoolkit uninstall || $(PYTHON) -m pip uninstall mytoolkit -y
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ mytoolkit --help
|
|||||||
| `templates` | Inspect template root; external override still possible |
|
| `templates` | Inspect template root; external override still possible |
|
||||||
| `env` | Manage stored vars (api keys, paths, feishu app credentials) |
|
| `env` | Manage stored vars (api keys, paths, feishu app credentials) |
|
||||||
| `ssh` | `list` / `connect` / `tunnel` / `cmd` |
|
| `ssh` | `list` / `connect` / `tunnel` / `cmd` |
|
||||||
| `git` | `proxy` (set/unset/list/status) |
|
| `git` | `proxy` (start/stop/restart/status/set/unset/list/run) |
|
||||||
| `server` | Manage dev/serve servers |
|
| `server` | Manage dev/serve servers |
|
||||||
| `update` | Reinstall from local repo |
|
| `update` | Reinstall from local repo |
|
||||||
| `uninstall` | Uninstall |
|
| `uninstall` | Uninstall |
|
||||||
|
|||||||
+530
-93
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
@@ -9,11 +10,13 @@ import signal
|
|||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
|
||||||
from mytoolkit.config import config
|
from mytoolkit.config import CONFIG_PATH, config
|
||||||
|
|
||||||
# Parallels Desktop Shared / Host-Only nets on macOS: host is typically *.*.*.2
|
# 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")
|
_PARALLELS_NET_RE = re.compile(r"inet (10\.(?:211\.55|37\.129)\.\d+)\b")
|
||||||
@@ -59,6 +62,15 @@ def _unset_git_proxy(key: str) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _set_git_proxies(proxy_url: str) -> None:
|
||||||
|
subprocess.run(
|
||||||
|
["git", "config", "--global", "http.proxy", proxy_url], check=True
|
||||||
|
)
|
||||||
|
subprocess.run(
|
||||||
|
["git", "config", "--global", "https.proxy", proxy_url], check=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
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:
|
||||||
@@ -97,40 +109,72 @@ def _parallels_host_ips() -> list[str]:
|
|||||||
|
|
||||||
|
|
||||||
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))
|
||||||
|
|
||||||
|
|
||||||
|
def _pids_listening_on(host: str, port: int) -> list[int]:
|
||||||
|
"""PIDs with a TCP LISTEN on host:port (via lsof)."""
|
||||||
try:
|
try:
|
||||||
out = subprocess.check_output(
|
out = subprocess.check_output(
|
||||||
["lsof", f"-iTCP@{host}:{port}", "-sTCP:LISTEN", "-n", "-P"],
|
["lsof", f"-iTCP@{host}:{port}", "-sTCP:LISTEN", "-n", "-P", "-t"],
|
||||||
text=True,
|
text=True,
|
||||||
stderr=subprocess.DEVNULL,
|
stderr=subprocess.DEVNULL,
|
||||||
)
|
)
|
||||||
return bool(out.strip())
|
|
||||||
except (OSError, subprocess.CalledProcessError):
|
except (OSError, subprocess.CalledProcessError):
|
||||||
return False
|
return []
|
||||||
|
pids: list[int] = []
|
||||||
|
for line in out.splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
pid = int(line)
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
if pid not in pids:
|
||||||
|
pids.append(pid)
|
||||||
|
return pids
|
||||||
|
|
||||||
|
|
||||||
|
def _fmt_listen_row(
|
||||||
|
addr: str, ok: bool, note: str = "", *, indent: int = 2
|
||||||
|
) -> str:
|
||||||
|
mark = "OK" if ok else "DOWN"
|
||||||
|
note_s = f" {note}" if note else ""
|
||||||
|
return f"{' ' * indent}{addr:<22} {mark:<4}{note_s}"
|
||||||
|
|
||||||
|
|
||||||
def _report_proxy_listen(port: int, ips: list[str] | None = None) -> None:
|
def _report_proxy_listen(port: int, ips: list[str] | None = None) -> None:
|
||||||
"""Print loopback + Parallels listen state and guest proxy URLs."""
|
"""Print loopback + Parallels listen state and guest proxy URLs."""
|
||||||
loop_ok = _port_listening_on("127.0.0.1", port)
|
loop_ok = _port_listening_on("127.0.0.1", port)
|
||||||
click.secho("\nProxy listen check:", fg="cyan")
|
click.echo("")
|
||||||
|
click.secho("Proxy listen:", fg="cyan")
|
||||||
click.echo(
|
click.echo(
|
||||||
f" 127.0.0.1:{port} {'OK' if loop_ok else 'DOWN'} (Mac / git http.proxy)"
|
_fmt_listen_row(f"127.0.0.1:{port}", loop_ok, "(Mac / git http.proxy)")
|
||||||
)
|
)
|
||||||
if ips is None:
|
if ips is None:
|
||||||
ips = _parallels_host_ips()
|
ips = _parallels_host_ips()
|
||||||
if not ips:
|
if not ips:
|
||||||
click.secho(
|
click.secho(
|
||||||
" (no Parallels Shared/Host-Only IPs auto-detected — "
|
" (no Parallels Shared/Host-Only IPs — "
|
||||||
"guest VMs cannot use this proxy via host IP; "
|
"set XIAOHE_CI_PARALLELS_HOST_IP if a VM needs the proxy)",
|
||||||
"override with XIAOHE_CI_PARALLELS_HOST_IP)",
|
|
||||||
fg="yellow",
|
fg="yellow",
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
click.echo(" Parallels guest → Mac (socat):")
|
click.echo(" Parallels guest → Mac (socat):")
|
||||||
|
down: list[str] = []
|
||||||
for ip in ips:
|
for ip in ips:
|
||||||
ok = _port_listening_on(ip, port)
|
ok = _port_listening_on(ip, port)
|
||||||
mark = "OK" if ok else "DOWN"
|
if not ok:
|
||||||
click.echo(f" {ip}:{port} {mark} → guest use http://{ip}:{port}")
|
down.append(ip)
|
||||||
down = [ip for ip in ips if not _port_listening_on(ip, port)]
|
click.echo(
|
||||||
|
_fmt_listen_row(
|
||||||
|
f"{ip}:{port}",
|
||||||
|
ok,
|
||||||
|
f"→ http://{ip}:{port}",
|
||||||
|
indent=4,
|
||||||
|
)
|
||||||
|
)
|
||||||
if down:
|
if down:
|
||||||
click.secho(
|
click.secho(
|
||||||
" WARN: some Parallels forwards are DOWN — Windows/Linux VMs "
|
" WARN: some Parallels forwards are DOWN — Windows/Linux VMs "
|
||||||
@@ -140,7 +184,7 @@ def _report_proxy_listen(port: int, ips: list[str] | None = None) -> None:
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
click.secho(
|
click.secho(
|
||||||
" Guest tip: curl -I -x http://<ip>:%d https://api.github.com" % port,
|
f" tip: curl -I -x http://<ip>:{port} https://api.github.com",
|
||||||
fg="cyan",
|
fg="cyan",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -165,12 +209,18 @@ def _start_parallels_forwards(port: int) -> list[subprocess.Popen]:
|
|||||||
err=True,
|
err=True,
|
||||||
)
|
)
|
||||||
return []
|
return []
|
||||||
click.echo(f"socat: {socat}")
|
click.echo("")
|
||||||
click.echo(f"Parallels host IPs (auto): {', '.join(ips)}")
|
click.secho("Parallels forwards:", fg="cyan")
|
||||||
|
click.echo(f" socat: {socat}")
|
||||||
|
click.echo(f" host IPs: {', '.join(ips)}")
|
||||||
procs: list[subprocess.Popen] = []
|
procs: list[subprocess.Popen] = []
|
||||||
for ip in ips:
|
for ip in ips:
|
||||||
|
addr = f"{ip}:{port}"
|
||||||
if _port_listening_on(ip, port):
|
if _port_listening_on(ip, port):
|
||||||
click.secho(f"Parallels forward already up: {ip}:{port}", fg="green")
|
click.secho(
|
||||||
|
_fmt_listen_row(addr, True, "→ 127.0.0.1:%d (already up)" % port),
|
||||||
|
fg="green",
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
proc = subprocess.Popen(
|
proc = subprocess.Popen(
|
||||||
[
|
[
|
||||||
@@ -180,18 +230,22 @@ def _start_parallels_forwards(port: int) -> list[subprocess.Popen]:
|
|||||||
],
|
],
|
||||||
stdout=subprocess.DEVNULL,
|
stdout=subprocess.DEVNULL,
|
||||||
stderr=subprocess.DEVNULL,
|
stderr=subprocess.DEVNULL,
|
||||||
|
start_new_session=True,
|
||||||
)
|
)
|
||||||
# Brief wait so bind failures surface.
|
# Brief wait so bind failures surface.
|
||||||
time.sleep(0.3)
|
time.sleep(0.3)
|
||||||
if proc.poll() is not None:
|
if proc.poll() is not None:
|
||||||
click.secho(
|
click.secho(
|
||||||
f"WARN: socat failed for {ip}:{port} (exit {proc.returncode})",
|
_fmt_listen_row(addr, False, f"socat exit {proc.returncode}"),
|
||||||
fg="yellow",
|
fg="yellow",
|
||||||
err=True,
|
err=True,
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
procs.append(proc)
|
procs.append(proc)
|
||||||
click.secho(f"Parallels forward {ip}:{port} → 127.0.0.1:{port}", fg="green")
|
click.secho(
|
||||||
|
_fmt_listen_row(addr, True, f"→ 127.0.0.1:{port}"),
|
||||||
|
fg="green",
|
||||||
|
)
|
||||||
return procs
|
return procs
|
||||||
|
|
||||||
|
|
||||||
@@ -207,6 +261,317 @@ def _stop_procs(procs: list[subprocess.Popen]) -> None:
|
|||||||
proc.wait()
|
proc.wait()
|
||||||
|
|
||||||
|
|
||||||
|
def _kill_pids(pids: list[int], label: str) -> None:
|
||||||
|
"""SIGTERM then SIGKILL listed PIDs."""
|
||||||
|
for pid in pids:
|
||||||
|
try:
|
||||||
|
os.kill(pid, signal.SIGTERM)
|
||||||
|
click.secho(f"Sent SIGTERM to {label} (PID {pid})", fg="green")
|
||||||
|
except ProcessLookupError:
|
||||||
|
click.secho(f"Process {pid} not found", fg="yellow")
|
||||||
|
continue
|
||||||
|
for _ in range(10):
|
||||||
|
time.sleep(0.3)
|
||||||
|
try:
|
||||||
|
os.kill(pid, 0)
|
||||||
|
except ProcessLookupError:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
os.kill(pid, signal.SIGKILL)
|
||||||
|
click.secho(f"Sent SIGKILL to {label} (PID {pid})", fg="red")
|
||||||
|
except ProcessLookupError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _run_dir() -> Path:
|
||||||
|
path = CONFIG_PATH.parent / "run"
|
||||||
|
path.mkdir(parents=True, exist_ok=True)
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def _state_path() -> Path:
|
||||||
|
return _run_dir() / "proxy-current.json"
|
||||||
|
|
||||||
|
|
||||||
|
def _read_state() -> dict[str, Any] | None:
|
||||||
|
path = _state_path()
|
||||||
|
if not path.exists():
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
data = json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
except (OSError, json.JSONDecodeError):
|
||||||
|
return None
|
||||||
|
return data if isinstance(data, dict) else None
|
||||||
|
|
||||||
|
|
||||||
|
def _write_state(data: dict[str, Any]) -> None:
|
||||||
|
path = _state_path()
|
||||||
|
path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def _clear_state() -> None:
|
||||||
|
path = _state_path()
|
||||||
|
try:
|
||||||
|
path.unlink()
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _require_proxy(name: str) -> tuple[str, str]:
|
||||||
|
"""Return (name, url) or exit."""
|
||||||
|
proxies = _get_proxies()
|
||||||
|
if name not in proxies:
|
||||||
|
click.echo(f"Unknown proxy: {name}", err=True)
|
||||||
|
click.echo(f"Available: {', '.join(proxies.keys()) or '(none)'}", err=True)
|
||||||
|
sys.exit(1)
|
||||||
|
return name, proxies[name]
|
||||||
|
|
||||||
|
|
||||||
|
def _current_daemon_name() -> str | None:
|
||||||
|
"""Name of the managed daemon, if any.
|
||||||
|
|
||||||
|
Prefer state file when its loopback port is still listening; else infer
|
||||||
|
from config URLs whose loopback port is up.
|
||||||
|
"""
|
||||||
|
state = _read_state()
|
||||||
|
if state and isinstance(state.get("name"), str):
|
||||||
|
name = state["name"]
|
||||||
|
proxies = _get_proxies()
|
||||||
|
url = proxies.get(name)
|
||||||
|
if url:
|
||||||
|
port = _proxy_loopback_port(url)
|
||||||
|
if port is not None and _port_listening_on("127.0.0.1", port):
|
||||||
|
return name
|
||||||
|
if port is None:
|
||||||
|
return name
|
||||||
|
# Stale state — fall through
|
||||||
|
for name, url in _get_proxies().items():
|
||||||
|
port = _proxy_loopback_port(url)
|
||||||
|
if port is not None and _port_listening_on("127.0.0.1", port):
|
||||||
|
return name
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _wait_for_listen(
|
||||||
|
host: str,
|
||||||
|
port: int,
|
||||||
|
proc: subprocess.Popen | None = None,
|
||||||
|
timeout_s: float = 15.0,
|
||||||
|
) -> bool:
|
||||||
|
deadline = time.time() + timeout_s
|
||||||
|
while time.time() < deadline:
|
||||||
|
if _port_listening_on(host, port):
|
||||||
|
return True
|
||||||
|
if proc is not None and proc.poll() is not None:
|
||||||
|
return False
|
||||||
|
time.sleep(0.25)
|
||||||
|
return _port_listening_on(host, port)
|
||||||
|
|
||||||
|
|
||||||
|
def _proxy_log_path(name: str) -> Path:
|
||||||
|
return _run_dir() / f"proxy-{name}.log"
|
||||||
|
|
||||||
|
|
||||||
|
def _spawn_proxy_process(
|
||||||
|
name: str,
|
||||||
|
cmd: list[str],
|
||||||
|
*,
|
||||||
|
new_session: bool,
|
||||||
|
) -> tuple[subprocess.Popen, Path]:
|
||||||
|
"""Start proxy binary with stdout/stderr redirected to a log file.
|
||||||
|
|
||||||
|
Keeps FastGithub (etc.) logs off the TTY so status lines stay readable.
|
||||||
|
"""
|
||||||
|
log_file = _proxy_log_path(name)
|
||||||
|
click.secho(f"Starting: {' '.join(cmd)}", fg="cyan")
|
||||||
|
click.echo(f" log: {log_file}")
|
||||||
|
# Keep the handle open for the child lifetime (Popen does not close it).
|
||||||
|
log = open(log_file, "a", encoding="utf-8") # noqa: SIM115
|
||||||
|
log.write(f"\n--- start {time.strftime('%Y-%m-%d %H:%M:%S')} ---\n")
|
||||||
|
log.flush()
|
||||||
|
proc = subprocess.Popen(
|
||||||
|
cmd,
|
||||||
|
stdout=log,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
start_new_session=new_session,
|
||||||
|
)
|
||||||
|
# Parent no longer writes; child owns the fd. Closing our handle is fine.
|
||||||
|
log.close()
|
||||||
|
return proc, log_file
|
||||||
|
|
||||||
|
|
||||||
|
def _collect_forward_pids(port: int) -> list[int]:
|
||||||
|
pids: list[int] = []
|
||||||
|
for ip in _parallels_host_ips():
|
||||||
|
for pid in _pids_listening_on(ip, port):
|
||||||
|
if pid not in pids:
|
||||||
|
pids.append(pid)
|
||||||
|
return pids
|
||||||
|
|
||||||
|
|
||||||
|
def _start_daemon(name: str, *, set_git: bool) -> None:
|
||||||
|
_, proxy_url = _require_proxy(name)
|
||||||
|
port = _proxy_loopback_port(proxy_url)
|
||||||
|
|
||||||
|
old_http = _get_git_proxy("http.proxy")
|
||||||
|
old_https = _get_git_proxy("https.proxy")
|
||||||
|
proxy_pid: int | None = None
|
||||||
|
forward_pids: list[int] = []
|
||||||
|
|
||||||
|
if port is None:
|
||||||
|
click.secho(
|
||||||
|
f"{name} is not a loopback proxy — use `git proxy set {name}` "
|
||||||
|
"for git config only (no local daemon).",
|
||||||
|
fg="yellow",
|
||||||
|
)
|
||||||
|
if set_git:
|
||||||
|
_set_git_proxies(proxy_url)
|
||||||
|
_write_state(
|
||||||
|
{
|
||||||
|
"name": name,
|
||||||
|
"url": proxy_url,
|
||||||
|
"port": None,
|
||||||
|
"proxy_pid": None,
|
||||||
|
"forward_pids": [],
|
||||||
|
"set_git": True,
|
||||||
|
"old_http": old_http,
|
||||||
|
"old_https": old_https,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
click.secho(f"Git proxy set to {name}: {proxy_url}", fg="green")
|
||||||
|
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
|
||||||
|
else:
|
||||||
|
binary = shutil.which(name)
|
||||||
|
if not binary:
|
||||||
|
click.secho(
|
||||||
|
f"ERROR: nothing on 127.0.0.1:{port} and `{name}` not on PATH",
|
||||||
|
fg="red",
|
||||||
|
err=True,
|
||||||
|
)
|
||||||
|
click.echo(
|
||||||
|
"Install FastGithub and ensure the binary is named/on PATH "
|
||||||
|
"as the proxy name (see skill references/others.md).",
|
||||||
|
err=True,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
proc, log_file = _spawn_proxy_process(
|
||||||
|
name, [binary], new_session=True
|
||||||
|
)
|
||||||
|
if not _wait_for_listen("127.0.0.1", port, proc=proc):
|
||||||
|
click.secho(
|
||||||
|
f"ERROR: {name} failed to listen on 127.0.0.1:{port} "
|
||||||
|
f"(see {log_file})",
|
||||||
|
fg="red",
|
||||||
|
err=True,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
proxy_pid = proc.pid
|
||||||
|
click.secho(
|
||||||
|
f" listening 127.0.0.1:{port} pid={proxy_pid}",
|
||||||
|
fg="green",
|
||||||
|
)
|
||||||
|
|
||||||
|
forward_procs = _start_parallels_forwards(port)
|
||||||
|
forward_pids = [p.pid for p in forward_procs if p.pid]
|
||||||
|
# Include already-up forwards so stop can clean them.
|
||||||
|
for pid in _collect_forward_pids(port):
|
||||||
|
if pid not in forward_pids and pid != proxy_pid:
|
||||||
|
forward_pids.append(pid)
|
||||||
|
|
||||||
|
if set_git:
|
||||||
|
_set_git_proxies(proxy_url)
|
||||||
|
click.secho(f"Git proxy set to {name}: {proxy_url}", fg="green")
|
||||||
|
|
||||||
|
_write_state(
|
||||||
|
{
|
||||||
|
"name": name,
|
||||||
|
"url": proxy_url,
|
||||||
|
"port": port,
|
||||||
|
"proxy_pid": proxy_pid,
|
||||||
|
"forward_pids": forward_pids,
|
||||||
|
"set_git": set_git,
|
||||||
|
"old_http": old_http if set_git else None,
|
||||||
|
"old_https": old_https if set_git else None,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
_report_proxy_listen(port)
|
||||||
|
|
||||||
|
|
||||||
|
def _stop_daemon(name: str | None) -> None:
|
||||||
|
if name is None:
|
||||||
|
name = _current_daemon_name()
|
||||||
|
if name is None:
|
||||||
|
click.secho(
|
||||||
|
"No current proxy daemon (pass a name: "
|
||||||
|
"mytoolkit git proxy stop <name>)",
|
||||||
|
fg="yellow",
|
||||||
|
err=True,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
click.echo(f"Stopping current: {name}")
|
||||||
|
|
||||||
|
_, proxy_url = _require_proxy(name)
|
||||||
|
port = _proxy_loopback_port(proxy_url)
|
||||||
|
state = _read_state()
|
||||||
|
state_matches = bool(state and state.get("name") == name)
|
||||||
|
|
||||||
|
if port is not None:
|
||||||
|
forward_pids = _collect_forward_pids(port)
|
||||||
|
if state_matches:
|
||||||
|
for pid in state.get("forward_pids") or []:
|
||||||
|
if isinstance(pid, int) and pid not in forward_pids:
|
||||||
|
forward_pids.append(pid)
|
||||||
|
if forward_pids:
|
||||||
|
_kill_pids(forward_pids, f"{name} socat")
|
||||||
|
else:
|
||||||
|
click.secho(f"{name} socat: nothing listening on guest IPs", fg="yellow")
|
||||||
|
|
||||||
|
proxy_pids = _pids_listening_on("127.0.0.1", port)
|
||||||
|
if proxy_pids:
|
||||||
|
_kill_pids(proxy_pids, name)
|
||||||
|
else:
|
||||||
|
click.secho(
|
||||||
|
f"{name} is not listening on 127.0.0.1:{port}", fg="yellow"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
click.secho(f"{name}: no local loopback daemon to stop", fg="yellow")
|
||||||
|
|
||||||
|
if state_matches and state.get("set_git"):
|
||||||
|
old_http = state.get("old_http")
|
||||||
|
old_https = state.get("old_https")
|
||||||
|
if old_http in (None, "(not set)"):
|
||||||
|
_unset_git_proxy("http.proxy")
|
||||||
|
else:
|
||||||
|
subprocess.run(
|
||||||
|
["git", "config", "--global", "http.proxy", str(old_http)],
|
||||||
|
capture_output=True,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
if old_https in (None, "(not set)"):
|
||||||
|
_unset_git_proxy("https.proxy")
|
||||||
|
else:
|
||||||
|
subprocess.run(
|
||||||
|
["git", "config", "--global", "https.proxy", str(old_https)],
|
||||||
|
capture_output=True,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
click.secho("Git proxy restored", fg="green")
|
||||||
|
|
||||||
|
if state_matches or (
|
||||||
|
state and state.get("name") == name
|
||||||
|
):
|
||||||
|
_clear_state()
|
||||||
|
click.secho(f"Stopped {name}", fg="green")
|
||||||
|
|
||||||
|
|
||||||
@click.group(name="git")
|
@click.group(name="git")
|
||||||
def git_cmd() -> None:
|
def git_cmd() -> None:
|
||||||
"""Git utilities."""
|
"""Git utilities."""
|
||||||
@@ -215,7 +580,7 @@ def git_cmd() -> None:
|
|||||||
|
|
||||||
@git_cmd.group(name="proxy")
|
@git_cmd.group(name="proxy")
|
||||||
def proxy_cmd() -> None:
|
def proxy_cmd() -> None:
|
||||||
"""Manage git proxy settings."""
|
"""Manage git proxy settings and local proxy daemons."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -229,71 +594,137 @@ def _complete_proxy_names(_ctx, _param, incomplete):
|
|||||||
@proxy_cmd.command("set")
|
@proxy_cmd.command("set")
|
||||||
@click.argument("name", shell_complete=_complete_proxy_names)
|
@click.argument("name", shell_complete=_complete_proxy_names)
|
||||||
def proxy_set(name: str) -> None:
|
def proxy_set(name: str) -> None:
|
||||||
"""Set git proxy by name."""
|
"""Set git http(s).proxy by name (config only; does not start a process)."""
|
||||||
proxies = _get_proxies()
|
_, proxy_url = _require_proxy(name)
|
||||||
if name not in proxies:
|
_set_git_proxies(proxy_url)
|
||||||
click.echo(f"Unknown proxy: {name}", err=True)
|
|
||||||
click.echo(f"Available: {', '.join(proxies.keys())}", err=True)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
proxy_url = proxies[name]
|
|
||||||
subprocess.run(
|
|
||||||
["git", "config", "--global", "http.proxy", proxy_url], check=True
|
|
||||||
)
|
|
||||||
subprocess.run(
|
|
||||||
["git", "config", "--global", "https.proxy", proxy_url], check=True
|
|
||||||
)
|
|
||||||
click.secho(f"Git proxy set to {name}: {proxy_url}", fg="green")
|
click.secho(f"Git proxy set to {name}: {proxy_url}", fg="green")
|
||||||
|
|
||||||
|
|
||||||
@proxy_cmd.command("unset")
|
@proxy_cmd.command("unset")
|
||||||
def proxy_unset() -> None:
|
def proxy_unset() -> None:
|
||||||
"""Unset git proxy."""
|
"""Unset git http(s).proxy."""
|
||||||
_unset_git_proxy("http.proxy")
|
_unset_git_proxy("http.proxy")
|
||||||
_unset_git_proxy("https.proxy")
|
_unset_git_proxy("https.proxy")
|
||||||
click.secho("Git proxy unset", fg="green")
|
click.secho("Git proxy unset", fg="green")
|
||||||
|
|
||||||
|
|
||||||
|
@proxy_cmd.command("start")
|
||||||
|
@click.argument("name", shell_complete=_complete_proxy_names)
|
||||||
|
@click.option(
|
||||||
|
"--set-git",
|
||||||
|
is_flag=True,
|
||||||
|
help="Also set global git http(s).proxy (default: process only)",
|
||||||
|
)
|
||||||
|
def proxy_start(name: str, set_git: bool) -> None:
|
||||||
|
"""Start a proxy daemon in the background (loopback + Parallels socat).
|
||||||
|
|
||||||
|
Does not change global git config unless --set-git is passed.
|
||||||
|
CI should keep using per-job GIT_CONFIG_PARAMETERS instead.
|
||||||
|
"""
|
||||||
|
_start_daemon(name, set_git=set_git)
|
||||||
|
|
||||||
|
|
||||||
|
@proxy_cmd.command("stop")
|
||||||
|
@click.argument("name", required=False, shell_complete=_complete_proxy_names)
|
||||||
|
def proxy_stop(name: str | None) -> None:
|
||||||
|
"""Stop a proxy daemon. Omit NAME to stop the current one."""
|
||||||
|
_stop_daemon(name)
|
||||||
|
|
||||||
|
|
||||||
|
@proxy_cmd.command("restart")
|
||||||
|
@click.argument("name", required=False, shell_complete=_complete_proxy_names)
|
||||||
|
@click.option(
|
||||||
|
"--set-git",
|
||||||
|
is_flag=True,
|
||||||
|
help="Also set global git http(s).proxy after restart",
|
||||||
|
)
|
||||||
|
def proxy_restart(name: str | None, set_git: bool) -> None:
|
||||||
|
"""Restart a proxy daemon. Omit NAME to restart the current one."""
|
||||||
|
if name is None:
|
||||||
|
name = _current_daemon_name()
|
||||||
|
if name is None:
|
||||||
|
click.secho(
|
||||||
|
"No current proxy daemon (pass a name: "
|
||||||
|
"mytoolkit git proxy restart <name>)",
|
||||||
|
fg="yellow",
|
||||||
|
err=True,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
click.echo(f"Restarting current: {name}")
|
||||||
|
_stop_daemon(name)
|
||||||
|
_start_daemon(name, set_git=set_git)
|
||||||
|
|
||||||
|
|
||||||
@proxy_cmd.command("status")
|
@proxy_cmd.command("status")
|
||||||
def proxy_status() -> None:
|
@click.argument("name", required=False, shell_complete=_complete_proxy_names)
|
||||||
"""Show current git proxy status."""
|
def proxy_status(name: str | None) -> None:
|
||||||
|
"""Show proxy daemon + git config status. Omit NAME to list all."""
|
||||||
|
proxies = _get_proxies()
|
||||||
|
if name is not None:
|
||||||
|
_require_proxy(name)
|
||||||
|
proxies = {name: proxies[name]}
|
||||||
|
|
||||||
http_proxy = _get_git_proxy("http.proxy")
|
http_proxy = _get_git_proxy("http.proxy")
|
||||||
https_proxy = _get_git_proxy("https.proxy")
|
https_proxy = _get_git_proxy("https.proxy")
|
||||||
|
git_name = next(
|
||||||
|
(p for p, url in _get_proxies().items() if url == http_proxy), None
|
||||||
|
)
|
||||||
|
current = _current_daemon_name()
|
||||||
|
state = _read_state()
|
||||||
|
|
||||||
proxies = _get_proxies()
|
click.secho("Git proxy daemons:", fg="cyan")
|
||||||
current_name = next(
|
if not proxies:
|
||||||
(p for p, url in proxies.items() if url == http_proxy), None
|
click.echo(" (none configured in ~/.mytoolkit/config.json)")
|
||||||
|
for proxy_name, url in proxies.items():
|
||||||
|
port = _proxy_loopback_port(url)
|
||||||
|
markers = []
|
||||||
|
if proxy_name == current:
|
||||||
|
markers.append("current")
|
||||||
|
if proxy_name == git_name:
|
||||||
|
markers.append("git")
|
||||||
|
mark = f" [{', '.join(markers)}]" if markers else ""
|
||||||
|
if port is None:
|
||||||
|
click.echo(f" {proxy_name:12} {url} (non-loopback){mark}")
|
||||||
|
continue
|
||||||
|
loop_ok = _port_listening_on("127.0.0.1", port)
|
||||||
|
pids = _pids_listening_on("127.0.0.1", port)
|
||||||
|
pid_s = ",".join(str(p) for p in pids) if pids else "-"
|
||||||
|
loop_s = "UP" if loop_ok else "DOWN"
|
||||||
|
guest_ips = _parallels_host_ips()
|
||||||
|
if guest_ips:
|
||||||
|
guest_ok = all(_port_listening_on(ip, port) for ip in guest_ips)
|
||||||
|
guest_s = "socat UP" if guest_ok else "socat DOWN"
|
||||||
|
else:
|
||||||
|
guest_s = "socat n/a"
|
||||||
|
color = "green" if loop_ok else "black"
|
||||||
|
click.secho(
|
||||||
|
f" {proxy_name:12} {url} loopback={loop_s} pid={pid_s} "
|
||||||
|
f"{guest_s}{mark}",
|
||||||
|
fg=color,
|
||||||
)
|
)
|
||||||
|
|
||||||
click.secho("Git proxy status:", fg="cyan")
|
click.secho("\nGit config:", fg="cyan")
|
||||||
if current_name:
|
if git_name:
|
||||||
click.echo(f" Active: {current_name}")
|
click.echo(f" Active name: {git_name}")
|
||||||
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}")
|
||||||
|
if state:
|
||||||
port = (
|
click.echo(
|
||||||
_proxy_loopback_port(http_proxy)
|
f" State file: {state.get('name')} "
|
||||||
if http_proxy != "(not set)"
|
f"(set_git={state.get('set_git')}) → {_state_path()}"
|
||||||
else None
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Detailed listen for single-name or current loopback.
|
||||||
|
focus = name or current
|
||||||
|
if focus and focus in _get_proxies():
|
||||||
|
port = _proxy_loopback_port(_get_proxies()[focus])
|
||||||
if port is not None:
|
if port is not None:
|
||||||
_report_proxy_listen(port)
|
_report_proxy_listen(port)
|
||||||
else:
|
|
||||||
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 " "
|
|
||||||
click.echo(f" [{marker}] {proxy_name:12} {url}")
|
|
||||||
|
|
||||||
|
|
||||||
@proxy_cmd.command("list")
|
@proxy_cmd.command("list")
|
||||||
def proxy_list_cmd() -> None:
|
def proxy_list_cmd() -> None:
|
||||||
"""List available git proxies."""
|
"""List available git proxies from config."""
|
||||||
proxies = _get_proxies()
|
proxies = _get_proxies()
|
||||||
click.secho("Available proxies:", fg="cyan")
|
click.secho("Available proxies:", fg="cyan")
|
||||||
for name, url in proxies.items():
|
for name, url in proxies.items():
|
||||||
@@ -304,31 +735,18 @@ def proxy_list_cmd() -> None:
|
|||||||
@click.argument("name", shell_complete=_complete_proxy_names)
|
@click.argument("name", shell_complete=_complete_proxy_names)
|
||||||
@click.argument("command", nargs=-1, required=False)
|
@click.argument("command", nargs=-1, required=False)
|
||||||
def proxy_run(name: str, command: tuple[str, ...]) -> None:
|
def proxy_run(name: str, command: tuple[str, ...]) -> None:
|
||||||
"""Set proxy, run a command, auto-unset on exit.
|
"""Foreground helper: set git proxy, run process, restore on exit.
|
||||||
|
|
||||||
If no COMMAND is given, tries to run a program with the same name as the proxy.
|
Prefer ``start``/``stop`` for a background daemon (e.g. CI host).
|
||||||
If that program is not found, sets the proxy and waits for Ctrl-C.
|
If no COMMAND is given, tries to run a program with the same name as
|
||||||
|
the proxy. For loopback proxies, starts socat forwards for Parallels.
|
||||||
For loopback proxies (e.g. fastgithub on 127.0.0.1), start the proxy
|
|
||||||
process first, then socat-forward auto-detected Parallels host IPs so
|
|
||||||
Windows/Linux VMs can connect. Socat must not bind before the proxy.
|
|
||||||
"""
|
"""
|
||||||
proxies = _get_proxies()
|
_, proxy_url = _require_proxy(name)
|
||||||
if name not in proxies:
|
|
||||||
click.echo(f"Unknown proxy: {name}", err=True)
|
|
||||||
click.echo(f"Available: {', '.join(proxies.keys())}", err=True)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
old_http = _get_git_proxy("http.proxy")
|
old_http = _get_git_proxy("http.proxy")
|
||||||
old_https = _get_git_proxy("https.proxy")
|
old_https = _get_git_proxy("https.proxy")
|
||||||
|
|
||||||
proxy_url = proxies[name]
|
_set_git_proxies(proxy_url)
|
||||||
subprocess.run(
|
|
||||||
["git", "config", "--global", "http.proxy", proxy_url], check=True
|
|
||||||
)
|
|
||||||
subprocess.run(
|
|
||||||
["git", "config", "--global", "https.proxy", proxy_url], check=True
|
|
||||||
)
|
|
||||||
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] = []
|
forward_procs: list[subprocess.Popen] = []
|
||||||
@@ -369,28 +787,27 @@ def proxy_run(name: str, command: tuple[str, ...]) -> None:
|
|||||||
try:
|
try:
|
||||||
# Loopback proxy (fastgithub): ensure 127.0.0.1:port is up BEFORE socat.
|
# Loopback proxy (fastgithub): ensure 127.0.0.1:port is up BEFORE socat.
|
||||||
if port is not None:
|
if port is not None:
|
||||||
|
log_file: Path | None = None
|
||||||
if _port_listening_on("127.0.0.1", port):
|
if _port_listening_on("127.0.0.1", port):
|
||||||
click.secho(
|
click.secho(
|
||||||
f"{name} already listening on 127.0.0.1:{port}", fg="cyan"
|
f" already listening 127.0.0.1:{port}", fg="cyan"
|
||||||
)
|
)
|
||||||
elif cmd_to_run:
|
elif cmd_to_run:
|
||||||
click.secho(f"Running: {' '.join(cmd_to_run)}", fg="cyan")
|
proxy_proc, log_file = _spawn_proxy_process(
|
||||||
proxy_proc = subprocess.Popen(cmd_to_run)
|
name, cmd_to_run, new_session=False
|
||||||
ready = False
|
)
|
||||||
for _ in range(60):
|
if not _wait_for_listen("127.0.0.1", port, proc=proxy_proc):
|
||||||
if _port_listening_on("127.0.0.1", port):
|
|
||||||
ready = True
|
|
||||||
break
|
|
||||||
if proxy_proc.poll() is not None:
|
|
||||||
break
|
|
||||||
time.sleep(0.25)
|
|
||||||
if not ready:
|
|
||||||
click.secho(
|
click.secho(
|
||||||
f"ERROR: {name} failed to listen on 127.0.0.1:{port}",
|
f"ERROR: {name} failed to listen on 127.0.0.1:{port}"
|
||||||
|
+ (f" (see {log_file})" if log_file else ""),
|
||||||
fg="red",
|
fg="red",
|
||||||
err=True,
|
err=True,
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
click.secho(
|
||||||
|
f" listening 127.0.0.1:{port} pid={proxy_proc.pid}",
|
||||||
|
fg="green",
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
click.secho(
|
click.secho(
|
||||||
f"ERROR: nothing on 127.0.0.1:{port} and `{name}` not found",
|
f"ERROR: nothing on 127.0.0.1:{port} and `{name}` not found",
|
||||||
@@ -400,9 +817,26 @@ def proxy_run(name: str, command: tuple[str, ...]) -> None:
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
forward_procs = _start_parallels_forwards(port)
|
forward_procs = _start_parallels_forwards(port)
|
||||||
|
_write_state(
|
||||||
|
{
|
||||||
|
"name": name,
|
||||||
|
"url": proxy_url,
|
||||||
|
"port": port,
|
||||||
|
"proxy_pid": proxy_proc.pid if proxy_proc else (
|
||||||
|
(_pids_listening_on("127.0.0.1", port) or [None])[0]
|
||||||
|
),
|
||||||
|
"forward_pids": [p.pid for p in forward_procs if p.pid],
|
||||||
|
"set_git": True,
|
||||||
|
"old_http": old_http,
|
||||||
|
"old_https": old_https,
|
||||||
|
}
|
||||||
|
)
|
||||||
_report_proxy_listen(port)
|
_report_proxy_listen(port)
|
||||||
|
click.echo("")
|
||||||
|
if log_file is not None:
|
||||||
|
click.echo(f" proxy log: {log_file}")
|
||||||
click.secho(
|
click.secho(
|
||||||
"Press Ctrl-C to stop and auto-unset proxy\n", fg="cyan"
|
"Press Ctrl-C to stop and auto-unset proxy", fg="cyan"
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
if proxy_proc is not None:
|
if proxy_proc is not None:
|
||||||
@@ -416,8 +850,9 @@ def proxy_run(name: str, command: tuple[str, ...]) -> None:
|
|||||||
# Non-loopback proxy: original behavior.
|
# Non-loopback proxy: original behavior.
|
||||||
if cmd_to_run:
|
if cmd_to_run:
|
||||||
click.secho(f"Running: {' '.join(cmd_to_run)}", fg="cyan")
|
click.secho(f"Running: {' '.join(cmd_to_run)}", fg="cyan")
|
||||||
|
click.echo("")
|
||||||
click.secho(
|
click.secho(
|
||||||
"Press Ctrl-C to stop and auto-unset proxy\n", fg="cyan"
|
"Press Ctrl-C to stop and auto-unset proxy", fg="cyan"
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
proxy_proc = subprocess.Popen(cmd_to_run)
|
proxy_proc = subprocess.Popen(cmd_to_run)
|
||||||
@@ -426,6 +861,7 @@ def proxy_run(name: str, command: tuple[str, ...]) -> None:
|
|||||||
pass
|
pass
|
||||||
sys.exit(proxy_proc.returncode if proxy_proc is not None else 0)
|
sys.exit(proxy_proc.returncode if proxy_proc is not None else 0)
|
||||||
|
|
||||||
|
click.echo("")
|
||||||
click.secho(
|
click.secho(
|
||||||
"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"
|
||||||
)
|
)
|
||||||
@@ -435,3 +871,4 @@ def proxy_run(name: str, command: tuple[str, ...]) -> None:
|
|||||||
pass
|
pass
|
||||||
finally:
|
finally:
|
||||||
cleanup()
|
cleanup()
|
||||||
|
_clear_state()
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
"""Unit tests for git proxy helpers."""
|
||||||
|
|
||||||
|
from mytoolkit.commands.git import _proxy_loopback_port
|
||||||
|
|
||||||
|
|
||||||
|
def test_proxy_loopback_port_http():
|
||||||
|
assert _proxy_loopback_port("http://127.0.0.1:38457") == 38457
|
||||||
|
assert _proxy_loopback_port("http://localhost:10080") == 10080
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
@@ -541,7 +541,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mytoolkit"
|
name = "mytoolkit"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
{ name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
||||||
|
|||||||
Reference in New Issue
Block a user