flashstudio 0.2.2__tar.gz → 0.2.3__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 (128) hide show
  1. {flashstudio-0.2.2/flashstudio.egg-info → flashstudio-0.2.3}/PKG-INFO +1 -1
  2. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/__init__.py +1 -1
  3. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/launcher.py +101 -43
  4. {flashstudio-0.2.2 → flashstudio-0.2.3/flashstudio.egg-info}/PKG-INFO +1 -1
  5. {flashstudio-0.2.2 → flashstudio-0.2.3}/pyproject.toml +1 -1
  6. {flashstudio-0.2.2 → flashstudio-0.2.3}/tests/test_launcher.py +47 -0
  7. {flashstudio-0.2.2 → flashstudio-0.2.3}/LICENSE +0 -0
  8. {flashstudio-0.2.2 → flashstudio-0.2.3}/README.md +0 -0
  9. {flashstudio-0.2.2 → flashstudio-0.2.3}/README_PYPI.md +0 -0
  10. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/app.py +0 -0
  11. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/cli.py +0 -0
  12. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/components/__init__.py +0 -0
  13. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/components/dependencies.py +0 -0
  14. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/components/project_manager.py +0 -0
  15. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/components/sidebar.py +0 -0
  16. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/components/styles.py +0 -0
  17. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/components/wizard.py +0 -0
  18. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/components/zone_drawer/__init__.py +0 -0
  19. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/components/zone_drawer/index.html +0 -0
  20. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/constants.py +0 -0
  21. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/core/__init__.py +0 -0
  22. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/core/constants.py +0 -0
  23. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/__init__.py +0 -0
  24. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/dashboard/__init__.py +0 -0
  25. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/dashboard/_common.py +0 -0
  26. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/dashboard/overview/__init__.py +0 -0
  27. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/dashboard/overview/metrics.py +0 -0
  28. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/dashboard/overview/pipeline_status.py +0 -0
  29. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/dashboard/overview/project_banner.py +0 -0
  30. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/dashboard/overview/quick_actions.py +0 -0
  31. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/dashboard/overview/recent_runs.py +0 -0
  32. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/dashboard/overview/tab.py +0 -0
  33. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/dashboard/page.py +0 -0
  34. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/data/__init__.py +0 -0
  35. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/data/_common.py +0 -0
  36. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/data/download/__init__.py +0 -0
  37. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/data/download/tab.py +0 -0
  38. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/data/edit/__init__.py +0 -0
  39. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/data/edit/tab.py +0 -0
  40. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/data/fix/__init__.py +0 -0
  41. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/data/fix/annotations.py +0 -0
  42. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/data/fix/tab.py +0 -0
  43. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/data/helpers.py +0 -0
  44. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/data/page.py +0 -0
  45. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/data/preview/__init__.py +0 -0
  46. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/data/preview/tab.py +0 -0
  47. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/data/quality/__init__.py +0 -0
  48. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/data/quality/tab.py +0 -0
  49. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/data/upload/__init__.py +0 -0
  50. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/data/upload/tab.py +0 -0
  51. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/data/verify/__init__.py +0 -0
  52. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/data/verify/tab.py +0 -0
  53. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/export/__init__.py +0 -0
  54. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/export/_common.py +0 -0
  55. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/export/optimization/__init__.py +0 -0
  56. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/export/optimization/tab.py +0 -0
  57. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/export/page.py +0 -0
  58. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/export/weights/__init__.py +0 -0
  59. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/export/weights/exporter.py +0 -0
  60. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/export/weights/settings.py +0 -0
  61. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/inference/__init__.py +0 -0
  62. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/inference/_common.py +0 -0
  63. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/inference/data/__init__.py +0 -0
  64. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/inference/data/tab.py +0 -0
  65. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/inference/model/__init__.py +0 -0
  66. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/inference/model/tab.py +0 -0
  67. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/inference/page.py +0 -0
  68. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/inference/run/__init__.py +0 -0
  69. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/inference/run/detection.py +0 -0
  70. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/inference/run/live.py +0 -0
  71. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/inference/run/tab.py +0 -0
  72. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/inference/run/video.py +0 -0
  73. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/inference/solution/__init__.py +0 -0
  74. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/inference/solution/tab.py +0 -0
  75. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/model/__init__.py +0 -0
  76. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/model/_common.py +0 -0
  77. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/model/advanced/__init__.py +0 -0
  78. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/model/advanced/tab.py +0 -0
  79. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/model/architecture/__init__.py +0 -0
  80. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/model/architecture/tab.py +0 -0
  81. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/model/augment/__init__.py +0 -0
  82. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/model/augment/tab.py +0 -0
  83. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/model/hyperparams/__init__.py +0 -0
  84. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/model/hyperparams/tab.py +0 -0
  85. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/model/page.py +0 -0
  86. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/model/utils/__init__.py +0 -0
  87. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/model/utils/config.py +0 -0
  88. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/training/__init__.py +0 -0
  89. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/training/_common.py +0 -0
  90. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/training/compare/__init__.py +0 -0
  91. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/training/compare/tab.py +0 -0
  92. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/training/launch/__init__.py +0 -0
  93. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/training/launch/controls.py +0 -0
  94. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/training/launch/dialogs.py +0 -0
  95. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/training/launch/preflight.py +0 -0
  96. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/training/launch/runner.py +0 -0
  97. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/training/launch/tab.py +0 -0
  98. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/training/monitor/__init__.py +0 -0
  99. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/training/monitor/checkpoints.py +0 -0
  100. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/training/monitor/curves.py +0 -0
  101. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/training/monitor/dashboard.py +0 -0
  102. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/training/monitor/gt_verification.py +0 -0
  103. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/training/monitor/log_viewer.py +0 -0
  104. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/training/monitor/parsers.py +0 -0
  105. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/training/monitor/run_meta.py +0 -0
  106. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/training/monitor/tab.py +0 -0
  107. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/training/monitor/visualizations.py +0 -0
  108. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/pages/training/page.py +0 -0
  109. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/utils/__init__.py +0 -0
  110. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/utils/config.py +0 -0
  111. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/utils/device.py +0 -0
  112. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/utils/filesystem.py +0 -0
  113. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/utils/jobs.py +0 -0
  114. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/utils/model_loader.py +0 -0
  115. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/utils/trainer_launcher.py +0 -0
  116. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/utils/training_hooks.py +0 -0
  117. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio/utils/weight_hub.py +0 -0
  118. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio.egg-info/SOURCES.txt +0 -0
  119. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio.egg-info/dependency_links.txt +0 -0
  120. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio.egg-info/entry_points.txt +0 -0
  121. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio.egg-info/requires.txt +0 -0
  122. {flashstudio-0.2.2 → flashstudio-0.2.3}/flashstudio.egg-info/top_level.txt +0 -0
  123. {flashstudio-0.2.2 → flashstudio-0.2.3}/setup.cfg +0 -0
  124. {flashstudio-0.2.2 → flashstudio-0.2.3}/tests/test_app.py +0 -0
  125. {flashstudio-0.2.2 → flashstudio-0.2.3}/tests/test_cli.py +0 -0
  126. {flashstudio-0.2.2 → flashstudio-0.2.3}/tests/test_constants.py +0 -0
  127. {flashstudio-0.2.2 → flashstudio-0.2.3}/tests/test_pipeline_flow.py +0 -0
  128. {flashstudio-0.2.2 → flashstudio-0.2.3}/tests/test_save_dir_compat.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flashstudio
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: Interactive Training & Inference UI for FlashDet — runs on Google Colab
5
5
  Author: FlashVision
