MCP

воскресенье, 29 декабря 2013 г.

Про Internet Explorer

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

Итак, во всех рекламных статьях про новый браузер Microsoft упорно напирает на скорость и поддержку стандартов. И тут я действительно вижу улучшения. Всё достаточно шустро уже с 9-ой версии (правда, 8-ая была тем ещё тормозным ужасом), и уже можно пользоваться для обычной жизни. Но есть одно но. Имя ему интерфейс.

Судя по всему, интерфейс браузера пишет отдельная команда, и делает это из рук вон плохо. Я уже привык к мелочам, что ошибки от браузера не добьёшся. Только дурацкая страница, что ты дурак и сделал что-то не то. Недавний пример: браузер захотел идти по TLS1.2, а сервер (IIS) отдал ему TLS1.1. В результате — дурацкий текст ошибки. Хорошо, что был хром, который открыл страницу и всё объяснил.

Но у интерфейса есть более серьёзные проблемы. Во-первых, IE ещё ни разу не восстановил корректно вкладки. Постоянно меняется порядок и появляются фантомные дефолтные страницы. Эта катавасия тянется с тех пор, как в нём появились вкладки. Подобных проблем нет ни у одного другого браузера.
Во-вторых, модальные окна и невозможность остановить скрипты, если что-то пошло не так (например, зацикленный alert можно убить или с помощью ловкости или убийством из таск менеджера всех процессов IE — а потому что не найти нужный). Но модальность, на то и модальность, чтобы доводить до белого каления. Представьте, у вас есть сайт, который использует NTLM-авторизацию (или клиентские сертификаты). При его открытии вылезает окно с требованием ввести логин и пароль. Если в это время перейти на другую вкладку, и сделать это в очень правильный момент, можно получить ситуацию, когда браузер упорно не реагирует на команды, ибо я должен сделать что-то в модальном окне, которое спрятано в другой вкладке. Всё, приехали. Task Manager, killall iexplore.exe

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

В-чётвёртых, в IE достаточно неплохие инструменты разработки, но оооочень тормозные. DOM-дерево строится иногда секунд по 10 и требует постоянного ручного рефреша, а другие браузеры не напрягаясь рисуют его динамически сразу.

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

Вчера, последней каплей, заставившей меня написать истеричный вопль была рядовая ситуация: я открыл сайт, открыл инструменты разработчика, включил отладку Javascript, тут браузер упал весь, а после поднятия не осталось ни одной из 15-ти вкладок. Сессия потерялась вовсе, как будто бы её и не было. В результате я, посреди отладки, начал вываливаться из контекста и стал заниматься попытками восстановления нужных вкладок, чтобы потом продолжить работу. Ну вот как с таким можно жить?

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

Забавности с async/await

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

public static void Process()
{
try
{
DoSomething();
}
catch (Exception)
{
}
}

public static async void DoSomething()
{
throw new Exception();
}

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

Подумали? Теперь правильный ответ. Будет необработанный эксепшен и аварийное завершение приложения. Бздынь! Размещаем метод DoSomething где-нибудь подальше и наслаждаемся неожиданными крешами приложения в самый неожиданный момент.

Вот это один из примеров разрыва шаблонов. Чтобы такого не повторилось DoSomething должен возвращать не void а человеческий Task, после этого, чтобы избавиться от предупреждения о некорректном поведении в методе Process надо написать await DoSomething, а чтобы скомпилировалось к Process добавить async. И вот только после этого всё будет работать как и ожидалось.

Приятного кодинга 

суббота, 16 ноября 2013 г.

Windows, делаем бекап самого дорогого

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

Сегодня же, я хочу рассказать, как сделать бекап самого ценного для винды, а именно реестра. Дело в том, что в настоящий момент, практически всё необходимое для жизни винды хранится именно там. Вплоть до того, что если у вас есть сохранённая копия реестра, то вы можете поставить новую систему, и подменить ей реестр. И она заработает как старая, со всеми предыдущими настройками! Конечно, есть некоторые оговорки: повалятся некоторые сервисы и программы, будут проблемы с драйверами. Но эти проблемы можно уже решить в рабочем порядке. Главное — на вас не будет смотреть голая система.

Кому-то такое покажется странным, ведь некоторые люди переставляют систему постоянно, или периодически по настроению, и любят всё настраивать идеально (а через несколько месяцев — повторить). Я же подобное не люблю, и у меня в системе попадаются ошмётки десятилетней давности, просто потому что я привык к своему рабочему инструменту, и не хочу что-то кардинально менять без веской причины и привыкать заново.

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

Где хранится реестр?

Он хранится в нескольких файлах. Во-первых, в папке C:\Windows\System32\config\ Там находятся файлы вида SYSTEM, SOFTWARE, SAM, COMPONENTS, DRIVERS. Это соответствующие ветки реестра. Например, SYSTEM — информация о сервисах и драйверах, SOFTWARE — установленные драйвера, SAM — данные о пользователях с внутренними идентификаторами и паролями (именно данная ветка обычно хорошо так меняется при установке, из-за чего система становится "особенно другой").
Также в этой папке находится системный профиль (профиль компьютера, который с точки зрения системы почти настоящий пользователь), и всякая другая мелочёвка.
При перезагрузке системы копия реестра сохраняется в папке RegBack. Я не очень представляю, когда данные из RegBack могут использоваться автоматически, но в случае проблем можно попытаться подменить файлы реестра из данной папки, чтобы восстановить работоспособность.

Также, в папке с профилем каждого пользователя (C:\Users\username\) находится файл ntuser.dat, являющийся реестром для данного пользователя.

Собственно, эти файлы мы и будем архивировать.

Как сделать архивную копию?

Какзалось бы, просто взять и скопировать. Но не всё так просто. Дело в том, что данные файлы заблокированы системой (справедливости ради, из RegBack можно достать файлы достаточно просто, но они будут не очень свежими в случае отсутствия перезагрузок, да и реестр пользователя не архивируется самостоятельно).

Т.е. с большинством файлов ничего не сделать на рабочей системе. В качестве аварийного решения, можно загрузиться в recovery режиме или с установочного диска/флешки, и вручную заархивировать данные файлы. Но данный вариант обычно используется, когда всё плохо, и данная копия делается при попытке восстановления, на случай, если после восстановления станет ещё хуже и надо будет откатиться.

Мы же не будем искать аварийных путей, и займёмся использованием встроенных средств Windows, а именно Volume Shadow Copy, теневой копией.

При создании теневой копии диска, создаётся снимок всей файловой системы на заданный момент времени, с неизменяемыми файлами. Который можно использовать для чтения заблокированных файлов или использовать в качестве версии файлов на определённый момент времени (так работает System Restore в Windows, она делает примерно тоже самое, в определённых случаях).

Итак, что надо сделать:
  1. Сделать теневую копию диска
  2. Подмонтировать её симлинком для удобства работы
  3. Скопировать нужные файлы
  4. Удалить копию
Всё это можно сделать из консоли, используя стандартные утилиты, я для удобства написал всё это на Powershell.

Итак по пунктам (полный скрипт в конце поста):

Делаем теневую копию и монтируем её:
Function CreateShadow($disk)
{
    # e.g. c: -> c:\
    $disk = $disk[0] + ':\';

    $shadowObj = (gwmi -List Win32_ShadowCopy).Create($disk, "ClientAccessible")
    $currentShadow = gwmi Win32_ShadowCopy | ? { $_.ID -eq $shadowObj.ShadowID }
    $shadowID = $currentShadow.ID
    $name  = $currentShadow.DeviceObject + "\"
    $dummy = cmd /c "mklink /d .\$shadowID $name"
    Write-Host $dummy
    return $shadowID
}

