Показаны сообщения с ярлыком Отладка приложений. Показать все сообщения
Показаны сообщения с ярлыком Отладка приложений. Показать все сообщения

понедельник, 8 февраля 2010 г.

Диагностика проблем загрузки сборок

Практически каждый разработчик сталкивался с неприятной ситуацией, когда во время загрузки приложения, разработанного с использованием .NET Framework, возникают какие-то ошибки, связанные с поиском или загрузкой сборок и запуск приложения завершается предложением отправить отчет в Майкрософт. Кроме того, практически каждый, кто читал замечательную книгу Джеффри Рихтера, ужаснулся тому многообразию вариантов, откуда может быть загружена сборка, а также богатым возможностям администрирования .Net приложений (probing, dependend assemblies, codebase, Publisher Policy и др.) [1], [2]. Помимо проблем с поиском нужной сборки подливают масла в огонь вероятные ошибки загрузки сборок, связанные с вопросами безопасности (в результате чего генерируется SecurityException), а также форматом сборки (исключение BadImageFormat).

Хотя многие разработчики относительно быстро справляются с подобными проблемами, потому что прекрасно знают архитектуру своего приложения и за несколько минут могут определить, что у пользователя не установлен сторонний компонент, не хватает нужных сборок в подпапке приложения или еще что-либо в этом духе, для неподготовленных (или утомленных) умов диагностика подобных проблем может занимать довольно много времени и отнимать массу нервов.

За загрузку сборок в CLR отвечает специальный загрузчик, получивший кодовое имя Fusion. Если в процессе загрузки сборок возникают проблемы, то для упрощения диагностики существует возможность включить логгирование этого процесса. Для этого необходимо задать следующие параметры в реесте: установить параметр HKLM\Software\Microsoft\Fusion\ForceLog в 1, а в значении параметра HKLM\Software\Microsoft\Fusion\LogPath указать путь хранения лог-файла (по умолчанию, этих параметров в реестре нет, соответственно, диагностика загрузки сборок не производится).

Для проверки ошибок загрузки сборок я создал простое решение (solution) с двумя проектами: консольным приложением TestFusion и библиотекой классов TestFusionLib. Я добавил в библиотеку класс TestClass, а в TestFusion добавил использование этого класса. После компиляции обоих проектов, я удалил из папки bin файл TestFustionLib и запустил TestFustion.exe.

D:\Sources\VS2008\TestFusion\TestFusion\bin\Debug>TestFusion.exe

Необработанное исключение: System.IO.FileNotFoundException: Невозможно загрузить файл или сборку "TestFusionLib, Version

=1.0.0.0, Culture=neutral, PublicKeyToken=null" или один из зависимых от них компонентов. Не удается найти указанный файл.

Имя файла: "TestFusionLib, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"

   в TestFusion.Program.Main(String[] args)

Диспетчер сборки загружен с:  C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\mscorwks.dll

Выполняется в контексте исполняемого файла  D:\Sources\VS2008\TestFusion\TestFusion\bin\Debug\TestFusion.exe

--- Подробный журнал ошибок.

=== Информация о состоянии предварительной привязки ===

Журнал: User = HOME\Sergey

Журнал: DisplayName = TestFusionLib, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null

 (Fully-specified)

Журнал: Appbase = file:///D:/Sources/VS2008/TestFusion/TestFusion/bin/Debug/

Журнал: Initial PrivatePath = NULL

Вызов сборки: TestFusion, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null.

===

Журнал: данная привязка начинается в контексте загрузки default.

Журнал: файл конфигурации приложения не найден.

Журнал: используется файл конфигурации компьютера из C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\config\machine.config

.

Журнал: политика в данный момент не применяется к ссылке (личная, пользовательская, частичная привязка сборки или привязка по местоположению).

Журнал: попытка загрузки нового URL file:///D:/Sources/VS2008/TestFusion/TestFusion/bin/Debug/TestFusionLib.DLL.

Журнал: попытка загрузки нового URL file:///D:/Sources/VS2008/TestFusion/TestFusion/bin/Debug/TestFusionLib/TestFusionLi

b.DLL.

Журнал: попытка загрузки нового URL file:///D:/Sources/VS2008/TestFusion/TestFusion/bin/Debug/TestFusionLib.EXE.

Журнал: попытка загрузки нового URL file:///D:/Sources/VS2008/TestFusion/TestFusion/bin/Debug/TestFusionLib/TestFusionLib.EXE.

Помимо просмотра и конфигурирования процесса логирования загрузки сборок вручную, в составе .NET Framework SDK поставляется полезная утилита с названием Fuslogvw.exe (Fusion Log Viewer) [3], которая в значительной степени упрощает подобный процесс диагностики.

Внимание! Утилиту Fuslogvw.exe необходимо запускать с правами Администратора, в противном случае вы не сможете изменить ни какие параметры.

Примечание. Путь к Fuslogvw.exe: %ProgramFiles%\MicrosoftSDKs\Windows\v6.0A\bin\Fuslogvw.exe

Если после неудачного запуска приложения (в нашем случае TestFusion.exe) запустить Fuslogvw.exe (или нажать кнопку Refresh, если эта утилита уже была запущена), то мы увидим следующую картину:

Fuslogvw

Нас интересует вторая строка. Если на ней нажать View Log, получим следующие данные:

*** Запись журнала привязки сборки  (06.02.2010 @ 17:47:02) ***

Операция выполнена со сбоем.

Результат привязки: hr = 0x80070002. Не удается найти указанный файл.

Диспетчер сборки загружен с:  C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\mscorwks.dll

Выполняется в контексте исполняемого файла  D:\Sources\VS2008\TestFusion\TestFusion\bin\Debug\TestFusion.exe

--- Подробный журнал ошибок.

=== Информация о состоянии предварительной привязки ===

Журнал: User = HOME\Sergey

Журнал: DisplayName = TestFusionLib, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null

 (Fully-specified)

Журнал: Appbase = file:///D:/Sources/VS2008/TestFusion/TestFusion/bin/Debug/

Журнал: Initial PrivatePath = NULL

Журнал: Dynamic Base = NULL

Журнал: Cache Base = NULL

Журнал: AppName = NULL

Вызов сборки: TestFusion, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null.

===

Журнал: данная привязка начинается в контексте загрузки default.

Журнал: файл конфигурации приложения не найден.

Журнал: используется файл конфигурации компьютера из C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\config\machine.config.

Журнал: политика в данный момент не применяется к ссылке (личная, пользовательская, частичная привязка сборки или привязка по местоположению).

Журнал: попытка загрузки нового URL file:///D:/Sources/VS2008/TestFusion/TestFusion/bin/Debug/TestFusionLib.DLL.

Журнал: попытка загрузки нового URL file:///D:/Sources/VS2008/TestFusion/TestFusion/bin/Debug/TestFusionLib/TestFusionLib.DLL.

Журнал: попытка загрузки нового URL file:///D:/Sources/VS2008/TestFusion/TestFusion/bin/Debug/TestFusionLib.EXE.

Журнал: попытка загрузки нового URL file:///D:/Sources/VS2008/TestFusion/TestFusion/bin/Debug/TestFusionLib/TestFusionLib.EXE.

Журнал: все попытки проверки URL закончились неудачно.

Конечно же, приведенный пример надуман, и здесь даже в описании исходного исключения видно, в чем проблема, но реальные сценарии очень часто являются достаточно сложными, и в таких случаях механизм просмотра логов загрузки сборок может сэкономить огромное количество времени вам и вашим пользователям.

Отступление от темы. Диагностика проблем загрузки неуправляемых библиотек

