вторник, 10 декабря 2013 г.

Visual Studio 2012: сборка C++ приложений с обратной совместимостью до Windows XP

После сборки на VS 2012 выясняется, что приложение попросту отказывается запускаться на Windows XP, хотя и нет ничего специфичного. Беда...

Что нужно сделать чтоб это исправить:
1. Обновляем Visual Studio до ServicePack 1 или новее (на момент написания актуален SP4). Без сервиспака, увы, ничего не выйдет. Эх, microsoft, microsoft...

2. В свойствах проекта на вкладке Configuration - Properties - параметр Platform Toolset выбираем пункт Visual Studio 2012 - Windows XP (v110_xp)

3. Обычно этого бывает достаточно. Но не всегда. В Windows XP нет функции GetTickCount64 в KERNEL32.DLL, а многие либы и программы ее используют (на удивление, даже если вы ей не пользовались - с большой вероятностью зависимость к ней будет). Чтобы избежать ее использования, нужно в свойствах проекта во вкладке C++ - Preprocessor - параметр Preprocessor Definitions добавить:
WINVER=0x0501;_WIN32_WINNT=0x0501

4. Если ваш софт использует функции PS API, то надо еще добавить в директивы препроцессора:
PSAPI_VERSION=1
Иначе работать не будет даже под вистой - только семерки и восьмерки.

После этого совместимость с WinXP будет достигнута

P.S. Если собираете boost на VS 2012 - он будет использовать GetTickCount64. Чтобы исправить, в параметры вызова сборщика bjam.exe добавить те же дефайны: define=WINVER=0x0501 define=_WIN32_WINNT=0x0501 define=NTDDI_VERSION=0x05010000 define=PSAPI_VERSION=1

Читать дальше...

Visual Studio Remote Debugger через интернет

Многие вероятно задавались этим вопросом, особенно когда нет VPN, а есть только проброшенный порт.

Алгоритм:
1. Копируем Remote debugger от своей Visual Studio (только так! какой попало не подойдет) на target машину. Важна еще разрядность: для отладки x64 программ копируйте x64 версию, а если программа запущена на Win64, но имеет разрядность x86 - то нужен x86 remote debugger! Он предупредит о том, что версия x64 умеет и x86 процессы дебажить, но в реальности это оказалось неправдой... по крайней мере у меня не получилось

2. Запускаем там msvsmon, желательно с правами админа. Выбираем аутентификацию (лучше Windows, можно без аутентификации). Важно выбрать порт тот, который проброшен в интернет. На роутере настроить forwarding для TCP порта. По умолчанию 4016, но подойдет любой.
Не забываем разрешить соединение по этому порту в фаерволе, либо отключаем его вообще.

3. Соединяемся удаленной Visual Studio с Remote Debugger по внешнему IP и проброшенному порту. К примеру 123.123.123.123:4016, выбираем транспорт (Native в случае отсутствия аутентификации, либо Default - в случае Windows аутентификации), соединяемся... и видим что-то такое:

Unable to connect to 'TARGET'. The Microsoft Visual Studio Remote Debugging Monitor (MSVSMON.EXE) does not appear to be running on remote computer. This may be because a firewall is preventing communication to the remote computer. Please see Help for assistance on configuring remote debugging

В случае с Windows Authentication еще успеем ввести логин и пароль, но итог будет тот же.

При этом удаленный дебаггер даже не шелохнулся - нет информации о соединении, либо о каких-то ошибках.

Форумы MSDN расскажут что задуманное невозможно  http://social.msdn.microsoft.com/Forums/vstudio/en-US/897782e5-dada-4df1-8ef2-f119cdce7e5e/remote-debugging-over-internet?forum=vsdebug
Ну или порекомендуют VPN...

