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. И вот только после этого всё будет работать как и ожидалось.

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

суббота, 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, и ваше приложение должно с ним работать, и даже работает, только падает. Иногда. С непонятным стэктрейсом. И вы пытаетесь найти хвосты и поправить всё это безобразие.

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

понедельник, 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);
   }
  }
 }
}

среда, 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, и получить ошибку в случае если сервер реально пропал.
На этом пока всё, если будут ещё интересные моменты, то буду обновлять пост.

понедельник, 23 июля 2012 г.

MVC. А нужны ли Контроллеры?


Мне концепция MVC в принципе, достаточно нравится, но в мелочах сильно раздражает. У нас есть бесправные View, которые по факту про всё знают и активно занимаются навигацей. У нас есть тупые Модели, которые периодически берут на себя заботу о данных, и есть Контроллеры, занимающиеся непонятно чем, только не тем, чем нужно.
MVC позволяет писать гибко, но за эту гибкость приходится расплачиваться жёсткими гайдлайнами или же полной неразберихой. Кто должен готовить данные для View? Контроллер, Модель, внешний слой бизнес-логики? И если внешний слой, то он должен возвращать Модель, или модель должна его использовать для наполнения, или контроллер должен заполнить модель, имеющимися данными?
Модель должна быть ViewModel или настоящая, живая? А может быть просто DTO?

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

Все контроллеры в приложении примерно равны

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

Контроллеры разделены логически, а не функционально

Вот смотрите, есть у нас класс User и какой-нибудь UserManager, который отвечает за создание пользователей, получение данных, редактирование и удаление. Всё просто и логично. Теперь делаем приложение, контроллеры и получаем UserRegistrationController, UserProfileController, UserAdministrationController и использование пользователей в каких-нибудь RoleManagerController, GroupManagerContoller, LoginController и куче других. Можно пытаться сделать один большой контроллер, но получим помойку, т.к. функционал администратора управляющего пользователями сильно отличается от функционала нового, регистрирующегося пользователя. В результате, с одной стороны мы раскидываем работу с пользователями по куче контроллеров, с другой, при попытке их объединить мы получаем жуткую чехарду с функционалом и контекстом.

Контроллеры занимаются самыми разными вещами

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

Вот такая вот подлость с этими контроллерами. А как решать эту проблему — я не знаю. У меня не настолько светлый ум.  Но если хотите моего мнения (а если не хотите, то всё равно напишу): утащить всю логику в некое WebAPI со своей маршрутизацией. Это WebAPI будет заниматься получением и сохранением данных, не трогая контроллеры. А за самими контроллерами оставить простейшую логику по отдаче нужных страничек по урлу, максимально вырезав из них всё остальное. И идти дальше, разбираться с View и Models.

суббота, 14 июля 2012 г.

Кнопка Пуск для Windows 8

Внизу поста есть ссылка на программу, остальное — мои занудные размышления о вечном и всяком отстое.


Я давно говорил, что я ненавижу то, как Microsoft запёхивает Metro UI куда надо и куда не надо, но поскольку Microsoft'у глубоко насрать на моё мнение, придётся жить с этим ужасом ближайшее время. Собственно, почему ужас? Ну вот хотя бы как выглядит свежий тестовый сервер на Server 2012 RC после двух часов с момента установки:


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

Но ситуация ещё хуже. В Microsoft выкорчевали кнопку "Пуск". Да, я знаю, что можно увести мышку в левый нижний угол, или нажать на кнопку Win. Но есть грабли, когда открыта консоль RDP на часть экрана — эти способы не работают. Чтобы открыть меню поиска необходимо попасть в малюсенький квадратик. Примерно такого размера:

Если вы не видите эту красную точку, увеличьте в браузере размер страницы.

Вот за это хочется оторвать (если остались) руки дизайнерам Windows 8.

