Искать ошибки в программах — непростая задача. Здесь нет никаких готовых методик или рецептов успеха. Можно даже сказать, что это — искусство. Тем не менее есть общие советы, которые помогут вам при поиске. В статье описаны основные шаги, которые стоит предпринять, если ваша программа работает некорректно.
Шаг 1: Занесите ошибку в трекер
После выполнения всех описанных ниже шагов может так случиться, что вы будете рвать на себе волосы от безысходности, все еще сидя на работе, когда поймете, что:
- Вы забыли какую-то важную деталь об ошибке, например, в чем она заключалась.
- Вы могли делегировать ее кому-то более опытному.
Трекер поможет вам не потерять нить размышлений и о текущей проблеме, и о той, которую вы временно отложили. А если вы работаете в команде, это поможет делегировать исправление коллеге и держать все обсуждение в одном месте.
Вы должны записать в трекер следующую информацию:
- Что делал пользователь.
- Что он ожидал увидеть.
- Что случилось на самом деле.
Это должно подсказать, как воспроизвести ошибку. Если вы не сможете воспроизвести ее в любое время, ваши шансы исправить ошибку стремятся к нулю.
Шаг 2: Поищите сообщение об ошибке в сети
Если у вас есть сообщение об ошибке, то вам повезло. Или оно будет достаточно информативным, чтобы вы поняли, где и в чем заключается ошибка, или у вас будет готовый запрос для поиска в сети. Не повезло? Тогда переходите к следующему шагу.
Шаг 3: Найдите строку, в которой проявляется ошибка
Если ошибка вызывает падение программы, попробуйте запустить её в IDE под отладчиком и посмотрите, на какой строчке кода она остановится. Совершенно необязательно, что ошибка будет именно в этой строке (см. следующий шаг), но, по крайней мере, это может дать вам информацию о природе бага.
Шаг 4: Найдите точную строку, в которой появилась ошибка
Как только вы найдете строку, в которой проявляется ошибка, вы можете пройти назад по коду, чтобы найти, где она содержится. Иногда это может быть одна и та же строка. Но чаще всего вы обнаружите, что строка, на которой упала программа, ни при чем, а причина ошибки — в неправильных данных, которые появились ранее.
Если вы отслеживаете выполнение программы в отладчике, то вы можете пройтись назад по стектрейсу, чтобы найти ошибку. Если вы находитесь внутри функции, вызванной внутри другой функции, вызванной внутри другой функции, то стектрейс покажет список функций до самой точки входа в программу (функции main()
). Если ошибка случилась где-то в подключаемой библиотеке, предположите, что ошибка все-таки в вашей программе — это случается гораздо чаще. Найдите по стектрейсу, откуда в вашем коде вызывается библиотечная функция, и продолжайте искать.
Шаг 5: Выясните природу ошибки
Ошибки могут проявлять себя по-разному, но большинство из них можно отнести к той или иной категории. Вот наиболее частые.
- Ошибка на единицу
Вы начали циклfor
с единицы вместо нуля или наоборот. Или, например, подумали, что метод.count()
или.length()
вернул индекс последнего элемента. Проверьте документацию к языку, чтобы убедиться, что нумерация массивов начинается с нуля или с единицы. Эта ошибка иногда проявляется в виде исключенияIndex out of range
. - Состояние гонки
Ваш процесс или поток пытается использовать результат выполнения дочернего до того, как тот завершил свою работу. Ищите использованиеsleep()
в коде. Возможно, на мощной машине дочерний поток выполняется за миллисекунду, а на менее производительной системе происходят задержки. Используйте правильные способы синхронизации многопоточного кода: мьютексы, семафоры, события и т. д. - Неправильные настройки или константы
Проверьте ваши конфигурационные файлы и константы. Я однажды потратил ужасные 16 часов, пытаясь понять, почему корзина на сайте с покупками виснет на стадии отправки заказа. Причина оказалась в неправильном значении в/etc/hosts
, которое не позволяло приложению найти ip-адрес почтового сервера, что вызывало бесконечный цикл в попытке отправить счет заказчику. - Неожиданный null
Бьюсь об заклад, вы не раз получали ошибку с неинициализированной переменной. Убедитесь, что вы проверяете ссылки наnull
, особенно при обращении к свойствам по цепочке. Также проверьте случаи, когда возвращаемое из базы данных значениеNULL
представлено особым типом. - Некорректные входные данные
Вы проверяете вводимые данные? Вы точно не пытаетесь провести арифметические операции с введенными пользователем строками? - Присваивание вместо сравнения
Убедитесь, что вы не написали=
вместо==
, особенно в C-подобных языках. - Ошибка округления
Это случается, когда вы используете целое вместоDecimal
, илиfloat
для денежных сумм, или слишком короткое целое (например, пытаетесь записать число большее, чем 2147483647, в 32-битное целое). Кроме того, может случиться так, что ошибка округления проявляется не сразу, а накапливается со временем (т. н. Эффект бабочки). - Переполнение буфера и выход за пределы массива
Проблема номер один в компьютерной безопасности. Вы выделяете память меньшего объема, чем записываемые туда данные. Или пытаетесь обратиться к элементу за пределами массива. - Программисты не умеют считать
Вы используете некорректную формулу. Проверьте, что вы не используете целочисленное деление вместо взятия остатка, или знаете, как перевести рациональную дробь в десятичную и т. д. - Конкатенация строки и числа
Вы ожидаете конкатенации двух строк, но одно из значений — число, и компилятор пытается произвести арифметические вычисления. Попробуйте явно приводить каждое значение к строке. - 33 символа в varchar(32)
Проверяйте данные, передаваемые вINSERT
, на совпадение типов. Некоторые БД выбрасывают исключения (как и должны делать), некоторые просто обрезают строку (как MySQL). Недавно я столкнулся с такой ошибкой: программист забыл убрать кавычки из строки перед вставкой в базу данных, и длина строки превысила допустимую как раз на два символа. На поиск бага ушло много времени, потому что заметить две маленькие кавычки было сложно. - Некорректное состояние
Вы пытаетесь выполнить запрос при закрытом соединении или пытаетесь вставить запись в таблицу прежде, чем обновили таблицы, от которых она зависит. - Особенности вашей системы, которых нет у пользователя
Например: в тестовой БД между ID заказа и адресом отношение 1:1, и вы программировали, исходя из этого предположения. Но в работе выясняется, что заказы могут отправляться на один и тот же адрес, и, таким образом, у вас отношение 1:многим.
Если ваша ошибка не похожа на описанные выше, или вы не можете найти строку, в которой она появилась, переходите к следующему шагу.
Шаг 6: Метод исключения
Если вы не можете найти строку с ошибкой, попробуйте или отключать (комментировать) блоки кода до тех пор, пока ошибка не пропадет, или, используя фреймворк для юнит-тестов, изолируйте отдельные методы и вызывайте их с теми же параметрами, что и в реальном коде.
Попробуйте отключать компоненты системы один за другим, пока не найдете минимальную конфигурацию, которая будет работать. Затем подключайте их обратно по одному, пока ошибка не вернется. Таким образом вы вернетесь на шаг 3.
Шаг 7: Логгируйте все подряд и анализируйте журнал
Пройдитесь по каждому модулю или компоненту и добавьте больше сообщений. Начинайте постепенно, по одному модулю. Анализируйте лог до тех пор, пока не проявится неисправность. Если этого не случилось, добавьте еще сообщений.
Ваша задача состоит в том, чтобы вернуться к шагу 3, обнаружив, где проявляется ошибка. Также это именно тот случай, когда стоит использовать сторонние библиотеки для более тщательного логгирования.
Шаг 8: Исключите влияние железа или платформы
Замените оперативную память, жесткие диски, поменяйте сервер или рабочую станцию. Установите обновления, удалите обновления. Если ошибка пропадет, то причиной было железо, ОС или среда. Вы можете по желанию попробовать этот шаг раньше, так как неполадки в железе часто маскируют ошибки в ПО.
Если ваша программа работает по сети, проверьте свитч, замените кабель или запустите программу в другой сети.
Ради интереса, переключите кабель питания в другую розетку или к другому ИБП. Безумно? Почему бы не попробовать?
Если у вас возникает одна и та же ошибка вне зависимости от среды, то она в вашем коде.
Шаг 9: Обратите внимание на совпадения
- Ошибка появляется всегда в одно и то же время? Проверьте задачи, выполняющиеся по расписанию.
- Ошибка всегда проявляется вместе с чем-то еще, насколько абсурдной ни была бы эта связь? Обращайте внимание на каждую деталь. На каждую. Например, проявляется ли ошибка, когда включен кондиционер? Возможно, из-за этого падает напряжение в сети, что вызывает странные эффекты в железе.
- Есть ли что-то общее у пользователей программы, даже не связанное с ПО? Например, географическое положение (так был найден легендарный баг с письмом за 500 миль).
- Ошибка проявляется, когда другой процесс забирает достаточно большое количество памяти или ресурсов процессора? (Я однажды нашел в этом причину раздражающей проблемы «no trusted connection» с SQL-сервером).
Шаг 10: Обратитесь в техподдержку
Наконец, пора попросить помощи у того, кто знает больше, чем вы. Для этого у вас должно быть хотя бы примерное понимание того, где находится ошибка — в железе, базе данных, компиляторе. Прежде чем писать письмо разработчикам, попробуйте задать вопрос на профильном форуме.
Ошибки есть в операционных системах, компиляторах, фреймворках и библиотеках, и ваша программа может быть действительно корректна. Но шансы привлечь внимание разработчика к этим ошибкам невелики, если вы не сможете предоставить подробный алгоритм их воспроизведения. Дружелюбный разработчик может помочь вам в этом, но чаще всего, если проблему сложно воспроизвести вас просто проигнорируют. К сожалению, это значит, что нужно приложить больше усилий при составлении багрепорта.
Полезные советы (когда ничего не помогает)
- Позовите кого-нибудь еще.
Попросите коллегу поискать ошибку вместе с вами. Возможно, он заметит что-то, что вы упустили. Это можно сделать на любом этапе. - Внимательно просмотрите код.
Я часто нахожу ошибку, просто спокойно просматривая код с начала и прокручивая его в голове. - Рассмотрите случаи, когда код работает, и сравните их с неработающими.
Недавно я обнаружил ошибку, заключавшуюся в том, что когда вводимые данные в XML-формате содержали строкуxsi:type='xs:string'
, все ломалось, но если этой строки не было, все работало корректно. Оказалось, что дополнительный атрибут ломал механизм десериализации. - Идите спать.
Не бойтесь идти домой до того, как исправите ошибку. Ваши способности обратно пропорциональны вашей усталости. Вы просто потратите время и измотаете себя. - Сделайте творческий перерыв.
Творческий перерыв — это когда вы отвлекаетесь от задачи и переключаете внимание на другие вещи. Вы, возможно, замечали, что лучшие идеи приходят в голову в душе или по пути домой. Смена контекста иногда помогает. Сходите пообедать, посмотрите фильм, полистайте интернет или займитесь другой проблемой. - Закройте глаза на некоторые симптомы и сообщения и попробуйте сначала.
Некоторые баги могут влиять друг на друга. Драйвер для dial-up соединения в Windows 95 мог сообщать, что канал занят, при том что вы могли отчетливо слышать звук соединяющегося модема. Если вам приходится держать в голове слишком много симптомов, попробуйте сконцентрироваться только на одном. Исправьте или найдите его причину и переходите к следующему. - Поиграйте в доктора Хауса (только без Викодина).
Соберите всех коллег, ходите по кабинету с тростью, пишите симптомы на доске и бросайте язвительные комментарии. Раз это работает в сериалах, почему бы не попробовать?
Что вам точно не поможет
- Паника
Не надо сразу палить из пушки по воробьям. Некоторые менеджеры начинают паниковать и сразу откатываться, перезагружать сервера и т. п. в надежде, что что-нибудь из этого исправит проблему. Это никогда не работает. Кроме того, это создает еще больше хаоса и увеличивает время, необходимое для поиска ошибки. Делайте только один шаг за раз. Изучите результат. Обдумайте его, а затем переходите к следующей гипотезе. - «Хелп, плиииз!»
Когда вы обращаетесь на форум за советом, вы как минимум должны уже выполнить шаг 3. Никто не захочет или не сможет вам помочь, если вы не предоставите подробное описание проблемы, включая информацию об ОС, железе и участок проблемного кода. Создавайте тему только тогда, когда можете все подробно описать, и придумайте информативное название для нее. - Переход на личности
Если вы думаете, что в ошибке виноват кто-то другой, постарайтесь по крайней мере говорить с ним вежливо. Оскорбления, крики и паника не помогут человеку решить проблему. Даже если у вас в команде не в почете демократия, крики и применение грубой силы не заставят исправления магическим образом появиться.
Ошибка, которую я недавно исправил
Это была загадочная проблема с дублирующимися именами генерируемых файлов. Дальнейшая проверка показала, что у файлов различное содержание. Это было странно, поскольку имена файлов включали дату и время создания в формате yyMMddhhmmss
. Шаг 9, совпадения: первый файл был создан в полпятого утра, дубликат генерировался в полпятого вечера того же дня. Совпадение? Нет, поскольку hh
в строке формата — это 12-часовой формат времени. Вот оно что! Поменял формат на yyMMddHHmmss
, и ошибка исчезла.
Перевод статьи «How to fix bugs, step by step»
Представляем перевод статьи «How to fix bugs, step by step», опубликованный сайтом tproger.ru.
Искать ошибки в программах — непростая задача. Здесь нет никаких готовых методик или рецептов успеха. Можно даже сказать, что это — искусство. Тем не менее есть общие советы, которые помогут вам при поиске. В статье описаны основные шаги, которые стоит предпринять, если ваша программа работает некорректно.
Шаг 1: Занесите ошибку в трекер
После выполнения всех описанных ниже шагов может так случиться, что вы будете рвать на себе волосы от безысходности, все еще сидя на работе, когда поймете, что:
- Вы забыли какую-то важную деталь об ошибке, например, в чем она заключалась.
- Вы могли делегировать ее кому-то более опытному.
Трекер поможет вам не потерять нить размышлений и о текущей проблеме, и о той, которую вы временно отложили. А если вы работаете в команде, это поможет делегировать исправление коллеге и держать все обсуждение в одном месте.
Вы должны записать в трекер следующую информацию:
- Что делал пользователь.
- Что он ожидал увидеть.
- Что случилось на самом деле.
Это должно подсказать, как воспроизвести ошибку. Если вы не сможете воспроизвести ее в любое время, ваши шансы исправить ошибку стремятся к нулю.
Шаг 2: Поищите сообщение об ошибке в сети
Если у вас есть сообщение об ошибке, то вам повезло. Или оно будет достаточно информативным, чтобы вы поняли, где и в чем заключается ошибка, или у вас будет готовый запрос для поиска в сети. Не повезло? Тогда переходите к следующему шагу.
Шаг 3: Найдите строку, в которой проявляется ошибка
Если ошибка вызывает падение программы, попробуйте запустить её в IDE под отладчиком и посмотрите, на какой строчке кода она остановится. Совершенно необязательно, что ошибка будет именно в этой строке (см. следующий шаг), но, по крайней мере, это может дать вам информацию о природе бага.
Шаг 4: Найдите точную строку, в которой появилась ошибка
Как только вы найдете строку, в которой проявляется ошибка, вы можете пройти назад по коду, чтобы найти, где она содержится. Иногда это может быть одна и та же строка. Но чаще всего вы обнаружите, что строка, на которой упала программа, ни при чем, а причина ошибки — в неправильных данных, которые появились ранее.
Если вы отслеживаете выполнение программы в отладчике, то вы можете пройтись назад по стектрейсу, чтобы найти ошибку. Если вы находитесь внутри функции, вызванной внутри другой функции, вызванной внутри другой функции, то стектрейс покажет список функций до самой точки входа в программу (функции main()
). Если ошибка случилась где-то в подключаемой библиотеке, предположите, что ошибка все-таки в вашей программе — это случается гораздо чаще. Найдите по стектрейсу, откуда в вашем коде вызывается библиотечная функция, и продолжайте искать.
Шаг 5: Выясните природу ошибки
Ошибки могут проявлять себя по-разному, но большинство из них можно отнести к той или иной категории. Вот наиболее частые.
- Ошибка на единицу
Вы начали циклfor
с единицы вместо нуля или наоборот. Или, например, подумали, что метод.count()
или.length()
вернул индекс последнего элемента. Проверьте документацию к языку, чтобы убедиться, что нумерация массивов начинается с нуля или с единицы. Эта ошибка иногда проявляется в виде исключенияIndex out of range
. - Состояние гонки
Ваш процесс или поток пытается использовать результат выполнения дочернего до того, как тот завершил свою работу. Ищите использованиеsleep()
в коде. Возможно, на мощной машине дочерний поток выполняется за миллисекунду, а на менее производительной системе происходят задержки. Используйте правильные способы синхронизации многопоточного кода: мьютексы, семафоры, события и т. д. - Неправильные настройки или константы
Проверьте ваши конфигурационные файлы и константы. Я однажды потратил ужасные 16 часов, пытаясь понять, почему корзина на сайте с покупками виснет на стадии отправки заказа. Причина оказалась в неправильном значении в/etc/hosts
, которое не позволяло приложению найти ip-адрес почтового сервера, что вызывало бесконечный цикл в попытке отправить счет заказчику. - Неожиданный null
Бьюсь об заклад, вы не раз получали ошибку с неинициализированной переменной. Убедитесь, что вы проверяете ссылки наnull
, особенно при обращении к свойствам по цепочке. Также проверьте случаи, когда возвращаемое из базы данных значениеNULL
представлено особым типом. - Некорректные входные данные
Вы проверяете вводимые данные? Вы точно не пытаетесь провести арифметические операции с введенными пользователем строками? - Присваивание вместо сравнения
Убедитесь, что вы не написали=
вместо==
, особенно в C-подобных языках. - Ошибка округления
Это случается, когда вы используете целое вместоDecimal
, илиfloat
для денежных сумм, или слишком короткое целое (например, пытаетесь записать число большее, чем 2147483647, в 32-битное целое). Кроме того, может случиться так, что ошибка округления проявляется не сразу, а накапливается со временем (т. н. Эффект бабочки). - Переполнение буфера и выход за пределы массива
Проблема номер один в компьютерной безопасности. Вы выделяете память меньшего объема, чем записываемые туда данные. Или пытаетесь обратиться к элементу за пределами массива. - Программисты не умеют считать
Вы используете некорректную формулу. Проверьте, что вы не используете целочисленное деление вместо взятия остатка, или знаете, как перевести рациональную дробь в десятичную и т. д. - Конкатенация строки и числа
Вы ожидаете конкатенации двух строк, но одно из значений — число, и компилятор пытается произвести арифметические вычисления. Попробуйте явно приводить каждое значение к строке. - 33 символа в varchar(32)
Проверяйте данные, передаваемые вINSERT
, на совпадение типов. Некоторые БД выбрасывают исключения (как и должны делать), некоторые просто обрезают строку (как MySQL). Недавно я столкнулся с такой ошибкой: программист забыл убрать кавычки из строки перед вставкой в базу данных, и длина строки превысила допустимую как раз на два символа. На поиск бага ушло много времени, потому что заметить две маленькие кавычки было сложно. - Некорректное состояние
Вы пытаетесь выполнить запрос при закрытом соединении или пытаетесь вставить запись в таблицу прежде, чем обновили таблицы, от которых она зависит. - Особенности вашей системы, которых нет у пользователя
Например: в тестовой БД между ID заказа и адресом отношение 1:1, и вы программировали, исходя из этого предположения. Но в работе выясняется, что заказы могут отправляться на один и тот же адрес, и, таким образом, у вас отношение 1:многим.
Если ваша ошибка не похожа на описанные выше, или вы не можете найти строку, в которой она появилась, переходите к следующему шагу.
Шаг 6: Метод исключения
Если вы не можете найти строку с ошибкой, попробуйте или отключать (комментировать) блоки кода до тех пор, пока ошибка не пропадет, или, используя фреймворк для юнит-тестов, изолируйте отдельные методы и вызывайте их с теми же параметрами, что и в реальном коде.
Попробуйте отключать компоненты системы один за другим, пока не найдете минимальную конфигурацию, которая будет работать. Затем подключайте их обратно по одному, пока ошибка не вернется. Таким образом вы вернетесь на шаг 3.
Шаг 7: Логгируйте все подряд и анализируйте журнал
Пройдитесь по каждому модулю или компоненту и добавьте больше сообщений. Начинайте постепенно, по одному модулю. Анализируйте лог до тех пор, пока не проявится неисправность. Если этого не случилось, добавьте еще сообщений.
Ваша задача состоит в том, чтобы вернуться к шагу 3, обнаружив, где проявляется ошибка. Также это именно тот случай, когда стоит использовать сторонние библиотеки для более тщательного логгирования.
Шаг 8: Исключите влияние железа или платформы
Замените оперативную память, жесткие диски, поменяйте сервер или рабочую станцию. Установите обновления, удалите обновления. Если ошибка пропадет, то причиной было железо, ОС или среда. Вы можете по желанию попробовать этот шаг раньше, так как неполадки в железе часто маскируют ошибки в ПО.
Если ваша программа работает по сети, проверьте свитч, замените кабель или запустите программу в другой сети.
Ради интереса, переключите кабель питания в другую розетку или к другому ИБП. Безумно? Почему бы не попробовать?
Если у вас возникает одна и та же ошибка вне зависимости от среды, то она в вашем коде.
Шаг 9: Обратите внимание на совпадения
- Ошибка появляется всегда в одно и то же время? Проверьте задачи, выполняющиеся по расписанию.
- Ошибка всегда проявляется вместе с чем-то еще, насколько абсурдной ни была бы эта связь? Обращайте внимание на каждую деталь. На каждую. Например, проявляется ли ошибка, когда включен кондиционер? Возможно, из-за этого падает напряжение в сети, что вызывает странные эффекты в железе.
- Есть ли что-то общее у пользователей программы, даже не связанное с ПО? Например, географическое положение (так был найден легендарный баг с письмом за 500 миль).
- Ошибка проявляется, когда другой процесс забирает достаточно большое количество памяти или ресурсов процессора? (Я однажды нашел в этом причину раздражающей проблемы «no trusted connection» с SQL-сервером).
Шаг 10: Обратитесь в техподдержку
Наконец, пора попросить помощи у того, кто знает больше, чем вы. Для этого у вас должно быть хотя бы примерное понимание того, где находится ошибка — в железе, базе данных, компиляторе. Прежде чем писать письмо разработчикам, попробуйте задать вопрос на профильном форуме.
Ошибки есть в операционных системах, компиляторах, фреймворках и библиотеках, и ваша программа может быть действительно корректна. Но шансы привлечь внимание разработчика к этим ошибкам невелики, если вы не сможете предоставить подробный алгоритм их воспроизведения. Дружелюбный разработчик может помочь вам в этом, но чаще всего, если проблему сложно воспроизвести вас просто проигнорируют. К сожалению, это значит, что нужно приложить больше усилий при составлении багрепорта.
Полезные советы (когда ничего не помогает)
- Позовите кого-нибудь еще.
Попросите коллегу поискать ошибку вместе с вами. Возможно, он заметит что-то, что вы упустили. Это можно сделать на любом этапе. - Внимательно просмотрите код.
Я часто нахожу ошибку, просто спокойно просматривая код с начала и прокручивая его в голове. - Рассмотрите случаи, когда код работает, и сравните их с неработающими.
Недавно я обнаружил ошибку, заключавшуюся в том, что когда вводимые данные в XML-формате содержали строкуxsi:type='xs:string'
, все ломалось, но если этой строки не было, все работало корректно. Оказалось, что дополнительный атрибут ломал механизм десериализации. - Идите спать.
Не бойтесь идти домой до того, как исправите ошибку. Ваши способности обратно пропорциональны вашей усталости. Вы просто потратите время и измотаете себя. - Сделайте творческий перерыв.
Творческий перерыв — это когда вы отвлекаетесь от задачи и переключаете внимание на другие вещи. Вы, возможно, замечали, что лучшие идеи приходят в голову в душе или по пути домой. Смена контекста иногда помогает. Сходите пообедать, посмотрите фильм, полистайте интернет или займитесь другой проблемой. - Закройте глаза на некоторые симптомы и сообщения и попробуйте сначала.
Некоторые баги могут влиять друг на друга. Драйвер для dial-up соединения в Windows 95 мог сообщать, что канал занят, при том что вы могли отчетливо слышать звук соединяющегося модема. Если вам приходится держать в голове слишком много симптомов, попробуйте сконцентрироваться только на одном. Исправьте или найдите его причину и переходите к следующему. - Поиграйте в доктора Хауса (только без Викодина).
Соберите всех коллег, ходите по кабинету с тростью, пишите симптомы на доске и бросайте язвительные комментарии. Раз это работает в сериалах, почему бы не попробовать?
Что вам точно не поможет
- Паника
Не надо сразу палить из пушки по воробьям. Некоторые менеджеры начинают паниковать и сразу откатываться, перезагружать сервера и т. п. в надежде, что что-нибудь из этого исправит проблему. Это никогда не работает. Кроме того, это создает еще больше хаоса и увеличивает время, необходимое для поиска ошибки. Делайте только один шаг за раз. Изучите результат. Обдумайте его, а затем переходите к следующей гипотезе. - «Хелп, плиииз!»
Когда вы обращаетесь на форум за советом, вы как минимум должны уже выполнить шаг 3. Никто не захочет или не сможет вам помочь, если вы не предоставите подробное описание проблемы, включая информацию об ОС, железе и участок проблемного кода. Создавайте тему только тогда, когда можете все подробно описать, и придумайте информативное название для нее. - Переход на личности
Если вы думаете, что в ошибке виноват кто-то другой, постарайтесь по крайней мере говорить с ним вежливо. Оскорбления, крики и паника не помогут человеку решить проблему. Даже если у вас в команде не в почете демократия, крики и применение грубой силы не заставят исправления магическим образом появиться.
Ошибка, которую я недавно исправил
Это была загадочная проблема с дублирующимися именами генерируемых файлов. Дальнейшая проверка показала, что у файлов различное содержание. Это было странно, поскольку имена файлов включали дату и время создания в формате yyMMddhhmmss
. Шаг 9, совпадения: первый файл был создан в полпятого утра, дубликат генерировался в полпятого вечера того же дня. Совпадение? Нет, поскольку hh
в строке формата — это 12-часовой формат времени. Вот оно что! Поменял формат на yyMMddHHmmss
, и ошибка исчезла.
Время на прочтение
7 мин
Количество просмотров 8.7K
В этой статье мы разберем как с нуля создать приложение, которое будет работать со службами windows и отображать системные ошибки в WinForm (C#).
План этой статьи:
- Создание службы
- Event Viewer
- Код службы
- Проверка работы службы(Запуск службы вручную)
- Отображение WinForm
Создание службы
Открываем Visual Studio. Дальше File → New → Project → (Windows Desktop) → Windows Service (.Net Framework) → Ok.
Дальше нужно создать установщик. В открывшемся окне щелкаем ПКМ и выбираем «Add Installer». У вас создастся «ProjectInstaller.cs[Design]» после чего нужно будет перейти к коду «F7» или ПКМ «View Code». Нужно найти строку «InitializeComponent();», поставить на нее курсор и нажать «F12», дальше нужно добавить следующие строки:
this.serviceProcessInstaller1.Account = System.ServiceProcess.ServiceAccount.LocalSystem; //Чтоб не запрашивало имя пользователя и пароль
this.serviceInstaller1.Description = "Show me system error."; //Описание которое будет видно в списке служб
this.serviceInstaller1.DisplayName = "GetSystemError"; //Название службы которое будет видно в списке служб
Но добавлять эти строки нужно только в следующей последовательности и месте. Иначе будет ошибка при установке сервиса.
Event Viewer
Это нужно для проверки правильной работы нашей программы.
Event Viewer — программа для просмотра журнала событий которая есть на каждом компьютере с windows. Каждая программа, которая запускается на компьютере, публикует уведомление в журнале событий до того, как останавливается. Любой доступ к системе, изменение безопасности, подстройка операционной системы, аппаратный сбой и сбой драйвера — все это попадает в журнал событий. Event Viewer сканирует файлы текстового журнала, объединяет их и помещает в интерфейс.
Как его открыть? — Пуск → Event Viewer(в поиске) → «Просмотр журналов событий».
Дальше «Настраиваемые представления (Custom Views)» → «События управления (Administrative Events)». Тут мы можем увидеть все ошибки, предупреждения и информацию о них.
Есть 3 типа журнала: приложение(Application), системные(System) и безопасность(Security). Нам нужен только системный(System).
Код службы
Находим файл .cs с названием службы, у меня это «Service1.cs», открываем. В файле должно быть заготовлено 2 переопределенных метода:
- OnStart(string[] args) — выполняется при запуске службы,
- OnStop() — выполняется при остановке службы.
Есть так же еще несколько методов но они нам сейчас не потребуются. Найти их можно самостоятельно.
Данные которые мы получаем будем хранить в обновляемом текстовом файле, поэтому добавляем
using System.IO;
Добавляем код в метод OnStart(string[] args):
EventLog myLog; //Можно читать и записывать элементы журнала событий
string filepath = AppDomain.CurrentDomain.BaseDirectory + @"ServiceLog.txt";; //Хранится путь к файлу
List<string> list; //Список всех найденных ошибок
protected override void OnStart(string[] args)
{
myLog = new EventLog();
myLog.Log = "System"; //По умолчанию выставлен тип ошибок - приложение(Application), а нам нужны системные(System).
myLog.Source = "System Error";
for (int index = myLog.Entries.Count - 1; index > 0; index--) //Перебираем все системные ошибки от новых к более старым
{
var errEntry = myLog.Entries[index]; \Чтение записи из журнала событий
if (errEntry.EntryType == EventLogEntryType.Error) \Проверка ошибка ли это
{
//Получаем нужные данные из записи
var appName = errEntry.Source;
list = new List<string>();
list.Add("Entry Type: " + Convert.ToString(errEntry.EntryType));
list.Add("Event log: " + (string)myLog.Log);
list.Add("Machine Name: " + (string)errEntry.MachineName);
list.Add("App Name: " + (string)errEntry.Source);
list.Add("Message: " + (string)errEntry.Message);
list.Add("Time Written: " + errEntry.TimeWritten.ToString());
list.Add("-*-");
WriteToFile(list); //Записываем данные в файл
}
}
}
public void WriteToFile(List<string> list) //Запись в файл
{
using (StreamWriter sw = File.AppendText(filepath))
{
for (int i = 0; i < list.Count; i++)
sw.WriteLine(list[i]);
}
}
Дальше нужно собрать решение «Solution» -> «Rebuild Solution». После успешной сборки можно проверять работу.
Проверка работы службы(Запуск службы вручную)
Службу windows нельзя запускать как обычное приложение. Можно запустить только через командную строку от имени администратора.
Запускаем командную строку от имени администратора. Ввести следующие команды:
cd C:WindowsMicrosoft.NETFrameworkv4.0.30319
InstallUtil.exe ПутьИмя вашей службы.exe
(InstallUtil.exe C:UsersАнжеликаsourcereposWindowsService1WindowsService1 binDebugWindowsService1.exe)
Команда для удаления сервиса: InstallUtil.exe -u ПутьИмя вашей службы.exe
Дальше нажимаем клавишу Win+R. Вводим «Services.msc». Находим в списке свой сервис, нажимаем на него, и нажимаем «Запустить(Start)». После успешного запуска сформируется файл по указанному в коде пути в котором будет находиться список системных ошибок.
Не забываем удалить службу после проверки.
Отображение WinForm
Для отображения в консоли если постараться то можно найти статьи, но для отображения в WinForm я так и не нашла, так что вот. По умолчанию проект службы создается типа «Application». Для отображения через консоль этот параметр в настройках нужно поменять, для WinForm оставить как есть. Дальше нужно добавить форму в проект. «WindowsService1» → ПКМ → Add → Windows Form → Add. И делаем примерно следующий дизайн. Дальше меняем файл «Program.cs».
Добавляем using:
using System.Windows.Forms;
using System.Security.Principal;
using System.ComponentModel;
using System.Diagnostics;
И меняем метод Main:
static void Main(string[] args)
{
WindowsPrincipal windowsPricipal = new WindowsPrincipal(WindowsIdentity.GetCurrent());
bool hasAdministrativeRight = windowsPricipal.IsInRole(WindowsBuiltInRole.Administrator);
if (hasAdministrativeRight == false) //проверка на права администратора
{
ProcessStartInfo processInfo = new ProcessStartInfo(); //создаем новый процесс
processInfo.Verb = "runas"; //процесс должен быть запущен с правами администратора
processInfo.FileName = Application.ExecutablePath;
try
{
Process.Start(processInfo); //пытаемся запустить процесс
}
catch (Win32Exception){}
Application.Exit();
}
else
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Form1(args)); //Запускаем форму
}
}
Добавляем новый класс «SystemError». («WindowsService1» -> ПКМ -> Add -> Class -> Add). Тут мы будем хранить данные об ошибках. Меняем его:
public string EntryType{ get; set; }
public string EventLog{ get; set; }
public string MachineName { get; set; }
public string AppName { get; set; }
public string Message { get; set; }
public string TimeWritten { get; set; }
Дальше в «Service1.cs» добавляем метод «RunFromForm(string[] args)» который запускает службу.
public void RunFromForm(string[] args)
{
OnStart(args);
OnStop();
}
Добавляем новый класс «GetListErrors». («WindowsService1» -> ПКМ -> Add -> Class -> Add). Тут мы будем доставать данные из файла. Добавляем using:
using System.IO;
Меняем его:
string filepath = AppDomain.CurrentDomain.BaseDirectory + @"ServiceLog.txt";
SystemError systemError;
public List<SystemError> listSysErrs;
public void ReadFile() //Метод чтения из файла и запись в список системных ошибок
{
systemError = new SystemError();
listSysErrs = new List<SystemError>();
using (StreamReader sr = new StreamReader(filepath))
{
string line;
while ((line = sr.ReadLine()) != null)
{
if (line.Contains("-*-"))
{
listSysErrs.Add(systemError);
systemError = new SystemError(); //Создается новый элемент системной ошибки в списке
}
if (line.Contains("Entry Type"))
{
systemError.EntryType = line.Substring(12);
}
else if (line.Contains("Event log"))
{
systemError.EventLog = line.Substring(11);
}
else if (line.Contains("Machine Name"))
{
systemError.MachineName = line.Substring(14);
}
else if (line.Contains("App Name"))
{
systemError.AppName = line.Substring(10);
}
else if (line.Contains("Message"))
{
systemError.Message = line.Substring(9);
}
else if (line.Contains("Time Written"))
{
systemError.TimeWritten = line.Substring(14);
}
}
}
}
Дальше меняем код формы «Form1.cs». Добавляем using:
using System.ServiceProcess;
using System.Diagnostics;
Меняем его:
Service1 service = new Service1();
List<SystemError> listSysErrs;
string[] args;
public Form1(string[] args)
{
InitializeComponent();
this.args = args;
if (Environment.UserInteractive)//Проверяет выполняется ли текущий процесс в режиме взаимодействия с пользователем
{
service.RunFromForm(args); //Запускаем службу
GetListErrors getListErrors = new GetListErrors();
getListErrors.ReadFile(); //получаем данные из файла результата службы
listSysErrs = getListErrors.listSysErrs;
FillDataGridView(); //Отображаем список в таблице
}
else
{
ServiceBase[] ServicesToRun;
ServicesToRun = new ServiceBase[] { service };
ServiceBase.Run(ServicesToRun);
}
}
public void FillDataGridView() //Заполняет таблицу данными из списка
{
foreach (SystemError item in listSysErrs)
{
dataGridView1.Rows.Add(item.AppName, item.Message);
}
}
private void dataGridView1_CellClick(object sender, DataGridViewCellEventArgs e) //При нажатии на любую ячейку на панели будут отображаться больше данных именно по ошибке которую нажали
{
SystemError sys = listSysErrs[dataGridView1.CurrentRow.Index];
label1.Text = "Entry Type: " + sys.EntryType +
"nEvent log: " + sys.EventLog +
"nMachine Name: " + sys.MachineName +
"nApp Name: " + sys.AppName +
"nnMessage: " + FormatMessage(sys.Message) +
"nnTime Written: " + sys.TimeWritten;
}
private string FormatMessage(string msg) //Так как весь текст не помещается на панель, то нужно подкорректировать строку так чтоб она была видна полностью а не уходила бы в закат
{
string retMsg = "";
int count = 75;
if (msg.Length > count - 9)
{
retMsg += msg.Substring(0, count - 9) + "n";
msg = msg.Substring(count - 9);
}
while (msg.Length > count)
{
retMsg += msg.Substring(0, count) + "n";
msg = msg.Substring(count);
}
retMsg += msg + "n";
return retMsg;
}
private void button1_Click(object sender, EventArgs e) //По нажатию на кнопку мы перезапускаем службу как и ранее
{
if (Environment.UserInteractive)
{
while (dataGridView1.Rows.Count != 0)
{
dataGridView1.Rows.Remove(dataGridView1.Rows[dataGridView1.Rows.Count - 1]);
}
service.RunFromForm(args);
GetListErrors getListErrors = new GetListErrors();
getListErrors.ReedFile();
listSysErrs = getListErrors.listSysErrs;
FillDataGridView();
}
else
{
ServiceBase[] ServicesToRun;
ServicesToRun = new ServiceBase[] { service };
ServiceBase.Run(ServicesToRun);
}
}
private void button2_Click(object sender, EventArgs e)
{
Process.Start(AppDomain.CurrentDomain.BaseDirectory); //Выбираем файл
}
Теперь можно запускать службу как обычное приложение. Выглядит результат следующим образом:
Вчера всё работало, а сегодня не работает / Код не работает как задумано
или
Debugging (Отладка)
В чем заключается процесс отладки? Что это такое?
Процесс отладки состоит в том, что мы останавливаем выполнения скрипта в любом месте, смотрим, что находится в переменных, в функциях, анализируем и переходим в другие места; ищем те места, где поведение отклоняется от правильного.
Заметка: Отладка производится как правило в IDE (Интегрированная среда разработки). Что это такое можно чуть подробнее ознакомиться в вопросе
Какие есть способы предупреждения ошибок, их нахождения и устранения?
В данном случае будет рассмотрен пример с Visual Studio, но отладить код можно и в любой другой IDE.
Подготовка
Достаточно иметь в наличии IDE, например Visual Studio
Запуск
Для начала в левой части панели с кодом на любой строке можно кликнуть ЛКМ, тем самым поставив точку останова (breakpoint — брейкпойнт). Это то место, где отладчик автоматически остановит выполнение C#, как только до него дойдёт. Количество breakpoint’ов не ограничено. Можно ставить везде и много.
Отладка запускается сочетанием F5 или выбором в верхнем меню Debug
→ Start Debugging
В данном случае, т.к. функция вызывается сразу на той же странице, то при нажатии кнопки Debug
— отладчик моментально вызовет метод, выполнение «заморозится» на первом же брейкпойнте. В ином случае, для активации требуется исполнить действие, при котором произойдет исполнение нужного участка кода (клик на кнопку в UI, передача POST запроса с данными и прочие другие действия)
Цифрами обозначены:
- Стэк вызовов, все вложенные вызовы, которые привели к текущему месту кода.
- Переменные. На текущий момент строки ниже номера 8 ещё не выполнилась, поэтому определена лишь
data
иnumsStringArr
- Показывает текущие значения любых переменных и выражений. В любой момент здесь можно вписать имя любой переменной в поле
name
и посмотреть её значение в реальном времени. Напримерdata
илиnums[0]
, а можно иnums[i]
иitem.test.data.name[5].info[key[1]]
и т.д. На текущий момент строки ниже номера 24 ещё не выполнилась, поэтомуnum
(которая появляется лишь в циклеforeach
) во вкладкеWatch
обозначена красным цветом с надписью «The name ‘num’ does not exist in the current context».
Процесс
Для самого процесса используются элементы управления (см. изображение выше, выделено зеленым прямоугольником)
Show Next Statement (Alt+Num *) — переносит в файл и текущую линию отлаживаемого скрипта. Например если файлов много, решили посмотреть что в других вкладках, а потом забыли где у вас отладка
Step Over (F10) — делает один шаг не заходя внутрь функции. Т.е. если на текущей линии есть какая-то функция, а не просто переменная со значением, то при клике данной кнопки, отладчик не будет заходить внутрь неё.
Step Into (F11) — делает шаг. Но в отличие от предыдущей, если есть вложенный вызов (например функция), то заходит внутрь неё.
Step Out (Shift+F11) — выполняет команды до завершения текущей функции. Удобна, если случайно вошли во вложенный вызов и нужно быстро из него выйти, не завершая при этом отладку.
Restart (Ctrl+Shift+F5) — Перезапустить отладку
Continue (F5) — Продолжает выполнения скрипта с текущего момента. Если больше нет других точек останова, то отладка заканчивается и скрипт продолжает работу. В ином случае работа прерывается на следующей точке останова.
Stop Debugging (Shift+F5) — Завершить отладку
…
Итак, в текущем коде видно значение входного параметра:
data = "23 24 11 18"
— строка с данными через пробелnumsStringArr = {"23", "24", "11", "18"}
— массив строк, который получился из входной переменной.
Если нажмем F10 2 раза, то окажемся на строке 11; во вкладках Watch
и Variables
и в самой странице с кодом увидим, что nums
была инициализирована и в ней лежит массив целых чисел {23, 24, 11, 18}
.
Если теперь нажмем F10, то попадем внутрь цикла foreach
и нажимая теперь F10 пока не окончится цикл, можно будет наблюдать на каждой итерации, как значение num
и sum
постоянно изменяются. num
теперь можно будет наблюдать во вкладке Watch
, потому что сейчас она доступна внутри цикла. Тем самым мы можем проследить шаг за шагом весь процесс изменения любых переменных и значений на любом этапе, который интересует.
Дальнейшие нажатия F10 переместит линию кода на строки 15, 16 и, наконец, 20.
Дополнительно
Если перейти во вкладку Breakpoints
в нижней панели, то можно посмотреть все брейкпойнты, удалить все брейкпойнты, включить/выключить, а также еще более тонко настроить условие, при котором на данной отметке надо остановиться. В методе выше, например, нужно остановиться только когда sum
превысит значение 20.
Это удобно, если останов нужен только при определённом значении, а не всегда (особенно в случае с циклами).
Больше информации об отладке можно посмотреть в Сведения об отладке кода C# с помощью Visual Studio.
Чтобы найти краткие объяснения кодов ошибок в Windows, запустите окно командной строки, введите команду «CertUtil /error ErrorCode», где «ErrorCode» — это фактический код, и нажмите Enter.
Коды ошибок Windows пугают, и они часто появляются без текстового описания того, о чем они. К счастью, на вашем ПК есть встроенная утилита, которую вы можете использовать для поиска кратких объяснений различных кодов ошибок, сгенерированных в вашей системе. Мы покажем вам, как его использовать.
Эта утилита называется CertUtil и запускается из командной строки. Вы используете код ошибки (например, 0x80004005) в качестве аргумента в утилите, и она дает вам краткое описание этого кода.
Используйте CertUtil для поиска описаний кодов ошибок Windows
Чтобы начать использовать CertUtil, запустите окно командной строки на своем ПК. Вы можете сделать это, открыв меню «Пуск», выполнив поиск «Командная строка» и выбрав инструмент.
Когда откроется окно командной строки, введите следующую команду и нажмите Enter. В этой команде замените ErrorCode кодом, для которого вы хотите получить объяснение.
CertUtil /error ErrorCode Совет. Вы можете скопировать код ошибки в буфер обмена и вставить его в командную строку, чтобы сэкономить время.
Например, чтобы найти текстовое описание кода ошибки 0x80070490, введите в командной строке следующее и нажмите Enter:
CertUtil/ошибка 0x80070490
Когда отобразится результат, рядом с «Текст сообщения об ошибке» вы найдете краткое описание указанного вами кода ошибки.
С этим описанием ошибки вы теперь знаете, к чему относится ваш код ошибки, и можете предпринять действия, необходимые для исправления этого синего экрана смерти.