Но мы не такие! Если поглубже посмотреть в источник причины, то выясняется, что Remote debugger использует pipes для соединения. А пайпы через интернет не того... особенно через порты проброшенные. В реальности ведь удаленному отладчику не очень нужны преимущества пайпов, если вы дебажите native код. DCOM и всякие RPC пригодятся только для отладки managed кода. Единственное что привязывает удаленный отладчик и пайпы - это имя target машины.

РЕШЕНИЕ

Короче говоря, самый простой способ, который я нашел - прописываем в файлике \Windows\System32\etc\hosts имя target машины и привязываем ее к внешнему IP, по которому она доступна, и соединяемся по имени. Важно задать реальное имя машины.
Т.е. в hosts пишем что то вроде
123.123.123.123 TARGETPC

и соединяемся из Visual Studio к TARGETPC:4016
И все работает! правда только для отладки native кода.

Относительно managed кода сказать могу мало что - не пробовал, но будут проблемы. Для версий .NET 4.0 и 4.5 реально используется DCOM, поэтому, по всей видимости, здесь только одно нормальное решение - VPN. Можно попробовать пробросить еще и DCOM порты (вот какие надо http://www.verdiem.com/kb/configure-firewall-dcom-ports), но будет ли оно работать - не знаю...

Читать дальше...

понедельник, 12 декабря 2011 г.

WinDBG kernel debugging with VirtualBox

Настройка WinDBG kernel debugger для отладки гостевой системы в VirtualBox.
Коротко, ничего лишнего.

WinDBG устанавливается на хосте, отлаживается система внутри VirtualBox.
Считаем что ОС Windows Vista или Windows 7 (x86 или x64) уже установлены.

1. Останавливаем эмуляцию, открываем свойства виртуальной машины.
Переходим на вкладку "COM-порты", включаем порт COM1.
Режим порта "Хост-канал", чекбокс "Создать канал" установлен.
Прерывание 4, порт 0x3F8.
Путь к порту/файлу: \\.\pipe\wmdbgcom1

2. Включаем эмуляцию, ждем загрузки гостевой ОС. Открываем cmd.exe c правами админа.
Вводим:
bcdedit /dbgsettings SERIAL DEBUGPORT:1 BAUDRATE:115200
bcdedit /bootdebug on
bcdedit /debug on


3. Запускаем WinDBG на хосте, открываем File - Kernel Debug, вкладка "COM"
В окошке вводим наши параметры:
Port: \\.\pipe\wmdbgcom1
Baud Rate: 115200
Установить галочку "Pipe"

Нажимаем ОК

4. Перезагружаем гостевую ОС в VirtualBox

5. Загружаем отладочные символы из интернета: ждем когда отладчик прицепится, ждем загрузки гостевой ОС, переходим в WinDBG, жмем Ctrl+Break. После остановки и перехода в режим отладки, в главном меню WinDBG выбираем пункт File - Symbol search path
Вводим: SRV*f:\localsymbols*http://msdl.microsoft.com/download/symbols
Жмем ОК.
В командной строке отладчика вводим: .reload
Ждем завершения операции

6. Все, отлаживаемся на здоровье

Важно: без запущенного отладчика гостевая ОС будет стартовать оооочень долго. Поэтому без WinDBG теперь эта виртуалка работать не будет. Чтобы вернуть все назад, нужно отключить отладку в гостевой ОС. Или как вариант - выключить COM порт в настройках VirtualBox (это проще)

Читать дальше...

понедельник, 26 сентября 2011 г.

Проблема extension-методов с типом dynamic

"Динамический тип" появился в .NET 4.0. Суть его состоит в привязывании методов не на уровне компиляции, а на уровне исполнения. Почитать про него можно к примеру тут http://habrahabr.ru/blogs/net/43773/

Но ужасное открытие меня ожидало при разработке библиотеки с использованием типов dynamic. "Расширительные" методы на нем не только не работают, но даже не выдают ошибки. Точнее, генерируют исключение уже во время работы.

Хотя такое поведение логично (тип-то динамический), в коде оно создает дикую путаницу, особенно при рефакторинге. Выхода три:
1. Не использовать extension методы
2. Не использовать dynamic типы
3. Терпеть как есть, сохранять бдительность и качественно тестировать код. Как вариант - можно вызывать методы расширения как обычные статические методы для dynamic:

dynamic dynamicObject = new object();
object obj = new object();

// вызов расширения для object
obj.ExtensionMethod();

// вызов расширения для dynamic
ObjectExtensions.ExtensionMethod(dynamicObject);

Если писать так - то все работает. Но профита в методах расширения уже нет

Читать дальше...

Вызов методов с Generic параметрами через Reflection

Допустим, есть метод с Generic-параметром

public void GenericMethod<T>()
{
}

и есть метод с параметром типа Type

public void TypeMethod(Type type)
{
}

Вызвать TypeMethod из GenericMethod не составляет труда:

public void GenericMethod<T>()
{
TypeMethod(typeof(T));
}

А как вызвать GenericMethod из TypeMethod?
Поможет Reflection


public static object InvokeGenericMethod(object obj, string methodName, Type genericParam, params object[] pars)
{
var methodInfo = obj.GetType().GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance);
var method = methodInfo.MakeGenericMethod(new[] { genericParam });

return method.Invoke(obj, pars);
}