6
6
  License-Expression: Apache-2.0
@@ -1,5 +1,5 @@
1
1
  """FlashStudio — Interactive Training & Inference UI for FlashDet."""
2
2
 
3
- __version__ = "0.2.2"
3
+ __version__ = "0.2.3"
4
4
 
5
5
  from flashstudio.launcher import launch # noqa: F401
@@ -1,9 +1,12 @@
1
1
  """Launch FlashStudio UI — works locally and inside Google Colab."""
2
2
 
3
+ import atexit
4
+ import platform
3
5
  import subprocess
4
6
  import sys
5
7
  import os
6
8
  import re
9
+ import tempfile
7
10
  import time as _time
8
11
  from pathlib import Path
9
12
 
@@ -69,59 +72,115 @@ def launch(port: int = 8501, share: bool = True, ngrok_token: str | None = None)
69
72
  # Tunnel helpers
70
73
  # ---------------------------------------------------------------------------
71
74
 
72
- def _start_cloudflared_tunnel(port: int) -> str | None:
73
- """Start a free Cloudflare tunnel (no sign-up required).
75
+ # Keep tunnel processes referenced for the lifetime of the session. If these
76
+ # are garbage collected or terminated, the trycloudflare.com hostname starts
77
+ # returning Cloudflare error 1033 ("tunnel cannot resolve").
78
+ _LIVE_TUNNELS: list = []
79
+
80
+
81
+ def _shutdown_tunnels():
82
+ for proc in list(_LIVE_TUNNELS):
83
+ try:
84
+ if proc.poll() is None:
85
+ proc.terminate()
86
+ except OSError:
87
+ pass
88
+
89
+
90
+ atexit.register(_shutdown_tunnels)
91
+
92
+
93
+ def _cloudflared_binary() -> str | None:
94
+ """Return a usable cloudflared binary path, downloading it if needed."""
95
+ import shutil
96
+
97
+ existing = shutil.which("cloudflared")
98
+ if existing:
99
+ return existing
74
100
 
75
- Returns the public https URL or None on failure.
76
- """
77
101
  binary = "/usr/local/bin/cloudflared"
