rottenstuff commited on
Commit
34943bb
·
verified ·
1 Parent(s): 81966b1

Upload 12 files

Browse files
colab_tunnel/_diagnostics.py CHANGED
@@ -84,15 +84,16 @@ def _stop_test_server(server_bin: Path) -> None:
84
  def _check_url_latency(
85
  url: str,
86
  timeout: float = 10.0,
87
- warmup_retries: int = 2,
88
  warmup_delay: float = 5.0,
89
  ) -> Optional[float]:
90
  """
91
  Проверяет доступность публичного URL и возвращает RTT.
92
 
93
- Делает повторные попытки с паузой — некоторые провайдеры (cloudflared,
94
- tunnelite) анонсируют URL раньше, чем маршрут фактически распространяется
95
- по их сети. Cloudflared обычно требует 5–10 секунд после старта.
 
96
 
97
  Returns:
98
  RTT в секундах, или None если URL недоступен после всех попыток.
@@ -102,7 +103,9 @@ def _check_url_latency(
102
 
103
  for attempt in range(warmup_retries):
104
  if attempt > 0:
105
- logger.debug(f'Повтор проверки доступности (попытка {attempt + 1}): {clean_url}')
 
 
106
  time.sleep(warmup_delay)
107
  try:
108
  t = time.time()
@@ -112,11 +115,12 @@ def _check_url_latency(
112
  return elapsed
113
  logger.debug(f'HTTP {resp.status_code} от {clean_url}')
114
  except Exception as e:
115
- logger.debug(f'Попытка {attempt + 1}/{warmup_retries} не удалась: {e}')
116
 
117
  return None
118
 
119
 
 
120
  def _print_report(results: list[TunnelBenchmarkResult]) -> None:
121
  """Выводит форматированную таблицу результатов."""
122
  W = {'name': 14, 'status': 10, 'tunnel': 14, 'latency': 12}
 
84
  def _check_url_latency(
85
  url: str,
86
  timeout: float = 10.0,
87
+ warmup_retries: int = 3,
88
  warmup_delay: float = 5.0,
89
  ) -> Optional[float]:
90
  """
91
  Проверяет доступность публичного URL и возвращает RTT.
92
 
93
+ Некоторые провайдеры (cloudflared, tunnelite) анонсируют URL раньше,
94
+ чем маршрут фактически распространяется по их сети. cloudflared
95
+ обычно требует 5–15 секунд после получения URL.
96
+ Делаем несколько попыток с паузой между ними.
97
 
98
  Returns:
99
  RTT в секундах, или None если URL недоступен после всех попыток.
 
103
 
104
  for attempt in range(warmup_retries):
105
  if attempt > 0:
106
+ logger.debug(
107
+ f'Повтор проверки доступности {attempt + 1}/{warmup_retries}: {clean_url}'
108
+ )
109
  time.sleep(warmup_delay)
110
  try:
111
  t = time.time()
 
115
  return elapsed
116
  logger.debug(f'HTTP {resp.status_code} от {clean_url}')
117
  except Exception as e:
118
+ logger.debug(f'Попытка {attempt + 1}: {e}')
119
 
120
  return None
121
 
122
 
123
+
124
  def _print_report(results: list[TunnelBenchmarkResult]) -> None:
125
  """Выводит форматированную таблицу результатов."""
126
  W = {'name': 14, 'status': 10, 'tunnel': 14, 'latency': 12}
colab_tunnel/_tunnels.py CHANGED
@@ -37,6 +37,7 @@ def get_revproxy_url(
37
  write_link: bool = False,
38
  stdin_input: str | None = None,
39
  extra_env: dict[str, str] | None = None,
 
40
  ) -> str:
41
  """
42
  Универсальный запуск бинарного туннельного провайдера.
@@ -56,6 +57,7 @@ def get_revproxy_url(
56
  stdin_input: Строка для отправки в stdin процесса.
57
  extra_env: Дополнительные переменные окружения для процесса.
58
  Накладываются поверх os.environ (не заменяют его).
 
59
 
60
  Returns:
61
  Публичный URL туннеля.
@@ -119,6 +121,7 @@ def get_revproxy_url(
119
  url_pattern=url_pattern,
120
  timeout=timeout,
121
  read_from_stderr=read_from_stderr,
 
122
  )
123
  except RuntimeError:
124
  kill_process_by_name(bin_path.name)
@@ -315,7 +318,6 @@ def get_beeceptor_url(port: int) -> str:
315
 
316
  @tunnel_provider('bore')
317
  def get_bore_url(port: int) -> str:
318
- # Для актуальной версии запрашиваем GitHub API, только если бинарника ещё нет
319
  bin_url = 'https://github.com/ekzhang/bore/releases/download/v0.6.0/bore-v0.6.0-x86_64-unknown-linux-musl.tar.gz'
320
  if not bore_bin.exists():
321
  try:
@@ -332,16 +334,17 @@ def get_bore_url(port: int) -> str:
332
  need_unpack=True,
333
  bin_path=bore_bin,
334
  start_commands=[str(bore_bin), 'local', str(port), '--to', 'bore.pub'],
335
- read_from_stderr=True, # Rust логи идут в stderr
 
 
 
336
  url_pattern=r'bore\.pub:\d+',
337
- timeout=15.0,
338
- # Bore использует Rust-крейт `tracing`. Без RUST_LOG=info строка
339
- # "listening at bore.pub:XXXXX" (INFO-уровень) не выводится совсем.
340
- extra_env={'RUST_LOG': 'info'},
341
  )
342
  return f'http://{raw}/'
343
 
344
 
 
345
  @tunnel_provider('boredigital')
346
  def get_boredigital_url(port: int) -> str:
347
  return get_revproxy_url(
 
37
  write_link: bool = False,
38
  stdin_input: str | None = None,
39
  extra_env: dict[str, str] | None = None,
40
+ read_both_streams: bool = False,
41
  ) -> str:
42
  """
43
  Универсальный запуск бинарного туннельного провайдера.
 
57
  stdin_input: Строка для отправки в stdin процесса.
58
  extra_env: Дополнительные переменные окружения для процесса.
59
  Накладываются поверх os.environ (не заменяют его).
60
+ read_both_streams: Читать ли оба потока.
61
 
62
  Returns:
63
  Публичный URL туннеля.
 
121
  url_pattern=url_pattern,
122
  timeout=timeout,
123
  read_from_stderr=read_from_stderr,
124
+ read_both_streams=read_both_streams, # ← передаём дальше
125
  )
126
  except RuntimeError:
127
  kill_process_by_name(bin_path.name)
 
318
 
319
  @tunnel_provider('bore')
320
  def get_bore_url(port: int) -> str:
 
321
  bin_url = 'https://github.com/ekzhang/bore/releases/download/v0.6.0/bore-v0.6.0-x86_64-unknown-linux-musl.tar.gz'
322
  if not bore_bin.exists():
323
  try:
 
334
  need_unpack=True,
335
  bin_path=bore_bin,
336
  start_commands=[str(bore_bin), 'local', str(port), '--to', 'bore.pub'],
337
+ # read_both_streams: bore пишет в stderr через tracing-subscriber,
338
+ # но читаем оба потока — на случай если конкретная сборка пишет в stdout
339
+ read_from_stderr=False,
340
+ read_both_streams=True,
341
  url_pattern=r'bore\.pub:\d+',
342
+ timeout=20.0,
 
 
 
343
  )
344
  return f'http://{raw}/'
345
 
346
 
347
+
348
  @tunnel_provider('boredigital')
349
  def get_boredigital_url(port: int) -> str:
350
  return get_revproxy_url(
colab_tunnel/_utils.py CHANGED
@@ -192,84 +192,120 @@ def read_until_pattern(
192
  read_both_streams: bool = False,
193
  ) -> tuple[str, str]:
194
  """
195
- Неблокирующее чтение вывода процесса до нахождения URL-паттерна.
196
 
197
- Использует select() + os.set_blocking() вместо readline(),
198
- что полностью исключает зависание при нехватке строк вывода.
 
 
 
 
 
 
 
199
 
200
  Args:
201
  process: Запущенный Popen с открытыми потоками PIPE.
202
- url_pattern: Регулярное выражение для поиска URL.
203
- timeout: Максимальное время ожидания в секундах.
204
  read_from_stderr: Читать из stderr (иначе stdout).
205
- read_both_streams: Читать из обоих потоков (для SSH).
206
 
207
  Returns:
208
- Кортеж (matched_url, full_output).
209
 
210
  Raises:
211
- RuntimeError: Паттерн не найден за время timeout.
212
  """
 
 
213
  if read_both_streams:
214
- active_streams = [s for s in (process.stdout, process.stderr) if s]
215
  elif read_from_stderr:
216
- active_streams = [process.stderr] if process.stderr else []
217
  else:
218
- active_streams = [process.stdout] if process.stdout else []
219
 
220
- if not active_streams:
221
  raise RuntimeError('Потоки вывода процесса недоступны.')
222
 
223
- raws: list = []
224
- for stream in active_streams:
 
 
 
 
 
 
 
225
  raw = getattr(stream, 'raw', stream)
226
  try:
227
- os.set_blocking(raw.fileno(), False)
 
 
 
 
 
 
 
 
 
 
 
 
 
228
  except Exception:
229
  pass
230
- raws.append(raw)
 
 
 
 
 
 
 
231
 
232
  buffer = b''
233
  deadline = time.time() + timeout
 
234
 
235
  while time.time() < deadline:
 
236
  try:
237
- ready, _, _ = select.select(raws, [], [], 0.1)
238
- except Exception:
239
- break
 
 
 
 
 
 
 
 
 
 
 
 
240
 
241
- for raw in ready:
242
- try:
243
- chunk = raw.read(4096)
244
- if chunk:
245
- buffer += chunk
246
- except (BlockingIOError, ValueError):
247
- pass
248
- except Exception:
249
- pass
250
 
 
251
  text = buffer.decode('utf-8', errors='ignore')
252
  match = re.search(url_pattern, text)
253
  if match:
 
254
  return match.group(), text
255
 
256
- if process.poll() is not None:
257
- # Процесс завершился — финальное чтение остатков
258
- for raw in raws:
259
- try:
260
- remaining = raw.read(65536)
261
- if remaining:
262
- buffer += remaining
263
- except Exception:
264
- pass
265
- text = buffer.decode('utf-8', errors='ignore')
266
- match = re.search(url_pattern, text)
267
- if match:
268
- return match.group(), text
269
- break
270
 
271
- output_str = buffer.decode('utf-8', errors='ignore')
272
- raise RuntimeError(f'URL не найден за {timeout} сек. Вывод процесса:\n{output_str}')
273
 
274
 
275
  def move_path(old_path: Path | str, new_path: Path | str) -> None:
 
192
  read_both_streams: bool = False,
193
  ) -> tuple[str, str]:
194
  """
195
+ Читает вывод процесса с таймаутом, ища URL-паттерн.
196
 
197
+ Использует блокирующие read() в daemon-потоках вместо select() +
198
+ неблокирующих read(). select()-подход даёт гонку при быстрых коротких
199
+ записях (< 1мс): select сигнализирует «readable», но к моменту вызова
200
+ read() данные ещё не попали в буфер пайпа — read возвращает пусто.
201
+ Блокирующий read в потоке этой гонки не имеет: он просто ждёт данных.
202
+
203
+ После нахождения URL потоки автоматически переходят в режим дренирования:
204
+ продолжают читать и отбрасывать данные, предотвращая переполнение
205
+ системного буфера пайпа (обычно 64 КБ) за время жизни туннеля.
206
 
207
  Args:
208
  process: Запущенный Popen с открытыми потоками PIPE.
209
+ url_pattern: Regex для поиска URL.
210
+ timeout: Максимальное время ожидания (сек).
211
  read_from_stderr: Читать из stderr (иначе stdout).
212
+ read_both_streams: Читать из обоих потоков одновременно (для SSH).
213
 
214
  Returns:
215
+ Кортеж (matched_url, full_output_so_far).
216
 
217
  Raises:
218
+ RuntimeError: Паттерн не найден за timeout секунд.
219
  """