Чтобы хоть как-то с этим жить пришлось нарисовать псевдокнопку, которая представляет собой простейшную программу, которая нажимает на кнопку Win и имеет иконку, похожую на кнопку Пуск. Берём программу, перетаскиваем на таскбар, пинним её и получаем большую кнопку, на которую можно нажимать (только отсуп слева большой относительно семёрки, но тут уж ничего не поделать ).


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

Да, и система наверняка будет ругаться на эту программу, так как она не подписана сертификатом, а подписывать такую мелочь у меня нет никакого желания, так что можете поверить на слово, что программа не делает ничего плохого и состоит из 5 строчек и иконки в ресурсах   Программа не требует установки, и как вариант можете хоть на рабочем столе иконку разместить, поведение будет таким же.

startButton.exe
startButton.zip

вторник, 26 июня 2012 г.

Циклы. Нужны ли они?

Не стоит воспринимать данный текст слишком серьёзно. В нём присутствует некоторая доля бреда

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

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

Преобразование

Типичная задача: взять из списка объектов "Пользователь" только имя и работать с ним дальше. Есть вполне сформировавшиеся абстракции, например map в jQuery или Select в .NET. Согласитесь, что второй пример кода выглядит гораздо понятнее первого, т.к. отсутствует лишний вспомогательный код:

List<string> names = new List<string>(); 
foreach(var user in users) 
    names.Add(user.Name); 

var names = users.Select(x => x.Name);

Фильтрация

Тоже частая задача. Выбрать всех активных пользователей. Тут стандартные схемы filter и Where:
List<User> resUsers = new List<User>(); 
foreach(var user in users) 
    if(user.IsActive) 
        resUsers.Add(user); 

var resUsers = users.Where(x => x.IsActive);

Аггрегация 

Куда же без неё! Например, нам нужно посчитать количество пользователей (да, у нас сейчас нет встроенной функции):
var cnt = 0; 
foreach(var user in users) 
    cnt++; 
А в нормальном стиле (в общем виде) это можно сделать как-то так, через аккумулятор:
var cnt = users.Aggregate((e, acc) => acc + 1);

Обработка

В данном случае результат передаётся куда-то дальше, основной объект никак не трансформируется. Тут нам на помощь приходят each в jQuery и ForEach в .NET. Например, мы хотим вывести имена пользователей на консоль:
//names - List<int> из первого примера 
foreach(var name in names)  
    Console.WriteLine(name);  

names.ForEach(Console.WriteLine);

И всё вместе

И конечно же, циклы используются сразу для всех задач вместе, ещё больше запутывая код и подменяющие реализацию задачи ненужными деталями реализации (которые, действительно, никому никогда не нужны и только мешают дальнейшему чтению кода).
Итак, выбираем имена активных пользователей и находим самое длинное:
List<string> names = new List<string>();  
foreach(var user in users)  
    if(user.IsActive) 
        names.Add(user.Name);  

var maxName = ""; 
foreach(var name in names) 
    if(maxName.Length < name.length)  
        maxName = name; 

//-------------------- 
var maxName = users 
    .Where(x => x.IsActive) 
    .Select(x => x.Name) 
    .Aggregate("", (e, acc) => e.Length > acc.Length ? e : acc);

Согласитесь, второй вариант, гораздо понятнее и короче. Сразу по тексту видно что он делает (и это я ещё не воспользовался готовым методом Max). В первом же случае проще всего догадаться о том, что делает код по названию переменной, но это в данном простом тестовом примере. А в реальной жизни всё гораздо запутаннее, ведь обязательно набегут оптимизаторы и цикл превратится в более короткую и простую версию, которая понятнее (вроде бы), но в результате вся логика прибита гвоздями и её изменение становится уже более сложной задачей, связанной с практически полным переписыванием куска кода:
var maxName = ""; 
foreach(var user in users)  
    if(user.IsActive && user.Name.Length > maxName.Length) 
        maxName = name; 

Результат

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


Напоследок

