persistent tunnel
frdel committed
May 15, 2025 at 10:30 UTC
84f2d9447554d235b4edd30780fa9d840ca4c8d3
7 files changed
+184
-11
docker/run/fs/etc/supervisor/conf.d/supervisord.conf
+14
@@ -72,6 +72,20 @@ startretries=3
72
stopasgroup=true
73
killasgroup=true
74
75
+[program:run_tunnel_api]
76
+command=/exe/run_tunnel_api.sh
77
+environment=
78
+user=root
79
+stopwaitsecs=60
80
+stdout_logfile=/dev/stdout
81
+stdout_logfile_maxbytes=0
82
+stderr_logfile=/dev/stderr
83
+stderr_logfile_maxbytes=0
84
+autorestart=true
85
+startretries=3
86
+stopasgroup=true
87
+killasgroup=true
88
+
89
[eventlistener:the_listener]
90
command=python3 /exe/supervisor_event_listener.py
91
events=PROCESS_STATE_FATAL
docker/run/fs/exe/run_tunnel_api.sh
new
+19
@@ -0,0 +1,19 @@
1
+#!/bin/bash
2
+
3
+. "/ins/setup_venv.sh" "$@"
4
+. "/ins/copy_A0.sh" "$@"
5
+
6
+python /a0/prepare.py --dockerized=true
7
+python /a0/preload.py --dockerized=true
8
+
9
+echo "Starting A0..."
10
+exec python /a0/run_tunnel.py \
11
+ --dockerized=true \
12
+ --port=5070 \
13
+ --host="0.0.0.0" \
14
+ --code_exec_docker_enabled=false \
15
+ --code_exec_ssh_enabled=true \
16
+ # --code_exec_ssh_addr="localhost" \
17
+ # --code_exec_ssh_port=22 \
18
+ # --code_exec_ssh_user="root" \
19
+ # --code_exec_ssh_pass="toor"
docker/run/fs/exe/supervisor_event_listener.py
+1
-1
@@ -5,7 +5,7 @@ import logging
5
import subprocess
6
import time
7
8
-from supervisor.childutils import listener
8
+from supervisor.childutils import listener # type: ignore
9
10
11
def main(args):
python/api/tunnel.py
+11
-4
@@ -7,6 +7,9 @@ class Tunnel(ApiHandler):
7
action = input.get("action", "get")
8
9
tunnel_manager = TunnelManager.get_instance()
10
+
11
+ if action == "verify":
12
+ return {"success": True}
13
14
if action == "create":
15
# Get the port from the request or use default
@@ -25,10 +28,7 @@ class Tunnel(ApiHandler):
28
}
29
30
elif action == "stop":
28
- success = tunnel_manager.stop_tunnel()
29
- return {
30
- "success": success
31
- }
31
+ return self.stop()
32
33
elif action == "get":
34
tunnel_url = tunnel_manager.get_tunnel_url()
@@ -42,3 +42,10 @@ class Tunnel(ApiHandler):
42
"success": False,
43
"error": "Invalid action. Use 'create', 'stop', or 'get'."
44
}
45
+
46
+ def stop(self):
47
+ tunnel_manager = TunnelManager.get_instance()
48
+ tunnel_manager.stop_tunnel()
49
+ return {
50
+ "success": True
51
+ }
python/api/tunnel_proxy.py
new
+36
@@ -0,0 +1,36 @@
1
+from flask import Request, Response
2
+from python.helpers import dotenv, runtime
3
+from python.helpers.api import ApiHandler
4
+from python.helpers.tunnel_manager import TunnelManager
5
+import requests
6
+
7
+
8
+class TunnelProxy(ApiHandler):
9
+ async def process(self, input: dict, request: Request) -> dict | Response:
10
+ # Get configuration from environment
11
+ tunnel_api_port = (
12
+ runtime.get_arg("tunnel_api_port")
13
+ or int(dotenv.get_dotenv_value("TUNNEL_API_PORT", 0))
14
+ or 5070
15
+ )
16
+
17
+ # first verify the service is running:
18
+ service_ok = False
19
+ try:
20
+ response = requests.post(f"http://localhost:{tunnel_api_port}/", json={"action": "verify"})
21
+ if response.status_code == 200:
22
+ service_ok = True
23
+ except Exception as e:
24
+ service_ok = False
25
+
26
+ # forward this request to the tunnel service if OK
27
+ if service_ok:
28
+ try:
29
+ response = requests.post(f"http://localhost:{tunnel_api_port}/", json=input)
30
+ return response.json()
31
+ except Exception as e:
32
+ return {"error": str(e)}
33
+ else:
34
+ # forward to API handler directly
35
+ from python.api.tunnel import Tunnel
36
+ return await Tunnel(self.app, self.thread_lock).process(input, request)
run_tunnel.py
new
+97
@@ -0,0 +1,97 @@
1
+from functools import wraps
2
+import signal
3
+import threading
4
+import sys
5
+from flask import Flask, request, Response
6
+from python.helpers.files import get_abs_path
7
+from python.helpers import persist_chat, runtime, dotenv, process
8
+from python.helpers.print_style import PrintStyle
9
+
10
+from python.api.tunnel import Tunnel
11
+
12
+# initialize the internal Flask server
13
+app = Flask("app")
14
+app.config["JSON_SORT_KEYS"] = False # Disable key sorting in jsonify
15
+
16
+
17
+def run():
18
+ # Suppress only request logs but keep the startup messages
19
+ from werkzeug.serving import WSGIRequestHandler
20
+ from werkzeug.serving import make_server
21
+
22
+ PrintStyle().print("Starting tunnel server...")
23
+ class NoRequestLoggingWSGIRequestHandler(WSGIRequestHandler):
24
+ def log_request(self, code="-", size="-"):
25
+ pass # Override to suppress request logging
26
+
27
+ # Get configuration from environment
28
+ web_ui_port = (
29
+ runtime.get_arg("port")
30
+ or int(dotenv.get_dotenv_value("WEB_UI_PORT", 0))
31
+ or 5000
32
+ )
33
+ # Get configuration from environment
34
+ tunnel_api_port = (
35
+ runtime.get_arg("tunnel_api_port")
36
+ or int(dotenv.get_dotenv_value("TUNNEL_API_PORT", 0))
37
+ or 5070
38
+ )
39
+ host = (
40
+ runtime.get_arg("host") or dotenv.get_dotenv_value("WEB_UI_HOST") or "localhost"
41
+ )
42
+ server = None
43
+ lock = threading.Lock()
44
+ tunnel = Tunnel(app, lock)
45
+
46
+
47
+ # handle api request
48
+ @app.route("/", methods=["POST"])
49
+ async def handle_request():
50
+ return await tunnel.handle_request(request=request) # type: ignore
51
+
52
+
53
+ try:
54
+ server = make_server(
55
+ host=host,
56
+ port=tunnel_api_port,
57
+ app=app,
58
+ request_handler=NoRequestLoggingWSGIRequestHandler,
59
+ threaded=True,
60
+ )
61
+
62
+ printer = PrintStyle()
63
+
64
+ # def signal_handler(sig=None, frame=None):
65
+ # nonlocal tunnel, server, printer
66
+ # with lock:
67
+ # printer.print("Caught signal, stopping tunnel server...")
68
+ # if server:
69
+ # server.shutdown()
70
+ # process.stop_server()
71
+ # if tunnel:
72
+ # tunnel.stop()
73
+ # tunnel = None
74
+ # printer.print("Tunnel server stopped")
75
+ # sys.exit(0)
76
+
77
+ # signal.signal(signal.SIGINT, signal_handler)
78
+ # signal.signal(signal.SIGTERM, signal_handler)
79
+
80
+ process.set_server(server)
81
+ # server.log_startup()
82
+ server.serve_forever()
83
+ # Run Flask app
84
+ # app.run(
85
+ # request_handler=NoRequestLoggingWSGIRequestHandler, port=port, host=host
86
+ # )
87
+ finally:
88
+ # Clean up tunnel if it was started
89
+ if tunnel:
90
+ tunnel.stop()
91
+
92
+
93
+# run the internal server
94
+if __name__ == "__main__":
95
+ runtime.initialize()
96
+ dotenv.load_dotenv()
97
+ run()
webui/js/tunnel.js
+6
-6
@@ -12,7 +12,7 @@ document.addEventListener('alpine:init', () => {
12
13
async checkTunnelStatus() {
14
try {
15
- const response = await fetch('/tunnel', {
15
+ const response = await fetch('/tunnel_proxy', {
16
method: 'POST',
17
headers: {
18
'Content-Type': 'application/json',
@@ -35,7 +35,7 @@ document.addEventListener('alpine:init', () => {
35
36
if (storedTunnelUrl) {
37
// Use the stored URL but verify it's still valid
38
- const verifyResponse = await fetch('/tunnel', {
38
+ const verifyResponse = await fetch('/tunnel_proxy', {
39
method: 'POST',
40
headers: {
41
'Content-Type': 'application/json',
@@ -82,7 +82,7 @@ document.addEventListener('alpine:init', () => {
82
83
try {
84
// First stop any existing tunnel
85
- const stopResponse = await fetch('/tunnel', {
85
+ const stopResponse = await fetch('/tunnel_proxy', {
86
method: 'POST',
87
headers: {
88
'Content-Type': 'application/json',
@@ -170,7 +170,7 @@ document.addEventListener('alpine:init', () => {
170
171
try {
172
// Call the backend API to create a tunnel
173
- const response = await fetch('/tunnel', {
173
+ const response = await fetch('/tunnel_proxy', {
174
method: 'POST',
175
headers: {
176
'Content-Type': 'application/json',
@@ -201,7 +201,7 @@ document.addEventListener('alpine:init', () => {
201
202
// Check if tunnel is running now
203
try {
204
- const statusResponse = await fetch('/tunnel', {
204
+ const statusResponse = await fetch('/tunnel_proxy', {
205
method: 'POST',
206
headers: {
207
'Content-Type': 'application/json',
@@ -253,7 +253,7 @@ document.addEventListener('alpine:init', () => {
253
254
try {
255
// Call the backend to stop the tunnel
256
- const response = await fetch('/tunnel', {
256
+ const response = await fetch('/tunnel_proxy', {
257
method: 'POST',
258
headers: {
259
'Content-Type': 'application/json',