78
- if not os.path.isfile(binary):
102
+ if os.path.isfile(binary):
103
+ return binary
104
+
105
+ machine = platform.machine().lower()
106
+ arch = "arm64" if machine in ("aarch64", "arm64") else "amd64"
107
+ url = ("https://github.com/cloudflare/cloudflared/releases/latest/download/"
108
+ f"cloudflared-linux-{arch}")
109
+
110
+ try:
111
+ subprocess.check_call(
112
+ ["wget", "-q", url, "-O", binary],
113
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
114
+ )
115
+ except Exception:
79
116
  try:
80
- subprocess.check_call(
81
- ["wget", "-q",
82
- "https://github.com/cloudflare/cloudflared/releases/latest"
83
- "/download/cloudflared-linux-amd64",
84
- "-O", binary],
85
- stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
86
- )
87
- os.chmod(binary, 0o755)
117
+ import urllib.request
118
+ urllib.request.urlretrieve(url, binary)
88
119
  except Exception:
89
120
  return None
90
121
 
91
122
  try:
92
- tunnel_proc = subprocess.Popen(
93
- [binary, "tunnel", "--url", f"http://localhost:{port}"],
94
- stdout=subprocess.PIPE,
95
- stderr=subprocess.PIPE,
96
- )
123
+ os.chmod(binary, 0o755)
97
124
  except OSError:
98
125
  return None
126
+ return binary
99
127
 
100
- url_re = re.compile(r"https://[a-z0-9-]+\.trycloudflare\.com")
101
- deadline = _time.monotonic() + 20
128
+
129
+ def _start_cloudflared_tunnel(port: int) -> str | None:
130
+ """Start a free Cloudflare quick tunnel (no sign-up required).
131
+
132
+ The tunnel process is kept alive for the rest of the session so the URL
133
+ stays usable. Returns the public https URL or None on failure.
134
+ """
135
+ binary = _cloudflared_binary()
136
+ if not binary:
137
+ return None
138
+
139
+ fd, log_name = tempfile.mkstemp(prefix="flashstudio-cloudflared-",
140
+ suffix=".log")
141
+ os.close(fd)
142
+ log_path = Path(log_name)
102
143
  try:
103
- import select
104
- while _time.monotonic() < deadline:
105
- ready, _, _ = select.select([tunnel_proc.stderr], [], [], 0.5)
106
- if not ready:
107
- if tunnel_proc.poll() is not None:
108
- return None
109
- continue
110
- line = tunnel_proc.stderr.readline().decode("utf-8", errors="replace")
111
- if not line:
112
- if tunnel_proc.poll() is not None:
113
- return None
114
- continue
115
- match = url_re.search(line)
116
- if match:
117
- return match.group(0)
118
- finally:
119
- if tunnel_proc.poll() is None:
120
- try:
121
- tunnel_proc.terminate()
122
- except OSError:
123
- pass
144
+ with open(log_path, "wb") as log_file:
145
+ proc = subprocess.Popen(
146
+ [binary, "tunnel", "--no-autoupdate", "--protocol", "http2",
147
+ "--url", f"http://127.0.0.1:{port}"],
148
+ stdout=log_file,
149
+ stderr=subprocess.STDOUT,
150
+ )
151
+ except OSError:
152
+ log_path.unlink(missing_ok=True)
153
+ return None
124
154
 