Чтобы не останавливаться на элементарных примерах, я решил привести чуть более сложный. А именно, реализацию сортировки. Классический quicksort из учебника выглядит примерно так (только главная часть):
public void Sort(int start, int end) 
{ 
    if (end <= start) return; 
    if (end - start == 1) 
    { 
        if (_array[end] < _array[start]) Swap(start, end); 
        return; 
    } 
    int el = _array[(start + end)/2]; 
    int endIdx = end; 
    int startIdx = start; 
    while(startIdx <= endIdx) 
    { 
        if (_array[startIdx] < el) startIdx++; 
        else 
            if (_array[endIdx] <= el) 
            { 
                Swap(startIdx, endIdx); 
                endIdx--; 
                startIdx++; 
            } 
            else endIdx--; 
    } 
    Sort(start, endIdx); 
    Sort(endIdx + 1, end); 
}
Данный код, конечно можно подсократить, но это уже тонкости и детали реализации и оптимизации. Тем не менее, даже на такой пример я потратил достаточно времени, пока его реализовал и выловил все замеченные ошибки. И в теперь разобраться как сортирует алгоритм за всеми конструкциями весьма сложно. А теперь посмотрите, что я написал сходу на LINQ:

public IEnumerable<int> Sort(IEnumerable<int> arr) 
{ 
    if (arr.Count() <= 1) return arr; 
    int el = arr.First(); 
    return Sort(arr.Where(x => x < el))
           .Concat(arr.Where(x => x == el))
           .Concat(Sort(arr.Where(x => x > el))); 
}
Всего 5 строчек, и то разбитых для удобства! И смотрите как просто объяснить теперь алгоритм: берём первый попавшийся элемент, берём из массива все элементы меньше него, сортируем их данным же алгоритом, добавляем элементы равные данному, и большие, отсортированные тем же алгоритмом. Всё просто, банально и понятно.
Не хочем quicksort, хотим сортировку выбором? Нет ничего проще:
public IEnumerable<int> Sort(IEnumerable<int> arr) 
{ 
    if (arr.Count() <= 1) return arr; 
    int elMax = arr.Max(); 
    return Sort(arr.Where(x => x < elMax)).Concat(arr.Where(x => x == elMax)); 
}

Видите, вполне чёткая логика и понятность. Так зачем вам ещё циклы?

вторник, 27 марта 2012 г.

Проблема в Google Play и как МТС залез на смартфоны Samsung

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

Я исследовал проблему и выяснил, что проблема в Google Play (бывший Android Market), а именно в том, как он проверяет приложения на идентичность. 

Дело в том, что приложения в маркете идентифицируются не по названию приложения, а по названию пакета (package), которое имеет свои характеристики и это название есть "область имён" классов приложения. Изначально это было сделано, чтобы разные приложения не конфликтовали если в них окажется одинаковые классы. Т.е. учитывается полное имя приложения.

Например, возьмём приложение Карты Google, посмотрите какой адрес у него в маркете:
https://play.google.com/store/apps/details?id=com.google.android.apps.maps

Пакет с приложением называется com.google.android.apps.maps, т.е. по стандартному соглашению:
com — common или com от google.com (бывают разные соглашения о именовании)
google — название компании-производителя
android — это приложение для android
apps — это приложение, а не отдельная библиотека
maps — это приложение карты

Ну или как вариант, Facebook:
https://play.google.com/store/apps/details?id=com.facebook.katana
com — common или от facebook.com
facebook — компания-производитель
katana — так называется клиент. Оригинальное название

Я попробовал сделать приложение, которое называется также, как QuickOffice (только специфичный, предустановленный на телефоны от Motorola).
И тут возникла первая проблема у Google: мне удалось загрузить это приложение в Market. Мне разрешили это сделать!

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

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

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

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

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

