auto find next available local port
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import socket
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -49,6 +50,19 @@ def _run_node(cfg: dict) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _first_available_port(host: str, start: int = 7000, attempts: int = 100) -> int:
|
||||||
|
"""Return the first TCP port bindable on host, starting at start."""
|
||||||
|
bind_host = "" if host == "0.0.0.0" else host
|
||||||
|
for port in range(start, start + attempts):
|
||||||
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
||||||
|
try:
|
||||||
|
sock.bind((bind_host, port))
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
|
return port
|
||||||
|
raise OSError(f"no available TCP port in range {start}-{start + attempts - 1}")
|
||||||
|
|
||||||
|
|
||||||
def _cmd_default(args) -> int:
|
def _cmd_default(args) -> int:
|
||||||
"""No subcommand: wizard if no config, else start with saved config."""
|
"""No subcommand: wizard if no config, else start with saved config."""
|
||||||
from .config import load_config, save_config, merge_cli_overrides
|
from .config import load_config, save_config, merge_cli_overrides
|
||||||
@@ -145,7 +159,7 @@ def _cmd_start(args) -> int:
|
|||||||
# Build a transient config from flags (don't write to disk)
|
# Build a transient config from flags (don't write to disk)
|
||||||
cfg = dict(DEFAULTS)
|
cfg = dict(DEFAULTS)
|
||||||
cfg["tracker_url"] = args.tracker
|
cfg["tracker_url"] = args.tracker
|
||||||
cfg["port"] = args.port
|
cfg["port"] = args.port if args.port is not None else _first_available_port(args.host)
|
||||||
if args.model_id is None and "/" in args.model:
|
if args.model_id is None and "/" in args.model:
|
||||||
cfg["model_hf_repo"] = args.model
|
cfg["model_hf_repo"] = args.model
|
||||||
cfg["model_name"] = args.model.split("/")[-1]
|
cfg["model_name"] = args.model.split("/")[-1]
|
||||||
@@ -236,7 +250,7 @@ def main() -> None:
|
|||||||
# start subcommand (legacy / backward-compat)
|
# start subcommand (legacy / backward-compat)
|
||||||
start_cmd = subparsers.add_parser("start", help="Start node (legacy flags)")
|
start_cmd = subparsers.add_parser("start", help="Start node (legacy flags)")
|
||||||
start_cmd.add_argument("--tracker", default="http://localhost:8080")
|
start_cmd.add_argument("--tracker", default="http://localhost:8080")
|
||||||
start_cmd.add_argument("--port", type=int, default=7000)
|
start_cmd.add_argument("--port", type=int)
|
||||||
start_cmd.add_argument("--model", default="stub-model")
|
start_cmd.add_argument("--model", default="stub-model")
|
||||||
start_cmd.add_argument("--model-id", help="HuggingFace repo ID")
|
start_cmd.add_argument("--model-id", help="HuggingFace repo ID")
|
||||||
start_cmd.add_argument("--shard-start", type=int)
|
start_cmd.add_argument("--shard-start", type=int)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import socket
|
||||||
import sys
|
import sys
|
||||||
import types
|
import types
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -387,6 +388,43 @@ def test_legacy_start_treats_repo_model_as_model_id(monkeypatch):
|
|||||||
assert captured["model_id"] == "Qwen/Qwen2.5-0.5B-Instruct"
|
assert captured["model_id"] == "Qwen/Qwen2.5-0.5B-Instruct"
|
||||||
|
|
||||||
|
|
||||||
|
def test_legacy_start_without_port_uses_next_available_port(monkeypatch):
|
||||||
|
"""Omitting --port skips an occupied default port before startup loads the model."""
|
||||||
|
from meshnet_node.cli import main
|
||||||
|
|
||||||
|
captured = {}
|
||||||
|
|
||||||
|
def fake_run_startup(*args, **kwargs):
|
||||||
|
captured.update(kwargs)
|
||||||
|
class _FakeNode:
|
||||||
|
chat_completion_count = 0
|
||||||
|
def stop(self): pass
|
||||||
|
return _FakeNode()
|
||||||
|
|
||||||
|
occupied = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
occupied.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
occupied.bind(("127.0.0.1", 7000))
|
||||||
|
occupied.listen(1)
|
||||||
|
try:
|
||||||
|
monkeypatch.setattr(sys, "argv", [
|
||||||
|
"meshnet-node", "start",
|
||||||
|
"--tracker", "http://192.168.0.179:8081",
|
||||||
|
"--model", "Qwen/Qwen2.5-0.5B-Instruct",
|
||||||
|
"--host", "127.0.0.1",
|
||||||
|
])
|
||||||
|
|
||||||
|
with patch("meshnet_node.startup.run_startup", side_effect=fake_run_startup):
|
||||||
|
with patch("time.sleep", side_effect=KeyboardInterrupt):
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except SystemExit as exc:
|
||||||
|
assert exc.code == 0
|
||||||
|
finally:
|
||||||
|
occupied.close()
|
||||||
|
|
||||||
|
assert captured["port"] == 7001
|
||||||
|
|
||||||
|
|
||||||
def test_default_cli_passes_advertise_host(monkeypatch):
|
def test_default_cli_passes_advertise_host(monkeypatch):
|
||||||
"""The documented no-subcommand LAN flag reaches startup."""
|
"""The documented no-subcommand LAN flag reaches startup."""
|
||||||
from meshnet_node.cli import main
|
from meshnet_node.cli import main
|
||||||
|
|||||||
Reference in New Issue
Block a user