File size: 6,968 Bytes
e01ce83
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
from __future__ import annotations

import os
import secrets
import socket
import threading
import time
import warnings
from typing import Any

import httpx
import uvicorn
from uvicorn.config import Config

from trackio.launch_utils import colab_check, is_hosted_notebook

INITIAL_PORT_VALUE = int(os.getenv("GRADIO_SERVER_PORT", "7860"))
TRY_NUM_PORTS = int(os.getenv("GRADIO_NUM_PORTS", "100"))
LOCALHOST_NAME = os.getenv("GRADIO_SERVER_NAME", "127.0.0.1")


class _UvicornServer(uvicorn.Server):
    def install_signal_handlers(self) -> None:
        pass

    def run_in_thread(self) -> None:
        self.thread = threading.Thread(target=self.run, daemon=True)
        self.thread.start()
        start = time.time()
        while not self.started:
            time.sleep(1e-3)
            if time.time() - start > 60:
                raise RuntimeError(
                    "Server failed to start. Please check that the port is available."
                )


def _bind_host(server_name: str) -> str:
    if server_name.startswith("[") and server_name.endswith("]"):
        return server_name[1:-1]
    return server_name


def start_server(
    app: Any,
    server_name: str | None = None,
    server_port: int | None = None,
    ssl_keyfile: str | None = None,
    ssl_certfile: str | None = None,
    ssl_keyfile_password: str | None = None,
) -> tuple[str, int, str, _UvicornServer]:
    server_name = server_name or LOCALHOST_NAME
    url_host_name = "localhost" if server_name == "0.0.0.0" else server_name

    host = _bind_host(server_name)

    server_ports = (
        [server_port]
        if server_port is not None
        else range(INITIAL_PORT_VALUE, INITIAL_PORT_VALUE + TRY_NUM_PORTS)
    )

    port_used = None
    server = None
    for port in server_ports:
        try:
            s = socket.socket()
            s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            s.bind((LOCALHOST_NAME, port))
            s.close()
            config = Config(
                app=app,
                port=port,
                host=host,
                log_level="warning",
                ssl_keyfile=ssl_keyfile,
                ssl_certfile=ssl_certfile,
                ssl_keyfile_password=ssl_keyfile_password,
            )
            server = _UvicornServer(config=config)
            server.run_in_thread()
            port_used = port
            break
        except (OSError, RuntimeError):
            continue
    else:
        raise OSError(
            f"Cannot find empty port in range: {min(server_ports)}-{max(server_ports)}. "
            "Set GRADIO_SERVER_PORT or pass server_port to trackio.show()."
        )

    assert port_used is not None and server is not None

    if ssl_keyfile is not None:
        path_to_local_server = f"https://{url_host_name}:{port_used}/"
    else:
        path_to_local_server = f"http://{url_host_name}:{port_used}/"

    return server_name, port_used, path_to_local_server, server


def launch_trackio_dashboard(
    starlette_app: Any,
    *,
    server_name: str | None = None,
    server_port: int | None = None,
    share: bool | None = None,
    share_server_address: str | None = None,
    share_server_protocol: str | None = None,
    share_server_tls_certificate: str | None = None,
    mcp_server: bool = False,
    ssl_verify: bool = True,
    quiet: bool = False,
) -> tuple[str | None, str | None, str | None, Any]:
    from pathlib import Path

    from trackio._vendor.networking import normalize_share_url, setup_tunnel
    from trackio._vendor.tunneling import BINARY_PATH

    is_colab = colab_check()
    is_hosted_nb = is_hosted_notebook()
    space_id = os.getenv("SPACE_ID")

    if share is None:
        if is_colab or is_hosted_nb:
            if not quiet:
                print(
                    "It looks like you are running Trackio on a hosted Jupyter notebook, which requires "
                    "`share=True`. Automatically setting `share=True` "
                    "(set `share=False` in `show()` to disable).\n"
                )
            share = True
        else:
            share = os.getenv("GRADIO_SHARE", "").lower() == "true"

    sn = server_name
    if sn is None and os.getenv("SYSTEM") == "spaces":
        sn = "0.0.0.0"
    elif sn is None:
        sn = LOCALHOST_NAME

    server_name_r, server_port_r, local_url, uv_server = start_server(
        starlette_app,
        server_name=sn,
        server_port=server_port,
    )

    local_api_url = f"{local_url.rstrip('/')}/gradio_api/"
    try:
        httpx.get(f"{local_api_url}startup-events", verify=ssl_verify, timeout=10)
    except Exception as e:
        raise RuntimeError(
            f"Could not reach startup-events at {local_api_url}startup-events: {e}"
        ) from e

    if share and space_id:
        warnings.warn("Setting share=True is not supported on Hugging Face Spaces")
        share = False

    share_url: str | None = None
    if share:
        try:
            share_tok = secrets.token_urlsafe(32)
            proto = share_server_protocol or (
                "http" if share_server_address is not None else "https"
            )
            raw = setup_tunnel(
                local_host=server_name_r,
                local_port=server_port_r,
                share_token=share_tok,
                share_server_address=share_server_address,
                share_server_tls_certificate=share_server_tls_certificate,
            )
            share_url = normalize_share_url(raw, proto)
            if not quiet:
                print(f"* Running on public URL: {share_url}")
                print(
                    "\nThis share link expires in 1 week. For permanent hosting, deploy to Hugging Face Spaces."
                )
        except Exception as e:
            share_url = None
            if not quiet:
                from trackio._vendor.gradio_exceptions import ChecksumMismatchError

                if isinstance(e, ChecksumMismatchError):
                    print(
                        "\nCould not create share link. Checksum mismatch for frpc binary."
                    )
                elif Path(BINARY_PATH).exists():
                    print(
                        "\nCould not create share link. Check your internet connection or https://status.gradio.app."
                    )
                else:
                    print(
                        f"\nCould not create share link. Missing frpc at {BINARY_PATH}. {e}"
                    )

    if not share_url and not quiet:
        print("* To create a public link, set `share=True` in `trackio.show()`.")

    if mcp_server and not quiet:
        base = share_url or local_url.rstrip("/")
        print(f"\n* MCP streamable HTTP: {base}/gradio_api/mcp/")

    return local_url, share_url, local_api_url, uv_server


def url_ok_local(local_url: str) -> bool:
    from trackio._vendor.networking import url_ok

    return url_ok(local_url)