11 заметок с тегом

libmapi

«Переговорки»: панихида

Проект «Переговорки» — внутренний веб-сервис «Яндекса» для бронирования переговорок, синхронизированный с Microsoft Exchange. Это, наверное, самый сложный проект, который мне приходилось делать за всю жизнь.

Используется он теми, кто не может установить себе Outlook — ребятами с «Маками» и разнообразными «Линуксами» (а таких очень много). Кроме того, что от Exchange мы будем постепенно уходить, а интерфейс бронирования останется без изменений.

Никогда я ещё так не выкладывался, так не радовался, не расстраивался, не оставался с таким упорством допоздна в офисе. Звучит просто — вытаскивать время от времени данные из Exchange и отсылать обратно новую бронь. К сожалению, MS Exchange, который использовался тогда в компании, не умел общаться с внешним миром никак иначе, кроме протокола MAPI — бинарному исчадию ада, причём не просто через MAPI, а через так называемый MAPI через RPC — диалект DCE-RPC, который называется MSRPC.

Помню, первое, что пришло на ум использовать — impacket, модуль для Python, который позволял работать с «сырыми» пакетами, но к нему не было документации, а примеров в сети нашлось всего ничего — несколько эксплоитов и только. Впрочем, разобраться всё-таки удалось. Правда, попробовав описать вызовы верхнего уровня, я убедился, что это работа на года и решил поискать что-нубудь ещё.

Из бесплатных библиотек уровнем повыше нашлась одна-единственная — libmapi, поскольку она требовала наличия альфы Samba4 строго определённой версии, а на машине уже стояла Samba3 (через которую мы работали), решено было попробовать её скомпилировать. Оказалось, что такие попытки в компании уже предпринимались, но закончились неудачей. Решив что попытка не пытка, за несколько дней я её всё-таки собрал и запустил.

Это и положило начало проекта «Переговорки».

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

Через несколько дней после того как я в первый раз скомпилировал libmapi, тестовые утилиты на Си благополучно подключились к Exchange и уже умели отпределять дают им правильные данные для авторизации или нет. Эти несколько дней ушли на то, чтобы разобраться как создавать профиль, как настраивать библиотеку и Samba4 для работы и так далее. Оставалось как-то состыковать всё это дело с Python.

1275 строк основного модуля, 647 строк файла описаний структур MAPI, 4046 констант, 457 строк для описания Pid-свойств. А сколько не заработало, сколько пришлось переписать. Самая рутинная работа была — искать по всем исходникам libmapi и Samba описания структур, пытаться сообразить как их переписать на Python и состыковать со всеми остальными. Ребята, придумавшие модуль ctypes для Python, большие молодцы.Правда мне пришлось придумать и реализовать парсер для расширенного синтаксиса, чтобы описывать динамические структуры MAPI, не знаю как сейчас, а тогда libmapi не умел читать повторяющиеся встречи (кажется, и теперь не умеет), так что читать и описывать их приходилось на самом низком уровне, а значит как-то хранить эти данные и уметь ими манипулировать.

Ничего, пара дней корпения над бинарной распечаткой с последующими уточнениями в противоречивой документацией от Microsoft и я написал класс, который такие встречи умеет разбирать. Впрочем, это было уже позднее, а сначала надо было догадаться что делать после логина. Из документации у libmapi — только перечень функций и исходники на Си. Пришлось внимательно просматривать первое и читать второе. Тут нельзя не упомянуть Сергея Андрюхина, несколько очень ценных его идей и исследований вошли в тот код. Кстати, местами исходники на Си написаны так плохо, что приходилось упрощать код — не всё, что можно позволить себе в Си, можно позволить себе в менее шустром «Пайтоне».

Разобраться с порядком вызова этого великолепия было непросто, плюс ещё множество более мелких проблем, вроде странного формата хранения времени (Exchange хранит число минут с 1 января 1601 года). Плюс утечки libmapi, которые пришлось изолировать, решать проблему с передачей данных в родительский процесс, мучаться с Python 2.4, который, как оказалось, никогда не освобождает память, выделением памяти на куче «про запас» через Python API (спасибо ctypes), определением по косвенным признакам, что память закончилась, пора отдать данные в родительский и умереть, и так далее.

Сначала мы научились читать freebusy информацию — просто даты и время, когда переговорка чем-то занята, потом уже — заходить в папку переговорки и читать её календарь.

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

Спасибо Саше Покатилову, который правил мой код, пока я был в отпуске и занимался Django-частью проекта, с ним же мы проектировали базу. Например, мы решили просто размножать повторяющиеся встречи на год вперёд, избежав тем самым множество проблем. Год — это так называемое «окно бронирования», дальше этого срока наш Exchange встречи не видит. Каждый день Сашин механизм достаточно оптимально размножал всречи ещё на один день дальше, мой класс при помощи полезнейшего модуля dateutil.rrule переводил по достаточно хитрым правилам повторяющиеся встречи Exchange в список дат.

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

Думаю, если попробовать вспомнить всё, хватит на небольшую книгу… Я рад, что эту невероятную задачу удалось решить. С моей стороны, как мне кажется, это 100% моей «мощности» как программиста. Будущее покажет, но пока все задачи я буду оценивать по этой.