(Данный текст уже неактуален, мы можем мелко пакостить, но не сможем обновить приложение)
Как результат, мы можем делать поддельные приложения Facebook (изначально стоит на множестве устройств) или Google Search, Google Maps и любых других. Мы можем назвать их совершенно по-другому и они даже могут делать честные вещи. Например, отсылать SMS на платные номера. Что здесь честного? А мы можем назвать приложение и написать в описании, что мы делаем именно это, а то, что оно заменило другое приложение — случайное совпадение.

Выходит, что приложение для МТС и Samsung делал один разработчик (Эльдар Муртазин подтверждил это) и подписал одним ключом. Приложение называется com.seven.Z7 в обоих случаях. И из-за полного совпадения случился такой факап у пользователей (несмотря на разные названия и иконки в маркете).


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

PS: Своё приложение из маркета я убрал после проверки, надеюсь, никому не успел навредить.

воскресенье, 6 ноября 2011 г.

SVN vs DVCS

Последнее время, появилось множество фанатов, орущих про то, что SVN — сраное говно, а Git/Mercurial — активно рулят. При этом, несмотря на плюсы распределённых систем, у них ещё дофига минусов, которые решаются или костылями, или инструкциями по работе, или надеванием штанов через голову, потому что "это модно".

Покопавшись в интернете, я не нашёл особых статей со сравнением DVCS (распределённых системы управления версиями) и SVN (хотя не сильно-то и искал), так что решил написать свою, чтобы Трухин Юрий посмеялся. 
Собственно, буду сравнивать концепцию DVCS (без сильных завязок на Git или Mercurial) и SVN, как одного из самых ярких представителей централизованных систем (TFS тоже яркий, но это комбайн с кучей бантиков и практически только под Visual Studio)

Всё нижеперечисленное, моё личное мнение, которое может не совпадать с мнением фанатов какой-либо системы, да и вообще быть ошибочным.

Начнём с плюсов DVCS:
  • У каждого своя пользователя копия репозитория в которой он может делать всё что угодно, до заливки в основной репозиторий
  • Удобные бранчи и теги 
  • Более приличный мёрж (за счёт ченджсетов)
  • Отличная автономность
  • Возможность устраивать сложную систему иерархий в организации проекта
А теперь, про недостатки, про то, что не пишут или что обходится административными мерами:
  • Один репозиторий — один проект. Т.е. распределённые системы удобны для работы над одним проектом, и очень плохо разделять зону ответственности между связанными проектами. Технически, два независимых проекта, это два полностью независимых проекта, с разными репозиториями и отсутствием общей инфраструктуры
  • Подключение к проекту внешних модулей (например, общей между разными проектами, библиотеки). В svn это весьма коряво решается через svn:externals, и в git и в mercurial есть ещё более корявые решения, но в принципе решить задачу можно, хоть и очень коряво.
  • Проблема с хранением больших бинарных файлов (особенно, если они ещё и изменяются периодически). Т.к. у каждого разработчика по копии своего репозитория, то у каждого по копии всех этих файлов (и всей истории), даже если они ему и не нужны. Решение — использовать всяческие расширения или выносить эти файлы из DVCS в другие системы (например, тот же SVN).
  • Невозможность забрать всего-лишь несколько файлов из проекта. Например, менеджерам абсолютно не нужен код, им нужно только ТЗ, макеты и прочая мелочёвка. При этом, уровень грамотности у них в плане работы с VCS значительно ниже. В общем, как результат, проще документацию хранить отдельно, чем обучать менеджеров работе с DVCS.
  • Отсутствие единой сквозной нумерации. Прощай автоверсионирование в билдах и удобная привязка к багтрекеру. Проблема некритичная, но очень "радует" своим удобством.
  • Отсутствие возможности тонкого разграничения прав, например, запретить писать некоторым пользователям в важные конфигурационные файлы, или же заблокировать от изменений часть проекта.
  • Отсутствие возможности блокировки файлов. Фича редкая, но когда нужна, тогда без неё плохо. Можно обойти административными мерами, но их любят нарушать.
  • Практически всегда необходимо выделять центральный репозиторий (хотя бы для автомтатических билдов), как результат, он выполняет роль сервера SVN, т.е. в принципе, всё сводится назад, к централизованной системе с более умными клиентами у разработчиков.
  • Хуки на определённые группы файлов, и вообще слежение за изменениями. Необходимо прилагать дополнительные усилия для слежки, то, что в SVN делается из коробки.
  • Потеря исходников автоматически означает потерю всего репозитория, т.е. включает в себя всю историю, уже удалённые файлы, даты, список пользователей, ветвления. Т.е. ценность данной информации гораздо выше, стоимость потери — тоже. А с учётом пункта об проблемах с разграничением прав, всё становится совсем плохо. А потерять один ноутбук разработчика гораздо проще чем данные с одного сервера, охраняемого злобным цербером администратором. 