С помощью этого статического метода можно вызвать любой метод любого класса с одним Generic-параметром. Ну и если Generic-параметров больше:

public static object InvokeGenericMethod(object obj, string methodName, Type[] genericParams, params object[] pars)
{
var methodInfo = obj.GetType().GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance);
var method = methodInfo.MakeGenericMethod(genericParams);

return method.Invoke(obj, pars);
}

Пример использования:

public void GenericMethod<T>(int param1, object param2)
{
}

public void TypeMethod(Type type, int param1, object param2)
{
InvokeGenericMethod(this, "GenericMethod", type, param1, param2);
}


Читать дальше...

пятница, 23 сентября 2011 г.

АОП на .NET без Unity

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

Рассматривались три варианта решения:

1. Транспарентные прокси
Суть метода в том, что посредством Reflection мы создаем новый тип, который наследуется от нужного интерфейса и реализует прозрачный прокси-класс, выполняющий аспекты перед вызовом.
Этот метод посредством контейнера Unity я попробовал применить на живом проекте и у меня осталась масса впечатлений :)
http://fullfeedbag.blogspot.com/2010/09/aop-application-unity.html


Плюсы: после некоторых мучений АОП все же работает

Минусы: как по мне, то их оочень много
Самая большая беда: если что то сделать не так - аспекты могут отвалиться. Это не сильно страшно скажем для логирования. Но вот если на аспектах построен контроль синхронизации потоков, механизмы валидации или уровни доступа при авторизации, то отвалившиеся аспекты сделают целевую систему жутко уязвимой.
А сделать что то не так, к сожалению, очень даже легко, поскольку транспарентный прокси генерируется внешним компонентом, и если случайно его создать скажем с помощью оператора new - то аспекты не работают.
Вторая проблема: упавшая производительность. Падает она действительно очень сильно
Третья проблема: отладка кода сильно затрудняется. Отладчик Visual Studio неохотно ест сгенерированные в runtime типы данных
Четвертая проблема: багоемкость и высокий порог понимания для программистов. Объяснить принципы работы транспарентных прокси новому человеку, пришедшему на проект, ох как непросто. В совокупности с отваливающимися аспектами и диковатой отладкой это превращается в сущий ад


2. Модификация сборок в рантайме
Вообще, эта идея мне пришла сразу как только я начал применять АОП :) Мы просто берем и перехватываем метод класса. Без всяких прокси и прочей мути.
Один хороший человек разработал библиотечку для перехвата методов
http://www.codeproject.com/KB/dotnet/CLRMethodInjection.aspx
Если чуть пораскинуть мозгами - то ее можно применить для реализации АОП

Плюсы: решается проблема с быстродействием. Отваливания аспектов при грамотной реализации интерцептора не будет

