Я работаю в небольшом израильском стартапе, наш продукт — платформа для заказа еды из ресторанов, кафе и магазинов. В отличие от десятков подобных сервисов, мы монополисты на студенческом рынке в США. Мы обрабатываем на пике несколько сотен тысяч заказов в день и один из платежных шлюзов в продакшне построен на автоматизации GUI для Win32 приложения с помощью библиотеки pywinauto.
Во-первых, почему мы работаем только в университетах? Многие идеи часто хорошо выглядят только в перспективе. Например, та же идея доставки еды. Кажется, здорово написать приложение, чтобы можно было заказать из любого ресторана, не париться с оплатой и т.д. Все, кто пытается эту идею реализовать, натыкаются на так называемую Сhicken or the egg problem. Пока в приложении мало ресторанов, оно бессмысленно; а пока пользователей немного, рестораны не спешат интегрировать новое приложение в свой рабочий процесс. Обычно на этом моменте все умирает. Эта ситуация не уникальна для приложения для заказа еды.
Решением подобной проблемы обычно бывает некая гибридная модель, когда, кроме грядущей пользы от социализации, есть польза для каждого отдельного юзера. Например, Waze. Сейчас это приложение, которое знает обо всех пробках, авариях и полиции на дороге, но начинали они как бесплатное GPS-приложение, от которого польза была каждому. Бывают и другие решения, про одно из них расскажу на нашем примере.
Упершись в эту проблему, нас ждала та же судьба. Мы довольно безуспешно пытались пробиться на рынке Сан-Франциско, где полно подобных решений, и, прежде чем тихо умереть, решили попробовать еще одну идею. Она заключалась в том, чтобы не пытаться договориться с всеми ресторанами, а только с некой площадкой, у которой есть рычаги давления на локальные бизнесы.
Выбор пал на университеты. Предлагая процент от дохода и сервис для студентов, мы были отличным предложением для университета, проблемы локальных ресторанов, связанные с интеграцией нового приложения в свой бизнес процесс, их не интересовали, для них мы были прямой и косвенной выгодой. Они обязывали бизнес на территории кампуса работать с нашим приложением. Мы интегрировались во все рестораны на территории университета — для студентов наше приложение становилось очень удобным: можно заказывать обед во время лекций и не ждать своего заказа на перемене. Мы моментально захватываем большую часть кампуса, причем вирусный эффект очень велик. Когда человек сиротливо ждет в очереди, а рядом приходят люди, берут свой готовый заказ и сразу уходят, чаще всего наше приложение скачивают прямо на месте.
В какой-то момент у нас возникла проблема. Дело в том, что в Америке очень непросто получить кредитную карту, и у большинства студентов ее нет. С другой стороны студенческие удостоверения выдаются в виде пластиковых карт с магнитной полосой и являются инструментом оплаты внутри кампуса. Университет начисляет на них что-то вроде стипендий. Эти средства более структурированы: там есть отдельные части, которые можно тратить свободно, а есть средства только на еду. И эти карты принимают только на территории кампуса.
Мы, как компания, которая работает со студентами, конечно же хотели обслуживать эти карты. Но не все так просто. Компания, выпускающая эти карты — это не банк. Рынок обслуживания студенческих карт практически монополизирован и разделен между тремя компаниями на сотни университетов по всей Америке.
Одна из этих компаний пыталась запустить сервис, подобный нашему, и, проиграв конкуренцию, в лучших традициях песочных площадок «обиделась» и отказалась вести с нами любое сотрудничество. Дело в том, что по договору с университетом, бизнесу, имеющему право принимать оплату через студенческие карточки, компания-эмитент обязана предоставить терминал с десктопным приложением, куда можно ввести номер студенческой карты и снять деньги. Это очень удобно для всяких киосков и сосисочных. А вот API массового обслуживания по договору предоставлять не обязательно, и открывать они его нам не захотели. Пришлось работать с тем, что есть. В итоге мы нашли довольно необычное решение для платежного шлюза, которое за первые несколько месяцев своего существования довольно успешно работает в 6 университетах: проведены десятки тысяч транзакций на сотни тысяч долларов, а в следующем учебном году это решение будет масштабировано на несколько десятков американских университетов с оборотом средств в десятки миллионов долларов.
Как это работает?
О нашем решении. Должен заметить, что нам очень важно было найти решение, которое абсолютно легально. Например, Reverse Enginering — это нелегально, снифить трафик и симулировать фронтенд этого кассового терминала тоже может быть проблематично, а вот автоматизировать клики по десктопному приложению — абсолютно легально. В кампусе нам выделяется виртуальная машина на Windows с установленным десктопным кассовым приложением. Там же устанавливается связка Flask + Tornado. Когда пользователь с телефона оформляет заказ, на виртуальную машину приходит запрос с всеми необходимыми параметрами: сумма, номер студенческой карточки и т.п. Далее с помощью pywinauto мы вводим сумму, номер карточки, все необходимые параметры (там довольно сложная логика в плане скидок, бесплатных обедов в определенную часть дня и т.д.). Проводим транзакцию, проверяем результат и возвращаем ответ на сервер. На обработку одной транзакции изначально уходило около 20 секунд, но в итоге удалось сократить до 3.
При профилировании выяснились некоторые особенности библиотеки.
Вызовы Application().Connect() для подключения к приложению имеют много различных параметров и, например, идентификация приложения или окна по class_name работает в 20 раз быстрее, чем по по title_re (регулярное выражение заголовка окна).
Вторым неожиданным моментом оказалось, что если окно еще не открылось, то вызов Connect() берет много времени (до нескольких секунд), прежде чем бросить исключение. Его не стоит вызывать не будучи уверенным, что окно уже открыто, лучше найти эвристическую модель, которая позволит понять, что вызов будет успешным. В моем случае при открытии нового окна форма главного приложения прекращала быть 'active', это можно было отловить c помощью вызова form.WaitNot('active'), после возвращения которого можно смело вызывать Connect().
Ещё одна из проблем, с которой мы столкнулись: автоматизация через Win32 API не работает без открытой пользовательской сессии на машине. Это общая проблема для всех таких инструментов. Например, в библиотеке pywinauto не работают методы ClickInput и TypeKeys, если сессия залочена или RDP подключение свернуто. Это было мое первое в жизни знакомство с автоматизацией GUI и Win32 API, так что решение проблемы могло сильно затянуться. К счастью, у библиотеки замечательный мэйнтейнер vasily-v-ryabov. Выражаю ему огромную благодарность за подробные консультации.
Мы нашли решение всем проблемам, в основном многие вопросы решали альтернативными вызовами. Например, когда встретился нестандартный виджет переключения табов в приложении, который не реагировал ни на один стандартный вызов, решение нашлось путем анализа Windows Messages в Spy++ и отправки тех же сообщений с помощью метода PostMessage в pywinauto.
Библиотека активно развивается и, как показала практика, достаточно стабильна даже для того, чтобы использовать ее в продакшне для реализации платежного шлюза. Почти все проблемы, связанные с урезанными функционалом при отсутствии сессии, будут решены в следующем релизе (0.6.0), который готовится к выходу в этом году (ветка UIA на гитхабе). Например, метод send_chars позволит успешно передавать почти любые комбинации символов в неактивное или даже свёрнутое окно. И самое главное, в ветке UIA есть поддержка технологии Microsoft UI Automation (для WinForms, WPF, StoreApps, Qt и браузеров), но это уже тема отдельной статьи.
Если вам показалось, что реализация платежного шлюза путем автоматизации GUI — это не верх сюрреализма, то могу добавить, что один из университетов Техаса имеет очень жесткие ограничения по доступу к внутренней сети, так что деплой, дебаг и апгрейд модуля, который обрабатывает платежи десятков тысяч людей происходит по скайпу — «А теперь напиши git fetch» — и все в таком духе. Между прочим аптайм у нас > 99.6%. Спасибо pywinauto).
Автор: yabloki