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',