220
+ import queue as _queue
221
+
222
  if read_both_streams:
223
+ active = [s for s in (process.stdout, process.stderr) if s]
224
  elif read_from_stderr:
225
+ active = [process.stderr] if process.stderr else []
226
  else:
227
+ active = [process.stdout] if process.stdout else []
228
 
229
+ if not active:
230
  raise RuntimeError('Потоки вывода процесса недоступны.')
231
 
232
+ q: _queue.Queue[bytes | None] = _queue.Queue()
233
+ drain_mode = threading.Event()
234
+
235
+ def _reader(stream) -> None:
236
+ """
237
+ Блокирующий читатель в daemon-потоке.
238
+ Пока URL не найден — пушит чанки в очередь.
239
+ После установки drain_mode — читает и отбрасывает (дренирование).
240
+ """
241
  raw = getattr(stream, 'raw', stream)
242
  try:
243
+ # Явно возвращаем в блокирующий режим на случай,
244
+ # если где-то раньше FD был переведён в неблокирующий
245
+ os.set_blocking(raw.fileno(), True)
246
+ except Exception:
247
+ pass
248
+ try:
249
+ while True:
250
+ chunk = raw.read(4096)
251
+ if not chunk: # EOF — процесс закрыл поток
252
+ break
253
+ if drain_mode.is_set():
254
+ pass # Дренирование: читаем и отбрасываем
255
+ else:
256
+ q.put(chunk) # Ищем URL: передаём в очередь
257
  except Exception:
258
  pass
259
+ finally:
260
+ # Сигнал EOF только если мы всё ещё ищем URL
261
+ if not drain_mode.is_set():
262
+ q.put(None)
263
+
264
+ for stream in active:
265
+ t = threading.Thread(target=_reader, args=(stream,), daemon=True)
266
+ t.start()
267
 
268
  buffer = b''
269
  deadline = time.time() + timeout
270
+ n_sentinels = 0
271
 
272
  while time.time() < deadline:
273
+ remaining = max(0.01, deadline - time.time())
274
  try:
275
+ item = q.get(timeout=min(remaining, 0.5))
276
+ except _queue.Empty:
277
+ if process.poll() is not None:
278
+ # Процесс завершился — дочитываем остатки из очереди
279
+ while True:
280
+ try:
281
+ item = q.get_nowait()
282
+ except _queue.Empty:
283
+ break
284
+ if item is None:
285
+ n_sentinels += 1
286
+ else:
287
+ buffer += item
288
+ break
289
+ continue
290
 
291
+ if item is None:
292
+ n_sentinels += 1
293
+ if n_sentinels >= len(active):
294
+ break # Все читатели достигли EOF
295
+ continue
 
 
 
 
296
 
297
+ buffer += item
298
  text = buffer.decode('utf-8', errors='ignore')
299
  match = re.search(url_pattern, text)
300
  if match:
301
+ drain_mode.set() # Переключаем потоки в режим дренирования
302
  return match.group(), text
303
 
304
+ raise RuntimeError(
305
+ f'URL не найден за {timeout} сек. Вывод процесса:\n'
306
+ + buffer.decode('utf-8', errors='ignore')
307
+ )
 
 
 
 
 
 
 
 
 
 
308
 
 
 
309
 
310
 
311
  def move_path(old_path: Path | str, new_path: Path | str) -> None: