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.
Files changed (32) hide show
  1. {devlinker-1.4.3/devlinker.egg-info → devlinker-1.4.4}/PKG-INFO +1 -1
  2. {devlinker-1.4.3 → devlinker-1.4.4}/devlinker/detector.py +4 -4
  3. {devlinker-1.4.3 → devlinker-1.4.4}/devlinker/main.py +4 -4
  4. {devlinker-1.4.3 → devlinker-1.4.4}/devlinker/proxy.py +22 -5
  5. {devlinker-1.4.3 → devlinker-1.4.4}/devlinker/tunnel.py +44 -21
  6. {devlinker-1.4.3 → devlinker-1.4.4/devlinker.egg-info}/PKG-INFO +1 -1
  7. {devlinker-1.4.3 → devlinker-1.4.4}/pyproject.toml +1 -1
  8. {devlinker-1.4.3 → devlinker-1.4.4}/LICENSE +0 -0
  9. {devlinker-1.4.3 → devlinker-1.4.4}/MANIFEST.in +0 -0
  10. {devlinker-1.4.3 → devlinker-1.4.4}/README.md +0 -0
  11. {devlinker-1.4.3 → devlinker-1.4.4}/devlinker/__init__.py +0 -0
  12. {devlinker-1.4.3 → devlinker-1.4.4}/devlinker/config.py +0 -0
  13. {devlinker-1.4.3 → devlinker-1.4.4}/devlinker/detection_state.py +0 -0
  14. {devlinker-1.4.3 → devlinker-1.4.4}/devlinker/detector_ai.py +0 -0
  15. {devlinker-1.4.3 → devlinker-1.4.4}/devlinker/devlinker_loader_instant.html +0 -0
  16. {devlinker-1.4.3 → devlinker-1.4.4}/devlinker/devlinker_loader_snippet.html +0 -0
  17. {devlinker-1.4.3 → devlinker-1.4.4}/devlinker/doctor.py +0 -0
  18. {devlinker-1.4.3 → devlinker-1.4.4}/devlinker/fix.py +0 -0
  19. {devlinker-1.4.3 → devlinker-1.4.4}/devlinker/fixer.py +0 -0
  20. {devlinker-1.4.3 → devlinker-1.4.4}/devlinker/global_state.py +0 -0
  21. {devlinker-1.4.3 → devlinker-1.4.4}/devlinker/inspect.py +0 -0
  22. {devlinker-1.4.3 → devlinker-1.4.4}/devlinker/logger.py +0 -0
  23. {devlinker-1.4.3 → devlinker-1.4.4}/devlinker/monitor.py +0 -0
  24. {devlinker-1.4.3 → devlinker-1.4.4}/devlinker/runner.py +0 -0
  25. {devlinker-1.4.3 → devlinker-1.4.4}/devlinker/share.py +0 -0
  26. {devlinker-1.4.3 → devlinker-1.4.4}/devlinker.egg-info/SOURCES.txt +0 -0
  27. {devlinker-1.4.3 → devlinker-1.4.4}/devlinker.egg-info/dependency_links.txt +0 -0
  28. {devlinker-1.4.3 → devlinker-1.4.4}/devlinker.egg-info/entry_points.txt +0 -0
  29. {devlinker-1.4.3 → devlinker-1.4.4}/devlinker.egg-info/requires.txt +0 -0
  30. {devlinker-1.4.3 → devlinker-1.4.4}/devlinker.egg-info/top_level.txt +0 -0
  31. {devlinker-1.4.3 → devlinker-1.4.4}/setup.cfg +0 -0
  32. {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
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 = 1.0,
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 = 1.0) -> bool:
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 = 12,
99
- delay_seconds: float = 1.0,
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 = 15,
413
- delay_seconds: float = 1.0,
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 = 15,
432
- delay_seconds: float = 1.0,
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
- thread = threading.Thread(
729
- target=lambda: uvicorn.run(app, host="0.0.0.0", port=proxy_port, log_level="warning"),
730
- daemon=True,
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
- output = ""
60
- try:
61
- stdout, _ = process.communicate(timeout=startup_timeout)
62
- output = stdout or ""
63
- except TimeoutExpired as exc:
64
- # exc.stdout and exc.stderr may be str, bytes, bytearray, or memoryview; convert to str
65
- def to_str(val):
66
- if isinstance(val, str):
67
- return val
68
- if isinstance(val, (bytes, bytearray)):
69
- return val.decode(errors="replace")
70
- if isinstance(val, memoryview):
71
- return val.tobytes().decode(errors="replace")
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.terminate()
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
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.3"
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