Уроборос: запись вызовов работающей программы Отказы: когда инструмент не делает того, о чём его попросили
0%

Отказы: когда инструмент не делает того, о чём его попросили

Отказы: когда инструмент не делает того, о чём его попросили

У инструмента три места, где он отвечает «нет» или «не всё». Все три — осознанные, и каждое сообщает что-то полезное. Разбираем по очереди, с настоящим выводом.

Отказ первый: исходник не разобрался

Инструменту нужно найти в файле границы функций. Для этого он разбирает файл настоящим разборщиком того языка: у Python — стандартным ast, у C и C++ — libclang, у JavaScript — разборщиком из его же мира.

Если файл не разбирается, инструмент не сохраняет ничего.

Посмотрим. В песочнице уже лежит один нормальный файл, good.py. Пишем рядом заведомо сломанный:

printf 'def broken(:\n    return 1\n' | ouroboros write ./demo broken.py
[python] corrupted source in broken.py: invalid syntax (<unknown>, line 1)

Код возврата — 1. И, что важнее, файла в песочнице не появилось:

ls ./demo/draft/
good.py
ouroboros_runtime.py

broken.py нет. Не «есть, но пустой», не «есть, но необмазанный» — нет вовсе.

Почему так, а не иначе. Разборщик языка уже умеет отличать текст, который является программой, от текста, который ею не является. Раз он всё равно запускается на каждой записи, его вывод используется и как проверка. Файл, который не разобрался, не сохраняется — значит сломанного полуобмазанного состояния в песочнице не возникает никогда: либо файл разобран и обмазан, либо его там нет.

Отказ этот адресован вам, а не инструменту. Сообщение называет язык, файл, причину и строку — этого хватает, чтобы починить.

Отказ второй: вызов вошёл и не вернулся

Записей на вызов две. Значит вызов, который не завершился, оставляет одну — строку входа без парной строки выхода.

Такое бывает, когда программа зависла, получила сигнал или вышла жёстко, минуя обычный порядок. Проверим на программе, которая обрывает себя посередине:

import os


def never_returns(n):
    os._exit(3)


def caller():
    never_returns(7)


caller()

Обмазываем, запускаем, смотрим сводку:

ouroboros wrap-file halt.py
OUROBOROS_DEBUG_INFO=./halt.info python3 halt.py
ouroboros trace-stats ./halt.info
{
  "ok": true,
  "path": "halt.info",
  "calls_parsed": 0,
  "malformed": 0,
  "total_calls": 0,
  "in_flight": [
    {
      "name": "caller",
      "call_id": "4adeb28e-010a-44e0-a080-8cd798fe06bc",
      "started": "2026-09-10T19:59:33.724",
      "cpu": null,
      "thread": "19.128912376537600"
    },
    {
      "name": "never_returns",
      "call_id": "7d4d09a0-640d-4497-9645-6b34fcace314",
      "started": "2026-09-10T19:59:33.724",
      "cpu": null,
      "thread": "19.128912376537600"
    }
  ],
  "by_function": [],
  "by_thread": [],
  "duration_seconds": null,
  "timespan": null,
  "note": "counts/durations are over completed calls; `duration_seconds` are REAL per-call durations (exit−entry) from each call's `d`. `by_thread` groups calls by the `th` token (CPUs each thread ran on); empty for traces with no thread field. `in_flight` = entered (`p:in`) but never completed. `timespan` is first→last entry time."
}

Завершённых вызовов ноль. Незавершённых — два, и названы оба: программа вошла в caller, оттуда в never_returns и не вышла ни из одного.

Это не поломка трассы, а её ответ. Списка in_flight не было бы вовсе, если бы записывалась одна строка на вызов, — писать её было бы просто некуда. Ради этого случая записей и две.

Практически: увидели непустой in_flight — читайте его как «вот где оно осталось». Самый глубокий вызов в списке и есть место, дальше которого программа не прошла.

Отказ третий: в трассе оказалось не только записи

Разбор трассы устроен осторожно. Строка, которая не начинается со скобки { или не разбирается как JSON, не роняет чтение — она считается битой и попадает в счётчик malformed.

Добавим в трассу две посторонние строки и перечитаем:

cp debug.info debug.copy
printf 'не json\n{"p":"in"\n' >> ./debug.copy
ouroboros trace-stats ./debug.copy

Начало ответа — дальше идёт обычная сводка по функциям:

{
  "ok": true,
  "path": "debug.copy",
  "calls_parsed": 9,
  "malformed": 2,
  "total_calls": 9,
  "in_flight": [],
  "by_function": [

Девять вызовов разобрано, две строки признаны битыми, чтение прошло. Так и задумано: трасса может обрываться на середине строки (программа упала в момент записи), и терять из-за этого весь остальной разбор было бы глупо.

Отдельно стоит знать, что не считается битым. Песочница дописывает в debug.info служебную строку про каждую запущенную команду:

{"p":"exec","cmd":["python3","discount.py"],"rc":1,"out":"9999 False 9999.0\n10000 False 9000.0\n500 True 425.0\n","err":"Traceback (most recent call last):\n  …\nTypeError: '>=' not supported between instances of 'str' and 'int'\n"}

Многоточие тут наше: в трассе поле err держит всю трассировку целиком. Строка — правильный JSON, но она не про вызов функции. Разбор такие строки пропускает молча и в malformed не считает — иначе счётчик битых строк рос бы на каждом запуске и перестал бы что-либо значить.

Отсюда практическое чтение: malformed больше нуля — это про порчу, а не про посторонние записи. Стоит посмотреть, что там за строки.

Чего в этом списке нет

Инструмент не отказывается записывать вызов из-за того, что довод показался ему странным, длинным или непонятным. Длинные значения он обрезает (у Python предел — 200 знаков на значение), но запись всё равно делает.

Это стоит держать в голове, потому что обрезанное значение выглядит как значение. Если в трассе видно 'ааааа…' — возможно, там было гораздо больше. Про это подробнее в главе про границы.

Итого

отказ что значит что делать
corrupted source файл не разобрался, записи не было починить синтаксис
непустой in_flight вызовы вошли и не вернулись смотреть самый глубокий: там и встало
malformed больше нуля в трассе строки, которые не разобрались посмотреть, что за строки и откуда

Дальше — что записывать и как найти нужное: обмазывать весь файл нужно не всегда, а девять тысяч записей глазами не читают.

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

Статья описывает общий случай. Если у вас частный — можно разобрать его отдельно, платно. А если не хватает целого материала, предложите тему: её оплачивают вскладчину, и она выходит открытой для всех.

Доска запросов
Дальше