devlinker 1.4.3__tar.gz → 1.4.4__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.
- {devlinker-1.4.3/devlinker.egg-info → devlinker-1.4.4}/PKG-INFO +1 -1
- {devlinker-1.4.3 → devlinker-1.4.4}/devlinker/detector.py +4 -4
- {devlinker-1.4.3 → devlinker-1.4.4}/devlinker/main.py +4 -4
- {devlinker-1.4.3 → devlinker-1.4.4}/devlinker/proxy.py +22 -5
- {devlinker-1.4.3 → devlinker-1.4.4}/devlinker/tunnel.py +44 -21
- {devlinker-1.4.3 → devlinker-1.4.4/devlinker.egg-info}/PKG-INFO +1 -1
- {devlinker-1.4.3 → devlinker-1.4.4}/pyproject.toml +1 -1
- {devlinker-1.4.3 → devlinker-1.4.4}/LICENSE +0 -0
- {devlinker-1.4.3 → devlinker-1.4.4}/MANIFEST.in +0 -0
- {devlinker-1.4.3 → devlinker-1.4.4}/README.md +0 -0
- {devlinker-1.4.3 → devlinker-1.4.4}/devlinker/__init__.py +0 -0
- {devlinker-1.4.3 → devlinker-1.4.4}/devlinker/config.py +0 -0
- {devlinker-1.4.3 → devlinker-1.4.4}/devlinker/detection_state.py +0 -0
- {devlinker-1.4.3 → devlinker-1.4.4}/devlinker/detector_ai.py +0 -0
- {devlinker-1.4.3 → devlinker-1.4.4}/devlinker/devlinker_loader_instant.html +0 -0
- {devlinker-1.4.3 → devlinker-1.4.4}/devlinker/devlinker_loader_snippet.html +0 -0
- {devlinker-1.4.3 → devlinker-1.4.4}/devlinker/doctor.py +0 -0
- {devlinker-1.4.3 → devlinker-1.4.4}/devlinker/fix.py +0 -0
- {devlinker-1.4.3 → devlinker-1.4.4}/devlinker/fixer.py +0 -0
- {devlinker-1.4.3 → devlinker-1.4.4}/devlinker/global_state.py +0 -0
- {devlinker-1.4.3 → devlinker-1.4.4}/devlinker/inspect.py +0 -0
- {devlinker-1.4.3 → devlinker-1.4.4}/devlinker/logger.py +0 -0
- {devlinker-1.4.3 → devlinker-1.4.4}/devlinker/monitor.py +0 -0
- {devlinker-1.4.3 → devlinker-1.4.4}/devlinker/runner.py +0 -0
- {devlinker-1.4.3 → devlinker-1.4.4}/devlinker/share.py +0 -0
- {devlinker-1.4.3 → devlinker-1.4.4}/devlinker.egg-info/SOURCES.txt +0 -0
- {devlinker-1.4.3 → devlinker-1.4.4}/devlinker.egg-info/dependency_links.txt +0 -0
- {devlinker-1.4.3 → devlinker-1.4.4}/devlinker.egg-info/entry_points.txt +0 -0
- {devlinker-1.4.3 → devlinker-1.4.4}/devlinker.egg-info/requires.txt +0 -0
- {devlinker-1.4.3 → devlinker-1.4.4}/devlinker.egg-info/top_level.txt +0 -0
- {devlinker-1.4.3 → devlinker-1.4.4}/setup.cfg +0 -0
- {devlinker-1.4.3 → devlinker-1.4.4}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devlinker
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.4
|
|
4
4
|
Summary: A lightweight proxy that combines your frontend and backend into one link for easy development and sharing.
|
|
5
5
|
Author-email: Mani <mani1028@users.noreply.github.com>
|
|
6
6
|
Requires-Python: >=3.7
|
|
@@ -15,7 +15,7 @@ DEFAULT_BACKEND_PROBE_PATHS = (
|
|
|
15
15
|
|
|
16
16
|
def check_port(
|
|
17
17
|
port: int,
|
|
18
|
-
timeout: float =
|
|
18
|
+
timeout: float = 0.4,
|
|
19
19
|
probe_paths: Iterable[str] = DEFAULT_BACKEND_PROBE_PATHS,
|
|
20
20
|
) -> bool:
|
|
21
21
|
"""Return True when an HTTP service is reachable on localhost:port.
|
|
@@ -53,7 +53,7 @@ def check_port(
|
|
|
53
53
|
return False
|
|
54
54
|
|
|
55
55
|
|
|
56
|
-
def is_vite_port(port: int, timeout: float =
|
|
56
|
+
def is_vite_port(port: int, timeout: float = 0.4) -> bool:
|
|
57
57
|
"""Return True when port looks like a Vite dev server."""
|
|
58
58
|
for host in ("localhost", "127.0.0.1"):
|
|
59
59
|
try:
|
|
@@ -95,8 +95,8 @@ def _ordered_unique_ports(*port_groups: Iterable[int]) -> list[int]:
|
|
|
95
95
|
def detect_ports(
|
|
96
96
|
frontend: Optional[int] = None,
|
|
97
97
|
backend: Optional[int] = None,
|
|
98
|
-
retries: int =
|
|
99
|
-
delay_seconds: float =
|
|
98
|
+
retries: int = 16,
|
|
99
|
+
delay_seconds: float = 0.5,
|
|
100
100
|
) -> Tuple[Optional[int], Optional[int]]:
|
|
101
101
|
"""Detect frontend and backend ports with retry support for slow startups."""
|
|
102
102
|
frontend_ports = _ordered_unique_ports(
|
|
@@ -409,8 +409,8 @@ def _wait_for_readiness(
|
|
|
409
409
|
label: str,
|
|
410
410
|
port: int,
|
|
411
411
|
checker,
|
|
412
|
-
retries: int =
|
|
413
|
-
delay_seconds: float =
|
|
412
|
+
retries: int = 20,
|
|
413
|
+
delay_seconds: float = 0.5,
|
|
414
414
|
) -> bool:
|
|
415
415
|
_ui_status("⏳", f"Waiting for {label} ({port})...", style="cyan")
|
|
416
416
|
for attempt in range(1, retries + 1):
|
|
@@ -428,8 +428,8 @@ def _wait_for_readiness_live(
|
|
|
428
428
|
port: int,
|
|
429
429
|
checker,
|
|
430
430
|
live_status: _LiveStatus,
|
|
431
|
-
retries: int =
|
|
432
|
-
delay_seconds: float =
|
|
431
|
+
retries: int = 20,
|
|
432
|
+
delay_seconds: float = 0.5,
|
|
433
433
|
) -> bool:
|
|
434
434
|
live_status.update(label, f"⏳ Waiting ({port})...", style="cyan")
|
|
435
435
|
for attempt in range(1, retries + 1):
|
|
@@ -2,9 +2,10 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import os
|
|
5
|
+
import sys
|
|
5
6
|
import time
|
|
6
7
|
from typing import Dict, Optional
|
|
7
|
-
from urllib.parse import urlencode
|
|
8
|
+
from urllib.parse import parse_qsl, urlencode, urlsplit
|
|
8
9
|
|
|
9
10
|
import httpx
|
|
10
11
|
import uvicorn
|
|
@@ -67,6 +68,19 @@ def _extract_presented_token(headers: Dict[str, str], query_params) -> str | Non
|
|
|
67
68
|
query_token = query_params.get("dl_token")
|
|
68
69
|
if query_token:
|
|
69
70
|
return query_token
|
|
71
|
+
|
|
72
|
+
# WebSocket handshakes may not include dl_token in the WS URL, but the
|
|
73
|
+
# browser Referer usually contains the original page URL query string.
|
|
74
|
+
referer = headers.get("referer", "").strip()
|
|
75
|
+
if referer:
|
|
76
|
+
try:
|
|
77
|
+
referer_query = dict(parse_qsl(urlsplit(referer).query, keep_blank_values=True))
|
|
78
|
+
referer_token = (referer_query.get("dl_token") or "").strip()
|
|
79
|
+
if referer_token:
|
|
80
|
+
return referer_token
|
|
81
|
+
except ValueError:
|
|
82
|
+
pass
|
|
83
|
+
|
|
70
84
|
direct_header = headers.get("x-devlinker-token", "").strip()
|
|
71
85
|
if direct_header:
|
|
72
86
|
return direct_header
|
|
@@ -213,6 +227,7 @@ def _filter_websocket_headers(incoming: Dict[str, str]) -> Dict[str, str]:
|
|
|
213
227
|
connection_tokens = _connection_header_tokens(incoming)
|
|
214
228
|
excluded = HOP_BY_HOP_HEADERS | connection_tokens | {
|
|
215
229
|
"host",
|
|
230
|
+
"origin",
|
|
216
231
|
"sec-websocket-key",
|
|
217
232
|
"sec-websocket-version",
|
|
218
233
|
"sec-websocket-extensions",
|
|
@@ -725,10 +740,12 @@ def start_proxy(
|
|
|
725
740
|
_printed_live_header = False
|
|
726
741
|
PROXY_READY_EVENT.clear()
|
|
727
742
|
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
743
|
+
def _run_server() -> None:
|
|
744
|
+
if sys.platform.startswith("win") and hasattr(asyncio, "WindowsSelectorEventLoopPolicy"):
|
|
745
|
+
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
|
746
|
+
uvicorn.run(app, host="0.0.0.0", port=proxy_port, log_level="warning")
|
|
747
|
+
|
|
748
|
+
thread = threading.Thread(target=_run_server, daemon=True)
|
|
732
749
|
thread.start()
|
|
733
750
|
|
|
734
751
|
|
|
@@ -20,8 +20,11 @@ def stop_tunnel():
|
|
|
20
20
|
_CLOUDFLARED_PROCESSES.clear()
|
|
21
21
|
|
|
22
22
|
import re
|
|
23
|
+
import queue
|
|
23
24
|
import shutil
|
|
24
25
|
import subprocess
|
|
26
|
+
import threading
|
|
27
|
+
import time
|
|
25
28
|
from subprocess import TimeoutExpired
|
|
26
29
|
|
|
27
30
|
from pyngrok import ngrok
|
|
@@ -56,33 +59,53 @@ def _try_cloudflare(proxy_port: int, startup_timeout: float = 12.0) -> str | Non
|
|
|
56
59
|
text=True,
|
|
57
60
|
)
|
|
58
61
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
return str(val) if val is not None else ""
|
|
73
|
-
exc_stdout = to_str(exc.stdout)
|
|
74
|
-
exc_stderr = to_str(exc.stderr)
|
|
75
|
-
output = exc_stdout + exc_stderr
|
|
76
|
-
|
|
77
|
-
if not isinstance(output, str):
|
|
78
|
-
output = str(output)
|
|
62
|
+
# Read cloudflared logs incrementally so we can return as soon as the URL appears.
|
|
63
|
+
output_queue: queue.Queue[str | None] = queue.Queue()
|
|
64
|
+
|
|
65
|
+
def _reader() -> None:
|
|
66
|
+
stream = process.stdout
|
|
67
|
+
if stream is None:
|
|
68
|
+
output_queue.put(None)
|
|
69
|
+
return
|
|
70
|
+
try:
|
|
71
|
+
for line in stream:
|
|
72
|
+
output_queue.put(line)
|
|
73
|
+
finally:
|
|
74
|
+
output_queue.put(None)
|
|
79
75
|
|
|
76
|
+
threading.Thread(target=_reader, daemon=True).start()
|
|
77
|
+
|
|
78
|
+
output = ""
|
|
79
|
+
deadline = time.monotonic() + max(startup_timeout, 0.1)
|
|
80
|
+
stream_closed = False
|
|
81
|
+
while time.monotonic() < deadline:
|
|
82
|
+
try:
|
|
83
|
+
chunk = output_queue.get(timeout=0.1)
|
|
84
|
+
except queue.Empty:
|
|
85
|
+
chunk = ""
|
|
86
|
+
|
|
87
|
+
if chunk is None:
|
|
88
|
+
stream_closed = True
|
|
89
|
+
break
|
|
90
|
+
if chunk:
|
|
91
|
+
output += chunk
|
|
92
|
+
url = _extract_trycloudflare_url(output)
|
|
93
|
+
if url:
|
|
94
|
+
_CLOUDFLARED_PROCESSES.append(process)
|
|
95
|
+
return url
|
|
96
|
+
|
|
97
|
+
if process.poll() is not None and output_queue.empty():
|
|
98
|
+
break
|
|
99
|
+
|
|
100
|
+
# One final parse pass before treating startup as failed.
|
|
80
101
|
url = _extract_trycloudflare_url(output)
|
|
81
102
|
if url:
|
|
82
103
|
_CLOUDFLARED_PROCESSES.append(process)
|
|
83
104
|
return url
|
|
84
105
|
|
|
85
|
-
process.
|
|
106
|
+
if not stream_closed and process.poll() is None:
|
|
107
|
+
process.terminate()
|
|
108
|
+
|
|
86
109
|
return None
|
|
87
110
|
|
|
88
111
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devlinker
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.4
|
|
4
4
|
Summary: A lightweight proxy that combines your frontend and backend into one link for easy development and sharing.
|
|
5
5
|
Author-email: Mani <mani1028@users.noreply.github.com>
|
|
6
6
|
Requires-Python: >=3.7
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "devlinker"
|
|
7
|
-
version = "1.4.
|
|
7
|
+
version = "1.4.4"
|
|
8
8
|
description = "A lightweight proxy that combines your frontend and backend into one link for easy development and sharing."
|
|
9
9
|
authors = [
|
|
10
10
|
{ name = "Mani", email = "mani1028@users.noreply.github.com" }
|
|
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
|
|
File without changes
|
|
File without changes
|