И как закономерный результат: отлично DVCS применимы для распределённой разработки (как и следует из их названия) и слабо применимы для локальной группы разработчиков, когда все преимущества распределённости теряются, а недостатки никуда не деваются.

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

В-общем, мое мнение: DVCS нужно засунуть туда, куда они пришли, в распредённость, и не иметь проблем с ними, если не видно явного преимущества в данный момент. Ведь из SVN всегда можно сделать экспорт в другую систему, когда это понадобится. А вот обратно — уже никак, и от проблем DVCS уже никуда не получится деться.

воскресенье, 18 сентября 2011 г.

Определяем бездействие пользователя в Windows

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

  • Если нужен ответ от пользователя на не очень важный вопрос, можно принять решение самостоятельно или же продолжить заниматься другими вещами, отложив вопрос до появления
  • Если идут вычисления, можно поднять приоритет операции
  • Если идёт демонстрация некой информации, её можно остановить, чтобы зря не переводить ресурсы
  • Если есть подобие мессенжера, можно изменить статус
Итак, как это можо сделать:

GetLastInputInfo

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

SPI_GETSCREENSAVERRUNNING

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

SC_MONITORPOWER

Это глобальное сообщение, рассылаемое системной или приложением, через WM_SYSCOMMAND, сигнализирующее о том, что монитор включен/спит/выключен. Можно считать более продвинутым вариантом скринсейвера (ну и вручную отключать монитор, но это не тема этого поста).

WTS_SESSION_LOCK/WTS_SESSION_UNLOCK

Данные события помогут нам определить момент блокировки компьютера пользователем. К сожалению, текущее состояние блокировки, таким образом определить не получится, но по умолчанию можно считать, что раз кто-то запустил вашу программу, то система явно не блокирована. Блокировка — это событие явственно свидетельствующее о том, что пользователь убежал от компьютера, правда используется в основном в организациях в целях безопасности, дома мало кто это делает. Но, тем не менее событие очень хорошее, поэтому грех им не воспользоваться.
Для этого регистрируемся на получение события через WTSRegisterSessionNotification и в WndProc (основной функции, обработчике всех событий) ловим сообщение WM_WTSSESSION_CHANGE. Естественно, у нас должно быть некоторое окно, которое будет это обрабатывать. Если приложение не подразумевает наличие окон, можно создать невидимое. 

четверг, 23 июня 2011 г.

Мифы о многозадачности и прожорливости Android до памяти

Данный пост написан по мотивам подкаста Юрия Трухина и Эльдара Муртазина, где они не очень корректно высказались про то, как устроена многозадачность в андроиде, и зачем ему таскменеджеры. Многозадачность в Андроиде такая же, как в готовящемся Mango для WP7, с точностью до деталей реализации и названий в архитектурных решениях.

Некорректное понимание многозадачности в андроиде я встречаю достаточно часто, и думаю, что это вина Google, что они не могут нормально объяснить обычному пользователю, как всё внутри устроено, и что Task Manager'ы в большинстве своём вредны, нежелели полезны.

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