Минусы:
1. Отладка еще хуже чем в случае с транспарентными прокси
2. Из-за того что метод использует грязные хаки чуть менее чем полностью, он очень сильно завязывается на версии .net framework и на его внутренних структурах данных. Проблема при переносе кода x32/x64. Если что то не так - программа падает
В общем для коммерческого проекта такой метод применять не очень хочется. Но сам по себе он очень интересен. Вот еще годные линки по теме покопаться в недрах дотнета:
http://www.codeproject.com/KB/dotnet/DotnetInternals_Injection.aspx
http://msdn.microsoft.com/en-us/magazine/cc163791.aspx
http://ntcore.com/files/disasmsil.htm


3. Модификация сборок до загрузки с помощью Mono.Cecil
Ахтунг! Не смотря на название либы Mono.Cecil, к самому Mono она, по видимому, не имеет отношения


Замысел в том, чтобы пофиксать сборочку один раз на диске и прикрутить к ней почти полноценное АОП. Библиотека Mono.Cecil позволит нам модифицировать сборки не копаясь в памяти и во внутренней структуре.


Плюсы: такие же как у всех - работающее АОП в результате
По поводу отладки затрудняюсь сказать (не пробовал), но думаю что с ней такие же проблемы как и у предыдущих методов


Минусы:
1. Фиксап подписанной (signed) сборки приводит к слетанию ЭЦП и неработоспособности сборки (кстати предыдущий метод был лишен такой проблемы).
Лечится тем, что нужно запустить процедуру фиксапа сразу после компиляции, ДО операции подписывания DLL. Это подразумевает то, что DLL своего производства. С чужими DLL такое не прокатывает

2. Операцию инжекции аспектов в сборку нужно запускать отдельно от кода. Если ее случайно не запустить - ошибка не выдается, аспекты не работают.
Введение этапа посткомпиляции - достаточно некрасивое решение. Хотя что то подобное сделали для прекомпиляции в Qt (MOC), так что может не все так плохо?

У этого метода есть приличные готовые реализации:
http://habrahabr.ru/blogs/net/95211/


Мораль сей басни состоит в том, что таки-нету идеальной реализации АОП на .NET
Так что ждем поддержки средств АОП от Microsoft на уровне компилятора. Ну или хотя бы stable версию АОП-фреймверка с Mono.Cecil


P.S. Я забыл упомянуть о PostSharp. Он использует и транспарентные прокси, и модификацию IL кода. Но, блин, стОит он дороговато...

P.P.S Литература по теме применения аспектно-ориентированного программирования:
http://www.javable.com/columns/aop/workshop/01/
http://www.javable.com/columns/aop/workshop/02/
http://citforum.ru/internet/javascript/aop/

Читать дальше...

пятница, 1 апреля 2011 г.

Длинные пути файловой системы в .NET

Любимая всеми ОС Windows с не менее любимой ФС NTFS может поддерживать пути длинной до 32К символов. Однако  мало кто этим воспользовался, поскольку в обычной жизни DOS-пути (а ими являются все пути вида C:\Windows...) поддерживают всего лишь 260 символов. Частенько этого бывает мало. Скажем, необходимо восстановить инфраструктуру сайта, а пути там могут быть куда длиннее.
В таком случае хорошо бы воспользоваться обещанными 32К-длинными путями. Но вот незадача: при попытке сделать путь хотя бы немного длиннее дозволенного, мы схлопочем исключение:
The path is too long after being fully qualified. Make sure path is less than 260 characters.
Для использования длинных путей с помощью WinAPI делают так:
1. Используют UNICODE-версии функций: CreateFileW, CreateDirectoryW и т.д.
2. Формируют путь в стиле NT: "\\?\C:\Windows..."