Обсуждая вопрос диагностики загрузки управляемых библиотек, нельзя оставить без внимания вопросы загрузки неуправляемых библиотек (native dll). На различных форумах очень часто задают вопросы подобного рода: «Мое приложение при переносе с моей машины на какую-то другую, перестает запускаться. В чем может быть проблема?». Подобная проблема очень часто связана с тем, что при компиляции неуправляемого кода (например, mixed сборок, разработанных с помощью C++/CLI) помимо стандартных управляемых библиотек приложение использует C Runtime Library в виде отдельной dll. Поскольку на машине разработчика эта библиотека существует в папке System32 с тех самых пор, как на этот компьютер установлена среда разработки, это не вызывает никаких проблем у разработчика, но этой библиотеки очень часто не бывает на машине пользователя. Многие разработчики справляются с этой проблемой путем включения в свой инсталляционный пакет Visual С++ Redistributable Package  

В отладке подобных проблем первое, что нужно сделать, это скачать замечательную утилиту под названием Dependency Walker [5] и попытаться открыть ваш exe-файл с помощью этой утилиты на машине пользователя (или на машине с идентичной конфигурацией). Dependency Walker рекурсивно проходится по всем неуправляемым библиотекам и сразу же покажет вам, какой именно неуправляемой библиотеки не хватает.

Дополнительные ссылки

1.     Джеффри Рихтер, CLR via C#, Глава 2, раздел “Простое средство администрирования (конфигурационный файл)”.

2.     Джеффри Рихтер, CLR via C#, Глава 3, раздел “Дополнительные конфигурационные средства (конфигурационные файлы)”.

3.     Assembly Binding Log Viewer (Fuslogvw.exe) in MSDN documentation

4.     Debugging Assembly Loading Failures by Suzanne Cook (http://blogs.msdn.com/suzcook/archive/2003/05/29/57120.aspx)

5.     Утилита Dependency Walker

четверг, 31 декабря 2009 г.

Не запускается служба PostgreSql. Что делать?

Это сообщение предназначено прежде всего для меня лично и для нашей службы поддержки, т.к. проблема подобная описанной ниже происходит с завидной периодичностью и нужно помнить основные шаги по ее решению.

Итак, история начинается с того, что мне 30 декабря в 11-30 звонят с сообщением о том, что у одного из наших клиентов не запускается наша система, поскольку не может подключиться к базе данных (в качестве СУБД у нас используется PostgreSql версии 8.1). Люди объясняют это тем, что час назад вырубило свет и компьютер вырубился некорректно, а после включения – все перестало работать:)

Хорошие пользователи нашей системы знают, где находится кнопка пуск и знают, что в системе не двое часиков “одни с цифрами, а другие песочные”. Поэтому единственное, что удалось сделать по телефону, так это попытаться руками запустить службу СУБД, результат – служба таки не запускается. Пришлось пробрасывать на тот компьютер интернет (на компьютерах, где установлена наша система интернета быть не должно) для возможности удаленного подключения.

После подключения к удаленному компьютеру я попытался запустить службу и получил следующее сообщение: “Служба PostgreSql Database Server 8.1” на “Локальный компьютер” была запущена и затем остановлена. Некоторые службы автоматически останавливаются, если им нечего делать, например, служба журналов и оповещений производительности”. Мда…

Проблема в том, что на тот момент это была единственная доступная информация… Логи PostgreSql пусты, записей в них никаких, в системных логах – тоже пустота.

Отладка служб – процесс не простой, поэтому многие разработчики предусматривают механизмы запуска приложения-службы, как обыкновенного консольного приложения с помощью ключей командной строки. И PostgreSql в этом плане – не исключение; для запуска нужно использовать следующую команду (Hint: эту команду можно запустить только из под неадминистративного пользователя системы, правда, если вы об этом забудете, то PostgreSql очень быстро вам об этом напомнит):

postgres -D "<postgresql data directory>"

Запускаем, и смотрим на сообщение об ошибке. В моем случае это сообщение звучало примерно так:

FATAL - bogus data in lock file "postmaster.pid"

Безусловно, мне повезло, проблема оказалась поправимой.  Почему-то указанный файл оказался пустым, и мне понадобилось скопировать его содержимое из рабочего экземпляра СУБД, что не составило особого труда.

Мораль этого сообщения в том, что если база легла, или произошли какие-то другие проблемы с системой, то прежде чем переустанавливать СУБД (или систему целиком) и терять при этом все данные, нужно хотя бы попытаться выяснить в чем проблема, возможно, есть все шансы, что вам удастся восстановить работоспособность не такими радикальными способами.

З.Ы. Всех с наступающим Новым Годом и пожелания того, чтобы ваши системы были стабильными и надежными и не портили ваш сон, но даже если какие-то проблемы и возникали, то у вас всегда были наготове варианты, как с этой ситуацией справиться.

четверг, 19 ноября 2009 г.

Злые баги. Или почему неприятности приходят в самый неподходящий момент

Согласно закону Мэрфи если какая-то неприятность может произойти, то она обязательно произойдет, причем в самый неподходящий момент. Именно такая ситуация произошла в нашем проекте за несколько дней до выпуска бета-версии.

Начну по-порядку. В последнее время наша команда снова вернулась к проекту, который мы закончили несколько лет назад. Тогда реализация всей серверной части была сделана на С++, ядро системы оборачивалось в "управляемую" (managed) оболочку и использовалась в управляемом коде. Клиент-серверное взаимодействие реализовывалось на .Net Remoting, а два типа клиентских приложения были реализованы на .Net с использованием технологии Windows Forms. Все это было реализовано под .Net 1.1, и, естественно, новую версию мы захотели перевести на .Net 3.5, благо никаких особых проблем это не предвещало, но давало ряд преимуществ, главным из которых было повторное использование кода и архитектурных решений (за несколько лет у нас накопилось достаточное количество компонентов, реализованных под .Net 3.5), да и вообще, очень  сложно переходить с LINQ 2 Objects обратно на .Net 1.1, где нет даже Nullable-типов и типизированных коллекций.

Взаимодействие управляемого и неуправляемого кода попило немало крови в исходной версии проекта (под .Net Framework 1.1 есть свои особенности, связанные с необходимостью вручную вызывать функции __crt_dll_initialize() и __crt_dll_terminate()) и оставалось наиболее рисковой составляющей перехода на новую версию .Net Framework. Дело, к тому же, осложнялось тем, что мы приняли решение отказаться от использования старых драйверов взаимодействия с СУБД PostgreSql (libpqxx), в пользу использования оболочки, которая бы позволила использовать ADO.NET провайдера доступа к БД (в данном случае, Npgsql) из native-кода (механизм использования управляемых библиотек из неуправляемого кода изложены в моей статье "Взаимодействие управляемого и неуправляемого кода", RSDN Magazine, 3-2008). Сам перевод на новую технологию доступа к данным не занял особо много времени, благодаря четкому выделению слоя доступа к данным, но само взаимодействие управляемого и неуправляемого вызывало наибольшее количество беспокойство и, как оказалось, не зря.

Итак, работа над проектом двигалась вперед, мы показали людям сырую альфу, чтобы скорректировать направление нашего движения и пообещали предоставить бету через две недели. За пару дней до контрольного срока я заметил, что серверные части системы (которые реализованы в виде служб Windows NT, но имеют также и тестовые консольные версии), начали "падать" с какой-то ошибкой при завершении работы, причем "падать" стали не в момент завершения, а уже после него, выдавая такое замечательное сообщение: «Исключение unknown software exception (0xc0020001) в приложении по адресу 0x7c81eb33».