Отчасти это в чём-то правда, но тут есть весьма тонкий момент. Дело в том, что все программы для андроида модульные. Т.е. программа состоит из множества кусочков, которые работают независимо друг от друга (если явно не вызывают другой модуль). Т.е. наличие в памяти программы совершенно не показывает то, что она вся используется в данный момент. Она может вообще не исполнять никакого кода, а в памяти висеть просто потому, что память есть и почему бы не держать приложение в кеше, чтобы последующая активация произошла быстрее. Естественно, когда память будет нужна другим приложениям, самое ненужное (есть система приоритетов) будет выгружено. Т.е. это тоже самое что и концепция захоронения в WP7. Т.е. в данном случае таскменеджеры просто вредны, так как они выгружают приложения, которые потом будут загружаться снова, тратя ресурсы и время.
Само по себе, наличие приложения в памяти не тормозит телефон, освобождать память ради большой цифры free mem — бесполезное занятие, от этого ничего не изменится.

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


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

В андроиде все модули в программе делятся на три основных типа:

  • Activity
  • Broadcast Receivers
  • Services

Рассмотрю их подробнее на виртуальном примере музыкального плеера.

Activity

Это окна нашего приложения. Одно окно — одна активити. Т.е. в нашем воображаемом музыкальном плеере окно с названием песни, элементами управления и картинкой альбома, это активити. Их время жизни очень короткое, как вы переключаетесь на другое окно (даже в пределах своего приложения), то всё ставится на паузу, а через некоторое время освобождаются все ресурсы и активити убивается. Т.е. в фоне ничего не рисуется и не может рисоваться. Как переключились с нашего плеера, где был красивый эквалайзер, можно не беспокоиться что этот эквалайзер будет продолжать отрисовываться где-то в фоне, его больше нет. Эта часть приложения не работает совсем.
Если приложение состоит только из активитей (например, калькулятор), то когда мы с него переключились — оно больше не ест никаких ресурсов. Просто сидит тихо мирно в кеше, ожидая, что вы вернётесь.

Broadcast Receivers

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

Это части программ, отвечающие за приём глобальных сообщений. Их весьма много стандартных, плюс, можно ожидать абсолютно любое сообщение, сказав про это системе  (это бывает полезно для связи между различными программами). Сообщения бывают самые разные, например, сообщение о том, что появилась WiFi-сеть и можно бежать в интернет за новыми песнями, вставили телефон в док-станцию — рисуем красивое окошко с часиками. Нажали кнопку паузы на гарнитуре — остановим воспроизведение. Собственно таким образом можно отправить картинку в твиттер из галереи: твиттер регистрируется на событие вида "могу шарить картинки", галерея посылает событие всем подобным приложениям и пользователь выбирает, что он хочет сделать с картинкой. Благодаря этому и обеспечивается гибкость андроида в установке различных приложений.

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

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

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

Сервисы

Вот мы и подошли к самому главному потребителю ресурсов. Сервисы, эта часть программы, которая должна работать в фоне, и она предназначена ровно для этого и не для чего больше. Это вот те самые маленькие блоки, которые работают при многозадности и в Android, и в iOS, и в WP7 Mango.
Это сервисы синхронизации, обновления, загрузки. Для музыкального плеера играть музыку должен именно сервис! Даже во время звонка, часть программы, отвечающая за разговор — это сервис, который нужен, чтобы разговор шёл, а пользователь мог играть в Angry Birds в это время.
Собственно это и есть основные потребители ресурсов, но таскменеджеры их очень плохо определяют, лучше на них смотреть в стандартных настройках приложений (Running Services).
Но Android может убивать сервисы при нехватке памяти тоже, хоть они и имеют приоритет по времени жизни, что удивительно, он потом их постарается запустить заново, чтобы вернуть всё как было. Самый высокий приоритет у сервисов с иконкой в статусбаре, как это глупо не звучит. Просто эти сервисы своим видом демонстрируют пользователю, что они существуют и работают, и Android их бережёт до последнего. Именно поэтому большинство музыкальных плееров рисуют иконку в статус баре, такой вот архитектурный финт ушами.