Еще одна проблема в том, что пути NT никак не модерируются системой: если обычные DOS пути можно писать как попало (C:/Windows\\\\\system32), и система спокойно съест такое, то для NT путей это недопустимо. То есть вся ответственность за корректные пути ложится на программиста.
Вернемся к дотнету. Он, конечно же, не предоставит нам выбора какими функциями пользоваться для работы с файлом. Придется использовать P/Invoke.
Проблема не нова, и решения можно поискать например здесь.
Я же предлагаю сконцентрироваться на устранении последствий применения P/Invoke, которое:
а. Поломает архитектуру
б. Сделает программу ОС-зависимой

Пункт (б) побороть никак не получится (кроме как использовать условную компиляцию). А вот архитектуру надо бы сохранить. Если у вас готова уже половина кода проекта, то переписывать куски с учетом использования нестандартного механизма работы с файлами совсем не хочется. Поэтому напишем класс, который будет совместим с обычным FileStream, но будет работать с длинными путями файловой системы.
Для сохранения архитектуры (конечно если она построена на принципах dependency injection) достаточно будет заменить фабрики, создающие объект файла или внести поправки в инициализацию DI контейнера.

Итак, ближе к делу. Сначала подготовим статический класс WinApi, в котором импортируем функции из kernel32.dll и определим константы. Также внимание надо уделить нормализации путей (ее сюда же добавим):
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using Microsoft.Win32.SafeHandles;

namespace WindowsSpecific
{
    public static class WinApi
    {
        #region ECreationDisposition enum

        public static string NormalizePath(string path)
        {
            const int bufferSize = 32767;

            if (path.StartsWith(".") || !path.Contains(":"))
            {
                var stringBuilder = new StringBuilder {Length = bufferSize};
                GetCurrentDirectory(bufferSize-1, stringBuilder);

                path = stringBuilder + "/" + path.TrimStart(new[] { '.' });
            }

            path = path.Replace('/', '').Trim();
           
            while (path.Contains(@""))
                path = path.Replace(@"", @"");

            return @"?" + path;
        }

        public const uint ErrorAlreadyExist = 0x000000b7;

        public enum ECreationDisposition : uint
        {
            New = 1,
            CreateAlways = 2,
            OpenExisting = 3,
            OpenAlways = 4,
            TruncateExisting = 5,
        }
        #endregion

        #region EFileAccess enum
        [Flags]
        public enum EFileAccess : uint
        {
            GenericRead = 0x80000000,
            GenericWrite = 0x40000000,
            GenericExecute = 0x20000000,
            GenericAll = 0x10000000,
        }
        #endregion

        #region EFileAttributes enum
        [Flags]
        public enum EFileAttributes : uint
        {
            Readonly = 0x00000001,
            Hidden = 0x00000002,
            System = 0x00000004,
            Directory = 0x00000010,
            Archive = 0x00000020,
            Device = 0x00000040,
            Normal = 0x00000080,
            Temporary = 0x00000100,
            SparseFile = 0x00000200,
            ReparsePoint = 0x00000400,
            Compressed = 0x00000800,
            Offline = 0x00001000,
            NotContentIndexed = 0x00002000,
            Encrypted = 0x00004000,
            Write_Through = 0x80000000,
            Overlapped = 0x40000000,
            NoBuffering = 0x20000000,
            RandomAccess = 0x10000000,
            SequentialScan = 0x08000000,
            DeleteOnClose = 0x04000000,
            BackupSemantics = 0x02000000,
            PosixSemantics = 0x01000000,
            OpenReparsePoint = 0x00200000,
            OpenNoRecall = 0x00100000,
            FirstPipeInstance = 0x00080000
        }
        #endregion

        #region EFileShare enum
        [Flags]
        public enum EFileShare : uint
        {
            None = 0x00000000,
            Read = 0x00000001,
            Write = 0x00000002,
            Delete = 0x00000004,
        }
        #endregion