Сложность диагностики этой ошибки была вызвана тем, что я не смог определить момент, когда эта проблема начала проявляться (примерно в это время я начал настраивать дополнительную конфигурацию сборки и просто не понял, что я зделал не так). Кроме того, полная перекомпиляция проектов занимала более получаса, а время здорово поджимало.

Судя по симптомам, ошибка происходила где-то при освобождении статических объектов. Первая мысль пала на синглтоны от Loki, в частности на использование стратегии управления временем жизни Phoenix (если кто не знаком с синглтонами из библиотеки Loki (автор Андрей Александреску), то идея заключается в использовании паттерна "стратегия" для задания таких параметров, как способ выделения памяти, управление временем жизни и управление параллелизмом). Стратегия управления временем жизни Phoenix как раз предназначена для решения известной проблемы С++, связанной с неопределенным порядком создания и уничтожения статических объектов в различных единицах трансляции; например, при обращении в деструкторе одного синглтона к другому синглтону поведение будет не определенным.

При поиске ошибок каждый разработчик придерживается ряда правил, основанных человеческом опыте и отлично озвученных Джоном Роббинсоном:

Шаг 1. Воспроизведите ошибку

Шаг 2. Опишите ошибку

Шаг 3. Всегда предполагайте, что ошибка ваша

Шаг 4. Разделяйте и властвуйте

Шаг 5. Думайте творчески

Шаг 6. Используйте инструменты

Шаг 7. Начните тяжелую отладку

Шаг 8. Убедитесь, что ошибка исправлена

Шаг 9. Извлеките урок и поделитесь им с другими

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

Код ошибки 0xc0020001 определен следующим образом: BOOTUP_EXCEPTION_COMPLUS и может возникать при попытке достучаться к CLR до ее инициализации или после ее деинициализации. Как известно, в проекте C++/CLI любой cpp-файл (если не оговорено обратное) компилируется с ключем /clr, что позволяет создавать «смешанные» (mixed-mode) сборки, в которых сосуществует управляемый и неуправляемый код. Теперь, если объект некоторого класса будет сохранен в статической переменной, то время жизни этого объекта будет определяться моментом выгрузки этого модуля, что будет соответствовать моменту непосредственно перед завершением приложения. При этом совершенно неизвестно, будет ли «жива» CLR или нет, т.к. порядок выгрузки различных модулей неопределен (именно поэтому, в течение какого-то времени эта ошибка у меня не проявлялась).

Кроме этого, жизнь может осложняться следующим моментом. Предположим у вас есть заголовочный файл, в котором реализована (не только объявлена, но и реализована) некоторая функциональность (например, класс Foo). Тогда, при включении из cpp-файла (который компилируется с ключом /clr) этого заголовочного файла, класс Foo будет скомпилирован в mixed-mode, что не позволит объектам этого класса располагаться в статической памяти (точнее, располагаться-то позволит, а вот с освобождением этой памяти могут быть проблемы).

У этой проблемы есть несколько путей решения, каждый со своими преимуществами и недостатками.

Первое решение заключается в том, чтобы для каждого cpp-файла убрать ключ /clr, в результате, все классы будут компилироваться, как native-классы, и это позволит обращаться к их деструкторам уже после выгрузки CLR.

Второе решение легче показать на примере кода:

#pragma managed(push, off)

#include "Foo.h"

#pragma managed(pop)

Если заголовочный файл включается в cpp-файл, который, в свою очередь компилируется с ключом /clr, то класс (определенный в этом заголовочном файле) также будет также компилироваться в mixed-mode, что не позволит обращаться к его деструктору в момент завершения приложения. Обрамление заголовочного файла в #pragma managed приведет к компиляции класса, определенного в этом заголовочном файле в native-mode.

Третье решение связано с применением синглтонов от Loki. Как я уже писал выше, этот синглтон (как и многое другое в этой библиотеке) реализован с помощью стратегий, которые могут задавать некоторое поведение, одним из которых является управление временем жизни синглтона. Одной из существующих стратегий управления временем жизни является стратегия NoDestroy, при которой память, выделенная под синглтон, не будет освобождаться вовсе.

Самое интересное в этой истории то, что на самом деле это никакой не «злой баг», эта ситуация характеризуется как by design и описана в документе KB956195, смысл которого как раз и сводится к тому, что я описал выше.

Перечень ссылок по теме ошибки 0xc0020001:

1.     C++/CLI application throws 0xC0020001 on exit. http://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=365110

2.     Static variable in native method causes exception c0020001 during process exit. http://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=336844

3.     mixed-mode application (C++/CLI) throws unhandled exception '0xC0020001: The string binding is invalid.' on application exit. http://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=218663

4.     KB 956195. C++/CLI application crashes with 0xc0020001. http://support.microsoft.com/kb/956195

5.     An intereseting issue in mixed-mode application. http://blogs.msdn.com/ravi_kumar/archive/2007/12/30/an-intereseting-issue-in-mixed-mode-application.aspx

P.S. Интересно, что на «синглтоне Мейерса» проблема воспроизводится не всегда, а вот применение синглтона от Loki (со стратегией управления временем жизни не NoDestory), стабильно приводит к выдаче сообщения об ошибке 0xc0020001 в момент закрытия приложения.

P.S.S. Здесь расположен тестовый проект, который стабильно воспроизводит описанное поведение. Проверено на нескольких компьютерах с Windows XP SP2, VS2008 SP1 и на одном Windows 7, VS2008 SP1.

понедельник, 29 июня 2009 г.

Цитаты из книги Джона Роббинса "Отладка приложений для Microsoft .Net"

Уже достаточно давно чешутся руки выписывать интересные цитаты, которые встречаются в различных книгах. Это, весьма странное желание, обусловлено несколькими причинами. Во-первых, во многих хороших книгах интересных выражений наберется достаточное количество и появляется желание этими выражениями, так сказать, поделиться, во-вторых, если их не перенести на более надежный носитель информации, чем хранилище по-умолчанию, коим является наше серое вещество, то они слишком быстро забываются и потом становится довольно сложно их отыскать. Ну и в-третьих, ... хотя, ладно, получилось только во-первых и во-вторых, но думаю и так понятно, что польза от сего занятия в любом случае есть:) Итак, сегодня на повестке дня, Джон Роббинс и его интересные и, зачастую, веселые высказывания.
 

Об ошибках

Ошибки - это мрак. Точка. Ошибки - вот из-за чего вы вкалываете на безнадежными проектами с давно просроченными сроками сдачи, просиживаете у компьютера ночи на пролет и ссоритесь с вечно ворчащими коллегами. Ошибки действительно могут превратить вашу жизнь в кошмар, если достаточное их количество обнаружится в вашем программном обеспечении. Клиенты могут прекратить использовать ваш продукт, а вы можете потерять работу.
 
Ошибки - это серьезно! Очень часто люди, работающий в нашей индустрии, представляют себе ошибки просто как досадные мелочи. Сильнее заблуждаться невозможно. Любой разработчик расскажет вам о проектах с немыслимым количеством ошибок и даже о компаниях, загнувшихся оттого, что их продукт содержал столько ошибок, что был непригоден. Когда я писал первое издание этой книги, NASA потеряла космический зонд, направленный на Марс, из-за ошибок, допущенных на этапе формулировки требований и проектирования ПО.
 
Пока я писал второе издание на солдат американского спецназа упала бомба, направленная на другую цель. Причиной была программная ошибка, возникшая при смене источника питания в системе наведения. За неделю до того, как я написал это введение к третьему изданию, корпорация Microsoft выпустила пакет исправлений для пакета исправлений, который ранее создал огромную уязвимость, связанную с переполнением буфера в Microsoft Internet Explorer 6.
 
