unitlab 2.3.29__tar.gz → 2.3.32__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {unitlab-2.3.29/src/unitlab.egg-info → unitlab-2.3.32}/PKG-INFO +1 -1
- {unitlab-2.3.29 → unitlab-2.3.32}/setup.py +1 -1
- {unitlab-2.3.29 → unitlab-2.3.32}/src/unitlab/client.py +9 -12
- {unitlab-2.3.29 → unitlab-2.3.32}/src/unitlab/persistent_tunnel.py +107 -33
- {unitlab-2.3.29 → unitlab-2.3.32/src/unitlab.egg-info}/PKG-INFO +1 -1
- {unitlab-2.3.29 → unitlab-2.3.32}/LICENSE.md +0 -0
- {unitlab-2.3.29 → unitlab-2.3.32}/README.md +0 -0
- {unitlab-2.3.29 → unitlab-2.3.32}/setup.cfg +0 -0
- {unitlab-2.3.29 → unitlab-2.3.32}/src/unitlab/__init__.py +0 -0
- {unitlab-2.3.29 → unitlab-2.3.32}/src/unitlab/__main__.py +0 -0
- {unitlab-2.3.29 → unitlab-2.3.32}/src/unitlab/api_tunnel.py +0 -0
- {unitlab-2.3.29 → unitlab-2.3.32}/src/unitlab/auto_tunnel.py +0 -0
- {unitlab-2.3.29 → unitlab-2.3.32}/src/unitlab/binary_manager.py +0 -0
- {unitlab-2.3.29 → unitlab-2.3.32}/src/unitlab/cloudflare_api_tunnel.py +0 -0
- {unitlab-2.3.29 → unitlab-2.3.32}/src/unitlab/cloudflare_api_tunnel_backup.py +0 -0
- {unitlab-2.3.29 → unitlab-2.3.32}/src/unitlab/dynamic_tunnel.py +0 -0
- {unitlab-2.3.29 → unitlab-2.3.32}/src/unitlab/easy_tunnel.py +0 -0
- {unitlab-2.3.29 → unitlab-2.3.32}/src/unitlab/exceptions.py +0 -0
- {unitlab-2.3.29 → unitlab-2.3.32}/src/unitlab/main.py +0 -0
- {unitlab-2.3.29 → unitlab-2.3.32}/src/unitlab/simple_tunnel.py +0 -0
- {unitlab-2.3.29 → unitlab-2.3.32}/src/unitlab/tunnel_config.py +0 -0
- {unitlab-2.3.29 → unitlab-2.3.32}/src/unitlab/tunnel_service_token.py +0 -0
- {unitlab-2.3.29 → unitlab-2.3.32}/src/unitlab/utils.py +0 -0
- {unitlab-2.3.29 → unitlab-2.3.32}/src/unitlab.egg-info/SOURCES.txt +0 -0
- {unitlab-2.3.29 → unitlab-2.3.32}/src/unitlab.egg-info/dependency_links.txt +0 -0
- {unitlab-2.3.29 → unitlab-2.3.32}/src/unitlab.egg-info/entry_points.txt +0 -0
- {unitlab-2.3.29 → unitlab-2.3.32}/src/unitlab.egg-info/requires.txt +0 -0
- {unitlab-2.3.29 → unitlab-2.3.32}/src/unitlab.egg-info/top_level.txt +0 -0
@@ -286,21 +286,14 @@ class UnitlabClient:
|
|
286
286
|
from .persistent_tunnel import PersistentTunnel
|
287
287
|
logger.info("Using Persistent Tunnel with Cloudflare API")
|
288
288
|
self.tunnel_manager = PersistentTunnel(device_id=self.device_id)
|
289
|
-
#
|
290
|
-
self.jupyter_url =
|
291
|
-
self.ssh_url =
|
289
|
+
# Don't call run() here - it has infinite loop. Call start() in setup_tunnels()
|
290
|
+
self.jupyter_url = None
|
291
|
+
self.ssh_url = None
|
292
|
+
|
292
293
|
except ImportError as e:
|
293
294
|
logger.warning(f"Could not import PersistentTunnel: {e}")
|
294
295
|
# Fallback to easy tunnel
|
295
|
-
|
296
|
-
from .easy_tunnel import EasyTunnelAdapter
|
297
|
-
logger.info("Using Easy Dynamic Tunnel (random URL)")
|
298
|
-
self.tunnel_manager = EasyTunnelAdapter(device_id=self.device_id)
|
299
|
-
self.jupyter_url = None
|
300
|
-
self.ssh_url = None
|
301
|
-
except ImportError:
|
302
|
-
logger.error("No tunnel implementation available!")
|
303
|
-
raise
|
296
|
+
|
304
297
|
|
305
298
|
# Setup signal handlers
|
306
299
|
signal.signal(signal.SIGINT, self._handle_shutdown)
|
@@ -410,6 +403,10 @@ class UnitlabClient:
|
|
410
403
|
if self.tunnel_manager.start():
|
411
404
|
# Store the processes for monitoring
|
412
405
|
self.jupyter_proc = self.tunnel_manager.jupyter_process
|
406
|
+
# Update URLs after tunnel starts successfully
|
407
|
+
self.jupyter_url = self.tunnel_manager.jupyter_url
|
408
|
+
self.ssh_url = self.tunnel_manager.jupyter_url
|
409
|
+
logger.info(f"Tunnel started successfully at {self.jupyter_url}")
|
413
410
|
self.tunnel_proc = self.tunnel_manager.tunnel_process
|
414
411
|
self.jupyter_port = "8888" # Both use fixed port
|
415
412
|
|
@@ -67,9 +67,42 @@ class PersistentTunnel:
|
|
67
67
|
"Content-Type": "application/json"
|
68
68
|
}
|
69
69
|
|
70
|
-
def
|
71
|
-
"""
|
72
|
-
|
70
|
+
def get_or_create_tunnel(self):
|
71
|
+
"""Get existing tunnel or create a new one"""
|
72
|
+
# First, check if tunnel already exists
|
73
|
+
print("🔍 Checking for existing tunnel: {}...".format(self.tunnel_name))
|
74
|
+
|
75
|
+
list_url = "https://api.cloudflare.com/client/v4/accounts/{}/cfd_tunnel".format(self.cf_account_id)
|
76
|
+
headers = self._get_headers()
|
77
|
+
|
78
|
+
# Check if tunnel exists
|
79
|
+
response = requests.get(list_url, headers=headers)
|
80
|
+
if response.status_code == 200:
|
81
|
+
tunnels = response.json().get("result", [])
|
82
|
+
for tunnel in tunnels:
|
83
|
+
if tunnel["name"] == self.tunnel_name:
|
84
|
+
print("✅ Found existing tunnel: {}".format(tunnel["id"]))
|
85
|
+
self.tunnel_id = tunnel["id"]
|
86
|
+
|
87
|
+
# For persistent device IDs, always recreate to ensure fresh state
|
88
|
+
print("🔄 Recreating tunnel for persistent device...")
|
89
|
+
delete_url = "https://api.cloudflare.com/client/v4/accounts/{}/cfd_tunnel/{}".format(
|
90
|
+
self.cf_account_id, tunnel["id"]
|
91
|
+
)
|
92
|
+
del_resp = requests.delete(delete_url, headers=headers)
|
93
|
+
if del_resp.status_code in [200, 204]:
|
94
|
+
print("✅ Deleted old tunnel")
|
95
|
+
time.sleep(2)
|
96
|
+
else:
|
97
|
+
print("⚠️ Could not delete old tunnel, trying to create new one anyway")
|
98
|
+
break
|
99
|
+
|
100
|
+
# Create new tunnel
|
101
|
+
return self.create_new_tunnel()
|
102
|
+
|
103
|
+
def create_new_tunnel(self):
|
104
|
+
"""Create a brand new tunnel"""
|
105
|
+
print("🔧 Creating new tunnel: {}...".format(self.tunnel_name))
|
73
106
|
|
74
107
|
# Generate random tunnel secret (32 bytes)
|
75
108
|
import secrets
|
@@ -96,8 +129,8 @@ class PersistentTunnel:
|
|
96
129
|
"TunnelID": self.tunnel_id
|
97
130
|
}
|
98
131
|
|
99
|
-
# Save credentials to file
|
100
|
-
cred_file = "/tmp/tunnel-{}.json".format(self.
|
132
|
+
# Save credentials to file with tunnel name (not ID) for consistency
|
133
|
+
cred_file = "/tmp/tunnel-{}.json".format(self.tunnel_name)
|
101
134
|
with open(cred_file, 'w') as f:
|
102
135
|
json.dump(self.tunnel_credentials, f)
|
103
136
|
|
@@ -143,19 +176,7 @@ class PersistentTunnel:
|
|
143
176
|
|
144
177
|
def create_tunnel_config(self, cred_file):
|
145
178
|
"""Create tunnel config file"""
|
146
|
-
|
147
|
-
"ingress": [
|
148
|
-
{
|
149
|
-
"hostname": "{}.{}".format(self.subdomain, self.domain),
|
150
|
-
"service": "http://localhost:8888"
|
151
|
-
},
|
152
|
-
{
|
153
|
-
"service": "http_status:404"
|
154
|
-
}
|
155
|
-
]
|
156
|
-
}
|
157
|
-
|
158
|
-
config_file = "/tmp/tunnel-config-{}.yml".format(self.tunnel_id)
|
179
|
+
config_file = "/tmp/tunnel-config-{}.yml".format(self.tunnel_name)
|
159
180
|
with open(config_file, 'w') as f:
|
160
181
|
f.write("tunnel: {}\n".format(self.tunnel_id))
|
161
182
|
f.write("credentials-file: {}\n\n".format(cred_file))
|
@@ -167,25 +188,75 @@ class PersistentTunnel:
|
|
167
188
|
return config_file
|
168
189
|
|
169
190
|
def get_cloudflared_path(self):
|
170
|
-
"""Get or download cloudflared"""
|
191
|
+
"""Get or download cloudflared for any platform"""
|
171
192
|
import shutil
|
193
|
+
import platform
|
194
|
+
|
195
|
+
# Check if already in system PATH
|
172
196
|
if shutil.which("cloudflared"):
|
173
197
|
return "cloudflared"
|
174
198
|
|
175
|
-
|
199
|
+
# Determine binary location based on OS
|
200
|
+
system = platform.system().lower()
|
201
|
+
machine = platform.machine().lower()
|
202
|
+
|
203
|
+
if system == "windows":
|
204
|
+
local_bin = os.path.expanduser("~/cloudflared/cloudflared.exe")
|
205
|
+
else:
|
206
|
+
local_bin = os.path.expanduser("~/.local/bin/cloudflared")
|
207
|
+
|
208
|
+
# Check if already downloaded
|
176
209
|
if os.path.exists(local_bin):
|
177
210
|
return local_bin
|
178
211
|
|
179
|
-
# Download
|
180
|
-
print("📦 Downloading cloudflared...")
|
181
|
-
|
182
|
-
system
|
183
|
-
|
184
|
-
|
212
|
+
# Download based on platform
|
213
|
+
print("📦 Downloading cloudflared for {}...".format(system))
|
214
|
+
|
215
|
+
if system == "linux":
|
216
|
+
# Linux: detect architecture
|
217
|
+
if "arm" in machine or "aarch64" in machine:
|
218
|
+
arch = "arm64"
|
219
|
+
elif "386" in machine or "i686" in machine:
|
220
|
+
arch = "386"
|
221
|
+
else:
|
222
|
+
arch = "amd64"
|
223
|
+
url = "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-{}".format(arch)
|
224
|
+
|
225
|
+
os.makedirs(os.path.dirname(local_bin), exist_ok=True)
|
226
|
+
subprocess.run("curl -L {} -o {}".format(url, local_bin), shell=True, capture_output=True)
|
227
|
+
subprocess.run("chmod +x {}".format(local_bin), shell=True)
|
228
|
+
|
229
|
+
elif system == "darwin":
|
230
|
+
# macOS: supports both Intel and Apple Silicon
|
231
|
+
if "arm" in machine:
|
232
|
+
url = "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-arm64.tgz"
|
233
|
+
else:
|
234
|
+
url = "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-amd64.tgz"
|
235
|
+
|
236
|
+
os.makedirs(os.path.dirname(local_bin), exist_ok=True)
|
237
|
+
# Download and extract tar.gz
|
238
|
+
subprocess.run("curl -L {} | tar xz -C {}".format(url, os.path.dirname(local_bin)), shell=True, capture_output=True)
|
239
|
+
subprocess.run("chmod +x {}".format(local_bin), shell=True)
|
240
|
+
|
241
|
+
elif system == "windows":
|
242
|
+
# Windows: typically amd64
|
243
|
+
if "arm" in machine:
|
244
|
+
arch = "arm64"
|
245
|
+
elif "386" in machine:
|
246
|
+
arch = "386"
|
247
|
+
else:
|
248
|
+
arch = "amd64"
|
249
|
+
url = "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-windows-{}.exe".format(arch)
|
250
|
+
|
251
|
+
os.makedirs(os.path.dirname(local_bin), exist_ok=True)
|
252
|
+
# Use PowerShell on Windows to download
|
253
|
+
subprocess.run("powershell -Command \"Invoke-WebRequest -Uri {} -OutFile {}\"".format(url, local_bin), shell=True, capture_output=True)
|
254
|
+
|
255
|
+
else:
|
256
|
+
print("❌ Unsupported platform: {}".format(system))
|
257
|
+
raise Exception("Platform {} not supported".format(system))
|
185
258
|
|
186
|
-
|
187
|
-
subprocess.run("curl -L {} -o {}".format(url, local_bin), shell=True, capture_output=True)
|
188
|
-
subprocess.run("chmod +x {}".format(local_bin), shell=True)
|
259
|
+
print("✅ cloudflared downloaded successfully")
|
189
260
|
return local_bin
|
190
261
|
|
191
262
|
def start_jupyter(self):
|
@@ -198,7 +269,10 @@ class PersistentTunnel:
|
|
198
269
|
"--no-browser",
|
199
270
|
"--ip", "0.0.0.0",
|
200
271
|
"--NotebookApp.token=''",
|
201
|
-
"--NotebookApp.password=''"
|
272
|
+
"--NotebookApp.password=''",
|
273
|
+
"--NotebookApp.allow_origin='*'"
|
274
|
+
|
275
|
+
|
202
276
|
]
|
203
277
|
|
204
278
|
self.jupyter_process = subprocess.Popen(
|
@@ -241,8 +315,8 @@ class PersistentTunnel:
|
|
241
315
|
|
242
316
|
# API credentials are hardcoded, so we're ready to go
|
243
317
|
|
244
|
-
# 1.
|
245
|
-
cred_file = self.
|
318
|
+
# 1. Get existing or create new tunnel via API
|
319
|
+
cred_file = self.get_or_create_tunnel()
|
246
320
|
if not cred_file:
|
247
321
|
print("⚠️ Falling back to quick tunnel")
|
248
322
|
return self.start_quick_tunnel()
|
@@ -316,7 +390,7 @@ class PersistentTunnel:
|
|
316
390
|
)
|
317
391
|
requests.delete(url, headers=self._get_headers())
|
318
392
|
print("🗑️ Tunnel deleted")
|
319
|
-
except Exception
|
393
|
+
except Exception:
|
320
394
|
pass # Ignore cleanup errors
|
321
395
|
|
322
396
|
def run(self):
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|