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:
Abubakar Abid 2022-12-14 08:10:45 -06:00 committed by GitHub
parent 5182460886
commit 53005ab88a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 137 additions and 128 deletions

8
.gitignore vendored
View File

@ -44,4 +44,10 @@ workspace.code-workspace
*.h5
# log files
.pnpm-debug.log
.pnpm-debug.log
# Local virtualenv for devs
.venv*
# FRP
gradio/frpc_*

View File

@ -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.

View File

@ -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}

View File

@ -5,4 +5,5 @@ def greet(name):
demo = gr.Interface(fn=greet, inputs="text", outputs="text")
demo.launch()
if __name__ == "__main__":
demo.launch()

View File

@ -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."""

View File

@ -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

View File

@ -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

View File

@ -1 +1 @@
3.13.0
3.13.0

View File

@ -7,7 +7,6 @@ matplotlib
numpy
orjson
pandas
paramiko
pillow
pycryptodome
python-multipart

View File

@ -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

View File

@ -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)