Хотя к программным ошибкам уже начинают относится с бОльшим уважением, нам еще очень далеко до появления культуры разработки, в которой к ошибкам будут относиться чрезвычайно серьезно, а не как к незначительным проблемам, иногда возникающим в процессе разработки. Ошибки – это круто! Они помогают залезть в самую глубину и понять, как работают вещи. Мы все попали в этот бизнес, потому что нам нравится учиться, выслеживание ошибок – неотъемлемая часть обучения… Ведь так здорово бывает найти и исправить ошибку! Конечно же, самые хорошие ошибки – это те, которые обнаруживаются до того, как заказчик увидит ваш продукт. Таким образом, вы должны успевать сделать свою работу и найти ошибки до того, как это сделают ваши заказчики. Видеть, как заказчики обнаруживают ошибки, - это совершенно не круто.
 

Об отладке

Отладка - это очень захватывающая тема, независимо от того, какой язык вы используете и на какой платформе работаете. Это единственный этап процесса разработки программного обеспечения, на котором инженеры пинают свои компьютеры, орут на них и даже разбивают их об стену. Для обычно сдержанной замкнутой группы такой накал эмоций представляет собой что-то невероятное. Также отладка является тем этапом процесса разработки ПО, который чаще всего в нашем сознании связывается с "ночными бдениями". Мне еще не встречался инженер, который бы позвонил домой, чтобы сказать: "Дорогая, я не могу прийти домой, потому что мы так веселимся, составляя UML-диаграммы, что хотим посвятить этому развлечению всю ночь!" Однако многие из знакомых инженеров имели опыт жалобных звонков домой в стиле: "Дорогая, я не могу прийти домой, потому что наткнулся на чудовищную ошибку в программе".
 

О сроках

Всем нам приходилось бывать участниками команд разработчиков, для которых «менеджмент» устанавливал сроки выполнения, определенные при помощи толкования карт Таро или, если это было слишком дорого, путем метания дротиков в календарь. Хотя нам хотелось бы верить, что менеджеры несут ответственность за большинство нереалистичных графиков работы, чаще всего это все же не их вина. Обычно основой графика работы становится оценка, даваемая инженерами, и иногда они недооценивают время, которое им может понадобиться для разработки надежного продукта. Инженеры – забавные люди. Они интроверты, но практически всегда большие оптимисты. Получая техническое задание, они до глубины костей верят, что могут с легкостью заставить компьютеры встать и танцевать. Если менеджер зайдет к ним и скажет, что в приложение необходимо добавить блок преобразования XML, средний инженер ответит: «Без проблем, босс! Дайте мне три дня». Конечно же этот инженер может даже не знать, как правильно пишется «XML», но он уверен, что за три дня справится с чем угодно.
 

Об анализе рисков

В первый раз, когда на встрече разработчиков я сказал: «Что, если Боб умрет до того, как мы закончим фазу формулировки требований?», все стали заметно нервничать, так что теперь я формулирую вопросы в менее патологической форме, например: «Что, если Боб выиграет в лотерею и оставит нас ради беззаботной жизни богатого человека до того, как мы закончим фазу формулировки требований?». Однако идея все та же. Найдите в своих планах все места, вызывающие смущение и сомнение, и разберитесь с ними.
 

О дальновидности руководителей

К сожалению, слишком много компаний отказываются потратить 500 долларов на инструмент, способный устранить противную ошибку за несколько минут, но с легкостью тратят тысячи долларов, платя разработчикам за барахтанье в коде в течение нескольких недель в попытках решить ту же проблему.
 

Кошки и отладка

Как выяснилось, мои кошки – отличные отладчики, и они помогли мне решить огромное количество действительно неприятных ошибок. Собирая их вокруг своего рабочего места, я рисую проблему на белой доске и позволяю кошкам воздействовать на меня своими магическими лучами. Конечно же, однажды я делал это, не приняв душ и будучи одетым в одни шорты, - было немного трудно объяснить ситуацию курьеру службы доставки, который все это время ошеломленно стоял у двери.
 

Разное

Помните, что отладчик – это всего лишь инструмент, как например, отвертка. Он делает только то, что вы приказываете ему делать. Настоящий отладчик находится у вас в голове.
 
Было бы здорово, если бы за ошибки в коде можно было винить барабашек, но необходимо принять и смириться с фактом, что именно вы и ваши коллеги населяете код ошибками. (Если вы читаете эту книгу, то основная вина ложится, конечно же, на ваших коллег.)
 
«Если отладка – это процесс устранения ошибок, то программирование должно быть процессом их порождения»
Эдсгер Дейкстра
 
Кодируй так, как будто человек, поддерживающий твой код, - буйный психопат, знающий, где ты живешь.
 
Многие разработчики жалуются, что пользователи совершенно не хотят тратить время на то, чтобы хоть что-то узнать о компьютерах. Я всегда парирую: «Не могу поверить, как много разработчиков практически ничего не знают о своих средах разработки».

вторник, 3 марта 2009 г.

Полезные ссылки от Джона Роббинса

Вот решил собрать полезные ссылки, инструменты и статьи, которые советует Джон Роббинс в своей книге "Отладка приложений для Microsoft .Net".
СТАТЬИ
Оптимизация загрузки приложений Windows Forms
1. Practical Tips For Boosting The Performance Of Windows Forms Apps http://msdn.microsoft.com/en-us/magazine/cc163630.aspx
2. CLR Inside Out. Improving Application Startup Time http://msdn.microsoft.com/en-us/magazine/cc163655.aspx
Пользовательский интерфейс и юзабилити
1. Windows Vista User Experience Guidelines. http://msdn.microsoft.com/en-us/library/aa511258.aspx
2. The Joel Test: 12 Steps to Better Code. http://www.joelonsoftware.com/articles/fog0000000043.html
О финализаторах
1. Keep Your Code Running with the Reliability Features of the .NET Framework. http://msdn.microsoft.com/en-us/magazine/cc163716.aspx
О вреде разработки ПО под учетной записью с правами администратора:
2. MakeMeAdmin -- temporary admin for your Limited User account http://blogs.msdn.com/aaron_margosis/archive/2004/07/24/193721.aspx
Разное
1. SUPERASSERT.NET SUPERASSERT Goes .NET http://msdn.microsoft.com/en-us/magazine/cc188701.aspx
2. Автоматизация тестирования пользовательского интерфейса. Build Quick and Easy UI Test Automation Suites with Visual Studio .NET. http://msdn.microsoft.com/en-us/magazine/cc188784.aspx
4. SOS metadata commands. Drill Into .NET Framework Internals to See How the CLR Creates Runtime Objects. http://msdn.microsoft.com/ru-ru/magazine/cc163791(en-us).aspx
5. How do I debug assembles in the Global Assembly Cache (GAC)? http://blogs.msdn.com/junfeng/archive/2005/12/13/503059.aspx
6. Document Your Code in No Time At All with Macros in Visual Studio. http://msdn.microsoft.com/ru-ru/magazine/cc163757(en-us).aspx
8. Speeding up the C# Source Code Editor http://weblogs.asp.net/rweigelt/archive/2006/05/15/446536.aspx
9. Let The CLR Find Bugs For You With Managed Debugging Assistants. http://msdn.microsoft.com/ru-ru/magazine/cc163606(en-us).aspx

БЛОГИ