В данном куске скрипта мы через WMI создаём теневую копию, получаем её идентификатор, и монтируем её в качестве симлинка в текущей папке (с названием как идентификатор теневой копии).
Используя данный скрипт уже можно вручную скопировать файлы. Ну или автоматически, примерно так:
Function PerformBackup($shadowPath, $bakRoot)
{
    if ($shadowPath -like '%\')
    {
        $shadowPath = $shadowPath.SubString(0, $shadowPath.Length - 1)
    }

    if ($bakPath -like '%\')
    {
        $bakPath = $bakPath.SubString(0, $bakPath.Length - 1)
    }

    $tmpFile = "$PWD\tmpFileList.txt"

    # if script was incorrectly finished
    if (Test-Path $tmpFile)
    {
        Remove-Item $tmpFile
    }
    Get-ChildItem -Recurse "$($shadowPath)\Windows\System32\Config" -Force -ErrorAction Continue | % { $_.FullName.Remove(0, $shadowPath.Length + 1) | Out-File -FilePath $tmpFile -Encoding ascii -Append }

    Get-ChildItem "$($shadowPath)\Users\" -Directory | % { Get-ChildItem -File -Force "$($_.FullName)\ntuser.*" -ErrorAction SilentlyContinue | % { $_.FullName.Remove(0, $shadowPath.Length + 1) | Out-File -FilePath $tmpFile -Encoding ascii -Append } }

    $archName = Get-Date -Format 'yyyyMMdd'

    # if file exists, using _1, _2, ...
    $archPath = $bakRoot + '\' + $archName;
    $iterator = 1;
    while (Test-Path "$archPath.7z")
    {
        $archPath = $bakRoot + '\' + $archName + '_' + $iterator;
        $iterator++;
    }

    $curPWD = $PWD
    cd $shadowPath
    cmd /c "$($curPWD)\7z.exe a -m0=LZMA2 $archPath.7z @$tmpFile"
    cd $curPWD
    Remove-Item $tmpFile
}

В данном скрипте мы собираем список файлов для архивирования во временный файл, и передаём его архиватору 7z, чтобы он уже самостоятельно проделал всю необходимую работу.

Дальше мы удаляем лишнюю копию:
Function DropShadow($shadowID)
{
    $dummy = cmd /c "rmdir .\$shadowID"
    Write-Host $dummy
    $currentShadow = gwmi Win32_ShadowCopy | ? { $_.ID -eq $shadowID }
    $currentShadow.Delete()
}

И лишние файлы:
Function RemoveOldBackups($bakPath)
{
    dir $bakPath | ? { $_.CreationTime -le $(Get-Date).AddDays(-30) } | Remove-Item
}

Скрипт целиком можно взять тут: backup-registry.7z (вместе с 7zip). В скрипте надо будет поправить необходимые пути и настроить его на периодическое выполнение, например, раз в день (что мелочиться-то?).

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

суббота, 2 ноября 2013 г.

Как добавить клиентам радости

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

Что включает в себя комплект поставки бюджетного телефона? Сам телефон, простенькая гарнитура и зарядник, несколько бумажек. Всё. У более дорогих обычно всё тоже самое, может лишь отличие в том, что качество гарнитуры и зарядника чуть выше (да и то не всегда).

Что сделала Digma:
  • Положила в комплект симпатичный рыжий бампер, т.е. телефон сразу начинает привлекать внимание, за счёт яркого позитивного цвета, и приносит радость владельцу
  • симпатичный USB-зарядник с подсветкой порта. Мелочь, но приятно искать в темноте под столом разъём
  • Тряпочка для протирки 
  • Защитная плёнка на экран
  • При этом транспортировочная плёнка — прозрачная, без надписей. Т.е. можно для начала тестировать телефон не снимая её, не пытаясь прочесть информацию за огромными ненужными буквами
Т.е. компания потратила чуть больше денег, но впечатление от неё у покупателя осталось весьма позитивным, что с учётом толпы псевдопроизводителей телефонов на российском рынке (Keneksi, Highscreen, Texet, Explay, Fly), возможно что-то стоит. 

PS: Может показаться что статья рекламная, но тут только личные впечатления, телефон был выбран по суммарным показателям, а дополнения в коробке оказались приятным бонусом.

суббота, 12 октября 2013 г.

Впечатления от Windows 8.1

До выхода Windows 8.1 всё меньше и меньше времени, и до того момента, как на людей польётся тонна информации об "уникальных" и "удобнейших" нововведениях, хочу поделиться тем, что я увидел в этой системе нового, приятного и удобного:

НИЧЕГО!

Да, именно так. Абсолютно ничего не изменилось. Да, сломался firewall от Comodo и упорно отказывался ставиться. Заменил на другой. Да, тема из 8 Preview сломалась, починил, отвалившуюся спячку почил, неприлично долгую загрузку-как нибудь починю, остальные мелочи несущественны.

И в общем-то всё, передо мной та же самая Windows 8. Конечно, можно посмотреть в свойства системы и узнать, но это очевидно, не за этим делаются изменения.

Можете спросить про кнопку Пуск, она ведь появилась. Да, появилась, но подобное убожество я писал самостоятельно в июле прошлого года. Да, у меня менее красиво вышло, но я ведь и не рассказываю всем окружающим с помпой про мегафичу? В общем, вместо стандартного убожества из Win8.1 и ничего из Win8, у меня сейчас стоит StartIsBack, и я чувствую себя человеком.

Также теперь есть изменения в Metro Modern UI поведении. Теперь приложения при установке не попадают на главный экран. Ну что же, прорвёмся, можно открыть все программы, и найти только что скачанную игрушку. Или не находить, всё равно они скучные.

Есть какие-то копеечные изменения в стандартных Modern'овых приложениях, но это вообще-то должен быть обычный процесс разработки, не привязанный к новой версии винды.

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

Итак, моё мнение: обновление бесплатно, так что хуже не будет, обновляйтесь. Если вы до сих пор сидите на семёрке, ничего кардинально не изменится, не обновляйтесь, продолжайте сидеть и радоваться жизни от удобной системы.

PS: Надо отдать должное, есть 2 действительно новые фичи в Windows Server 2012 R2, но это не клиентская система, кроме того, про них уже прожужжали все уши интересующимся.

суббота, 5 октября 2013 г.

Тема из Windows 8 Release Preview в Windows 8.1

Данный пост является продолжением прошлогоднего: Смена темы в Windows 8 на тему из Release Preview. Как результат, контекст обсуждения и общие моменты по установке смотрите в прошлом посте.

Итак, совсем скоро Microsoft выпустит Windows 8.1, в которой будет 100500 улучшений, всё будет быстрее, лучше и красивее (по секрету скажу, что ничего стоящего не замечено).

Но Microsoft не был бы Microsoft'ом, если бы в новой версии что-нибудь бы не поломал. В частности, замечательная тема из Windows 8 RP, теперь выглядит гораздо хуже. Вот вам картинка для понимания процесса:



Если у кого плохо с монитором или внимательностью, то проблема в том, что вокруг заголовка появился прямоугольник слегка другого оттенка чем основной заголовок. Что не фатально, но раздражает.

После некоторого времени, потраченного на иследования я выяснил, что тут сложились два фактора:

  • Изменился рендеринг текста, стал ещё более простым (те. убрано свечение текста там, где оно не включено специально)
  • Тема от Windows 8 RP имеет в качестве цвета заголовка практически (но не полностью!) прозрачную рамку.
Т.е. получается, что из-за того что текст заголовка окна рисуется на фоне заданного в настройках персонализации цвета, а сам заголовок тем же цветом, но из-за неполной прозрачности чуть-чуть другого оттенка. И получается такая фигня.

Собственно, проблема есть, пишу решения:
Решение №1 использовать программу  Aero Glass for Windows 8, которая сама добавит свечение вокруг букв и решит проблему. Единственная проблема: заставить программу работать по-человечески, та ещё задача. 

Решение №2 поправить тему, заменив почти прозрачность на полную прозрачность, а так как мы сломаем оригинальную тему, то сломать дополнительно проверку подписи для тем.
Вторая проблема решается давно и неплохо (ибо люди любят кастомные темы).

Самый простой способ сломать проверку через UxStyle. Это небольшой сервис, котороый не меняет системные файлы, а проводит изменения в памяти. Как результат, можно подклаывать любую тему с deviantART. Единственная проблема, пока нельзя скачать программу для 8.1, ибо она была выпущена, но из-за какого-то критичного бага убрана. Так что ждём фикса бага и обновления.

Ну а саму тему из Win 8 RP, я уже поправил, и выкладываю здесь, чтобы можно было забрать и пользоваться:

А подробности по установке в прошлом посте.


суббота, 21 сентября 2013 г.

Маленький хинт для программистов

Иногда бывают ситуации, когда вам нужно что-то удалённо сделать с вашим домашним компьютером. Часто что-то стандартное, а под рукой только телефон.

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

  1. Поднять web-сайт на своём компьютере (IIS или node.js — не важно, что больше нравится)
  2. Выставить его в интернет (Dynamic DNS в помощь)
  3. Ограничить к нему доступ авторизацией или сертификатами
  4. Для стандартных задач сделать url'ы, выполняющие определённые команды (лучше внешние батники, чтобы их лего можно было менять)
  5. Если нужно что-то сложное, то можно уже сделать интерфейс для выбора или ввода команды вручную
  6. Открыть с телефона в браузере полученную ссылку
В общем-то и всё. Ключевое, конечно, в данном случае - ограничить доступ, иначе вы открываете огромную дыру себе в компьютер, остальное дело 10 минут. Код, выполяющий определённую команду на ASP.NET выглядит просто и банально:

var runCmd = ConfigurationManager.AppSettings["runCmd"];
var processStartInfo = new ProcessStartInfo
{
FileName = runCmd,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false
};
var process = Process.Start(processStartInfo);
var output = process.StandardOutput.ReadToEnd();
var error = process.StandardError.ReadToEnd();
process.WaitForExit();
lbOutput.Text = output + error;

При этом я уверен, что есть уже готовые внешние решения и всякие saas'ы, но ведь самому написать интереснее? 

Зачем я написал эту банальность? А дело в том, что практика показывает, что иногда простые и очевидные решения в голову приходят в самый последний момент (даже для программистов), и человек гораздо вероятнее будет гуглить computer management for android, когда у него всего-лишь одна задча: выполнить ping www.ru и поглядеть на получившийся результат. 

воскресенье, 25 августа 2013 г.

Деплой различных по стабильности версий продукта

Сейчас стало довольно модным использовать следующую модель версий продуктов (Google первый начал, остальные подхватили):
  • Developer — фактически транк, в котором тестируются новые фичи, стабильность не гарантируется и даже декларируется что её может не быть
  • Beta — фичи заморожены, идёт багфиксинг. Стабильность повышается со временем
  • Stable — полностью стабильная версия, обновления выходят только в случае обнаружения серьёзных проблем.
Кроме этого, обычно версионирование идёт по +1, т.е. Stable имеет версию n, Beta — n+1, Developer — n+2.
Когда Beta полностью оттестирована, она переходит в Stable и все версии щелкаются на единичку.

В общем, это всё всем понятно и известно. Но хочу поговорить о таком моменте: в любой момент времени свежесть версий должна идти в этом же порядке. Т.е. даже если сейчас идёт подготовка к релизу стабильной версии, в Beta и Developer должно быть по крайней мере тоже самое. Очевидно? Вроде бы да. Но я сейчас вижу перед глазами свежую Оперу, у которой на момент написания поста самая свежая версия это Beta (Next), а Developer, гораздо древнее. Т.е. явное нарушение концепции. Я не знаю точно, почему так происходит, но могу предположить, что у них одна команда, которая сегодня занимается исследованиями, а завтра в поте лица правит баги. Ну и чего они хотели добиться, внедряя технологию, которую сами же и не смогли осилить?

К чему я клоню? Нравится идея, выглядит интересной, а реализовать её в конкретных ваших условиях можно? Если нельзя, то и не стоит браться, а то всё начнёт превращаться в фарс. Как результат, ничего хорошего не происходит, а все начнут работать на концепцию, тратя драгоценные ресурсы. Или же, если тратить будет лень, сама концепция честно пойдёт лесом, и от неё останется только номинальное название. 

суббота, 17 августа 2013 г.

Методы и производительность динамического создания конструкторов

В .NET достаточно развитые средства рефлексии, и как результат этим активно пользуются. При этом вариантов и способов использования достаточно много. Эти способы отличаются в зависимости от того что надо: по типу действий (создать объект, вызвать его метод, получить/изменить значение поля), по информации об объекте (мы заранее знаем сигнатуру или не знаем), по типу доступа (публичные/приватные методы), по наличию параметров.

Я как-то делал доклад по этим технологиям на YaNA, но с тех пор прошло много времени, обнился Framework, и в моей жизни количество кода с рефлексией значительно возросло, поэтому я решил обновить знания, провести тесты и поделиться результатами.

Сегодня я рассмотрю достаточно простой способ: вызов конкструктора без параметров и с известными параметрами. Это достаточно часто встречающаяся задача, например, она может использоваться для создания плагинов и модулей. При этом мы знаем, что объект имеет определённый тип (отнаследован от интерфейса, например), так что в дальнейшем мы можем вызывать методы напрямую. Но тем не менее, мы не можем напрямую создать объект, т.к. у нас есть только информация о типе (Type), но изначально мы просто не можем написать new MyObject() — данные придут позднее.

Соответственно есть несколько путей решений:

Activator.CreateInstance

Самый простой и удобный метод создания объекта:
Activator.CreateInstance(myObjectType)

Также можно передать параметры для конструктора. Всё просто и очевидно.

ConstructorInfo.Invoke

"Классический рефлекшен". 
var constructorInfo = myObjectType.GetConstructor(new Type[0]);
constructorInfo.Invoke(new object[0]);

Т.е. мы получаем объект ConstructorInfo и через рефлексию создаём объект. В данном примере нет параметров, но их легко можно передать массивом объектов

Compiled Expression

Ещё один сравнительно новый вариант создания объекта, который заключается в построении Expression'а и дальнейшей компиляции его в делегат (анонимную функцию). Способ создания без параметров выглядит так:
var d = Expression.Lambda<Func<object>>(Expression.New(constructorInfo)).Compile();

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

Если конструктор имеет параметры и нам точно известны, какие они должны быть, мы можем использовать подобный код:
var p1 = Expression.Parameter(typeof(int));
var p2 = Expression.Parameter(typeof(string));
var newExpression = Expression.New(constructorInfo, p1, p2);
var d = Expression.Lambda<Func<int, string, object>>(newExpression, true, p1, p2).Compile();

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

Compiled Expression with Any Parameters

Чтобы избавиться от этой форы и рассмотреть случаи, когда мы заранее не можем знать параметры, надо сгенерировать что-то общее. Логично будет рассмотреть случай, когда у нас есть делегат, принимающий массив параметров типа object, при этом данный делегат внутри сам проводит преобразования для корректного вызова. Код будет выглядеть примерно так:

var argsExpr = Expression.Parameter(typeof(object[]));
var constructorParams = constructorInfo.GetParameters()
.Select((p, i) => Expression.Convert(Expression.ArrayAccess(argsExpr, Expression.Constant(i)), p.ParameterType));
var newExpr = Expression.New(constructorInfo, constructorParams);
var d = Expression.Lambda<Func<object[], object>>(newExpr, true, argsExpr).Compile();

Т.е. мы передаём массив, в котором находятся аргументы, потом из этого массива достаём параметры для конструктора, конвертируем к нужному типу и вызываем конструктор.

Dynamic Method

Самый сложный метод, который заключается в использовании DynamicMethod и ручной генерации MSIL'а. Поскольку решение в отличие от остальных не совсем тривиальное, детальное описание реализации оставлю для следующей статьи (будет ссылка, когда статья будет написана). Мы его также как и Expression можем сделать специфичным, или обобщённым. У обобщённого основная идея та же, что и в предыдущем методе: мы генерируем делегат, принимающий массив object'ов, внутри приводим аргументы к нужному типу и вызываем конструктор. Только делаем всё это нативно, генерируя MSIL и компилируя его.

Результаты производительности

Мерять тут можно много чего. Но я остановился на следующих условиях:
  • Только публичные конкструкторы (приватные значительно дольше, а необходимость в их использовании весьма специфичная)
  • Время подготовки нужного объекта не замеряется. Подразумевается, что если уж есть цель оптимизации, то этим временем можно пренебречь. Хотя оно может и вносить ощутимые задержки на первый вызов. Если для вас это будет существенно, наверное стоит провести замеры на ваших условиях и железе.
  • Объекты очень простые но не совершенно пустые (сложность этих объектов может сильно повлиять на расстановку сил, нивелировав или максимально усилив различия между методами
  • Проверяется Debug и Release режим, чтобы выяснить ускорение, получаемое кодом от оптимизации
  • Конкретные цифры я не покажу, ибо это сильно зависит от железа. Только отношение
  • Также для сравнения добавлен прямой вызов конструктора 

Вызов конструктора без параметров

reflection constructor performance

По графику можно сделать простой вывод: если скорость не очень критична, используйте Activator, а если объектов нужно создавать очень много, то Expression. И да, Release режим в некоторых случаях рулит. Рефлекшен через ConstructorInfo самый медленный. Если у вас есть желание, можете использовать DynamicMethod, он самый быстрый.

Вызов конструктора с параметрами

Я взял конструктор с двумя параметрами int и string, чтобы было заметно влияние боксинга. Результаты получились следующие:
reflection constructor performance

Те же графики, но без активатора, чтобы был лучше виден масштаб:
reflection constructor performance


В данном случае Activator показал дичайшие тормоза, что очень удивительно, относительно предыдущего варианта. Обобщённый экспрешшен в 2 раза медленнее специализированного (хоть на графике это плохо видно) и в 4-6 раз медленнее прямого вызова (для сравнения скорости). Т.е. вывод тут такой — используйте скомпилированные экспрешшены, по-возможности с явными типами, если нет, то с обобщёнными, потери не очень сильные. При этом обобщённый DynamicMethod быстрее обобщённого Expresssion'а, но медленнее типизированного. Т.е. если хочется максимальной скорости, придётся относительно много писать, чтобы сделать максимально типизированный и производительный DynamicMethod.

суббота, 29 июня 2013 г.

Эксепшены в Task'ах

Сегодня расскажу о замечательных индусах из Microsoft, которые вначале делают, а потом думают.

Конкретно речь пойдёт про Таски. Не буду про них расписывать, ибо не это тема сегодняшнего поста. Вкратце, это такие треды на стероидах с вкусными и удобными фичам.

Одна из фич, это проброс эксепшенов наружу. Примерно так:

var t = new Task(() => { throw new Exception(); });
t.Start();
// do something
try
{
 t.Wait();
}
catch (AggregateException)
{
 Console.WriteLine("Exception occurred");
 throw;
}

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

Но есть маленький нюанс. Эксепшен ведь пробрасывается, когда от таска что-то ждут (результат, или просто завершение). В стандартной терминологии это называется observable. Т.е. таск должен быть наблюдаемым. А что если мы запустили и забыли (или забили)?

new Task(() => { throw new Exception(); }).Start();

Получается, что эксепшен куда-то потеряется? Как бы не так! Доблестные спецы из Microsoft подумали, и придумали бросать исключение в деструкторе. Оцените изящество этого решения: нарушает все вменяемые стандарты кодирования, приводит к падению приложения, и выдаёт ошибки в то время и в том месте, где их отловить невозможно вообще. Супер!

Наверное, они решили, что раз исключение в обычном треде вызывает падение приложения, то и тут надо. Только забыли про это маленькое отличие с исключениями: если мы хотим сделать безопасным обычный тред, нам достаточно написать try/catch и все станут счастливы (всё равно никто за нас ошибки не обработает). А случае с тасками, кидать исключение изнутри наружу можно и местами иногда полезно. Но получается, что за этими тасками приходится следить как за маленькими детьми, хотя нам вполне может быть глубоко наплевать в данном месте на результат конкретной операции.

Решений данной проблемы в принципе можно придумать несколько (различные врапперы, следить за использованием тасков), одно из достаточно красивых такое:

public static Task IgnoreErrors(this Task t)
{
 t.ContinueWith(x => { var e = x.Exception; }, TaskContinuationOptions.OnlyOnFaulted);
 return t;
}

private static void DoTask()
{
 new Task(() => { throw new Exception(); }).IgnoreErrors().Start();
}

Т.е. мы создаём extension-метод, который прикрепляет к нашей таске продолжение (continuation), в котором мы берём значение свойства Exception (это обязательно надо сделать! именно так таска становится опять наблюдаемой (observable)), и на этом успокаиваем систему.

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

Бонус

Я вначале не зря писал, что в Microsoft вначале сделали, а потом начали думать. А подумав, они поняли, что поведение уж больно идиотское и убивать всё приложение никому не нужно. И в 4.5 изменили логику, так что для 4.5 уже мой пост неактуален.

Но без нюансов у Microsoft не обходится, ибо 4.5 по факту подменяет собой 4.0. Сюрприз! Т.е. установив 4.5, все приложения, написанные под 4.0 начинают по факту работать на 4.5, и слегка по-другому. Например, в данном случае, ошибка уже не появится (попадаются и некоторые другие различия в поведении, но это уж слишком выраженное). Вернуть старое поведение можно конфигом, но кто же заморачивается чтением всяких безумных и бесполезных настроек?

Т.е. вы как разработчик поставили все апдейты, написали приложение, протестировали в хвост и в гриву, а падает оно только у клиента. Ибо у клиента стоит Windows Server 2003, и ваше приложение должно с ним работать, и даже работает, только падает. Иногда. С непонятным стэктрейсом. И вы пытаетесь найти хвосты и поправить всё это безобразие.

Иногда просто хочется убиться головой об стену от таких гениальных поворотов сюжета.

вторник, 28 мая 2013 г.

Яндекс, ты пьян, иди домой!

Не силён в географии, и не факт что я ниже не займусь полным бредом, но Яндекс Карты взрывают мозг.
Вот кусок схемы дорог:

Это трасса М8, которая судя по схеме идёт через Лисавы, Тириброво и далее. Вроде всё в порядке.
Увеличиваем схему на один шаг.

Видно разницу? Если сразу не заметили, то распишу: через Лисавы и Тириброво уже идёт Ярославское шоссе, а трасса М8 неожиданно оказывается южнее.

WAT????
Т.е. на разных масштабах у Яндекса разные картографические данные. Как так?

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

Ну и как можно доверять яндесу в прокладке маршутов? При этом, что удивительно, но у Гугла всё гораздо лучше.

понедельник, 27 мая 2013 г.

Цифровая подпись больших файлов на .NET в потоковом режиме

Стандартный механизм в .NET по подписи через SignedCms, весьма симпатичный, но есть одна проблема: для подписи необходимо передать массив байт, т.е. если надо подписать большой файл, то вначале прочитать в память в .NET потом засунуть этот же объём системному API, что весьма затратно по памяти.

Конечно, в CryptoPro есть замечательный метод SignHash, который позволяет заранее посчитать хеш и его подписать (что идентично подписи самого файла), но это только у CryptoPro, стандартное API даже на низком уровне не позволяет провести такое.

Гугление выдало код, который может шифровать в потоковом режиме, но у него обнаружилось несколько проблем:

  • Почему-то используется FileStream, хотя достаточно просто Stream
  • Реально всё равно читается всё содержимое (!) а потом подписывается в потоковом режиме
  • Нет поддержки Detached подписи, хотя добавить её несложно
  • Внаглую из сертификата берётся приватный ключ, который опять же считается RSA (наивные! совершенно не знают что в России творится)
В общем, я поправил этот код, и выкладываю его здесь, ибо взял из блога исходники, надо их таким же образом вернуть, чтобы людям было приятно.

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

StreamCms.cs
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Runtime.InteropServices;
using System.ComponentModel;

namespace NS1
{
 [ComVisible(false)]
 public class StreamCms
 {
  // File stream to use in callback function
  private Stream _callbackFile;

  // Streaming callback function for encoding
  private bool StreamOutputCallback(IntPtr pvArg, IntPtr pbData, int cbData, bool fFinal)
  {
   if (cbData == 0) return true;
   // Write all bytes to encoded file
   var bytes = new byte[cbData];
   Marshal.Copy(pbData, bytes, 0, cbData);
   _callbackFile.Write(bytes, 0, cbData);

   if (fFinal)
   {
    // This is the last piece. Close the file
    _callbackFile.Flush();
    _callbackFile.Close();
    _callbackFile = null;
   }

   return true;
  }

  // Encode StreamCms with streaming to support large data
  public void Encode(X509Certificate2 cert, Stream inFile, Stream outFile, bool isDetached)
  {
   // Variables

   IntPtr hProv = IntPtr.Zero;
   IntPtr SignerInfoPtr = IntPtr.Zero;
   IntPtr CertBlobsPtr = IntPtr.Zero;
   IntPtr hMsg = IntPtr.Zero;

   try
   {
    // Prepare stream for encoded info
    _callbackFile = outFile;

    // Get cert chain
    var chain = new X509Chain();
    chain.Build(cert);
    var chainElements = new X509ChainElement[chain.ChainElements.Count];
    chain.ChainElements.CopyTo(chainElements, 0);

    // Get certs in chain
    var certs = new X509Certificate2[chainElements.Length];
    for (int i = 0; i < chainElements.Length; i++)
    {
     certs[i] = chainElements[i].Certificate;
    }

    // Get context of all certs in chain
    var CertContexts = new Win32.CERT_CONTEXT[certs.Length];
    for (int i = 0; i < certs.Length; i++)
    {
     CertContexts[i] = (Win32.CERT_CONTEXT)Marshal.PtrToStructure(certs[i].Handle, typeof(Win32.CERT_CONTEXT));
    }

    // Get cert blob of all certs
    var CertBlobs = new Win32.BLOB[CertContexts.Length];
    for (int i = 0; i < CertContexts.Length; i++)
    {
     CertBlobs[i].cbData = CertContexts[i].cbCertEncoded;
     CertBlobs[i].pbData = CertContexts[i].pbCertEncoded;
    }

    // Get CSP of client certificate

    Win32.CRYPT_KEY_PROV_INFO csp;
    GetPrivateKeyInfo(GetCertContext(cert), out csp);

    bool bResult = Win32.CryptAcquireContext(
     ref hProv,
     csp.pwszContainerName,
     csp.pwszProvName,
     (int)csp.dwProvType,
     0);
    if (!bResult)
    {
     throw new Exception("CryptAcquireContext error #" + Marshal.GetLastWin32Error().ToString(), new Win32Exception(Marshal.GetLastWin32Error()));
    }

    // Populate Signer Info struct
    var SignerInfo = new Win32.CMSG_SIGNER_ENCODE_INFO();
    SignerInfo.cbSize = Marshal.SizeOf(SignerInfo);
    SignerInfo.pCertInfo = CertContexts[0].pCertInfo;
    SignerInfo.hCryptProvOrhNCryptKey = hProv;
    SignerInfo.dwKeySpec = (int)csp.dwKeySpec;
    SignerInfo.HashAlgorithm.pszObjId = cert.SignatureAlgorithm.Value; // Win32.szOID_OIWSEC_sha1;

    // Populate Signed Info struct
    var SignedInfo = new Win32.CMSG_SIGNED_ENCODE_INFO();
    SignedInfo.cbSize = Marshal.SizeOf(SignedInfo);

    SignedInfo.cSigners = 1;
    SignerInfoPtr = Marshal.AllocHGlobal(Marshal.SizeOf(SignerInfo));
    Marshal.StructureToPtr(SignerInfo, SignerInfoPtr, false);
    SignedInfo.rgSigners = SignerInfoPtr;

    SignedInfo.cCertEncoded = CertBlobs.Length;
    CertBlobsPtr = Marshal.AllocHGlobal(Marshal.SizeOf(CertBlobs[0]) * CertBlobs.Length);
    for (int i = 0; i < CertBlobs.Length; i++)
    {
     Marshal.StructureToPtr(CertBlobs[i], new IntPtr(CertBlobsPtr.ToInt64() + (Marshal.SizeOf(CertBlobs[i]) * i)), false);
    }

    SignedInfo.rgCertEncoded = CertBlobsPtr;

    // Populate Stream Info struct
    var StreamInfo = new Win32.CMSG_STREAM_INFO
                      {
                       cbContent = (int)inFile.Length,
                       pfnStreamOutput = StreamOutputCallback
                      };

    // Open message to encode
    hMsg = Win32.CryptMsgOpenToEncode(
     Win32.X509_ASN_ENCODING | Win32.PKCS_7_ASN_ENCODING,
     isDetached ? Win32.CMSG_DETACHED_FLAG : 0,
     Win32.CMSG_SIGNED,
     ref SignedInfo,
     null,
     ref StreamInfo);

    if (hMsg.Equals(IntPtr.Zero))
    {
     throw new Exception("CryptMsgOpenToEncode error #" + Marshal.GetLastWin32Error().ToString(), new Win32Exception(Marshal.GetLastWin32Error()));
    }

    // Process the whole message
    ProcessMessage(hMsg, inFile);
   }
   finally
   {
    // Clean up

    if (inFile != null)
    {
     inFile.Close();
    }

    if (_callbackFile != null)
    {
     _callbackFile.Close();
    }

    if (!CertBlobsPtr.Equals(IntPtr.Zero))
    {
     Marshal.FreeHGlobal(CertBlobsPtr);
    }

    if (!SignerInfoPtr.Equals(IntPtr.Zero))
    {
     Marshal.FreeHGlobal(SignerInfoPtr);
    }

    if (!hProv.Equals(IntPtr.Zero))
    {
     Win32.CryptReleaseContext(hProv, 0);
    }

    if (!hMsg.Equals(IntPtr.Zero))
    {
     Win32.CryptMsgClose(hMsg);
    }
   }
  }

  // Decode StreamCms with streaming to support large data
  public void Decode(Stream dataFile, Stream signFile, Stream outFile, bool isDetached)
  {
   // Variables

   IntPtr hMsg = IntPtr.Zero;
   IntPtr pSignerCertInfo = IntPtr.Zero;
   IntPtr pSignerCertContext = IntPtr.Zero;
   IntPtr hStore = IntPtr.Zero;

   try
   {
    // Get data to decode

    // Prepare stream for decoded info
    _callbackFile = outFile;

    // Populate Stream Info struct
    var StreamInfo = new Win32.CMSG_STREAM_INFO
                      {
                       cbContent = (int)signFile.Length,
                       pfnStreamOutput = StreamOutputCallback
                      };

    // Open message to decode
    hMsg = Win32.CryptMsgOpenToDecode(
     Win32.X509_ASN_ENCODING | Win32.PKCS_7_ASN_ENCODING,
     isDetached ? Win32.CMSG_DETACHED_FLAG : 0,
     0,
     IntPtr.Zero,
     IntPtr.Zero,
     ref StreamInfo);
    if (hMsg.Equals(IntPtr.Zero))
    {
     throw new Exception("CryptMsgOpenToDecode error #" + Marshal.GetLastWin32Error().ToString(), new Win32Exception(Marshal.GetLastWin32Error()));
    }

    // Process the whole message
    if (isDetached)
    {
     ProcessMessage(hMsg, signFile);
     ProcessMessage(hMsg, dataFile);
    }
    else
    {
     ProcessMessage(hMsg, signFile);
    }

    // Get signer certificate info
    int cbSignerCertInfo = 0;
    bool bResult = Win32.CryptMsgGetParam(
     hMsg,
     Win32.CMSG_SIGNER_CERT_INFO_PARAM,
     0,
     IntPtr.Zero,
     ref cbSignerCertInfo);
    if (!bResult)
    {
     throw new Exception("CryptMsgGetParam error #" + Marshal.GetLastWin32Error().ToString(), new Win32Exception(Marshal.GetLastWin32Error()));
    }

    pSignerCertInfo = Marshal.AllocHGlobal(cbSignerCertInfo);

    bResult = Win32.CryptMsgGetParam(
     hMsg,
     Win32.CMSG_SIGNER_CERT_INFO_PARAM,
     0,
     pSignerCertInfo,
     ref cbSignerCertInfo);
    if (!bResult)
    {
     throw new Exception("CryptMsgGetParam error #" + Marshal.GetLastWin32Error().ToString(), new Win32Exception(Marshal.GetLastWin32Error()));
    }

    // Open a cert store in memory with the certs from the message
    hStore = Win32.CertOpenStore(
     Win32.CERT_STORE_PROV_MSG,
     Win32.X509_ASN_ENCODING | Win32.PKCS_7_ASN_ENCODING,
     IntPtr.Zero,
     0,
     hMsg);
    if (hStore.Equals(IntPtr.Zero))
    {
     throw new Exception("CertOpenStore error #" + Marshal.GetLastWin32Error().ToString(), new Win32Exception(Marshal.GetLastWin32Error()));
    }

    // Find the signer's cert in the store
    pSignerCertContext = Win32.CertGetSubjectCertificateFromStore(
     hStore,
     Win32.X509_ASN_ENCODING | Win32.PKCS_7_ASN_ENCODING,
     pSignerCertInfo);
    if (pSignerCertContext.Equals(IntPtr.Zero))
    {
     throw new Exception("CertGetSubjectCertificateFromStore error #" + Marshal.GetLastWin32Error().ToString(), new Win32Exception(Marshal.GetLastWin32Error()));
    }

    // Set message for verifying
    var SignerCertContext = (Win32.CERT_CONTEXT)Marshal.PtrToStructure(pSignerCertContext, typeof(Win32.CERT_CONTEXT));
    bResult = Win32.CryptMsgControl(
     hMsg,
     0,
     Win32.CMSG_CTRL_VERIFY_SIGNATURE,
     SignerCertContext.pCertInfo);
    if (!bResult)
    {
     throw new Exception("CryptMsgControl error #" + Marshal.GetLastWin32Error().ToString(), new Win32Exception(Marshal.GetLastWin32Error()));
    }
   }
   finally
   {
    // Clean up
    if (!pSignerCertContext.Equals(IntPtr.Zero))
    {
     Win32.CertFreeCertificateContext(pSignerCertContext);
    }

    if (!pSignerCertInfo.Equals(IntPtr.Zero))
    {
     Marshal.FreeHGlobal(pSignerCertInfo);
    }

    if (!hStore.Equals(IntPtr.Zero))
    {
     Win32.CertCloseStore(hStore, Win32.CERT_CLOSE_STORE_FORCE_FLAG);
    }

    if (dataFile != null)
    {
     dataFile.Close();
    }

    if (signFile != null)
    {
     signFile.Close();
    }

    if (_callbackFile != null)
    {
     _callbackFile.Close();
    }

    if (!hMsg.Equals(IntPtr.Zero))
    {
     Win32.CryptMsgClose(hMsg);
    }
   }
  }

  private void ProcessMessage(IntPtr hMsg, Stream dataStream)
  {
   long streamSize = dataStream.Length;
   if (streamSize == 0)
    throw new CryptographicException("Cannot encode zero length data");
   var gchandle = new GCHandle();
   const int ChunkSize = 1024 * 1024;
   var dwSize = (int)((streamSize < ChunkSize) ? streamSize : ChunkSize);
   var pbData = new byte[dwSize];

   try
   {
    var dwRemaining = streamSize;
    gchandle = GCHandle.Alloc(pbData, GCHandleType.Pinned);
    var pbPtr = gchandle.AddrOfPinnedObject();

    while (dwRemaining > 0)
    {
     dataStream.Read(pbData, 0, dwSize);

     // Update message piece by piece     
     var bResult = Win32.CryptMsgUpdate(hMsg, pbPtr, dwSize, (dwRemaining <= dwSize));
     if (!bResult)
     {
      throw new Exception(
       "CryptMsgUpdate error #" + Marshal.GetLastWin32Error().ToString(),
       new Win32Exception(Marshal.GetLastWin32Error()));
     }

     dwRemaining -= dwSize;
     if (dwRemaining < dwSize)
      dwSize = (int)dwRemaining;

     // if (gchandle.IsAllocated)
     //  gchandle.Free();
    }
   }
   finally
   {
    if (gchandle.IsAllocated)
    {
     gchandle.Free();
    }
   }
  }

  internal static Win32.CertHandle GetCertContext(X509Certificate2 certificate)
  {
   var handle = Win32.CertDuplicateCertificateContext(certificate.Handle);
   GC.KeepAlive(certificate);
   return handle;
  }

  internal static bool GetPrivateKeyInfo(Win32.CertHandle safeCertContext, out Win32.CRYPT_KEY_PROV_INFO parameters)
  {
   parameters = new Win32.CRYPT_KEY_PROV_INFO();
   var invalidHandle = new Win32.SafeHandle(IntPtr.Zero);
   uint pcbData = 0;
   if (!Win32.CertGetCertificateContextProperty(safeCertContext, 2, invalidHandle.DangerousGetHandle(), ref pcbData))
   {
    if (Marshal.GetLastWin32Error() != -2146885628)
    {
     throw new CryptographicException(Marshal.GetLastWin32Error());
    }

    return false;
   }

   invalidHandle = Win32.LocalAlloc(0, new IntPtr(pcbData));
   if (!Win32.CertGetCertificateContextProperty(safeCertContext, 2, invalidHandle.DangerousGetHandle(), ref pcbData))
   {
    if (Marshal.GetLastWin32Error() != -2146885628)
    {
     throw new CryptographicException(Marshal.GetLastWin32Error());
    }

    return false;
   }

   parameters = (Win32.CRYPT_KEY_PROV_INFO)Marshal.PtrToStructure(invalidHandle.DangerousGetHandle(), typeof(Win32.CRYPT_KEY_PROV_INFO));
   invalidHandle.Dispose();
   return true;
  }
 }
}
Win32.cs
using System;
using System.Runtime.InteropServices;

using Microsoft.Win32.SafeHandles;

namespace NS1
{
 [ComVisible(false)]
 public class Win32
 {
  #region "CONSTS"

  public const int X509_ASN_ENCODING = 0x00000001;

  public const int PKCS_7_ASN_ENCODING = 0x00010000;

  public const int CMSG_SIGNED = 2;

  public const int CMSG_DETACHED_FLAG = 0x00000004;

  public const int AT_KEYEXCHANGE = 1;

  public const int AT_SIGNATURE = 2;

  // public const string szOID_OIWSEC_sha1 = "1.3.14.3.2.26";

  public const int CMSG_CTRL_VERIFY_SIGNATURE = 1;

  public const int CMSG_CERT_PARAM = 12;

  public const int CMSG_SIGNER_CERT_INFO_PARAM = 7;

  public const int CERT_STORE_PROV_MSG = 1;

  public const int CERT_CLOSE_STORE_FORCE_FLAG = 1;

  #endregion

  #region "STRUCTS"

  [StructLayout(LayoutKind.Sequential)]
  [ComVisible(false)]
  public struct CRYPT_ALGORITHM_IDENTIFIER
  {
   public string pszObjId;
   public BLOB Parameters;
  }

  [StructLayout(LayoutKind.Sequential)]
  [ComVisible(false)]
  public struct CERT_ID
  {
   public int dwIdChoice;
   public BLOB IssuerSerialNumberOrKeyIdOrHashId;
  }

  [StructLayout(LayoutKind.Sequential)]
  [ComVisible(false)]
  public struct CMSG_SIGNER_ENCODE_INFO
  {
   public int cbSize;
   public IntPtr pCertInfo;
   public IntPtr hCryptProvOrhNCryptKey;
   public int dwKeySpec;
   public CRYPT_ALGORITHM_IDENTIFIER HashAlgorithm;
   public IntPtr pvHashAuxInfo;
   public int cAuthAttr;
   public IntPtr rgAuthAttr;
   public int cUnauthAttr;
   public IntPtr rgUnauthAttr;
   public CERT_ID SignerId;
   public CRYPT_ALGORITHM_IDENTIFIER HashEncryptionAlgorithm;
   public IntPtr pvHashEncryptionAuxInfo;
  }

  [StructLayout(LayoutKind.Sequential)]
  [ComVisible(false)]
  public struct CERT_CONTEXT
  {
   public int dwCertEncodingType;
   public IntPtr pbCertEncoded;
   public int cbCertEncoded;
   public IntPtr pCertInfo;
   public IntPtr hCertStore;
  }

  [StructLayout(LayoutKind.Sequential)]
  [ComVisible(false)]
  public struct BLOB
  {
   public int cbData;
   public IntPtr pbData;
  }

  [StructLayout(LayoutKind.Sequential)]
  [ComVisible(false)]
  public struct CMSG_SIGNED_ENCODE_INFO
  {
   public int cbSize;
   public int cSigners;
   public IntPtr rgSigners;
   public int cCertEncoded;
   public IntPtr rgCertEncoded;
   public int cCrlEncoded;
   public IntPtr rgCrlEncoded;
   public int cAttrCertEncoded;
   public IntPtr rgAttrCertEncoded;
  }

  [StructLayout(LayoutKind.Sequential)]
  [ComVisible(false)]
  public struct CMSG_STREAM_INFO
  {
   public int cbContent;
   public StreamOutputCallbackDelegate pfnStreamOutput;
   public IntPtr pvArg;
  }

  [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
  [ComVisible(false)]
  internal struct CRYPT_KEY_PROV_INFO
  {
   internal string pwszContainerName;
   internal string pwszProvName;
   internal uint dwProvType;
   internal uint dwFlags;
   internal uint cProvParam;
   internal IntPtr rgProvParam;
   internal uint dwKeySpec;
  }

  #endregion

  #region "DELEGATES"

  [ComVisible(false)]
  public delegate bool StreamOutputCallbackDelegate(IntPtr pvArg, IntPtr pbData, int cbData, Boolean fFinal);

  #endregion

  #region "API"

  [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
  [ComVisible(false)]
  public static extern Boolean CryptAcquireContext(
    ref IntPtr hProv,
    String pszContainer,
    String pszProvider,
    int dwProvType,
    int dwFlags);

  [DllImport("Crypt32.dll", SetLastError = true)]
  [ComVisible(false)]
  public static extern IntPtr CryptMsgOpenToEncode(
   int dwMsgEncodingType,
   int dwFlags,
   int dwMsgType,
   ref CMSG_SIGNED_ENCODE_INFO pvMsgEncodeInfo,
   String pszInnerContentObjID,
   ref CMSG_STREAM_INFO pStreamInfo);

  [DllImport("Crypt32.dll", SetLastError = true)]
  [ComVisible(false)]
  public static extern IntPtr CryptMsgOpenToDecode(
   int dwMsgEncodingType,
   int dwFlags,
   int dwMsgType,
   IntPtr hCryptProv,
   IntPtr pRecipientInfo,
   ref CMSG_STREAM_INFO pStreamInfo);

  [DllImport("Crypt32.dll", SetLastError = true)]
  [ComVisible(false)]
  public static extern Boolean CryptMsgClose(IntPtr hCryptMsg);

  [DllImport("Crypt32.dll", SetLastError = true)]
  [ComVisible(false)]
  public static extern Boolean CryptMsgUpdate(
   IntPtr hCryptMsg,
   Byte[] pbData,
   int cbData,
   Boolean fFinal);

  [DllImport("Crypt32.dll", SetLastError = true)]
  [ComVisible(false)]
  public static extern Boolean CryptMsgUpdate(
   IntPtr hCryptMsg,
   IntPtr pbData,
   int cbData,
   Boolean fFinal);

  [DllImport("Crypt32.dll", SetLastError = true)]
  [ComVisible(false)]
  public static extern Boolean CryptMsgGetParam(
   IntPtr hCryptMsg,
   int dwParamType,
   int dwIndex,
   IntPtr pvData,
   ref int pcbData);

  [DllImport("Crypt32.dll", SetLastError = true)]
  [ComVisible(false)]
  public static extern Boolean CryptMsgControl(
   IntPtr hCryptMsg,
   int dwFlags,
   int dwCtrlType,
   IntPtr pvCtrlPara);

  [DllImport("advapi32.dll", SetLastError = true)]
  [ComVisible(false)]
  public static extern Boolean CryptReleaseContext(
   IntPtr hProv,
   int dwFlags);

  [DllImport("Crypt32.dll", SetLastError = true)]
  [ComVisible(false)]
  public static extern IntPtr CertCreateCertificateContext(
   int dwCertEncodingType,
   IntPtr pbCertEncoded,
   int cbCertEncoded);

  [DllImport("Crypt32.dll", SetLastError = true)]
  [ComVisible(false)]
  public static extern bool CertFreeCertificateContext(IntPtr pCertContext);

  [DllImport("Crypt32.dll", SetLastError = true)]
  [ComVisible(false)]
  public static extern IntPtr CertOpenStore(
   int lpszStoreProvider,
   int dwMsgAndCertEncodingType,
   IntPtr hCryptProv,
   int dwFlags,
   IntPtr pvPara);

  [DllImport("Crypt32.dll", SetLastError = true)]
  [ComVisible(false)]
  public static extern IntPtr CertGetSubjectCertificateFromStore(
   IntPtr hCertStore,
   int dwCertEncodingType,
   IntPtr pCertId);

  [DllImport("Crypt32.dll", SetLastError = true)]
  [ComVisible(false)]
  public static extern IntPtr CertCloseStore(
   IntPtr hCertStore,
   int dwFlags);

  [DllImport("crypt32.dll", CharSet = CharSet.Auto, SetLastError = true)]
  internal static extern CertHandle CertDuplicateCertificateContext([In] IntPtr pCertContext);

  [DllImport("crypt32.dll", CharSet = CharSet.Auto, SetLastError = true)]
  internal static extern bool CertGetCertificateContextProperty([In] CertHandle pCertContext, [In] uint dwPropId, [In, Out] IntPtr pvData, [In, Out] ref uint pcbData);

  [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
  internal static extern SafeHandle LocalAlloc([In] uint uFlags, [In] IntPtr sizetdwBytes);

  [DllImport("kernel32.dll", SetLastError = true)]
  private static extern IntPtr LocalFree(IntPtr handle);

  #endregion

  public class SafeHandle : SafeHandleZeroOrMinusOneIsInvalid
  {
   public SafeHandle(IntPtr handle) : base(true)
   {
    SetHandle(handle);
   }


   public SafeHandle()
    : base(true)
   {
   }

   protected override bool ReleaseHandle()
   {
    return (LocalFree(handle) == IntPtr.Zero);
   }
  }

  public class CertHandle : SafeHandleZeroOrMinusOneIsInvalid
  {
   public CertHandle()
    : base(true)
   {
   }

   public CertHandle(bool ownsHandle)
    : base(ownsHandle)
   {
   }

   protected override bool ReleaseHandle()
   {
    return CertFreeCertificateContext(handle);
   }
  }
 }
}

суббота, 18 мая 2013 г.

Люди деньги и время

Забавный факт, что человек знает проценты, логарифмы, но обычно не умеет считать и не ценит своё время. Большинство людей, увидев батон хлеба за 20 рублей и зная что есть место где он стоит 10 рублей, возьмут тот, что за 10, даже если потратят кучу времени, чтобы добраться до дешёвого магазина.

При этом, покупая, скажем, телевизор, за 31000 руб и зная, что чуть постаравшись, можно взять его за 30000, не будут возиться и возьмут здесь и сейчас вариант, которой дороже. Разница-то в процентах небольшая! Хотя на сэкономленную тысячу можно тупо покупать дорогие батоны и не париться с поиском дешёвых.

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

Что это значит в программировании? Если вы путём неимоверных усилий сократите расчёты с одного часа до 55 минут, никто этого и не заметит. А если с двух секунд до одной, то вы просто гениальные оптимизаторы. Имейте это ввиду, и вкладывайте ресурсы туда, где пользователи их оценят, даже если другие части будут совершенно неоптимальными. 

понедельник, 6 мая 2013 г.

Торренты vs лицензия

Я довольно-таки редко играю в игры, но если уж играю, и игра понравилась, то стараюсь купить лицензию. При этом играю в ломанную версию с торрентов, а лицензионный диск часто даже не распаковываю (как пример, рядом лежит диск с XCOM Enemy Unknown, который я достаточно долго искал где бы купить, но таки купил, ибо надо платить за хороший продукт.

Но недавно, в связи с выходом Heart of Swarm от близзарда решил не ждать кряков а просто купить лицензионный диск... Лучше бы я этого не делал.
По факту оказалось, что ещё надо докупить Wings of Liberty, без него работать ничего не будет, но это мой личный факап, и надо было внимательнее гуглить про это. Проблемы же начались гораздо раньше.

Во-первых, нужно завести аккаунт на Battle.net, указав о себе тучу личной информации, чуть ли не заставили указать цвет носков. Блин, я тупо хочу поиграть локально, не надо мне всё это! Но меня никто не спрашивал.
Минут 10 я потратил на придумывание пароля. Он был то слишком длинным, то слишком сложным (в нём нельзя использовать спецсимволы, но обязательно цифры и большие буквы, ну что за бред?). Письмо с подтверждением регистрации я получил только через час. Т.е. я сижу с диском, который не работает и ничего не могу сделать.

Но в коцне-концов зарегистрировался, выяснил, что надо купить ещё одну игру, я её купил и... И я теперь могу играть в Wings of Liberty! Но не могу играть в Heart of Swarm, потому что близзарду нужно 72 часа, чтобы что-то там сделать у себя перед тем как вводить ключ из коробки с диском. Пипец. Приехали. Я купил диск, купил ещё одну игру, но должен ждать, пока что-то там произойдёт.

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

Но... вчера произошла история, которая и заставила меня написать этот пост. Ибо мы решили посмотреть, как Starcraft ведёт себя на маке, а в результате я получил заблокированный аккаунт. Приехали. Не понравилась близзарду моя активность, и вместо письма со ссылкой — если это ты, нажми сюда, мне просто заблокировали аккаунт. Хорошо хоть, что можно пройти стандартную процедуру восстановления пароля и разблокировать его, но что блин, за наезд? Нам не понравился твой компьютер, мы тебе больше не дадим играть!

В общем, после такого, только торренты! Там нет никаких идиотских защит, не надо постоянно вводить пароли и выслушивать предупреждения о том, как я незащищён. Я просто хочу запустить игру и начать играть! А лицензии так и буду покупать задним числом, а коробку и распаковывать не буду. Всё равно внутри ничего полезного нет.

Update: Мой аккаунт блокировали 3 раза, пока не добавил мобильный аутентификатор.

суббота, 6 апреля 2013 г.

Странные изменения в Такси

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

Раньше, я звонил, говорил: "Хочу машину из пункта А в пункт Б". Мне уточняли по срокам и принимали заказ. Сейчас политика изменилась на: "Я вам перезвоню" без уточнения заказа. Т.е. тебя сразу ставят уже в позу ожидания, хотя ты даже не выяснил будут ли принимать заказ.

Возможно, они думали, что я на этом остановлюсь... Хех... я позвонил в 3 такси. Ну а чего, с таким качеством информирования об услуге — сами напросились.

Потом оказалось, что 20 минут меня дожидалось одно из такси, при этом по телефону говорили что машина едет, а с водителем связаться не получалось. Это уже просто качество сервиса говно.

Дальше началось интересно. Я уже ехал на машине, как всё-таки одни позвонили и уточнили, нужна ли ещё машина. А вот вторые 3 раза звонили роботом чтобы сказать: Вашей машины ещё нет! Хех, я только в третий раз дослушал, оказывается, чтобы отказаться нужно нажать 1, а если просто отбиться, это означает, что ты ждёшь. Да не жду я уже! Впрочем, спустя час мне всё-таки отправили смс и позвонили голосом, что машина приехала. Я уже был дома. Наверное стал даже стал плохим клиентом.

В общем, жду с нетерпением, когда можно у меня в городе заказывать Такси через телефон с выбором. Ибо несмотря на удобство, выбора у нас нет (всего 2 службы такси и заказываются разными программами).

среда, 27 марта 2013 г.

HttpWebRequest некоторые неочевидности

Я последнее время достаточно много работал с классом HttpWebRequest (позволяет делать запросы к серверу на .NET) и столкнулся с некоторыми моментами, которые весьма неочевидны, но о которых следует знать, если им активно пользоваться.


  • Сколько бы вы не создали клиентов, в реальности одновременно к серверу будут идти 2 запроса, остальные попадут в очередь. Планируйте это при реализации параллельности. Изменить количество можно установив ServicePoint.ConnectionLimit или уникальный ConnectionGroupName
  • Балансировка запросов в настоящий момент достаточно туповатая, поэтому, если вам в принципе достаточно небольшого количества одновременных коннекций, но некоторые могут занимать долгое время, вы можете получить нехорошую проблему: запрос зависнет в очереди. Происходит это тогда, когда кончаются все доступные коннекшены, запрос уходит в очередь к какому-либо из них, и продолжится когда тот закончится. Если при этом освобождаются другие — это ни на что не влияет. Т.е. вы можете получить пиковую нагрузку, очередь и толпу отвалившихся запросов, в случае если один будет долгим, а другие пристроятся ему в хвост.
  • Timeout на самом деле ограничивает время выполнения всего запроса, а не ожидания данных. Т.е. если вы не спеша получаете файл в 10Gb, вам плюнется таймаут. Не забудьте увеличить. А проблемы с долгим соединением решайте вручную парой Begin/End
  • Если вы используете Begin/End (BeginGetRequestStream, например), и решаете забросить запрос в случае долгого ответа, не забудьте сделать Abort, а то запрос может дойти, когда его уже никто не ждёт и не хочет обрабатывать
  • Несмотря на возможность установить AllowWriteStreamBuffering в false, память всё равно будет сжираться где-то в его недрах (возможно поправят поведение, ибо кажется багом, т.к. данные уходят по факту), так что при передаче гигабайт данных для надёжноти включите чанкинг
  • Размер данных, при которых будет отправка не регулируется, опытным путём установлено, что при чанкинге он 1024 байта. Т.е. если вы хотите медленно посылать небольшие порции данных на сервер, вы можете посылать их тупо в буфер, так что лучше не делать подобное и посылать всё целиком (получать данные с сервера маленькими кусками можно)
  • Если на сервере используется недоверенный https сертификат, то чтобы не получать ошибку соединения, надо подписаться на ServicePointManager.ServerVerificationCallback и там проверять сертификат на корректность. Метод статичный, так что следите за тем, чтобы не перехватывать чужие запросы (если в вашем приложении ещё кто-то посылает данные)
  • HttpWebRequest использует своё кеширование DNS, по умолчанию 2 минуты. Что странно, т.к. резолв идёт через системный DNS-кеш, т.е. накладных расходов не происходит. Можно изменить это время в ServicePointManager.DnsRefreshTimeout, и аккуратнее с KeepAlive, т.к. при его наличии будет держаться коннекшен к серверу некоторе время, и резолва не произойдёт.
  • Если сервер вам посылает данные очень медленно, то проверить не умер ли он, можно установив ServicePoint.SetTcpKeepAlive, что позволит пинговать сервер средствами TCP, и получить ошибку в случае если сервер реально пропал.
На этом пока всё, если будут ещё интересные моменты, то буду обновлять пост.

пятница, 8 февраля 2013 г.

История про винду поганую и крипто-про не лучше

Жил был компьютер. И жил он долго и счастливо, и решил владелец обновить его, да с седьмой винды, да на восьмую.
И обновился компьютер, и сказал, буду я работать и всё у меня будет хорошо, но не буду я апдейты ставить. Ибо 800B0001!

И пошёл владелец папки catroot2 да SoftwareDistribution удалять, очередь у службы bits вычищать, регистрировать и перерегистрировать COM-компоненты. Да не помогло это ни хрена. А гугл буржуйский только тех же страдальцев показывает, да переустановить систему с нуля предлагает.
И выяснил тут совершенно случайно владелец, что CryptoPro, продукт православный может козни подобные строить, и удалил он этот продукт с концами. И заработали апдейты, и поставились патчи, и заговорила Винда человеческим голосом: "Не выключай меня, добрый молодец, ибо обновляюсь я!". И всё стало хорошо до поры до времени.

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

Но не лыком был шит добрый молодец, помнил он ещё про крипто про подлое, ну да делать нечего, ставить крипто про надо заново. Авось да починится.
И пошёл добрый молодец крипто про ставить злобное. Семь дней и ночей ставил он крипто про без устали, а ему в лицо мерзость всякая падала: дескать, сервис мой, да интерактивный, и работать он и не будет, а и драйвер мой удаляется, да не может удалиться, ибо процесс идёт установочный!

Но одолел страшилище добрый молодец, и включился тут SSL православный и апдейты, и те не сломались!

И я там был, винду материл, крипто про материл, всё подряд материл, да невыматериволся!

вторник, 5 февраля 2013 г.

Программы для восстановления полноценной кнопки Пуск в Windows 8

Я как-то писал, что сделал суррогатную замену кнопки Пуск, но в реальности, конечно жить с этим трудно и подходит только для узкого редкого применения в виртуалках, когда кнопки Win тупо нет.

Собственно, люди уже написали просто гору различных программ, которые восстанавливают или эмулируют эту кнопку, и почему бы их и не использовать?

В общем, я проверил около десятка программ, которые восстанавливают именно классическое семёрочное меню, те, которые делают что-то своё не рассматривал. Например, весьма популярная Classic Shell имеет хороший функционал, бесплатная, но меню совершенно другое, так что для моих условий не подошла. Т.е. я упростил себе задачу и мне не пришлось перебирать 30 программ. Всего лишь десяток.  

В общем, большинство решений пришлось сразу отбросить. Причины:
  1. У авторов нет вкуса и выглядит всё ущербно
  2. Совсем непохоже или сильно непохоже в мелочах на стандартное меня
  3. Глючит, тормозит и тупит
И по факту оказалось, что достойны внимания всего 3 программы, две из которых платные, а одна хак. 

Собственно делюсь:

Ex7forW8

ссылка

Это, собственно хак, который заключается в запуске проводника из Windows 7 под Windows 8. В результате исчезает вся восьмёрочность, и выглядит всё классически. Пользовался я им мало, поскольку восьмёрочность всё-таки иногда нужна, но особых глюков не заметил.

StartIsBack

ссылка

Судя по всему сделана русским разработчиком. Триал на 30 дней, программа стоит $3 за 2 лицензии (т.е. на двух компьютерах можно поставить). Т.е. вполне бюджетно. По внешнему виду выглядит почти идентично семёрочному, но есть скины, которые могут сделать небольшой микс в сторону восьмёрки, и результат будет совсем замечательным. Проблем не выявлено.

Start8

ссылка

Очень навороченный продукт от StarDock с кучей скинов и настроек. В общем, рюшечек хоть отбавляй. Стоит $5 за одну лицензию. Проблем не выявлено. Работает как надо.




Есть куча других бесплатных аналогов, но как я говорил уже — в них какие-то недостатки есть, которые при работе весьма раздражают, а если уж восстанавливать кнопку пуск, то надо сделать её максимально "правильной", чтобы не раздражала и не увеличивала количество ненависти во вселенной.

вторник, 22 января 2013 г.

Про вегетарианцев

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

Во-первых, с другой стороны они много вопят о своей пищевой ориентации. Да, есть исключения, когда они особо не афишируют это, обычно вопят. 
Во-вторых, они обычно достаточно злые, их вопли в большинстве своём преследуют цель унизить мясоедящего собеседника, объяснив ему, какой он муда.
В-третьих, они очень любят готовить из травы и сои что-то напоминающее мясо по виду и желательно по вкусу. Этот момент меня совсем убивает — вам не нравится мясо, но вы активно его эмулируете. В чём смысл?

Ну так вот, я долго не мог понять, в чём же тут дело, что за когнитивный диссонанс у людей. Но тут, кажется начало доходить, и хочется поделиться этой идеей. На мой взгляд, проблема у вегетарианцев не в том, что им не нравится мясо, а в том что они его не могут есть по психологически причинам. Попытаюсь объяснить: у вегетарианцев есть некое психическое заболевание, и когда они едят мясо, им становится плохо от этого (не знаю что они представляют, может опарышей, а может невинно убиенную коровку (хотя сомневаюсь)). Как плохо? Представьте, едите вы кролика, и тут кто-то говорит: "А это ведь не кролик, это похоже кошка!". Я думаю многих от такого заявления стошнит, кто-то будет терпеть это всё, хотя его изнутри будет выворачивать, при этом само мясо не стало менее вкусным, просто психологически его уже неприятно есть. Так и с вегетарианцами, только у них проблема на любое мясо.

А дальше вступает правило: Есть две причины, истинная и та, которая красиво звучит. Вегетарианцы не могут признаться в том, что им хочется мясо, но они не могут его есть. И они начинают сублимировать про коровок, про то как это полезно и прочее. Но при этом, внутри они таки продолжают завидовать тем, кто питается мясом, и эта зависть выражается ненавистью и оскорблением.

Так что новость о том, что вегетарианство причислили к психическим заболеваниям, мне теперь не кажется такой уж странной.