cjovs commited on
Commit
8a530f3
·
verified ·
1 Parent(s): a3c82d1

Fix HF Space web UI gateway port handoff

Browse files
docker/entrypoint.sh CHANGED
@@ -102,10 +102,11 @@ if [ $# -eq 0 ] && [ -n "${SPACE_ID:-}${SPACE_HOST:-}" ]; then
102
  export HERMES_BIN
103
 
104
  export API_SERVER_ENABLED="${API_SERVER_ENABLED:-true}"
105
- export API_SERVER_HOST="${API_SERVER_HOST:-127.0.0.1}"
106
- export API_SERVER_PORT="${API_SERVER_PORT:-8642}"
107
  export PORT="${PORT:-7860}"
108
- export UPSTREAM="${UPSTREAM:-http://127.0.0.1:${API_SERVER_PORT}}"
 
 
 
109
  export AUTH_DISABLED="${AUTH_DISABLED:-false}"
110
  export AUTH_TOKEN="${AUTH_TOKEN:-wangjx0515}"
111
 
 
102
  export HERMES_BIN
103
 
104
  export API_SERVER_ENABLED="${API_SERVER_ENABLED:-true}"
 
 
105
  export PORT="${PORT:-7860}"
106
+ # Let hermes-web-ui's GatewayManager own port selection via config.yaml.
107
+ # Exporting a default API_SERVER_PORT here forces child gateway processes
108
+ # back onto 8642, which breaks health checks when the UI assigns 8643+.
109
+ export UPSTREAM="${UPSTREAM:-$(python3 "$RUNTIME_HELPER" api-server-upstream)}"
110
  export AUTH_DISABLED="${AUTH_DISABLED:-false}"
111
  export AUTH_TOKEN="${AUTH_TOKEN:-wangjx0515}"
112
 
docker/space_runtime.py CHANGED
@@ -6,10 +6,19 @@ import os
6
  import shlex
7
  import sys
8
  from collections.abc import Mapping, Sequence
 
9
 
10
  SPACE_WEB_ARGS = ["dashboard", "--host", "0.0.0.0", "--port", "7860", "--no-open"]
11
  DEFAULT_HERMES_HOME = "/opt/data"
12
  SPACE_HERMES_HOME = "/data/hermes"
 
 
 
 
 
 
 
 
13
 
14
 
15
  def is_hf_space(env: Mapping[str, str]) -> bool:
@@ -39,6 +48,94 @@ def resolve_hermes_command(
39
  return [executable, *resolve_default_command(env, argv)]
40
 
41
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  def _command_string(env: Mapping[str, str], argv: Sequence[str]) -> str:
43
  executable = env.get("HERMES_EXECUTABLE", "hermes")
44
  return shlex.join(resolve_hermes_command(env, argv, executable=executable))
@@ -56,6 +153,14 @@ def main(argv: Sequence[str] | None = None) -> int:
56
  print(_command_string(env, argv[1:]))
57
  return 0
58
 
 
 
 
 
 
 
 
 
59
  if argv[0] == "exec":
60
  print_only = False
61
  args = argv[1:]
@@ -73,7 +178,9 @@ def main(argv: Sequence[str] | None = None) -> int:
73
  os.execvpe(command[0], command, dict(env))
74
 
75
  prog = os.path.basename(sys.argv[0] or "space_runtime.py")
76
- raise SystemExit(f"Usage: {prog} [home|command|exec [--print-only] [args...]]")
 
 
77
 
78
 
79
  if __name__ == "__main__":
 
6
  import shlex
7
  import sys
8
  from collections.abc import Mapping, Sequence
9
+ from pathlib import Path
10
 
11
  SPACE_WEB_ARGS = ["dashboard", "--host", "0.0.0.0", "--port", "7860", "--no-open"]
12
  DEFAULT_HERMES_HOME = "/opt/data"
13
  SPACE_HERMES_HOME = "/data/hermes"
14
+ DEFAULT_API_SERVER_PORT = "8642"
15
+ DEFAULT_API_SERVER_UPSTREAM_HOST = "127.0.0.1"
16
+ API_SERVER_PORT_PATHS = (
17
+ ("platforms", "api_server", "extra", "port"),
18
+ ("platforms", "api_server", "port"),
19
+ ("api_server", "extra", "port"),
20
+ ("api_server", "port"),
21
+ )
22
 
23
 
24
  def is_hf_space(env: Mapping[str, str]) -> bool:
 
48
  return [executable, *resolve_default_command(env, argv)]
49
 
50
 
51
+ def _resolve_config_path(env: Mapping[str, str]) -> Path:
52
+ return Path(resolve_hermes_home(env)) / "config.yaml"
53
+
54
+
55
+ def _strip_inline_comment(text: str) -> str:
56
+ quote: str | None = None
57
+ result: list[str] = []
58
+ for char in text:
59
+ if char in {"'", '"'}:
60
+ if quote == char:
61
+ quote = None
62
+ elif quote is None:
63
+ quote = char
64
+ if char == "#" and quote is None:
65
+ break
66
+ result.append(char)
67
+ return "".join(result).rstrip()
68
+
69
+
70
+ def _parse_scalar(text: str) -> str | None:
71
+ value = _strip_inline_comment(text).strip()
72
+ if not value:
73
+ return None
74
+ if len(value) >= 2 and value[0] == value[-1] and value[0] in {"'", '"'}:
75
+ return value[1:-1]
76
+ return value
77
+
78
+
79
+ def _read_yaml_path_value(text: str, target_paths: Sequence[tuple[str, ...]]) -> str | None:
80
+ stack: list[tuple[int, str]] = []
81
+ for raw_line in text.splitlines():
82
+ line = raw_line.rstrip()
83
+ stripped = line.lstrip(" ")
84
+ if not stripped or stripped.startswith("#") or stripped.startswith("-"):
85
+ continue
86
+
87
+ indent = len(line) - len(stripped)
88
+ key, separator, remainder = stripped.partition(":")
89
+ if not separator:
90
+ continue
91
+
92
+ while stack and indent <= stack[-1][0]:
93
+ stack.pop()
94
+
95
+ path = tuple(part for _, part in stack) + (key.strip(),)
96
+ value = _parse_scalar(remainder)
97
+ if path in target_paths and value is not None:
98
+ return value
99
+ if value is None:
100
+ stack.append((indent, key.strip()))
101
+
102
+ return None
103
+
104
+
105
+ def _config_api_server_port(env: Mapping[str, str]) -> str | None:
106
+ config_path = _resolve_config_path(env)
107
+ try:
108
+ config_text = config_path.read_text(encoding="utf-8")
109
+ except OSError:
110
+ return None
111
+
112
+ value = _read_yaml_path_value(config_text, API_SERVER_PORT_PATHS)
113
+ if value is None:
114
+ return None
115
+
116
+ try:
117
+ port = int(value, 10)
118
+ except ValueError:
119
+ return None
120
+ if 0 < port <= 65535:
121
+ return str(port)
122
+ return None
123
+
124
+
125
+ def resolve_api_server_port(env: Mapping[str, str]) -> str:
126
+ explicit = (env.get("API_SERVER_PORT") or "").strip()
127
+ if explicit:
128
+ return explicit
129
+ return _config_api_server_port(env) or DEFAULT_API_SERVER_PORT
130
+
131
+
132
+ def resolve_api_server_upstream(env: Mapping[str, str]) -> str:
133
+ explicit = (env.get("UPSTREAM") or "").strip()
134
+ if explicit:
135
+ return explicit
136
+ return f"http://{DEFAULT_API_SERVER_UPSTREAM_HOST}:{resolve_api_server_port(env)}"
137
+
138
+
139
  def _command_string(env: Mapping[str, str], argv: Sequence[str]) -> str:
140
  executable = env.get("HERMES_EXECUTABLE", "hermes")
141
  return shlex.join(resolve_hermes_command(env, argv, executable=executable))
 
153
  print(_command_string(env, argv[1:]))
154
  return 0
155
 
156
+ if argv[0] == "api-server-port":
157
+ print(resolve_api_server_port(env))
158
+ return 0
159
+
160
+ if argv[0] == "api-server-upstream":
161
+ print(resolve_api_server_upstream(env))
162
+ return 0
163
+
164
  if argv[0] == "exec":
165
  print_only = False
166
  args = argv[1:]
 
178
  os.execvpe(command[0], command, dict(env))
179
 
180
  prog = os.path.basename(sys.argv[0] or "space_runtime.py")
181
+ raise SystemExit(
182
+ f"Usage: {prog} [home|command|api-server-port|api-server-upstream|exec [--print-only] [args...]]"
183
+ )
184
 
185
 
186
  if __name__ == "__main__":
tests/hermes_cli/test_space_runtime.py CHANGED
@@ -1,6 +1,8 @@
1
  from pathlib import Path
2
 
3
  from docker.space_runtime import (
 
 
4
  resolve_default_command,
5
  resolve_hermes_command,
6
  resolve_hermes_home,
@@ -12,7 +14,7 @@ def test_space_runtime_uses_dashboard_defaults():
12
 
13
  assert resolve_hermes_home(env) == "/data/hermes"
14
  assert resolve_default_command(env, []) == [
15
- "web",
16
  "--host",
17
  "0.0.0.0",
18
  "--port",
@@ -38,7 +40,7 @@ def test_space_runtime_builds_hermes_command():
38
 
39
  assert resolve_hermes_command(env, [], executable="hermes") == [
40
  "hermes",
41
- "web",
42
  "--host",
43
  "0.0.0.0",
44
  "--port",
@@ -51,6 +53,43 @@ def test_non_space_runtime_defaults_to_cli_command():
51
  assert resolve_hermes_command({}, [], executable="hermes") == ["hermes"]
52
 
53
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  def test_readme_declares_docker_space_metadata():
55
  readme = Path("README.md").read_text(encoding="utf-8")
56
 
 
1
  from pathlib import Path
2
 
3
  from docker.space_runtime import (
4
+ resolve_api_server_port,
5
+ resolve_api_server_upstream,
6
  resolve_default_command,
7
  resolve_hermes_command,
8
  resolve_hermes_home,
 
14
 
15
  assert resolve_hermes_home(env) == "/data/hermes"
16
  assert resolve_default_command(env, []) == [
17
+ "dashboard",
18
  "--host",
19
  "0.0.0.0",
20
  "--port",
 
40
 
41
  assert resolve_hermes_command(env, [], executable="hermes") == [
42
  "hermes",
43
+ "dashboard",
44
  "--host",
45
  "0.0.0.0",
46
  "--port",
 
53
  assert resolve_hermes_command({}, [], executable="hermes") == ["hermes"]
54
 
55
 
56
+ def test_space_runtime_reads_api_server_port_from_config(tmp_path):
57
+ config_path = tmp_path / "config.yaml"
58
+ config_path.write_text(
59
+ "platforms:\n"
60
+ " api_server:\n"
61
+ " extra:\n"
62
+ " port: 8643\n",
63
+ encoding="utf-8",
64
+ )
65
+
66
+ env = {"SPACE_ID": "cjovs/HermesAgent", "HERMES_HOME": str(tmp_path)}
67
+
68
+ assert resolve_api_server_port(env) == "8643"
69
+ assert resolve_api_server_upstream(env) == "http://127.0.0.1:8643"
70
+
71
+
72
+ def test_space_runtime_preserves_explicit_api_server_overrides(tmp_path):
73
+ config_path = tmp_path / "config.yaml"
74
+ config_path.write_text(
75
+ "platforms:\n"
76
+ " api_server:\n"
77
+ " extra:\n"
78
+ " port: 8643\n",
79
+ encoding="utf-8",
80
+ )
81
+
82
+ env = {
83
+ "SPACE_ID": "cjovs/HermesAgent",
84
+ "HERMES_HOME": str(tmp_path),
85
+ "API_SERVER_PORT": "9999",
86
+ "UPSTREAM": "http://127.0.0.1:9998",
87
+ }
88
+
89
+ assert resolve_api_server_port(env) == "9999"
90
+ assert resolve_api_server_upstream(env) == "http://127.0.0.1:9998"
91
+
92
+
93
  def test_readme_declares_docker_space_metadata():
94
  readme = Path("README.md").read_text(encoding="utf-8")
95
 
tests/hermes_cli/test_space_web_ui_runtime_requirements.py CHANGED
@@ -28,3 +28,9 @@ def test_space_entrypoint_marks_hf_space_as_container_for_web_ui():
28
 
29
  assert 'touch "/.dockerenv"' in entrypoint
30
  assert "SPACE_ID" in entrypoint or "SPACE_HOST" in entrypoint
 
 
 
 
 
 
 
28
 
29
  assert 'touch "/.dockerenv"' in entrypoint
30
  assert "SPACE_ID" in entrypoint or "SPACE_HOST" in entrypoint
31
+
32
+
33
+ def test_space_entrypoint_does_not_force_api_server_port_defaults():
34
+ entrypoint = Path("docker/entrypoint.sh").read_text(encoding="utf-8")
35
+
36
+ assert 'export API_SERVER_PORT="${API_SERVER_PORT:-8642}"' not in entrypoint