1. Блог Майкла Столла CLR Debugging Team: http://blogs.gotdotnet.com/jmstall/
2. Блог Марка Руссиновича: http://blogs.technet.com/markrussinovich/
3. Блог Джо Даффи: http://www.bluebytesoftware.com/blog/
4. Блог Tess Ferrandez: http://blogs.msdn.com/tess/
5. Блог Jeff Atwood: http://www.codinghorror.com/blog/
6. Блог Aaron Margosis "Non-Admin" WebLog: http://blogs.msdn.com/aaron_margosis/
7. Блоги Wintellectuals' Blogs: http://www.wintellect.com/cs/blogs/
ИНСТРУМЕНТЫ, УТИЛИТЫ
1. Reflector Лутца Родера
1.1. Сам рефлектор: http://www.aisto.com/roeder/dotnet/
1.2. Denis Bauer Reflector.FileDisassembler. Позволяет дизасемблировать целиком сборку и сохранить ее исходный код. http://www.denisbauer.com/NETTools/FileDisassembler.aspx
1.3. Reflector Diff AddIn. http://www.codingsanity.com/diff.htm
1.4. Kevin Dente's Running Assembly Reflector Add-in. http://Weblogs.asp.net/kdente/articles/438539.aspx На этом перечень AddIn-ов к рефлектору не заканчивается и в интернете можно свободно найти достаточное количество полезных дополнений.
2. Иструменты от Sysinternals. Process Explorer, Process Monitor, DebugView и многое другое. http://technet.microsoft.com/en-us/sysinternals/default.aspx
3. Автономная утилита анализа кода FxCop msdn.microsoft.com/en-us/library/bb429476(VS.80).aspx
4. Debugging Tools for Windows. WinDBG, SOS, ADPlus. www.microsoft.com/whdc/devtools/debugging/ Очень полезные инструменты для отладки, исследования памяти, поиска deadlock-ов и многих других задач.
6. NCover - A test code coverage tool for C# .NET http://ncover.sourceforge.net/
7. Dependency Walker from http://www.dependencywalker.com/
8. Набор инструментов от Sells Brothers. RegexDesigner.NET и другие. http://www.sellsbrothers.com/tools/
9. CopySourceAsHtml (CSAH). Встраиваемый модуль в Visual Studio, который позволяет копировать куски кода виде html. www.jtleigh.com/CopySourceAsHtml/
10. Windows Installer XML (WIX). http://wix.sourceforge.net/ Должно быть интересно посмотреть на бесплатный инструмент от Майкрософт, с помощью которого создаются инсталляционные пакеты для Microsoft Office, Visual Studio и SQL Server. wix.sourceforge.net.
1. GhostDoc. Free add-in for Visual Studio that automatically generates XML documentation comments for C#. http://www.roland-weigelt.de/ghostdoc/
14. Инструменты автоматизации тестирования пользовательского интерфейса: Mercury WinRunner, Rational Robot
15. PInvoke.Net. Do Interop the WIKI way. http://www.pinvoke.net/
17. Regex Kit: Regular Expression Visualizers for VS 2005 http://weblogs.asp.net/rosherove/archive/2005/11/26/AnnoucingRegexKit10.aspx

понедельник, 23 февраля 2009 г.

Книга Джона Роббинса "Отладка приложений для Microsoft .NET"

imageНе зависимо от используемого языка программирования и операционной системы, среды разработки и методологии, вашего опыта и профессионализма вашей команды, вы рано или поздно столкнетесь с одной из самых неприятных сторон нашей работы – с ошибками.

Ошибки неизбежное зло, которое рано или поздно настигнет вас и будет преследовать до тех пор, пока вы с ними не справитесь (или пока они не справятся с вами, что закончится вашим переводом на другой проект или в другой коллектив). Ошибки могут быть самыми различными, начиная от несогласованного пользовательского интерфейса, и низкой производительности, заканчивая аварийным завершением работы программы с последующим форматированием жесткого диска.

«Ошибки – это круто! Они помогают залезть в самую глубину и понять, как работают вещи. Мы все попали в этот бизнес, потому что нам нравится учиться, выслеживание ошибок – неотъемлемая часть обучения… Ведь так здорово бывает найти и исправить ошибку! Конечно же, самые хорошие ошибки – это те, которые обнаруживаются до того, как заказчик увидит ваш продукт. Таким образом, вы должны успевать сделать свою работу и найти ошибки до того, как это сделают ваши заказчики. Видеть, как заказчики обнаруживают ошибки, - это совершенно не круто».

Поскольку процесс поиска и исправления ошибок является такой же неотъемлемой частью разработки программного обеспечения, как и анализ требований, проектирование, кодирование и тестирование, польза этой книги очевидна. Очень интересной является первая глава книги, в которой автор рассказывает о типах ошибок и причинах их возникновения. Ведь с многими причинами, такими как нереальные сроки выполнения проекта или подход «сначала кодируем, потом думаем», сталкиваются большинство из нас. И хотя сам факт правоты автора никак не повлияет на судьбу простого разработчика, вполне вероятно, что уже сейчас вы способны решать вопросы, влияющие на качество вашего продукта, и тогда советы автора окажутся просто бесценными.

Во второй главе, «Подготовка к отладке», автор описывает основополагающие вещи, без которой не то что отладка, а сам процесс разработки практически невозможен. Здесь речь идет о системах управления версиями и отслеживания ошибок, о важности модульных и дымовых (smoke) тестов, о важности отладочных символов и хранилища символов, о предупреждениях компилятора и средствах анализа кода. Все это (или многое) уже давно известно многим из нас. Но это позволит критическим взглядом окинуть ваш процесс разработки и сделать соответствующие выводы.

Третья глава посвящена «упреждающему программированию» или «отладке во время кодирования», а именно утверждениям, трассировке и комментированию кода. Наиболее интересным является обзор и реализация инструмента SUPERASSERT.NET, который способен в значительной степени облегчить процесс последующего поиска ошибок.

Далее, автор касается чрезвычайно широкого спектра вопросов. Это и отладка запуска служб, и решение проблем, связанных с многопоточностью и отладкой взаимоблокировок, и реализация обработки исключений, и решение проблем с загрузкой сборок. Также описаны расширенные возможности отладки с использованием Visual Studio, утилиты WinDBG, SOS, FxCop и даже написание собственных правил Code Analysis.

Автор дает ответы на многие вопросы, связанные с разработкой и отладкой программного обеспечения. Но что еще больше впечатляет, так это перечень тем, которые автор поднимает на страницах своей книги. Книга не отличается глубиной изложения, в ней не рассматривается во всех подробностях IL, работа компилятора или внутренности среды CLR. Самым полезным является расширение вашего кругозора, возможность проанализировать собственный опыт и более подробно изучить хромающую область.

Омрачает эту светлую картину только одна проблема – перевод. В книге хватает как откровенных ляпов, так и просто некорректно переведенных терминов или фраз. В книге встречаются переходные разработчики (intermediate developer), собственные приложения (native applications), вползание функций (feature creep), язык посредника (intermediate language) и многое другое. Нельзя сказать, что подобных проблем слишком много или что они сильно сказываются на восприятии материала, но все же издательству следует подходить к таким вопросам более серьезно.

Несмотря на это книга, безусловно, стоит затраченных средств и усилий и будет полезна большинству разработчиков. И самое главное, «помните, что отладчик – это всего лишь инструмент, как, например, отвертка. Он делает только то, что вы приказываете ему делать. Настоящий отладчик находится у вас в голове».

пятница, 20 февраля 2009 г.

List Visualizer и сериализация с использованием суррогатов

Введение

