Статьи

Очаровательные змеи и бритвенные яки

Первоначально созданный Барри Варшавой

В последние пару дней я отлаживал забавную проблему в инструменте Ubuntu под названием Jockey. Jockey — это инструмент для управления драйверами устройств в Ubuntu . На самом деле он содержит и командную строку, и графический интерфейс, и серверный сервис dbus, который выполняет всю работу (с надлежащей аутентификацией, поскольку он изменяет вашу систему). Ничто из этого не имеет отношения к проблеме, хотя бит dbus вернется, чтобы преследовать нас позже.

Важно то, что Jockey — это приложение на Python, написанное с использованием многих модулей Python, взаимодействующих с низкоуровневыми инструментами, такими как apt и dbus. Оригинальный отчет об ошибке был очень запутанным. Помимо того, что оно не воспроизводилось ни мной, ни другими, фактическое исключение не имело никакого смысла! По сути, именно такой код генерировал ошибку TypeError:

_actions = []
# _actions gets appended to at various times and later...
for item in _actions[:]:
# do something

 

Все, кто сообщал о проблеме, говорили, что ошибка TypeError была добавлена ​​в строку for-Statement. Сообщение об исключении указывало на то, что Python получал какой-то объект, который он пытался преобразовать в целое число, но потерпел неудачу. Как вы могли получить это исключение, когда делали копию списка или перебирали эту копию? Был ли поврежден список? Разве это был не список, а какой-то похожий на список объект, который каким-то образом возвращал нецелые числа для своих индексов min и max?

Что еще хуже, этот небольшой фрагмент кода был в стандартной библиотеке Python, в модуле подпроцесса. Быстрый поиск в базе данных ошибок Python выявил некоторые недавние темы об изменениях, сделанных таким образом, чтобы гарантировать, что раскрытые объекты были должным образом очищены сборщиком мусора, если они не были удалены программой явно. Обратите внимание, что мы используем Python 2.7 здесь, и после некоторого прочтения проблем с трекером и кода Python subprocess.py я просто не смог увидеть здесь никаких проблем или, по крайней мере, ни одной, которая могла бы привести к появлению ошибки.

Когда я первоначально посмотрел на отчет об ошибке Launchpad, это было примерно настолько, насколько я понял. Я не мог увидеть, как эта ошибка могла произойти, и я не мог воспроизвести ее, поэтому я установил ошибку в Incomplete. К сожалению, он продолжал бить бета-тестеры Ubuntu Natty, поэтому он не собирался уходить. К счастью, Мартин Питт нашел тестовый рецепт, с помощью которого я мог воспроизвести ошибку 100% времени. Ура! По крайней мере, это, вероятно, не будет гоночным условием.

Как это отладить? Обычно я просто присоединяю gdb к объекту и начинаю трассировку, но проблема заключалась в том, что когда я настраивал бэкэнд Jockey dbus на использование отладочной версии Python, ошибка исчезала (или, скорее, превращалась во что-то, что действительно не было связано) , Вот где я начал точить мой любимый бритвенный нож.

Притча о бритье яков настолько уместна для подобных проблем, что я сделаю быстрый обход. Одним из моих любимых шоу (и моего брата) в 90-х годах были Рен и Стимпи . Это шоу было новаторским, и вы можете увидеть его элементы практически в каждом мультфильме NickToons на кабеле сегодня. Некоторые эпизоды блестящие, а другие ужасные, но в целом Рен и Стимпи — бесспорная классика американской анимации. В одном особом удивительном эпизоде ​​Рен и Стимпи празднуют канун Ястребов Яков, где из ящика поднимается як и бреет его щетину, оставляя утром Стимпи подарок крайне желанной для бритья мрази. Исходя из этого эпизода (скорее всего!) Используется программный термин « бритья яка»«Обычно это означает, что нужно сделать обход после бессмысленного обхода, прежде чем вы сможете действительно решить проблему, с которой вы столкнулись.

В контексте этой ошибки попытка использовать отладочную сборку Python в бэкэнде Jockey dbus была первой попыткой побриться. Поскольку я хотел получить больше информации из процесса, я пытался подключиться к работающему внутреннему процессу, но это оказалось довольно сложным. Я отлаживал это на 64-битной виртуальной машине, а gdb + debug-python просто не взаимодействовал ,

Теперь возникает вопрос: вы решаете эту проблему (или, по крайней мере, попадаете в место, где вы можете решить, сообщать об ошибке или нет), или вы пытаетесь применить другой подход? Первый наиболее определенно бреет яка; это не приблизит вас к решению исходной проблемы, но определенная трата вашего времени будет достаточно, чтобы заставить вас казаться продуктивным. Конечно, неизбежно, что если вы последуете за этой вторичной ошибкой, она приведет к третьей, четвертой и т. Д., Пока вы не будете глубоко погружены в бритье и не приблизитесь к решению исходной проблемы. , Вы должны быть постоянно начеку против этих якских, кроличьих ям.

Я избавлю вас от мрачных подробностей о том, как исправлять ошибки в моем текстовом редакторе, об ошибках в сборке Python в многоархивной системе.и другие пути, которые все ведут к гладким подбородочным якам, но не к счастливым питонам. Когда я попытался присоединиться к бэкэнду Jockey, я на самом деле решил попытаться выяснить, где именно произошла ошибка TypeError, используя поворот для устаревших операторов печати. Путем поиска исходного кода Python я обнаружил около десятка появлений сообщения об ошибке «требуется целое число». Какой из них споткнулся? Я добавил небольшой маркерный текст для каждого такого случая и пересобрал пакет Python, чтобы помочь в отладке.

Вот где я сделал обход бритья яка. Исходный пакет Ubuntu для самого Python запускает полный набор тестов при каждой сборке. Это замечательно для обеспечения высокого качества пакета Python, но это ужасно для времени обработки, когда экспериментальный взлом Python. Сборка пакета с помощью DEB_BUILD_OPTIONS = «nocheck nobench» должна работать, но по какой-то причине не с моей средой sbuild . Я полагаю, что отладка была бы как бритье левого чека яка, но вместо этого было намного проще брить его правым чеком. Поэтому я провел некоторое время с моей бритвой, вырезая огромные секции файла debian / rules, чтобы Python 2.7 работал как можно быстрее. Хотя, казалось бы, бессмысленная задача,это действительно очень помогло, так как я смог опробовать идеи с гораздо более коротким циклом.

Во всяком случае, со многими итерациями над идеей маркировки исключений, я наконец прибил преступника. Это было в Python C API-функции PyInt_AsLong (), но даже здесь я не был уверен, какое из условий условного запускается. Еще один раунд теста hack-build-scp-reset-test, и я обнаружил то, что подозревал: PyInt_AsLong () передавал объект, который нельзя было преобразовать в целое число. Но что это был за объект?

Итак, вернемся к исходной проблеме с GDB. Чтобы решить эту проблему, я скачал и собрал 32-битную виртуальную машину и смог воспроизвести ошибку там. К счастью, в 32-битной среде я был гораздо более успешным в подключении к работающему бэкэнд-процессу Jockey dbus, и хотя у меня не было доступного исходного кода Python (и нет, я не буду говорить о том, куда привел этот конкретный обход бритья яка ), Я мог довольно легко напечатать объекты в отладчике в коде-нарушителе, где я узнал, что PyInt_AsLong () вызывается с None в качестве аргумента. И да, вы не можете превратить None в целое число в Python!

Но как эта функция вызывается с None? Появление стека вызовов привело меня в конечном итоге к PyArg_Parse (), более старой функции C API, которая анализирует кортеж Python в набор объектов на основе некоторых флагов формата. Это используется при реализации функций Python в C для анализа списка аргументов. Всплывающий стек снова привел меня к некоторому Python-подходящему коду. python-apt — это библиотека C ++, которая предоставляет систему APT программам Python. Он довольно зрелый и надежный, но я не был знаком со всеми его темными углами, как с Python.

Теперь мой первый инстинкт — никогда не обнаруживать ошибку в Python. Это не значит, что их не существует, но просто то, что Python существует так долго, так хорошо протестирован и используется настолько широко, что я всегда подозреваю в подобных случаях (то есть странных, необъяснимых ошибок, которые не имеют смысла) модулей расширения и стороннего кода. И действительно, моя работа привела меня к Python-apt, именно такой сложный модуль расширения Python, в котором могут быть странные скрытые ошибки. Тем не менее, проблема, с которой я теперь столкнулся, заключалась в следующем: стек вызовов привел меня к пути кода, который не имел ничего общего с итерацией по списку или копированием этого списка. Итак, что дает?

Что ж, полезно знать, как работают исключения Python на уровне C. В общих чертах, когда некоторый C-код вызывает исключение, он в основном устанавливает некоторое глобальное состояние, а затем возвращает коды ошибок в стеке, пока либо что-то не перехватит его и не обработает, либо оно не просочится до верхнего цикла eval в Python. Ключевым моментом здесь является то, что обычно существуют два состояния: глобальное значение исключения, действующее в настоящее время, и код ошибки, который возвращается в стек вызовов Си. Обычно этот код возврата равен нулю или единице, но в некоторых случаях он также может быть равен NULL или -1. Документация по Python C API очень хороша для их описания.

Итак, теперь, когда я внимательно посмотрел на код на python-apt, я увидел, что происходит, и все это стало иметь смысл! Пакет Python-apt был пульсирующиминдикатор выполнения, настроенный как обратный вызов клиентом кода python-apt. То есть python-apt не может реально контролировать, что будет возвращать этот обратный вызов. python-apt ожидал, что обратный вызов вернет Python как True или False, но он мог вернуть все, включая ничего! В Python True может быть приведен к целому числу 1, а False — к целому числу 0, а python-apt хочет целое число, поэтому действительно стек вызовов ведет прямо к вызову PyArg_Parse (), чтобы превратить возвращаемый объект обратного вызова в целое число. Что произойдет, если обратный вызов не вернет то, что может быть превращено в целое число, или, что еще хуже, вообще ничего не вернет?

В Python функция всегдавозвращает что-то, даже если нет явного оператора return. В этих случаях None неявно возвращается. Да, вы видите это сейчас. И если нет, то в коде Python-apt была такая подсказка: «Большую часть времени пользователь, который подклассирует метод pulse (), забыл добавить return {True, False}, поэтому мы просто предполагаем, что он хочет True». Перевод: Эй, парень! Вы забыли добавить «return True» или «return False» в свой метод pulse (), и он, вероятно, закончился, дав нам значение None, которое мы покорно передали PyArg_Parse ().

PyArg_Parse () сделал свое дело, когда получил None, правильно установив глобальное состояние исключения в TypeError и вернув нулевой код, указывающий на возникшую ошибку. Но, глядя на код Python-Apt, он распознает код ошибки, нозабывает, что было установлено глобальное исключительное состояние ! То есть, хотя python-apt игнорировал исключение, Python все еще знал об этом. Но поскольку контроль не передавался в цикл eval Python, исключение просто скрывалось там, как злой небритый як, ожидая, что его обнаружат. И на самом деле, в следующий раз, когда сам Python проверил состояние исключения, было, да, цикл for итерировал по совершенно точному объекту списка. Python запускает цикл for, находит скрытую ошибку TypeError и поднимает ее в месте, которое буквально не имеет ничего общего с исходным исключением.

Исправление однострочное. В python-apt, где игнорируется любое исключение, возвращаемое PyArg_Parse (), он должен как проглотить код ошибки (что он делал), так иочистить глобальное состояние исключения (чего он не делал). Добавляя вызов PyErr_Clear (), python-apt поддерживал согласованное состояние интерпретатора и правильно игнорировал ошибку разбора аргумента, тем самым исправляя ошибку.

Как я уже говорил своему коллеге Колину Уотсону, отладка оказалась забавной, хотя и не такой «веселой», как тот, над которой он недавно работал .

Счастливый Як для бритья.

Источник: http://www.wefearchange.org/2011/03/charming-snakes-and-shaving-yaks.html