155
+ _LIVE_TUNNELS.append(proc)
156
+
157
+ url_re = re.compile(r"https://[a-z0-9-]+\.trycloudflare\.com")
158
+ deadline = _time.monotonic() + 30
159
+ while _time.monotonic() < deadline:
160
+ if proc.poll() is not None:
161
+ break
162
+ try:
163
+ output = log_path.read_text(encoding="utf-8", errors="replace")
164
+ except OSError:
165
+ output = ""
166
+ match = url_re.search(output)
167
+ if match:
168
+ return match.group(0)
169
+ _time.sleep(0.5)
170
+
171
+ # Timed out or cloudflared exited — clean up so we don't leak a dead tunnel.
172
+ if proc.poll() is None:
173
+ try:
174
+ proc.terminate()
175
+ proc.wait(timeout=5)
176
+ except Exception:
177
+ proc.kill()
178
+ if proc in _LIVE_TUNNELS:
179
+ _LIVE_TUNNELS.remove(proc)
180
+ try:
181
+ log_path.unlink(missing_ok=True)
182
+ except OSError:
183
+ pass
125
184
  return None
126
185
 
127
186
 
@@ -226,9 +285,8 @@ def _launch_colab(app_path: str, port: int, share: bool, ngrok_token: str | None
226
285
  print(" Skipped (using Colab proxy)")
227
286
 
228
287
  # 3) Colab's built-in proxy (fallback — desktop browser only)
229
- colab_proxy_url = _get_colab_proxy_url(port)
230
288
  if not public_url:
231
- public_url = colab_proxy_url
289
+ public_url = _get_colab_proxy_url(port)
232
290
 
233
291
  # --- Display results ---
234
292
  print(f"\n{'=' * 60}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flashstudio
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: Interactive Training & Inference UI for FlashDet — runs on Google Colab
5
5
  Author: FlashVision
6
6
  License-Expression: Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "flashstudio"
7
- version = "0.2.2"
7
+ version = "0.2.3"
8
8
  description = "Interactive Training & Inference UI for FlashDet — runs on Google Colab"
9
9
  readme = "README_PYPI.md"
10
10
  license = "Apache-2.0"
@@ -58,3 +58,50 @@ class TestLaunchFunction:
58
58
  def test_import(self):
59
59
  from flashstudio.launcher import launch
60
60
  assert callable(launch)
61
+
62
+
63
+ class TestCloudflaredTunnel:
64
+ def test_tunnel_process_is_kept_alive(self):
65
+ """The tunnel must stay running, otherwise trycloudflare returns 1033."""
66
+ from flashstudio import launcher
67
+
68
+ class FakeProc:
69
+ def __init__(self):
70
+ self.returncode = None
71
+
72
+ def poll(self):
73
+ return self.returncode
74
+
75
+ def terminate(self):
76
+ self.returncode = -15
77
+
78
+ def wait(self, timeout=None):
79
+ return self.returncode
80
+
81
+ def kill(self):
82
+ self.returncode = -9
83
+
84
+ fake_proc = FakeProc()
85
+
86
+ def fake_popen(cmd, stdout=None, stderr=None):
87
+ stdout.write(b"Your quick Tunnel: https://unit-test-tunnel.trycloudflare.com\n")
88
+ stdout.flush()
89
+ return fake_proc
90
+
91
+ launcher._LIVE_TUNNELS.clear()
92
+ try:
93
+ with patch.object(launcher, "_cloudflared_binary", return_value="/usr/bin/cloudflared"), \
94
+ patch.object(launcher.subprocess, "Popen", side_effect=fake_popen):
95
+ url = launcher._start_cloudflared_tunnel(8501)
96
+
97
+ assert url == "https://unit-test-tunnel.trycloudflare.com"
98
+ assert fake_proc.poll() is None
99
+ assert fake_proc in launcher._LIVE_TUNNELS
100
+ finally:
101
+ launcher._LIVE_TUNNELS.clear()
102
+
103
+ def test_missing_binary_returns_none(self):
104
+ from flashstudio import launcher
105
+
106
+ with patch.object(launcher, "_cloudflared_binary", return_value=None):
107
+ assert launcher._start_cloudflared_tunnel(8501) is None
File without changes
File without changes
File without changes
File without changes