Отладчик Visual Studio предоставляет множество полезных инструментов, без которых сложно себе представить разработку сложных коммерческих приложений. Одним из главных инструментов в процессе отладки являются окна семейства Watch, предназначенные для отображения и редактирования текущего состояния объектов. С его помощью вы можете увидеть значение любого поля или свойства, независимо от того, насколько сложным является объект. Но, как и любой механизм общего назначения, окна семейства Watch содержат ряд ограничений, существенно усложняющих процесс отладки.. Для просмотра и редактирования сложных объектов, разработчики отладчика Visual Studio создали механизм визуализаторов (Visualizer), способных представлять данные объектов в их естественной форме. В комплекте Visual Studio поставляются визуализаторы строковых типов данных (Text Visualizer, Xml Visualizer и Html Visualizer), а также визуализаторы контейнеров ADO.NET (DataSet Visualizer, DataTable Visualizer, DataView Visualizer и DataViewManager Visualizer). Но значительно более важным является возможность добавления собственных визуализаторов для создания в отладчике альтернативных представлений данных в удобном пользовательском интерфейсе.

Архитектура визуализаторов

Архитектура визуализаторов основана на том, что в процессе отладки участвуют две составляющие: сторона отладчика (Debugger Side) – код, работающий под управлением Visual Studio (окна Watch, DataTips, QuickWatch и др.) и отлаживаемая сторона (Debuggee Side) – код, который вы отлаживаете (ваша программа). Алгоритм работы визуализатора следующий. Вначале отладчик должен загрузить классы визуализаторов, которые располагаются в одном из двух каталогов: каталог_установки_Visual_studio\Common7\ Packages\Debugger\Visualizers, для загрузки визуализаторов, доступных всем пользователям; \Documents and Setting\%profile%\My Documents\Visual Studio\Visualizers, для загрузки визуализаторов, доступных только текущему пользователю. Отладчик узнает, что сборка содержит визуализатор, когда в сборке есть хотя бы один атрибут DebuggerVisualizerAttribute. Этот атрибут сообщает отладчику класс визуализатора, класс, ответственный за передачу данных между Debuggee Side и Debugger Side, тип объекта, предназначенного для отображения и редактирования, а также описание визуализатора. Когда в окне семейства Watch выводится значение, для типа которого определен визуализатор, то в столбце Value будет находиться значок увеличительного стекла. Если щелкнуть на нем, отладчик выберет и запустит последний визуализатор, который использовался для данного класса (рисунок 1).
Рисунок 1 – Визуализатор класса string
После активации визуализатора отладчик сериализует объект на отлаживаемой стороне с использованием класса, указанного в атрибуте DebuggerVisualizerAttribute. Обычно для этих целей используется класс VisualizerObjectSource, который для сериализации/десериализации использует BinaryFormatter. Затем состояние объекта в сериализованной форме передается стороне отладчика, где он десериализуется и отображается в окне пользовательского интерфейса. Если визуализатор предназначен не только для отображения, но и для изменения объекта, этот процесс повторяется в обратном порядке, после чего измененный объект передается на отлаживаемую сторону и заменяет исходный объект.

Создание простого визуализатора

Теперь перейдем к реализации простого визуализатора, предназначенного для отображения списка объектов.
[assembly: DebuggerVisualizer(
  //Класс визуализатора
  typeof(ListVisualizer.SerializableListVisualizer), 
  //Класс, осуществляющий передачу данных между Debuggee Side и Debugger Side
  typeof(VisualizerObjectSource), 
  //Тип объекта, предназначенного для отображения 
  // и редактирования визуализатором
  Target = typeof(List<>), 
  //Текстовое описание, которое будет видеть пользователь 
  //при выборе вашего визуализатора
  Description = "List Visualizer (for serializable data ONLY!)"
  )]
namespace ListVisualizer
{
  /// <summary>
  /// Получает данные от отлаживаемой программы. Отображает их.
  /// "Отправляет" измененные данные обратно.
  /// </summary>
  public class SerializableListVisualizer : DialogDebuggerVisualizer
  {
    protected override void Show(
      IDialogVisualizerService windowService, 
      IVisualizerObjectProvider objectProvider)
    {
      IList list = (IList)objectProvider.GetObject();
 
      Debug.Assert(list != null, "list != null");
 
      if (list != null)
      {
        using (var form =
            new ListVisualizerForm(list, objectProvider.IsObjectReplaceable))
        {
          if (windowService.ShowDialog(form) == DialogResult.OK)
          {
            if (objectProvider.IsObjectReplaceable)
            {
              var ms = new MemoryStream();
              VisualizerObjectSource.Serialize(ms, form.List);
              objectProvider.ReplaceData(ms);
            }
          }
        }
      }
 
    }
 
    /// <summary>
    /// Предназначен для тестирования. Может быть использован в
    /// модульных тестах, консольных приложениях etc.
    /// </summary>
    /// <param name="objectToVisualize">
    /// Данные, необходимые для визуализации</param>
    public static void TestListVisualizer(object objectToVisualize)
    {
      var visualizerHost = new VisualizerDevelopmentHost(objectToVisualize, 
                                 typeof(SerializableListVisualizer));
      visualizerHost.ShowVisualizer();
    }
  }
}
Вверху файла находится атрибут DebugerVisualizerAttribute, который отладчик ищет в момент загрузки визуализатора. Как уже отмечалось выше, данный атрибут содержит 4 параметра: класс визуализатора, класс, предназначенный для поддержки сериализации, тип объекта, для которого предназначен данный визуализатор, а также описание визуализатора.
 
ПРИМЕЧАНИЕ
В качестве свойства Target атрибута DebuggerVisualizerAttribute необходимо указывать класс объекта, предназначенного для редактирования и отображения визуализатором. В таком случае визуализатор будет доступен для объектов указанного класса, а также для всех объектов производных классов. В свойстве Target нельзя указать тип интерфейса. В нашем примере следующее значение свойства Target недопустимо: Target = typeof(IList<>).
 
Сам класс визуализатора, являющийся наследником DialogDebuggerVisualizer, содержит единственный метод Show, который и реализует всю работу визуализатора. В первой строке вызывается метод objectProvider.GetObject() с помощью которого визуализатор получает данные, необходимые для отображения. Затем создается форма, которая отображается с использованием интерфейса IDialogVisualizerService после чего проверяется возможность редактирования данных с помощью свойства IsObjectReplaceable интерфейса IVisualizerObjectProvider, и если такая возможность присутствует – вызываю метод ReplaceData, для замены данных в отлаживаемой программе. Второй метод класса – SerializableListVisualizer TestListVisualizer предназначен для упрощения задачи тестирования визуализатора, и может вызываться из консольного приложения или модульного теста. После копирования сборки визуализатора (со всеми зависимостями) в одну из соответствующих папок (речь о которых шла выше) данный визуалитор можно будет использовать в любом проекте Visual Studio в последующих сеансах отладки. Поскольку SerializableListVisualizer для передачи данных между процессами использует VisualizerObjectSource, который (как уже говорилось выше) в свою очередь использует BinaryFormatter для сериализации/десериализации объектов, то данный визуализатор будет работать только с объектами, помеченными атрибутом SerializableAttribute. Однако при попытке использовать данный визуализатор с классом, не помеченным атрибутом SerializableAttribute (и не реализующим интерфейс ISerializable), вы получите исключение, в котором говорится о том, что указанный класс не является сериализуемым. Для тестирования работы визуализатора воспользуемся следующим тестовым классом:
[Serializable]
public class SomeSerializableClass
{
    public string S1 { get; set; }
    public string S2 { get; set; }
    public int I1 { get; set; }
}
Рисунок 2. List Visualizer для сериализиуемых данных.

