mirror of
https://github.com/gradio-app/gradio.git
synced 2025-04-12 12:40:29 +08:00
Switch from SSH tunneling to FRP (#2509)
* FRP Poc (#2396) * FRP Poc * Gracefully handle exceptions in thread tunneling * comments * Fix share error message when files are built locally (#2502) * fix share error message * changelog * formatting * tunneling rename * version * formatting * remove test * changelog * version Co-authored-by: Abubakar Abid <abubakar@huggingface.co> Co-authored-by: Wauplin <lucainp@gmail.com> * 2509 * updated url to testing.gradiodash.com * gradiotesting * format, version * gradio.live * temp fix for https * remove unnecessary tests * version * updated tunnel logic * formatting and tests * load testing * changes * Make private method + generate privilege key (#2519) * rm load test * frp * formatting * Update run.py * Update run.py * updated message * share=True * [DO NOT MERGE] Add pymux for FRP (#2747) * Add pymux for FRP * Cleaning pyamux * Cleaning pyamux + make it work * Forgot the thread * Reformat * some logs to be removed afterwards * added share to hello world * Transform into object * I guess it's cleaner now * Handle 404 + Transform to object * Fix params names * Add debug * windows fix Co-authored-by: Wauplin <lucainp@gmail.com> Co-authored-by: Abubakar Abid <abubakar@huggingface.co> * removed share=True * formatting * hello world notebook * version * fixes * formatting * testing tunneling exists * tests * formatting * lint * Remove asyncio + kill proc on exit * version * version * update changelog * explicit message about reporting Co-authored-by: Adrien <adrien@xcid.fr> Co-authored-by: Wauplin <lucainp@gmail.com> Co-authored-by: Ali Abid <aabid94@gmail.com>
This commit is contained in:
parent
5182460886
commit
53005ab88a
8
.gitignore
vendored
8
.gitignore
vendored
@ -44,4 +44,10 @@ workspace.code-workspace
|
||||
*.h5
|
||||
|
||||
# log files
|
||||
.pnpm-debug.log
|
||||
.pnpm-debug.log
|
||||
|
||||
# Local virtualenv for devs
|
||||
.venv*
|
||||
|
||||
# FRP
|
||||
gradio/frpc_*
|
17
CHANGELOG.md
17
CHANGELOG.md
@ -1,7 +1,21 @@
|
||||
# Upcoming Release
|
||||
|
||||
## New Features:
|
||||
No changes to highlight.
|
||||
|
||||
### New Shareable Links
|
||||
|
||||
Replaces tunneling logic based on ssh port-forwarding to that based on `frp` by [XciD](https://github.com/XciD) and [Wauplin](https://github.com/Wauplin) in [PR 2509](https://github.com/gradio-app/gradio/pull/2509)
|
||||
|
||||
You don't need to do anything differently, but when you set `share=True` in `launch()`,
|
||||
you'll get this message and a public link that look a little bit different:
|
||||
|
||||
```
|
||||
Setting up a public link... we have recently upgraded the way public links are generated. If you encounter any problems, please downgrade to gradio version 3.13.0
|
||||
.
|
||||
Running on public URL: https://bec81a83-5b5c-471e.gradio.live
|
||||
```
|
||||
|
||||
These links are a more secure and scalable way to create shareable demos!
|
||||
|
||||
## Bug Fixes:
|
||||
* Allows `gr.Dataframe()` to take a `pandas.DataFrame` that includes numpy array and other types as its initial value, by [@abidlabs](https://github.com/abidlabs) in [PR 2804](https://github.com/gradio-app/gradio/pull/2804)
|
||||
@ -654,7 +668,6 @@ No changes to highlight.
|
||||
try to use `Series` or `Parallel` with `Blocks` by [@abidlabs](https://github.com/abidlabs) in [PR 2543](https://github.com/gradio-app/gradio/pull/2543)
|
||||
* Adds support for audio samples that are in `float64`, `float16`, or `uint16` formats by [@abidlabs](https://github.com/abidlabs) in [PR 2545](https://github.com/gradio-app/gradio/pull/2545)
|
||||
|
||||
|
||||
## Contributors Shoutout:
|
||||
No changes to highlight.
|
||||
|
||||
|
@ -1 +1 @@
|
||||
{"cells": [{"cell_type": "markdown", "id": 302934307671667531413257853548643485645, "metadata": {}, "source": ["# Gradio Demo: hello_world\n", "### The simplest possible Gradio demo. It wraps a 'Hello {name}!' function in an Interface that accepts and returns text.\n", " "]}, {"cell_type": "code", "execution_count": null, "id": 272996653310673477252411125948039410165, "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": 288918539441861185822528903084949547379, "metadata": {}, "outputs": [], "source": ["import gradio as gr\n", "\n", "def greet(name):\n", " return \"Hello \" + name + \"!\"\n", "\n", "demo = gr.Interface(fn=greet, inputs=\"text\", outputs=\"text\")\n", " \n", "demo.launch() "]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}
|
||||
{"cells": [{"cell_type": "markdown", "id": 302934307671667531413257853548643485645, "metadata": {}, "source": ["# Gradio Demo: hello_world\n", "### The simplest possible Gradio demo. It wraps a 'Hello {name}!' function in an Interface that accepts and returns text.\n", " "]}, {"cell_type": "code", "execution_count": null, "id": 272996653310673477252411125948039410165, "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": 288918539441861185822528903084949547379, "metadata": {}, "outputs": [], "source": ["import gradio as gr\n", "\n", "def greet(name):\n", " return \"Hello \" + name + \"!\"\n", "\n", "demo = gr.Interface(fn=greet, inputs=\"text\", outputs=\"text\")\n", " \n", "if __name__ == \"__main__\":\n", " demo.launch() "]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}
|
@ -5,4 +5,5 @@ def greet(name):
|
||||
|
||||
demo = gr.Interface(fn=greet, inputs="text", outputs="text")
|
||||
|
||||
demo.launch()
|
||||
if __name__ == "__main__":
|
||||
demo.launch()
|
@ -44,6 +44,7 @@ from gradio.context import Context
|
||||
from gradio.deprecation import check_deprecated_parameters
|
||||
from gradio.documentation import document, set_documentation_group
|
||||
from gradio.exceptions import DuplicateBlockError, InvalidApiName
|
||||
from gradio.tunneling import CURRENT_TUNNELS
|
||||
from gradio.utils import (
|
||||
TupleNoPrint,
|
||||
check_function_inputs_match,
|
||||
@ -1415,8 +1416,14 @@ class Blocks(BlockContext):
|
||||
raise RuntimeError("Share is not supported when you are in Spaces")
|
||||
try:
|
||||
if self.share_url is None:
|
||||
share_url = networking.setup_tunnel(self.server_port, None)
|
||||
self.share_url = share_url
|
||||
print(
|
||||
"\nSetting up a public link... we have recently upgraded the "
|
||||
"way public links are generated. If you encounter any "
|
||||
"problems, please report the issue and downgrade to gradio version 3.13.0\n."
|
||||
)
|
||||
self.share_url = networking.setup_tunnel(
|
||||
self.server_name, self.server_port
|
||||
)
|
||||
print(strings.en["SHARE_LINK_DISPLAY"].format(self.share_url))
|
||||
if not (quiet):
|
||||
print(strings.en["SHARE_LINK_MESSAGE"])
|
||||
@ -1606,6 +1613,8 @@ class Blocks(BlockContext):
|
||||
except (KeyboardInterrupt, OSError):
|
||||
print("Keyboard interruption in main thread... closing server.")
|
||||
self.server.close()
|
||||
for tunnel in CURRENT_TUNNELS:
|
||||
tunnel.kill()
|
||||
|
||||
def attach_load_events(self):
|
||||
"""Add a load event for every component whose initial value should be randomized."""
|
||||
|
@ -16,7 +16,7 @@ import requests
|
||||
import uvicorn
|
||||
|
||||
from gradio.routes import App
|
||||
from gradio.tunneling import create_tunnel
|
||||
from gradio.tunneling import Tunnel
|
||||
|
||||
if TYPE_CHECKING: # Only import for type checking (to avoid circular imports).
|
||||
from gradio.blocks import Blocks
|
||||
@ -26,7 +26,7 @@ if TYPE_CHECKING: # Only import for type checking (to avoid circular imports).
|
||||
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")
|
||||
GRADIO_API_SERVER = "https://api.gradio.app/v1/tunnel-request"
|
||||
GRADIO_API_SERVER = "https://api.gradio.app/v2/tunnel-request"
|
||||
|
||||
|
||||
class Server(uvicorn.Server):
|
||||
@ -157,14 +157,15 @@ def start_server(
|
||||
return server_name, port, path_to_local_server, app, server
|
||||
|
||||
|
||||
def setup_tunnel(local_server_port: int, endpoint: str) -> str:
|
||||
response = requests.get(
|
||||
endpoint + "/v1/tunnel-request" if endpoint is not None else GRADIO_API_SERVER
|
||||
)
|
||||
def setup_tunnel(local_host: str, local_port: int) -> str:
|
||||
response = requests.get(GRADIO_API_SERVER)
|
||||
if response and response.status_code == 200:
|
||||
try:
|
||||
payload = response.json()[0]
|
||||
return create_tunnel(payload, LOCALHOST_NAME, local_server_port)
|
||||
remote_host, remote_port = payload["host"], int(payload["port"])
|
||||
tunnel = Tunnel(remote_host, remote_port, local_host, local_port)
|
||||
address = tunnel.start_tunnel()
|
||||
return address
|
||||
except Exception as e:
|
||||
raise RuntimeError(str(e))
|
||||
else:
|
||||
@ -174,11 +175,11 @@ def setup_tunnel(local_server_port: int, endpoint: str) -> str:
|
||||
def url_ok(url: str) -> bool:
|
||||
try:
|
||||
for _ in range(5):
|
||||
time.sleep(0.500)
|
||||
with warnings.catch_warnings():
|
||||
warnings.filterwarnings("ignore")
|
||||
r = requests.head(url, timeout=3, verify=False)
|
||||
if r.status_code in (200, 401, 302): # 401 or 302 if auth is set
|
||||
return True
|
||||
time.sleep(0.500)
|
||||
except (ConnectionError, requests.exceptions.ConnectionError):
|
||||
return False
|
||||
|
@ -1,101 +1,98 @@
|
||||
"""
|
||||
This file provides remote port forwarding functionality using paramiko package,
|
||||
Inspired by: https://github.com/paramiko/paramiko/blob/master/demos/rforward.py
|
||||
"""
|
||||
import atexit
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import subprocess
|
||||
from typing import List
|
||||
|
||||
import select
|
||||
import socket
|
||||
import sys
|
||||
import threading
|
||||
import warnings
|
||||
from io import StringIO
|
||||
|
||||
from cryptography.utils import CryptographyDeprecationWarning
|
||||
|
||||
with warnings.catch_warnings():
|
||||
warnings.filterwarnings("ignore", category=CryptographyDeprecationWarning)
|
||||
import paramiko
|
||||
VERSION = "0.1"
|
||||
CURRENT_TUNNELS: List["Tunnel"] = []
|
||||
|
||||
|
||||
def handler(chan, host, port):
|
||||
sock = socket.socket()
|
||||
try:
|
||||
sock.connect((host, port))
|
||||
except Exception as e:
|
||||
verbose(f"Forwarding request to {host}:{port} failed: {e}")
|
||||
return
|
||||
class Tunnel:
|
||||
def __init__(self, remote_host, remote_port, local_host, local_port):
|
||||
self.proc = None
|
||||
self.url = None
|
||||
self.remote_host = remote_host
|
||||
self.remote_port = remote_port
|
||||
self.local_host = local_host
|
||||
self.local_port = local_port
|
||||
|
||||
verbose(
|
||||
"Connected! Tunnel open "
|
||||
f"{chan.origin_addr} -> {chan.getpeername()} -> {(host, port)}"
|
||||
)
|
||||
@staticmethod
|
||||
def download_binary():
|
||||
machine = platform.machine()
|
||||
if machine == "x86_64":
|
||||
machine = "amd64"
|
||||
|
||||
while True:
|
||||
r, w, x = select.select([sock, chan], [], [])
|
||||
if sock in r:
|
||||
data = sock.recv(1024)
|
||||
if len(data) == 0:
|
||||
break
|
||||
chan.send(data)
|
||||
if chan in r:
|
||||
data = chan.recv(1024)
|
||||
if len(data) == 0:
|
||||
break
|
||||
sock.send(data)
|
||||
chan.close()
|
||||
sock.close()
|
||||
verbose(f"Tunnel closed from {chan.origin_addr}")
|
||||
# Check if the file exist
|
||||
binary_name = f"frpc_{platform.system().lower()}_{machine.lower()}"
|
||||
binary_path = os.path.join(os.path.dirname(__file__), binary_name)
|
||||
|
||||
extension = ".exe" if os.name == "nt" else ""
|
||||
|
||||
def reverse_forward_tunnel(server_port, remote_host, remote_port, transport):
|
||||
transport.request_port_forward("", server_port)
|
||||
while True:
|
||||
chan = transport.accept(1000)
|
||||
if chan is None:
|
||||
continue
|
||||
thr = threading.Thread(target=handler, args=(chan, remote_host, remote_port))
|
||||
thr.setDaemon(True)
|
||||
thr.start()
|
||||
if not os.path.exists(binary_path):
|
||||
import stat
|
||||
|
||||
import requests
|
||||
|
||||
def verbose(s, debug_mode=False):
|
||||
if debug_mode:
|
||||
print(s)
|
||||
binary_url = f"https://cdn-media.huggingface.co/frpc-gradio-{VERSION}/{binary_name}{extension}"
|
||||
resp = requests.get(binary_url)
|
||||
|
||||
if resp.status_code == 403:
|
||||
raise OSError(
|
||||
f"Cannot set up a share link as this platform is incompatible. Please "
|
||||
f"create a GitHub issue with information about your platform: {platform.uname()}"
|
||||
)
|
||||
|
||||
def create_tunnel(payload, local_server, local_server_port):
|
||||
client = paramiko.SSHClient()
|
||||
client.set_missing_host_key_policy(paramiko.WarningPolicy())
|
||||
resp.raise_for_status()
|
||||
|
||||
verbose(f'Conecting to ssh host {payload["host"]}:{payload["port"]} ...')
|
||||
try:
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore")
|
||||
client.connect(
|
||||
hostname=payload["host"],
|
||||
port=int(payload["port"]),
|
||||
username=payload["user"],
|
||||
pkey=paramiko.RSAKey.from_private_key(StringIO(payload["key"])),
|
||||
)
|
||||
except Exception as e:
|
||||
print(f'*** Failed to connect to {payload["host"]}:{payload["port"]}: {e}')
|
||||
sys.exit(1)
|
||||
# Save file data to local copy
|
||||
with open(binary_path, "wb") as file:
|
||||
file.write(resp.content)
|
||||
st = os.stat(binary_path)
|
||||
os.chmod(binary_path, st.st_mode | stat.S_IEXEC)
|
||||
|
||||
verbose(
|
||||
f'Now forwarding remote port {payload["remote_port"]}'
|
||||
f"to {local_server}:{local_server_port} ..."
|
||||
)
|
||||
return binary_path
|
||||
|
||||
thread = threading.Thread(
|
||||
target=reverse_forward_tunnel,
|
||||
args=(
|
||||
int(payload["remote_port"]),
|
||||
local_server,
|
||||
local_server_port,
|
||||
client.get_transport(),
|
||||
),
|
||||
daemon=True,
|
||||
)
|
||||
thread.start()
|
||||
def start_tunnel(self) -> str:
|
||||
binary_path = self.download_binary()
|
||||
self.url = self._start_tunnel(binary_path)
|
||||
return self.url
|
||||
|
||||
return payload["share_url"]
|
||||
def kill(self):
|
||||
if self.proc is not None:
|
||||
print(f"Killing tunnel {self.local_host}:{self.local_port} <> {self.url}")
|
||||
self.proc.terminate()
|
||||
self.proc = None
|
||||
|
||||
def _start_tunnel(self, binary: str) -> str:
|
||||
CURRENT_TUNNELS.append(self)
|
||||
command = [
|
||||
binary,
|
||||
"http",
|
||||
"-n",
|
||||
"random",
|
||||
"-l",
|
||||
str(self.local_port),
|
||||
"-i",
|
||||
self.local_host,
|
||||
"--uc",
|
||||
"--sd",
|
||||
"random",
|
||||
"--ue",
|
||||
"--server_addr",
|
||||
f"{self.remote_host}:{self.remote_port}",
|
||||
"--disable_log_color",
|
||||
]
|
||||
|
||||
self.proc = subprocess.Popen(
|
||||
command, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
||||
)
|
||||
atexit.register(self.kill)
|
||||
url = ""
|
||||
while url == "":
|
||||
line = self.proc.stdout.readline()
|
||||
line = line.decode("utf-8")
|
||||
if "start proxy success" in line:
|
||||
url = re.search("start proxy success: (.+)\n", line).group(1)
|
||||
return url
|
||||
|
@ -1 +1 @@
|
||||
3.13.0
|
||||
3.13.0
|
||||
|
@ -7,7 +7,6 @@ matplotlib
|
||||
numpy
|
||||
orjson
|
||||
pandas
|
||||
paramiko
|
||||
pillow
|
||||
pycryptodome
|
||||
python-multipart
|
||||
|
@ -1,7 +1,6 @@
|
||||
"""Contains tests for networking.py and app.py"""
|
||||
|
||||
import os
|
||||
import unittest.mock as mock
|
||||
import urllib
|
||||
import warnings
|
||||
|
||||
@ -75,11 +74,6 @@ class TestStartServer:
|
||||
|
||||
|
||||
class TestURLs:
|
||||
def test_setup_tunnel(self):
|
||||
networking.create_tunnel = mock.MagicMock(return_value="test")
|
||||
res = networking.setup_tunnel(None, None)
|
||||
assert res == "test"
|
||||
|
||||
def test_url_ok(self):
|
||||
res = networking.url_ok("https://www.gradio.app")
|
||||
assert res
|
||||
|
@ -1,22 +1,11 @@
|
||||
import os
|
||||
import unittest.mock as mock
|
||||
import pytest
|
||||
|
||||
import paramiko
|
||||
import requests
|
||||
|
||||
from gradio import Interface, networking, tunneling
|
||||
|
||||
os.environ["GRADIO_ANALYTICS_ENABLED"] = "False"
|
||||
from gradio import Interface, networking
|
||||
|
||||
|
||||
class TestTunneling:
|
||||
def test_create_tunnel(self):
|
||||
response = requests.get(networking.GRADIO_API_SERVER)
|
||||
payload = response.json()[0]
|
||||
io = Interface(lambda x: x, "text", "text")
|
||||
_, path_to_local_server, _ = io.launch(prevent_thread_lock=True, share=False)
|
||||
_, localhost, port = path_to_local_server.split(":")
|
||||
paramiko.SSHClient.connect = mock.MagicMock(return_value=None)
|
||||
tunneling.create_tunnel(payload, localhost, port)
|
||||
paramiko.SSHClient.connect.assert_called_once()
|
||||
io.close()
|
||||
@pytest.mark.flaky
|
||||
def test_setup_tunnel():
|
||||
io = Interface(lambda x: x, "number", "number")
|
||||
io.launch(show_error=True, prevent_thread_lock=True)
|
||||
share_url = networking.setup_tunnel(io.server_name, io.server_port)
|
||||
assert isinstance(share_url, str)
|
||||
|
Loading…
x
Reference in New Issue
Block a user