К счастью, в новом Exchange, который появился у нас в компании, появилась поддержка SOAP и «Переговорки» переделали с использованием этого протокола. Есть предварительное согласие на выкладывание модуля, который у меня тогда получился в интернет. Пожалуй, сейчас он бесполезен (хотя, может авторы libmapi запрограммируют на его примере чтение повторяющихся встреч, попробую им его кинуть), но пусть он всё равно полежит у меня, как памятник полугоду моей жизни.

2009   libmapi   яндекс

libmapi: почему так долго?

Вчера один из моих читателей вскользь поинтересовался — неужели с libmapi так сложно работать.

Для примера, чтение данных переговорок (включая рекуррентные встречи), занимает сейчас у меня в «Пайтоне»: 1275 строк основного файла модуля, 647 строк файла описаний структур MAPI, 4046 строк констант MAPI, плюс 457 строк файла данных для Pid-свойств.

2009   libmapi

libmapi: насколько всё запущено

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

Однако к MAPI это отношения не имеет

Цитата из книги «Основы MAPI» Ирвинг де ла Круз, Лес Талер, вырванная из контекста.

2009   libmapi

Python 2.4 не освобождает память

Как я уже не раз говорил, libmapi нещадно течёт. Я сейчас работаю с версией из транка, 921-й комит, это довольно старая версия (текущий коммит — 1303), но утечки находят до сих пор и впереди их немало. Память течёт быстро и при получении больших объёмов данных утекает раньше, чем данные удаётся получить.

Поэтому я изолирую libmapi в отдельном процессе, получаю данные, пока могу и отдаю их «наверх» — родителю, потом делаю новый процесс и получаю следующую пачку. Для того, чтобы отдать данные родителю я использую os.pipe, а данные туда можно передавать только строкой. Так как данные у меня сложные, я их сериализую при помощи cPickle, а для сериализации нужна память.

Где взять память, если всё, что было доступно процессу, съела libmapi? Я пытался выделать память при помощи list(range(20 * 1024 * 1024)), а перед сериализацией удалять, но это не работало. Сегодня я узнал почему. Оказывается Python до версии 2.5 никогда не освобождает память. У меня как раз Python 2.4, приехали.

P.S. Чтобы предотвратить вопросы для чего я делаю list(range(…)), расскажу сразу: для совместимости с Python 3.0, на который мы все когда-нибудь перейдём. Там range возвращает итератор.

Добавлено позднее.

Я сейчас немного поисследовал, сделал замеры и выяснил, что если обращаться к аллокатору памяти Python напрямую (например, через ctypes), то память возвращается:

from ctypes import *

mem = pythonapi.PyMem_Malloc(c_size_t(100*1024*1024))

pythonapi.PyMem_Free(mem)
2009   libmapi   python

Libmapi: размер поля

распечатка рекуррентного события (81.80КиБ)

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

Вывод, кажется, такой: в документации от Microsoft поле ReminderSet имеет тип boolean и размер 8 бит, вот по данным кажется, что оно всё-таки должно иметь размер 32 бита. В таком случае, всё пока встаёт на место. Сейчас поправлю и буду ещё тесты гонять.

P.S. Пока тесты не падают. Значит размер поля ReminderSet в структуре ExceptionInfo действительно 32 бита (это структура внутри PidLidAppointmentRecur).

2009   libmapi

Libmapi: повторяющиеся встречи

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

Так что, увы, придётся получать поле PidLidAppointmentRecur (не забудьте, что его ID нуждается в трансляции через mapi_nameid_GetIDsFromNames) и самостоятельно его парсить. Документация от Microsoft есть — документ MS-OXOCAL. Поздравьте, я справился.

2009   libmapi

Основы MAPI. Ирвинг де ла Круз, Лес Талер

Нашёл в интернете, в интернет-магазине «Магистр» Ростова-на-Дону ровно одну книжку «Основы MAPI» 1997-го года выпуска. Книжка настоящий раритет: одна на весь интернет и та побывала в воде. Купил с доставкой всего за каких-то 190 рублей.

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

2009   libmapi

Спецификации Microsoft

Читаю спецификацию Microsoft про рекуррентные встречи в Exchange ([MS-OXOCAL]):

SHOULD be 0x00002023 but MAY be 0xFFFFFFFF

Т. е. «должно быть 0x00002023, но может быть 0xFFFFFFFF».

2009   libmapi

CAL? SAKA?

В исходниках libmapi (файл gen_ndr/property.h) обнаружилась константа CAL_SAKA. Константа обозначает, что выбран индийский календарь, константа читает как «кал сака». Гыгы.

2009   libmapi

Магия libmapi

Можно я опять про libmapi?

В общем, из libmapi мне приходят два магических числа, они содержат диапазон — начало и конец периода, данные за который возвращает функция GetUserFreeBusyData. По внешнему виду видно, что это не unix timestamp (для несведующих, Unix timestamp — это число секунд с 1 января 1970 года).

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

// в t приходит <магическое число>

uint32_t t;
uint64_t time;
struct timespec ret;

time = t
time *= 60;
time *= 10000000;

ret.tv_nsec = (long) ((time % (1000 * 1000 * 10)) * 100);
d /= 1000*1000*10;

d -= 11644473600L;

// тут ещё парочка условий, которые не добавляют смысла

ret.tv_sec = (time_t)d;

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

Так что мой итоговый код на Python выглядит (без системных вызовов) так:

publish_start, publish_end = map(
    lambda x: x * 60 - 11644473600, # 369 лет
     (publish_start, publish_end)
)
2009   libmapi
Ранее Ctrl + ↓