Сериализация с использованием суррогатов

Хотя класс SerializableListVisualizer является полноценным визуализатором списка объектов, его практическое применение слишком ограничено. Мало кто согласится добавить атрибут SerializableAttribute к своему классу только для того, чтобы объекты этого класса можно было посмотреть в красивом виде. Поэтому необходимо как-то обойти это досадное ограничение, и все же реализовать возможность отображения и редактирования списков несериализуемых объектов. Архитектура визуализаторов предусматривает возможность вмешаться в процесс сериализации и десериализации путем создания наследника от VisualizerObjectSource и указания этого типа в атрибуте DebuggerVisualizerAttribute. Таким образом, решение задачи отображения и редактирования несереализуемых объектов по сути своей, сводится к решению задачи сериализации и десериализации несериализируемых объектов. Инфраструктура сериализации в .Net Framework предусматривает возможность «делегирования» полномочий по сериализации некоторого объекта другим объектам. Для этого необходимо определить «суррогатный тип» («surrogate type»), который возьмет на себя операции сериализации и десериализации существующего типа (путем реализации интерфейса ISerializationSurrogate). Затем необходимо зарегистрировать экземпляр суррогатного типа в форматирующем объекте, сообщая ему, какой тип подменяется суррогатным. Когда форматирующий объект обнаруживает, что выполняется сериализация или десериализация экземпляра существующего типа, он вызывает методы, определенные в соответствующем суррогатном типе. Предположим, существует некоторый несериализуемый класс следующего вида:
public class NonSerializableClass
{
    public int Id { get; set; }
    public string Name { get; set; }
}
Класс не помечен атрибутом SerializableAttrubute и не реализует интерфейс ISerializable, т.е. не предусматривает сериализацию своих экземпляров. Это ограничение можно обойти, создав суррогатный тип, который возьмет на себя ответственность за сериализацию и десериализацию экземпляров указанного типа. Для этого нужно создать класс, реализующий интерфейс ISerializationSurrogate, который определен следующим образом:
public interface ISerializationSurrogate
{
    void GetObjectData(object obj,
      SerializationInfo info, StreamingContext context);
 
    object SetObjectData(object obj,
      SerializationInfo info, StreamingContext context,
      ISurrogateSelector selector);
}
Этот интерфейс аналогичен интерфейсу ISerializable. Отличие состоит в том, что методы интерфейса ISerializationSurrogate принимают дополнительный параметр – ссылку на реальный объект, подлежащий сериализации. Поскольку сам класс NonSerializableClass достаточно прост, то и реализация соответствующего суррогата будет простой. В методе GetObjectData первый параметр нужно привести к соответствующему типу и сохранить все поля в объекте SerializationInfo. Для десериализации объекта вызывается метод SetObjectData, при этом ссылка на десериализуемый объект возвращается статическим методом GetUnitializedObject, принадлежащим FormatterServices. Т.е. все поля объекта перед десериализацией пусты и для объекта не вызван никакой конструктор. Задача метода SetObjectData – инициализировать поля объекта, получая значения из объекта SerializationInfo.
public class NonSerializableClassSurrogate : ISerializationSurrogate
{
    public void GetObjectData(
      object obj, SerializationInfo info, StreamingContext context)
    {
        var nonSerializable = (NonSerializableClass)obj;
        info.AddValue("Id", nonSerializable.Id);
        info.AddValue("Name", nonSerializable.Name);
    }
 
    public object SetObjectData(
      object obj, SerializationInfo info,
      StreamingContext context, ISurrogateSelector selector)
    {
        var nonSerializable = (NonSerializableClass)obj;
        nonSerializable.Id = info.GetInt32("Id");
        nonSerializable.Name = info.GetString("Name");
        return obj;
    }
}
Единственная проблема, которая может возникнуть при создании суррогатных типов даже для простых объектов – это создание суррогатов для value-типов. Проблема в том, что первый параметр метода SetObjectData относится к типу Object, т.е. value-тип будет передан в упакованном виде, а в таких языках программирования как C# и Visual Basic просто не предусмотрена возможность изменения свойств непосредственно в упакованном объекте. Единственный способ сделать это – воспользоваться механизмом рефлексии (reflection) следующим образом:
public object SetObjectData(
  object obj, SerializationInfo info, 
  StreamingContext context, ISurrogateSelector selector)
{
  typeof(NonSerializableClass).GetProperty("Id").SetValue(
    obj, info.GetInt32("Id"), null);
  typeof(NonSerializableClass).GetProperty("Name").SetValue(
    obj, info.GetString("Name"), null);
  return obj;
}
Использование суррогатного типа следующее:
//Создание объекта, подлежащего сериализации
var ns1 = new NonSerializableClass { Id = 47, Name = "TestName" };
var formatter = new BinaryFormatter();
var ss = new SurrogateSelector();
// Зарегистрировать суррогатный класс
ss.AddSurrogate(typeof(NonSerializableClass),
    new StreamingContext(StreamingContextStates.All),
    new NonSerializableClassSurrogate());
// Указать селектор
formatter.SurrogateSelector = ss;
 
using (var ms = new MemoryStream())
{
    //Сериализирую объект класса NonSerializableClass
    formatter.Serialize(ms, ns1);
    //Устанавливаю в 0 позицию в потоке MemoryStream
    ms.Position = 0;
    //Десериализирую объект класса NonSerializableClass
    var ns2 = (NonSerializableClass)formatter.Deserialize(ms);
    //Осталось проверить правильность сериализации и десериализации
    Assert.AreEqual(ns1.Id, ns2.Id);
    Assert.AreEqual(ns1.Name, ns2.Name);
}
Теперь перейдем к реализации суррогатного типа, осуществляющего сериализацию/десериализацию несериализируемых типов. Основная работа по сериализации объекта осуществляет функция SerializeFields. Ее реализация основана на использовании механизма рефлексии, с помощью которого я получаю все поля объекта и, если поле является сериализуемым, добавляю значение поля в объект SerializationInfo. Поскольку я получаю только поля объекта, объявленные в текущем типе, функцию SerializeFields нужно вызвать рекурсивно для всех базовых классов сериализуемого объекта. Рекурсия останавливается при достижении класса Object. Десериализация осуществляется с помощью функции DeserializeFields и ее реализация является аналогичной. Ограничением данной реализации является то, что если сериализуемый объект в качестве поля будет содержать объект несериализуемого типа, то это поле останется неинициализированным, что в некоторых случаях может привести к непредсказуемому поведению.
/// <summary>
/// "Суррогат" сериализирует все сериализируемые поля объекта
/// </summary>
public class NonSerializableSurrogate : ISerializationSurrogate
{
    public void GetObjectData(
      object obj, SerializationInfo info, StreamingContext context)
    {
        SerializeFields(obj, obj.GetType(), info);
    }
 
    public object SetObjectData(
      object obj, SerializationInfo info,
      StreamingContext context, ISurrogateSelector selector)
    {
        DeserializeFields(obj, obj.GetType(), info);
        return obj;
    }
 
    private static void SerializeFields(
      object obj, Type type, SerializationInfo info)
    {
        // Попытка сериализации полей типа Object 
        // является ограничением рекурсии
        if (type == typeof(object))
            return;
 
        // Получаю все экземплярные поля, 
        // объявленные в объекте текущего класса
        var fields = type.GetFields(Flags);
        foreach (var field in fields)
        {
            // Игнорирую все несериализируемые поля
            if (field.IsNotSerialized)
                continue;
 
            var fieldName = type.Name + "+" + field.Name;
            // Добавляю значение поля в объект SerializationInfo
            info.AddValue(fieldName, field.GetValue(obj));
        }
        // Сериализирую базовую составляющую текущего объекта
        SerializeFields(obj, type.BaseType, info);
    }
 