Небольшая ремарка про аналог сервисов в Windows Phone 7 (в грядущем релизе Mango), там подобный функционал называется "Background Agents" (т.е. агенты, работающие в фоне)

  • Агенты более специализированные и реализуются под конкретную задачу (т.е. специальный агент по проигрыванию музыки, специальный агент для скачивания файлов)
  • Есть агенты для своих задач, но WP7 ограничивает их 10% CPU и 5Mb памяти, т.е. они не могут сильно повлиять на производительность телефона
  • У агентов есть ограничение на функционал, например, они не могут использовать камеру и сенсоры. Т.е. нельзя будет сделать видеорегистратор и шагомер (GPS-можно).
  • Агенты выводятся в отдельный хост-процесс, но это детали внутренней организации системы
  • Принципиально отсутствует Task Manager, как результат пользователь не может насильно оставить работу агента
В общем, если в WP7 вдаваться в детали, то там реализация выглядит отличающейся, но если смотреть глазами пользователя, то задача будет решаться одна и та же: небольшая часть приложения, которая делает конкретную часть работы.

Заключение

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

воскресенье, 6 февраля 2011 г.

OpenXML и цифровая подпись

Тут некоторое время помучался пытаясь подписать OpenXML, хочу поделиться тем, что у меня вышло с этим неприятным форматом.

Начну издалека сбоку, про то, что явно подразумевается во всех статьях но словами леняться проговорить. Итак, OpenXML это документы формата Open Packaging. Это такой стандартизованный zip-файл, в котором в основном должны храниться XML'ки, но можно и любую другую фигню. Поэтому этот формат в принципе можно использовать для передачи апдейтов вашей программы, или хранения набора патчей, в общем для любых целей хранения стандартизованного пакаджа, который удобно читать и разбирать. Собственно, в .NET за это отвечает класс Package, он всё и умеет.

Разобравшись с общим форматом, переходим к частностям: в этом OPC формате хранятся: .docx, .xlsx, .xps и много других вещей. Соответственно работать в коде с документами вы можете абстрактно, как с пакадаждем содержащим набор частей, или взять специализированного наследника (скачав, например, OpenXML SDK, ну или использовав XpsDocument) и работать уже более детально.

А теперь, собственно о цифровой подписи и проблемах. Делается всё это на уровне Package, т.е. если мы хотим подписать вордовый документ, нам на самом деле это не принципиально, мы берём класс PackageDigitalSignatureManager, гоговорим какие секции надо подписать и подписываем.
Но вот тут и начинаются проблемы:

  • В 2007-ом офисе по умолчанию подпись будет показываться ошибочной, нужно шаманить с параметрами
  • В 2010-ом подпись по умолчанию будет показываться partial, т.е. офис будет считать что вы подписали лишь некоторые части, а не весь документ, и нервничать (кстати, вы можете подписать только стили, позволив беспрепятственно изменять текст (не через Word), и таким образом подделывать документы, если пользователь не будет настаивать на том что подпись partial).
  • В .NET реализации явно подразумевается (в смысле кодом), что сертификат для цифровой подписи может быть только RSA, да ещё будут проблемы с токенами, ибо вместо использования стандартной инфраструктуры PKI для подписи (что делает, например, сам Word), тут пытаются взять насильно приватный ключ (это плохо! а местами запрещено), кастуют его к RSA (сразу выкидываем все другие алгоритмы, например наш ГОСТ) и подписывют.
В общем, хоть на бумаге всё и гладко — 15 строчек кода, на практике эо всё превращается в весьма опасную неюзабельную штуку, которая непонятно когда и где может выстрелить, поломав всё и вся. А всё из-за криворукости индусов, которые изначально не могли всё сделать грамотно, в результате теперь сделать грамотно невозможно никому.