        [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
        public static extern SafeFileHandle CreateFile
            (
            string lpFileName,
            EFileAccess dwDesiredAccess,
            EFileShare dwShareMode,
            IntPtr lpSecurityAttributes,
            ECreationDisposition dwCreationDisposition,
            EFileAttributes dwFlagsAndAttributes,
            IntPtr hTemplateFile
            );

        [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
        public static extern bool CreateDirectory
            (
            string lpFileName,
            IntPtr lpSecurityAttributes
            );

        [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
        public static extern bool CopyFile(
          string lpExistingFileName,
          string lpNewFileName,
          bool bFailIfExists
        );

        [DllImport("kernel32.dll")]
        public static extern uint GetCurrentDirectory(uint nBufferLength,
           [Out] StringBuilder lpBuffer);

        public static ECreationDisposition ECreationDispositionByFileMode(FileMode fileMode)
        {
            var winapiMode = ECreationDisposition.OpenAlways;
            switch (fileMode)
            {
                case FileMode.CreateNew:
                    winapiMode = ECreationDisposition.New;
                    break;

                case FileMode.Create:
                    winapiMode = ECreationDisposition.CreateAlways;
                    break;

                case FileMode.Open:
                    winapiMode = ECreationDisposition.OpenExisting;
                    break;

                case FileMode.OpenOrCreate:
                    winapiMode = ECreationDisposition.OpenAlways;
                    break;

                case FileMode.Truncate:
                    winapiMode = ECreationDisposition.TruncateExisting;
                    break;

                case FileMode.Append:
                    winapiMode = ECreationDisposition.OpenAlways;
                    break;
            }
            return winapiMode;
        }

        public static EFileAccess EFileAccessByFileAccess(FileAccess fileAccess)
        {
            var winapiAccess = EFileAccess.GenericAll;

            switch (fileAccess)
            {
                case FileAccess.Read:
                    winapiAccess = EFileAccess.GenericRead;
                    break;

                case FileAccess.Write:
                    winapiAccess = EFileAccess.GenericWrite;
                    break;

                case FileAccess.ReadWrite:
                    winapiAccess = EFileAccess.GenericRead | EFileAccess.GenericWrite;
                    break;
            }

            return winapiAccess;
        }
    }
}

Теперь напишем класс, перегрузив FileStream:
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using Microsoft.Win32.SafeHandles;

namespace WindowsSpecific
{
    public class LongPathFileStream : FileStream
    {
        private static SafeFileHandle Initialize(string fileName, FileMode fileMode, FileAccess fileAccess)
        {
            var longFileName = WinApi.NormalizePath(fileName);

            var fileHandle = WinApi.CreateFile(longFileName,WinApi.EFileAccessByFileAccess(fileAccess),
                WinApi.EFileShare.Read,IntPtr.Zero,WinApi.ECreationDispositionByFileMode(fileMode),0,IntPtr.Zero);

            var lastWin32Error = Marshal.GetLastWin32Error();
            if (fileHandle.IsInvalid)
                throw new System.ComponentModel.Win32Exception(lastWin32Error);

            return fileHandle;
        }

        public LongPathFileStream(string fileName)
            :base(Initialize(fileName,FileMode.OpenOrCreate,FileAccess.ReadWrite),FileAccess.ReadWrite)
        {
        }

        public LongPathFileStream(string fileName,FileAccess fileAccess)
            :base(Initialize(fileName,FileMode.OpenOrCreate,fileAccess),fileAccess)
        {
        }

        public LongPathFileStream(string fileName, FileMode fileMode, FileAccess fileAccess)
            :base(Initialize(fileName,fileMode,fileAccess),FileAccess.ReadWrite)
        {
            if (fileMode == FileMode.Append)
                AppendMode();
        }

        private void AppendMode()
        {
            Position = Length;
        }
    }
}

Вот и все! Теперь если вместо FileStream задействовать LongPathFileStream, то можно создавать и открывать файлы используя путь длинной до 32К символов.

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

Для полноты картины нужно бы обернуть еще WinAPI функции для создания и удаления директории, поиск файлов. Но это в другой раз.



Читать дальше...