    private static void DeserializeFields(
      object obj, Type type, SerializationInfo info)
    {
        // Попытка сериализации полей типа Object 
        // является ограничением рекурсии
        if (type == typeof(object))
            return;
 
        // Получаю все экземплярные поля, объявленные в объекте текущего класса
        var fields = type.GetFields(Flags);
 
        foreach (var field in fields)
        {
            // Игнорирую все несериализируемые поля
            if (field.IsNotSerialized)
                continue;
            var fieldName = type.Name + "+" + field.Name;
            // Получаю значение поля из объекта SerializationInfo
            var fieldValue = info.GetValue(fieldName, field.FieldType);
            // Устанавливаю значение соответствующего поля объекта
            field.SetValue(obj, fieldValue);
        }
        // Десериализирую базовую составляющую текущего объекта
        DeserializeFields(obj, type.BaseType, info);
    }
 
    private const BindingFlags Flags = BindingFlags.Instance
                                     | BindingFlags.DeclaredOnly
                                     | BindingFlags.NonPublic
                                     | BindingFlags.Public;
}
Для простоты использования класса NonSerializableSurrogate создадим соответствующий селектор (класс, реализующий интерфейс ISurrogateSelector), который будет возвращать NonSerializableSurrogate только при попытке сериализации класса, не поддерживающего сериализацию.
/// <summary>
/// Реализует выбор необходимого суррогата
/// </summary>
public class NonSerializableSurrogateSelector : ISurrogateSelector
{
    public void ChainSelector(ISurrogateSelector selector)
    {
        throw new NotImplementedException();
    }
 
    public ISurrogateSelector GetNextSelector()
    {
        throw new NotImplementedException();
    }
 
    public ISerializationSurrogate GetSurrogate(
      Type type, StreamingContext context, out ISurrogateSelector selector)
    {
        //Для несерилазируемых типов возвращаю суррогат, который
        //сериализирует все сериализуемые поля объекта
        selector = null;
        if (type.IsSerializable)
            return null;
        selector = this;
        return new NonSerializableSurrogate();
    }
 
}
Пример использования классов NonSerializableSurrogate и NonSerializableSurrogateSelector:
// Создание объекта, подлежащего сериализации
var ns1 = new NonSerializableClass { Id = 47, Name = "TestName" };
var formatter = new BinaryFormatter();
formatter.SurrogateSelector = new NonSerializableSurrogateSelector();
 
using (var ms = new MemoryStream())
{
    // Сериализирую объект класса NonSerializableClass
    formatter.Serialize(ms, ns1);
    ms.Position = 0;
 
    // Десериализирую объект класса NonSerializableClass
    var ns2 = (NonSerializableClass)formatter.Deserialize(ms);
    // Осталось проверить правильность сериализации и десериализации
    Assert.AreEqual(ns1.Id, ns2.Id);
    Assert.AreEqual(ns1.Name, ns2.Name);
}

Реализация визуализатора списка объектов, не поддерживающих сериализацию

Для реализации визуализатора списка объектов, не поддерживающих сериализацию, необходимо реализовать класс-наследник от VisualizerObjectSource, который с помощью суррогатного типа, определенного в предыдущем разделе, будет заниматься сериализацией/десериализацией списка объектов, не поддерживающих сериализацию.
/// <summary>
/// Предназначен для сериализации списка объектов
/// </summary>
public class ListVisualizerObjectSource : VisualizerObjectSource
{
    public override void GetData(object target, System.IO.Stream outgoingData)
    {
        var list = target as IList;
        if (list == null)
            return;
 
        SerializeList(list, outgoingData);
    }
 
    public override object CreateReplacementObject(
      object target, Stream incomingData)
    {
        return DeserializeList(incomingData);
    }
 
    public static IList DeserializeList(Stream stream)
    {
        var formatter = new BinaryFormatter();
        formatter.SurrogateSelector = new NonSerializableSurrogateSelector();
        return (IList)formatter.Deserialize(stream);
    }
 
    public static Stream SerializeList(IList list)
    {
        var stream = new MemoryStream();
        SerializeList(list, stream);
        return stream;
    }
 
    public static Stream SerializeList(IList list, Stream stream)
    {
        IFormatter formatter = new BinaryFormatter();
        formatter.SurrogateSelector = new NonSerializableSurrogateSelector();
        formatter.Serialize(stream, list);
        return stream;
    }
 
}
Реализовать визуализатор на основе уже разработанных классов совсем несложно.
[assembly: DebuggerVisualizer(
  // Класс визуализатора
  typeof(ListVisualizer.ListVisualizer), 
  // Класс, осуществляющий передачу данных 
  // между Debuggee Side и Debugger Side
  typeof(ListVisualizer.ListVisualizerObjectSource), 
    // Тип объекта, предназначенного для отображения 
    // и редактирования визуализатором
    Target = typeof(List<>), 
      //Текстовое описание, которое будет видеть пользователь 
      // при выборе вашего визуализатора
      Description = "Cool List Visualizer" 
  )]
 
namespace ListVisualizer
{
  public class ListVisualizer : DialogDebuggerVisualizer
  {
    protected override void Show(
      IDialogVisualizerService windowService, 
      IVisualizerObjectProvider objectProvider)
    {
      IList list = ListVisualizerObjectSource.DeserializeList(
        objectProvider.GetData());
 
      Debug.Assert(list != null, "list != null");
 
      if (list != null)
      {
        using (var form =
            new ListVisualizerForm(list, objectProvider.IsObjectReplaceable))
        {
          if (windowService.ShowDialog(form) == DialogResult.OK)
          {
            if (objectProvider.IsObjectReplaceable)
            {
              objectProvider.ReplaceData(
                ListVisualizerObjectSource.SerializeList(form.List));
            }
          }
        }
      }
 
    }
 
    public static void TestShowVisualizer(object objectToVisualize)
    {
      VisualizerDevelopmentHost visualizerHost = 
        new VisualizerDevelopmentHost(
          objectToVisualize, typeof(ListVisualizer), 
          typeof(ListVisualizerObjectSource));
 
      visualizerHost.ShowVisualizer();
    }
  }
}
Осталось скопировать полученную сборку в папку визуализаторов и запустить отладку. Для проверки работы визуализатора будем использовать несериализируемый класс следующего вида:
public class NonSerializableClass
{
    public NonSerializableClass()
    {
        Time = DateTime.Now;
    }
 
    public string S1 { get; set; }
    public string S2 { get; set; }
    public int I1 { get; set; }
    public DateTime Time { get; set; }
}
Рисунок 3 – List Visualizer для списков сериализуемых и несериализуемых объектов

Выводы

В этой небольшой статье я рассмотрел два, казалось бы, совершенно не связанных вопроса: реализация собственных визуализаторов и сериализацию с использованием суррогатов. Это связано с тем, что для работы визуализатора требуется сериализация/десериализация объектов между двумя процессами: процессом отладчика и процессом отлаживаемого кода. Наличие в арсенале разработчика визуализатора списка объектов может существенно упростить отладку и просмотр состояния объектов на этапе выполнения. Но ограничить себя просмотром и изменением только сериализуемых объектов — значит отказаться от этого инструмента в 90% случаев. Поэтому я предпринял попытку обойти это ограничение и реализовать более универсальный визуализатор, предназначенный для работы со списками как сериализируемых, так и не сериализируемых объектов.