пятница, 19 октября 2007 г.

Unable to attach to the process


Вот такое сообщение я получил от студии, когда попытался подключиться к IE, чтобы отладить панельку, написанную на C#. Кстати, хорошая и почти единственная статья о том, как сделать свой тулбар для эксплорера, написана вот здесь: Extending Explorer with Band Objects using .NET and Windows Forms. А теперь - детально. Панель для эксплорера представляет собой так называемый COM Callable Wrapper (CCW) и была написана на .NET 1.1 в Visual Studio 2003 - аттачился я, соответственно, в ней же. Как вы уже поняли, такой способ не сработал. Затем я попытался отладить компонент при помощи другого хост-контейнера (ActiveX Control Test Container) – та же ерунда. Думаю, ладно не студия, так WinDbg. Пусть не так удобно, но на безрыбье и рак рыба. Приатачился! Собирался подгрузить SOS.dll и тут обратил внимание, что в процесс IE загрузился не .NET 1.1 a .NET 2.0! Снес с машины второй фреймворк (к счастью, у меня была такая возможность). И опа! - все хоккей, студия замечательно подключилась. По правде говоря, с СОМом из .NET активно не работал и по этому был немало удивлен. Навел справки. Оказывается, при активации CCW по умолчанию запускается старшая по версии CLR из установленных на машине. (см. статью MSDN Configuring a COM Application for Side-By-Side Execution), однако это поведение можно изменить при помощи config-файла для приложения, которое активирует CCW. В статье есть примечание, что такой подход не работает для Internet Explorer и Microsoft Office :). Решил проверить. Поставил обратно NET 2.0 и убедился, что в IE грузится именно он. Затем создал файл IEXPLORE.EXE.config:
<?xml version ="1.0"?>
<configuration>
<startup>
<supportedRuntime version="v1.1.4322" safemode="false"/>
</startup>
</configuration>

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

вторник, 9 октября 2007 г.

События и многопоточный код

Недавно, просматривая главу книги Framework Design Guidelines, посвященную событиям (event) наткнулся на интересное замечание. Это же замечание про события процитировано и на блоге Brad Abrams Events, Delegate and Multithreading . Суть его в следующем. Предположим, что у нашего класса есть некое событие Clicked, объявленное следующим образом:
    public event EventHandler Clicked;
Если метод генерации события написан в виде:
    protected void OnUnsafeRaiseClick()
{
if (Clicked != null) Clicked(this, EventArgs.Empty);
}
- это приводит к небезопасному поведению в случае многопоточного исполнения, когда один поток пытается сгенерировать событие, а второй в этот момент отписывает последний делегат. При определенном стечении обстоятельств проверка
    Clicked != null
может пройти успешно (второй поток еще не отписал последний делегат), а попытка выполнить
    Clicked(this, EventArgs.Empty)
может привести к исключению System.NullReferenceException (если второй поток уже отписал последнее событие). Рекомендуемым способом генерации события будет следующий:
    protected void OnSafeRaiseClick()
{
EventHandler handler = Clicked;
if (handler != null)
handler(this, EventArgs.Empty);
}
По правде говоря, мне было совсем не очевидно, почему копирование ссылки делает исполнение кода безопасным в многопоточной среде, а поиск в Google конкретно по этому вопросу ничего не дал, поэтому провел свое маленькое исследование.
Немного теории. Если вы не очень хорошо знаете внутреннюю кухню событий и делегатов, то предварительно рекомендую просмотреть статью Делегаты и события. Как известно, подписка и отписка от события при помощи C#-операторов += и -= приводит к вызову статических методов Delegate.Combine и Delegate.Remove соответственно. Работа этих методов на наследниках от MulticastDelegate приводит к тому, что будет создан новый делегат, с новым списком вызовов (invocation list), а старый будет собран сборщиком мусора (если на него нет ссылок).
Вернемся теперь к интересной ситуации, когда один поток пытается исполнить метод OnSafeRaiseClick, а второй поток отписывает делегаты. После исполнения первым потоком кода
    EventHandler handler = Clicked
модификация вторым потоком Clicked приведет к созданию нового делегата с новым списком вызовов и событие Clicked будет хранить ссылку на новый делегат или null (если отписали последнее событие), а handler будет хранить ссылку на первоначальный делегат. Далее исполнение первым потоком
    if (handler != null)
handler(this, EventArgs.Empty);
будет вестись над исходным делегатом и проблемы не возникнет.
Интересен тот факт, что дополнительных накладных расходов в методе OnSafeRaiseClick по сравнению с OnUnsafeRaiseClick при однопоточном исполнении очень немного.

четверг, 4 октября 2007 г.

Текстовый файл в ресурсе сборки

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

  • Добавляем файл как Embedded Resource.
  • По имени файла в ресурсе получаем поток, позволяющий считать файл.
  • Работаем с потоком, чтобы получить нужный программный объект. Например, строку с содержимым текстового файла или объект изображения/иконки.

А теперь более подробно. Для внедрения файла в сборку, надо подключить его к проекту и выставить значение свойства Build Action в Embedded Resource. Теперь после компиляции файл будет внедрен в ресурсы сборки. Переходим к извлечению. Имя файла в ресурсе формируется из префикса, в качестве которого выступает Default namespace, из свойств проекта, и относительного пути к файлу (включая само имя файла).

Для надежности можно посмотреть имя внедренного файла через ildasm.exe:

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

static class ResourceHelper
{
private static Stream GetStream(string resourceName)
{
return typeof(ResourceHelper).Assembly.GetManifestResourceStream(resourceName);
}

internal static string GetText(string resourceName)
{
using (StreamReader reader = new StreamReader(GetStream(resourceName)))
{
return reader.ReadToEnd();
}
}

internal static Image GetImage(string resourceName)
{
using (Stream imageStream = GetStream(resourceName))
{
return Image.FromStream(imageStream);
}
}
}

И, наконец, пример использования вспомогательного класса:


private void Form1_Load(object sender, EventArgs e)

{

pictureBox1.Image = ResourceHelper.GetImage("WindowsApplication2.Images.Logo.PNG");

textBox1.Text = ResourceHelper.GetText("WindowsApplication2.Scripts.SampleSQL.txt");

}










вторник, 2 октября 2007 г.

Брейкпоинт в другом домене

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


Однако отладчик упорно игнорировал точку прерывания, хотя показывал, что отладочные символы для метода загружены. И я, откровенно говоря, был в замешательстве. Немного поразмыслив, на ум пришел тот факт, что исполнение кода в новом AppDomain приводит к тому, что в него все сборки загружаются отдельно. Отсюда я пришел к гипотезе, что в новый домен не загрузились отладочные символы. Для проверки моего предположения мы скопировали pdb-файл в тот же каталог, откуда загружалась и отлаживаемая сборка. Кстати, узнать путь реальной загрузки сборки можно либо из окна Output (View-> Output), либо окна Modules (Debug->Windows->Modules). И действительно, при очередной попытке исполнить проблемный код, отладчик наконец-то остановился на долгожданном брейкпоинте.