Русский
Русский
English
Статистика
Реклама

Разработка под windows

Update Tuesday Microsoft выпустила апрельские обновления безопасности

14.04.2021 14:22:42 | Автор: admin

Microsoft выпустила плановые обновления безопасности, закрывающие 114 уязвимостей, включая 6 уязвимостей в Microsoft Edge и 4 уязвимости в Exchange Server. 19 уязвимостей были классифицированы как Критические и 88 как Важные. Среди закрытых уязвимостей две были обнародованы публично, а эксплуатация одной из этих уязвимостей была зафиксирована в реальных атаках (0-day).

В данной статье я расскажу о самых главных моментах этого выпуска.

Сводная информация по количеству и типу уязвимостей в соответствующих продуктах приведена на графике.

На следующие уязвимости и обновления безопасности следует обратить особое внимание.

Была закрыта критическая уязвимость удаленного исполнения кода CVE-2021-28329 в компонентах среды исполнения RPC), которой подвержены все поддерживаемые версии Windows и Windows Server. Рейтинг CVSS составил 8.8, а согласно индексу эксплуатации у атак с использованием данной уязвимости низкий уровень вероятности.

Апрельские обновления также исправляют важную уязвимость повышения привилегий CVE-2021-28313 в компонентах Windows Diagnostics Hub. Данная уязвимость затрагивает последние версии Windows 10 и Windows Server 20H2, 2004, 1909. Рейтинг CVSS составил 7.8, а согласно индексу эксплуатации у атак с использованием данной уязвимости низкий уровень вероятности.

Также была закрыта важная уязвимость повышения привилегий CVE-2021-28347 в компонентах среды исполнения Windows Speech. Уязвимости подвержены все поддерживаемые версии Windows 10. Рейтинг CVSS составил 7.8, а согласно индексу эксплуатации у атак с использованием данной уязвимости низкий уровень вероятности.

Устранена важная уязвимость удаленного исполнения кода CVE-2021-27091 в компонентах службы RPC Endpoint Mapper, которая была обнародована публично. Данной уязвимости подвержена только ОС Windows Server 2012. Рейтинг CVSS составил 7.8, а согласно индексу эксплуатации у атак с использованием данной уязвимости низкий уровень вероятности.

Также обновления закрывают критическую уязвимость удаленного исполнения кода CVE-2021-27095 в видео-декодере Windows Media, которой подвержены все версии Windows. Рейтинг CVSS составил 7.8, а согласно индексу эксплуатации у атак с использованием данной уязвимости низкий уровень вероятности.

Была закрыта важная уязвимость удаленного исполнения кода CVE-2021-28445 в компонентах Windows Network File System (NFS. Данной уязвимости подвержены все ОС Windows кроме Windows 10 и Windows Server 1803. Рейтинг CVSS составил 8.1, а согласно индексу эксплуатации у атак с использованием данной уязвимости низкий уровень вероятности.

Устранена важная уязвимость удаленного исполнения кода CVE-2021-28451 в приложении Microsoft Excel. Данной уязвимости подвержены Microsoft Office 2019 for Windows/Mac, Microsoft Excel 2013-2016, Microsoft 365 Apps for Enterprise, Microsoft Office Online Server, Microsoft Office Web Apps Server 2013. Рейтинг CVSS составил 7.8, а согласно индексу эксплуатации у атак с использованием данной уязвимости низкий уровень вероятности.

Замыкает команду лидеров апрельского выпуска уязвимость нулевого дня, уже использующуюся в атаках это важная уязвимость повышения привилегий CVE-2021-28310 в компонентах Win32k. Данная уязвимость затрагивает версии Windows 10 и Windows Server 20H2, 2004, 1909, 1809, 1803. Рейтинг CVSS составил 7.8.

Отдельного внимание заслуживают ряд уязвимостей в почтовом сервере Microsoft Exchange. На этот раз затронуты все поддерживаемые версии Exchange Server 2013, 2016, 2019.

Примечание: помимо плановых обновлений в марте был внеплановый выпуск обновлений безопасности для локальных версий Microsoft Exchange Server 2010, 2013, 2016, 2019. Подробности об этом выпуске можно получить в нашем блоге.

Все 4 уязвимости в апрельском выпуске CVE-2021-28480, CVE-2021-28481, CVE-2021-28482, CVE-2021-28483 были классифицированы как Критические, и потенциально позволяют злоумышленнику удаленно выполнить произвольный код на почтовом сервере.

Информация о данных уязвимостях не была обнародована публично и на данный момент не было зафиксировано использование рабочих эксплоитов в реальных атаках. Максимальный рейтинг CVSS среди уязвимостей составил 9.8 из 10 (самый низкий рейтинг 8.8) и согласно индексу эксплуатации у атак с использованием этих уязвимостей высокий уровень вероятности.

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

  • Exchange Server 2013 CU23

  • Exchange Server 2016 CU19/CU20

  • Exchange Server 2019 CU8/CU9

Все подробности вы можете узнать в блоге команды Microsoft Exchange.

Остальные уязвимости были исправлены в компонентах Microsoft Edge (на движке Chromium), Microsoft Office (приложения и веб-сервисы), SharePoint Server, Visual Studio, VS Code, Azure DevOps Server, Azure Sphere, GitHub Pull Requests and Issues Extension и Maven Java Extension.

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

Также хочу напомнить, что в марте закончилась поддержка старой версии браузера Microsoft Edge (на движке EdgeHTML). При установке апрельского накопительного пакета старый браузер будет заменен новой версией Microsoft Edge на движке Chromium. Подробности - в нашем блоге.

Обновления стека обслуживания

Обновления Servicing Stack Updates (SSU) были выпущены для всех поддерживаемых ОС: Windows 10 версии 1809, 1909, 2004, 20H2 и Windows Server версии 2019, 1909.

А для версий 2004, 20H2 обновления SSU теперь поставляются в едином накопительном пакете вместе с остальными обновлениями. О том, как установить сразу и SSU, и обновления безопасности ОС в одном накопительном пакете, автоматически, соблюдая правильную последовательность, читайте в нашем блоге.

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

Вы также можете посмотреть запись нашего ежемесячного вебинара "Брифинг по безопасности" с более подробным разбором этого выпуска обновлений безопасности Microsoft.

А для того, чтобы быть в курсе самых актуальных новостей информационной безопасности Microsoft, подписывайтесь на канал https://aka.ms/artsin.


Помните, почти 90% всех уязвимостей уже имели патчи от производителей на момент своего обнародования*, поэтому так важно не просто обновлять ваше ПО, но и делать это своевременно.

Автор: Артём Синицын CISSP, CCSP, MCSE, MC: Azure Security Engineer
старший руководитель программ информационной безопасности в странах Центральной и Восточной Европы
Microsoft

Twitter: https://aka.ms/artsin
YouTube: https://aka.ms/artsinvideo

*Vulnerability Review Report by Flexera

Подробнее..

Перевод Протекающие абстракции и код, оставшийся со времён Windows 98

09.06.2021 14:20:05 | Автор: admin

В конце 1990-х команды разработчиков Windows Shell и Internet Explorer внедрили множество потрясающих и сложных структур, позволяющих использовать расширение оболочки и браузера для обработки сценариев, создаваемых третьими сторонами. Например, Internet Explorer поддерживал концепцию подключаемых протоколов ("Что если какой-то протокол, допустим, FTPS станет таким же важным, как и HTTP?"), а Windows Shell обеспечивала чрезвычайно гибкое множество абстрактного использования пространств имён, что позволяло третьим сторонам создавать просматриваемые папки, в основе которых не лежит файловая система от WebDAV ("ваш HTTP-сервер это папка") до папок CAB ("ваш архив CAB это папка"). Работая в 2004 году проект-менеджером в команде по созданию клипарта, я создал приложение .NET для просмотра клипарта прямо из веб-сервисов Office, и набросал черновик расширения Windows Shell, благодаря которому бы казалось, что огромный веб-архив клипарта Microsoft был установлен в локальной папке системы пользователя.

Вероятно, самым популярным (или печально известным) примером расширения пространства имён оболочки является расширение Compressed Folders, обрабатывающее просмотр файлов ZIP. Compressed Folders, впервые появившиеся в составе Windows 98 Plus Pack, а позже и в Windows Me+, позволяли миллиардам пользователей Windows взаимодействовать с файлами ZIP без скачивания стороннего ПО. Вероятно, это может вас удивить, но эта функция была куплена у третьих лиц Microsoft приобрела интеграцию для Explorer, представлявшую собой побочный проект Дэйва Пламмера, а лежащий в её основе движок DynaZIP разработала компания InnerMedia.

К сожалению, этот код уже давно не обновляли. Очень давно. Судя по временной метке модуля, последний раз он обновлялся на День святого Валентина в 1998 году; я подозреваю, что с тех пор в него вносили незначительные изменения (и одну функцию поддержку имён файлов в Unicode, работающую только для извлечения), но всё равно не секрет, что, как сказал Реймонд Чен, этот код "остался на стыке веков". Это значит, что он не поддерживает такие современные функции, как шифрование AES, а его производительность (время выполнения и степень сжатия) сильно отстают от современных реализаций, созданных третьими сторонами.

Тогда почему же его не обновляли? Отчасти в этом виноват принцип "не сломано не чини": реализация ZIP Folders выживала в Windows в течение 23 лет, и при этом вопли пользователей не становились невыносимыми, то есть их вполне всё устраивало.

К сожалению, есть вырожденные случаи, в которых поддержка ZIP оказывается по-настоящему поломанной. С одной из них я столкнулся на днях. Я увидел интересный пост в Twitter о шестнадцатеричных редакторах с возможностью аннотаций (что полезно при исследовании форматов файлов) и решил попробовать некоторые из них (я решил, что больше всего мне нравится ReHex). Но в процессе этого исследования я скачал portable-версию ImHex и попробовал переместить её в папку Tools на своём компьютере.

Я дважды щёлкнул по файлу ZIP размером 11,5 МБ, чтобы открыть его. Затем я нажал CTRL+A, чтобы выбрать все файлы, а затем (это важно) нажал CTRL+X, чтобы вырезать файлы с буфера обмена.


Затем я создал новую папку внутри C:\Tools и нажал CTRL+V, чтобы вставить файлы. И тут всё пошло наперекосяк Windows больше минуты отображала окно "Calculating", но кроме создания одной подпапки с одним файлом на 5 КБ больше ничего не происходило:


Чего? Я знал, что движок ZIP, который используется в ZIP Folders, не был оптимизирован, но раньше я никогда не видел ничего настолько плохого. Спустя ещё несколько минут распаковался ещё один файл на 6,5 МБ:


Безумие какое-то. Я открыл Диспетчер задач, но никакие процессы не занимали мой 12-поточный процессор, 64 ГБ памяти и NVMe SSD. Наконец, я открыл SysInternals Process Monitor, чтобы разобраться, в чём дело, и вскоре увидел первоисточник происходящего.

После нескольких мелких операций считывания из конца файла (где у файла ZIP хранится индекс), весь файл размером 11 миллионов байт считывался с диска по одному байту за раз:


Присмотревшись повнимательнее, я понял, что почти все операции считывали по одному байту, но время от времени после считывания определённого байта выполнялось считывание 15 байт:


Что же находится в этих любопытных смещениях (330, 337)? Байт 0x50, то есть буква P.


В прошлом мне доводилось писать тривиальный код для восстановления ZIP, поэтому я знал, в чём особенность символа P в файлах ZIP это первый байт маркеров блоков формата ZIP, каждый из которых начинается с 0x50 0x4B. По сути, код считывает файл от начала до конца в поисках конкретного блока размером 16 байт. Каждый раз, когда он встречает P, то просматривает следующие 15 байт, чтобы проверить, соответствуют ли они нужной сигнатуре, и если нет, то продолжает побайтовое сканирование в поисках новой P.

Есть ли что-то особенное в этом конкретном файле ZIP? Да.

Формат ZIP состоит из последовательности записей файлов, за которой идёт список этих записей файлов (Central Directory).

Каждая запись файла имеет собственный локальный заголовок файла, содержащий информацию о файле, в том числе размер, размер в сжатом виде и CRC-32; те же данные повторяются в Central Directory.

Однако формат ZIP позволяет локальным заголовкам файлов пропускать эти данные и записывать их как хвост после сжатых данных. Эта функция полезна при потоковом сжатии мы не знаем окончательный размер в сжатом виде, пока не закончим сжимать данные. Вероятно, большинство файлов ZIP не используют эту функцию, однако в скачанном мной файле она использовалась. (Разработчик сообщил, что этот файл ZIP был создан GitHub CI.)

Мы видим, что в заголовке CRC и размеры равны 0, и что они появляются сразу после сигнатуры 0x08074b50 (дескриптора данных (Data Descriptor)), непосредственно перед локальным заголовком следующего файла:


Бит 0x08 во флаге General Purpose обозначает эту опцию; пользователи 7-Zip могут увидеть её как Descriptor в столбце записи Characteristics:


Исходя из размера операции считывания (1+15 байт), я предполагаю, что код подстраивается под блоки Data Descriptor. Почему он это делает (вместо того, чтобы просто считать те же данные из Central Directory), я не знаю.

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

В конечном итоге, после 85 миллионов однобайтных считываний монитор процессов зависает:


Перезапустив его и сконфигурировав Process Monitor с Symbols, мы можем исследовать эти однобайтные операции считывания и получить представление о том, что же происходит:


Функцию GetSomeBytes перенагружают вызовы, передающие однобайтный буфер в коротком цикле внутри функции readzipfile. Но если посмотреть ниже по стеку, то становится очевидной первопричина этого хаоса это происходит потому, что после перемещения каждого файла из ZIP в папку файл ZIP должен обновиться для удаления перемещённого файла. Этот процесс удаления по сути своей не быстр (поскольку он приводит к изменению всех последующих байтов и обновлению индекса), а его реализация в функции readzipfile (с этим однобайтным буфером чтения) и вовсе чудовищно медленна.

Если вернуться назад, то стоит заметить, что я нажал CTRL+X, чтобы вырезать файлы, что привело к операции перемещения. Если бы вместо этого я нажал CTRL+C для копирования файлов, то ZIP не выполнял бы операцию удаления при извлечении каждого файла. Время, необходимое для распаковки файла ZIP снизилось бы с получаса до четырёх секунд. Для сравнения: 7-Zip распаковывает файл меньше чем за четверть секунды, хоть и немного жульничает.

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

Хотя довольно легко придумать способы значительного улучшения производительности в этой ситуации, подобный прецедент даёт понять, что вероятность совершенствования кода в Windows мала. Возможно, код изменят к его 25-й годовщине?



На правах рекламы


Если для работы необходим сервер на Windows, то вам однозначно к нам. Создавайте собственную конфигурацию в пару кликов, автоматическая установка винды на тарифах с 2 vCPU, 4 ГБ RAM, 20 ГБ NVMe или выше.

Присоединяйтесь к нашему чату в Telegram.

Подробнее..

Перевод Если вы пишете код в Windows, вы заслуживаете лучшего терминала

17.04.2021 18:19:17 | Автор: admin

Я хочу сделать признание. Когда дело доходит до моего компьютера, я оставляю все в значительной степени сыром виде. Конечно, у меня есть любимые маленькие инструменты. Я использую плагины Chrome, такие как Wappalyzer, и множество расширений VS Code, таких как Chrome Debugger и Live Server. Но я сознательно не использую темы, шрифты, средства форматирования и другие приятные для глаз настройки. В далеком прошлом, когда я только начинал программировать, я тратил слишком много времени на перестройку своей индивидуальной настройки на разных компьютерах и на новом оборудовании. Постоянные настройки устарели, поэтому я решил по возможности сократить до стокового.

Это мое оправдание, почему я провел много месяцев, по большей части игнорируя продукт Microsoft Windows Terminal. В конце концов, время, которое я провожу в командной строке, ограничено и ничем не примечательно. Я настраиваю свое приложение, устанавливаю пакеты npm или Nuget и двигаюсь дальше. Проводить время в окне терминала означает заходить в темный угол операционной системы и делать то, что нужно.

Но теперь я вынужден признать, что был неправ. Или, по крайней мере, есть еще один инструмент, для которого мне нужно освободить место. Поскольку Windows Terminal не просто заменяет скрипучую часть программного обеспечения ОС с кодовой базой 30-летней давности, он также добавляет некоторые действительно практичные функции.

Кодовой базе Windows Console 30 лет на самом деле она старше, чем разработчики, которые сейчас над ней работают. - Рич Тернер, менеджер по Microsoft

Терминал открыт

Прежде чем продолжить, стоит сделать краткий обзор того, что такое терминал на самом деле. Это потому, что мы, разработчики, работающие в Windows, привыкли объединять терминалы и программы оболочки в одну расплывчатую идею. Например, вы можете подумать, что когда Windows перешла со старой командной строки на объектно-ориентированную оболочку PowerShell, она заменила программу терминала. Но на самом деле это неправда.

Терминал - это часть программного обеспечения, которое обрабатывает текстовый ввод и отображение. Вы вводите текст в командной строке терминала. Вы смотрите в окно терминала. Но за кулисами ваш терминал взаимодействует с программой оболочки, которая действительно выполняет эту работу. В современной Windows стандартная программа терминала - ConHost.exe, и, черт возьми, она старая.

Вы думаете, что запускаете PowerShell, но на самом деле вы запускаете интерфейс ConHost, который взаимодействует с PowerShell.

Microsoft очень не хочет что-либо менять в работе ConHost, потому что это стержень вековой обратной совместимости. Фактически, основной принцип дизайна ConHost - не нарушать обратной совместимости любой ценой. Даже исправление ошибок рискует уничтожить век сценариев и инструментов, которые каким-то образом все еще работают в режиме совместимости в современной Windows.

Вместо этого Microsoft начала создавать новый терминал под названием Windows Terminal. Он существует уже почти год, но еще не дошел до включения в ОС Windows. Это означает, что если вам нужен Терминал Windows, вы должны установить его из Windows Store. (Или вы можете загрузить его с GitHub и собрать самостоятельно, потому что новый терминал, естественно, имеет открытый исходный код.)

Почему терминал Windows?

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

  • Несколько вкладок. Помните, когда в веб-браузерах была только одна вкладка? Как мы это ненавидели! Но мы терпели это в ConHost уже целое поколение. К счастью, Windows Terminal позволяет открывать столько вкладок, сколько нужно в одном окне.

    Иногда мелочи - это большие делаИногда мелочи - это большие дела
  • Несколько панелей. Это похоже на несколько вкладок, но вы можете видеть разные экземпляры терминала в аккуратном порядке бок о бок или сверху и снизу. И вы управляете всем этим с помощью удобных нажатий клавиш. Удерживая Alt + Shift, нажмите +, чтобы открыть новую панель справа, или -, чтобы открыть новую панель внизу. Затем вы можете переходить с панели на панель, удерживая Alt и нажимая клавиши со стрелками. Круто!

  • Одновременное использование нескольких оболочек. Терминал Windows поддерживает любую стандартную программу оболочки. Вы можете использовать старую добрую PowerShell, почти устаревшую командную строку, Azure Cloud Shell (для управления онлайн-ресурсами Azure) и даже bash, если вы включили Windows Linux Subsystem. И вы можете запускать их все рядом, на разных вкладках или панелях одного и того же окна Терминала Windows.

    Оболочки сошли с умаОболочки сошли с ума
  • Масштабирование, которое работает. Мое любимое сочетание клавиш масштабирования - удерживать Ctrl и вращать колесико мыши. Это работает и в ConHost, но при этом неудобно изменяет размер окна. Терминал Windows масштабирует более разумно, и он распознает удобное сочетание клавиш Ctrl + 0, чтобы вернуть все в нормальное состояние. И не повредит, что Windows Terminal поставляется с новым элегантным шрифтом Cascadia Code, который отлично смотрится при любом размере.

  • Современный курсор. Что это за блочная штука в ConHost? Он показывает вашу текущую позицию, а не точку вставки, поэтому легко забыть, если нажатие клавиши вставляет до или после текущего символа.

  • Изобилие настроек. Все они управляются через немного непонятный файл настроек JSON. Освойте его, чтобы управлять внешним видом окна терминала (размером, цветами, настройкой всегда поверх) и добавьте свои собственные сочетания клавиш.

Пользователи Linux скажут вам, что многие из этих функций у них были в течение многих лет. Опытные разработчики Windows скажут вам то же самое, потому что они уже используют какую-то альтернативу терминалу с открытым исходным кодом. В основном они правы, но теперь вам не нужно игнорировать их в тихом смущении.

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

  • Настраиваемая прозрачность с размытием фона. Вы даже можете настроить его на лету, удерживая Ctrl + Shift и вращая кнопку мыши. Но зачем?

  • Цветовые схемы и пользовательские фоновые изображения.

  • Анимированные фоны в формате GIF. (Привет, Windows Plus примерно из 1998 года.)

Конечно, если вы решите использовать эти функции, я не буду судить.

Краткое примечание о терминале VS Code

Если вы используете Visual Studio Code, вы, вероятно, знакомы с его интегрированным терминалом. Вы можете выбрать, какую оболочку использовать (например, PowerShell или bash), но вы всегда используете терминал VS Code, а не ConHost.

Тем не менее, терминал VS Code довольно прост. Терминал Windows не может заменить встроенный терминал. Однако вы можете настроить Windows Terminal так, чтобы он работал как внешний терминал для VS Code. Таким образом, когда вы запускаете терминал из VS Code, вы откроете отдельное окно Windows Terminal, что даст вам больше места для передышки и современные функции, которые вам действительно нужны.

Последнее слово

Терминал Windows неуклонно продвигается к версии 2.0, которая ожидается этой весной, и в конечном итоге включение в Windows. Планируется длинный список новых функций, включая возможность отрывать вкладки и перемещать их из одного окна терминала. к другому, бесконечная прокрутка и приятный пользовательский интерфейс для управления настройками. Будет ли он вызывать безумную любовь, как VS Code или язык C #? Нет. Но иногда достаточно сделать жизнь менее болезненной.

Скачать Windows Terminal можно здесь.

Подробнее..

Создаём по-настоящему надёжные плагины на платформе Managed Add-In Framework

22.04.2021 12:11:11 | Автор: admin

Однажды мы поняли, что для качественной и быстрой реализации разносторонних требований пользователей нам срочно нужны плагины. Изучив разнообразные платформ, мы выяснили, что наилучшим образом нам подойдёт Managed Add-In Framework (далее MAF) от Microsoft. Во-первых, она позволяет создавать плагины на базе .NET Framework, во-вторых, даёт возможность обмена данными и пользовательским интерфейсом между плагином и приложением-хостом, и в-третьих, обеспечивает безопасность и версионность, что делает плагины надёжными.

Жизнь показала, что мы были правы плагины работают, пользователи довольны, заказчик счастлив. Правда, у MAF есть ещё одна проблема недостаточное количество информации. Всё, что мы нашли это скудная документация да несколько постов на StackOverflow. Но этот пробел я частично заполню, описав, как создать плагин с нуля, с учетом всех нюансов работы с MAF. Эта статья будет полезна в качестве быстрого старта для тех, кто тоже решит освоить MAF для создания плагинов на базе .NET Framework.

Введение

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

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

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

  • Поискплагинов, которыепридерживаются контрактов, поддерживаемых хост-приложением.

  • Активация: загрузка, запуск и установление связи с плагином.

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

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

  • Управлениедлительностью использования:предсказуемая и простая в использовании загрузка и выгрузка доменов приложений и процессов.

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

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

Представления, контракт и адаптеры являются сегментами, а связанные друг с другом сегменты являются конвейером (pipeline). Конвейер это основа, за счёт которой реализуется поиск и активация плагинов, изоляция, взаимодействие, управление длительностью использования и версионность.

Довольно часто плагину требуется отображение интерфейса, интегрированного внутри хост-приложения. Так как разные технологии .NET Framework имеют разные реализации пользовательского интерфейса, WPF расширяет модель плагинов .NET Framework, поддерживая отображения интерфейса плагинов.

Технология появилась в рамках .NET Framework 3.5, и с момента первого релиза она почти не развивалась. Из-за этого технология выглядит устаревшей, а её сложность отпугивает. Необходимость реализации 5 промежуточных слоёв между хостом и плагином может показаться избыточной, но именно благодаря этой многослойности MAF обеспечивает безопасность и версионность.

Для примера напишем незатейливое приложение, которое показывает список файлов в указанной папке, а к нему сделаем плагин, который будет конвертировать выбранные файлы в pdf и копировать в другую папку.

Начало

Проекты и структура папок

Для начала нужно посмотреть на структуру подключения плагинов черезMAF:

Представленияхоста(Host views of add-ins)

Содержат интерфейсы или абстрактные классы, представляющие,как выглядит плагин для хоста и какими типами данных они обмениваются.

Контракты(Contracts)

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

Требования: Все контракты должны наследоваться от IСontract, класс ContractBase содержит базовую реализацию IСontract. Для безопасности в контактах допускается использование только типов, унаследованных от IСontract, примитивных типов (целочисленные и булевые), сериализуемых типов (типы из mscorlib.dll; типы, определённые в контрактах, и ссылочные типы), коллекций из mscorlib.dll.

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

Представления плагина(Add-inviews)

Содержат интерфейсы или абстрактные классы, представляющие, как выглядит хост для плагина и какими типами данных они обмениваются; также содержат интерфейс плагина.

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

Адаптерыхоста(Host-side adapters)

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

Адаптеры представления в контракт должны реализовывать соответствующий контракт (а значит, наследоваться от ContractBase), в то время как адаптеры контракта в представление должны реализовывать части представления, которые они конвертируют.

Для конструирования конвейера адаптеры необходимо отметить атрибутом HostAdapterAttribute.

Адаптерыплагина(Add-in-side adapters)

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

Для конструированияконвейера адаптеры отмечаютсяHostAdapterAttribute.

Каждому слою на физическом уровнесоответствуетсвой проект и своя папка(кроме хоста).Создаём все необходимые проекты дляконвейера (тип проектаClassLibrary):

  1. Pipeline.Contracts. Добавляем ссылки на System.AddIn и System.AddIn.Contract.

  2. Pipeline.AddInViews.ДобавляемссылкунаSystem.AddIn.

  3. Pipeline.HostViews.

  4. Pipeline.AddInAdapters.ДобавляемссылкинаSystem.AddIn,System.AddIn.Contract,Pipeline.AddInViews,Pipeline.Contracts.Для двух последних проектов отключаем в свойствах копирование (Copylocal=False),иначеMAFпри сборкеконвейера не сможет понять, какуюdllнадозагрузить в данной папкеи не подгрузит весь уровень.

  5. Pipeline.HostAdapters.Тоже самое, что и дляPipeline.AddInAdapters, только вместо проекта Pipeline.AddInViewsссылаемся наPipeline.HostViews.

  6. DemoPlugin(хост-приложение).

Все эти библиотеки должны располагаться в строгой иерархии папок:

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

Важно: каждый плагин должен лежать в отдельной папке, иначе MAF их не найдётВажно: каждый плагин должен лежать в отдельной папке, иначе MAF их не найдёт

Реализация взаимодействия хоста и плагина

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

[AddInBase]public interface IExportPluginView{  string DisplayName { get; }}

Когда используется MAF, интерфейсы на уровне хоста и плагина могут отличаться, а адаптеры будут подстраивать их друг под друга. Для простоты будем использовать одинаковые интерфейсы на всех уровнях конвейера. Так что создаём IExportPluginView в проектах Pipeline.AddInViews, Pipeline.Contracts и Pipeline.HostViews. Отличия между ними будут в том, что интерфейс в представлении плагина необходимо отметить атрибутом AddInBase, чтобы MAF по нему смог найти нужный плагин, а интерфейс в контрактах должен быть унаследован от IСontract, как упоминалось выше.

В свою очередь, в адаптерах прописываем стыковочные классы адаптеров. Сначала от представления плагина в контракты (проект Pipeline.AddInAdapters):

[AddInAdapter]public class ExportPluginViewToContractAdapter : ContractBase, Pipeline.Contracts.IExportPlugin{  private readonly Pipeline.AddInViews.IExportPluginView view;  public string DisplayName => view.DisplayName;    public ExportPluginViewToContractAdapter(Pipeline.AddInViews.IExportPluginView view)  {  this.view = view;  }}

В качестве аргумента конструктор принимает соответствующий интерфейс представления плагина. Также необходимо отметить этот класс атрибутом AddInAdapter для MAF.

Затем пишем адаптер от контрактов в представление хоста (проект Pipeline.HostAdapters):

[HostAdapter]public class ExportPluginContractToHostAdapter : IExportPluginView{  private readonly Contracts.IExportPlugin contract;  private readonly ContractHandle handle;    public string DisplayName => contract.DisplayName;    public ExportPluginContractToHostAdapter(Contracts.IExportPlugin contract)  {  this.contract = contract;    handle = new ContractHandle(contract);  }}

Здесь используется атрибут HostAdapter, а в качестве аргумента конструктор принимает соответствующий интерфейс контракта конвейера, экземпляр которого контролируется при помощи ContractHandle. Так как плагин и хост чаще всего находятся в разных доменах приложений или даже в разных процессах, стандартная сборка мусора не работает корректно, поэтому используется класс ContractHandle со встроенными токенами для отслеживания получения и освобождения экземпляра контракта.

Активация и интеграция плагина

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

string pipelinePath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "Pipeline"); AddInStore.Update(pipelinePath); 

MAFсоздаётдвафайла:

  • Pipeline/PipelineSegments.storeкешсегментовконвейера

  • Pipeline/AddIns/AddIns.storeкешплагиноввпапкеAddIns

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

Если плагины располагаются в других папках, нужно сгенерировать кеши и в них:

AddInStore.UpdateAddIns(otherPluginsPath);

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

Затем нужно найти все плагины, которые реализуют нужный нам интерфейс, то есть IExportPlugin:

varaddInTokens=AddInStore.FindAddIns(typeof(IExportPluginView),pipelinePath);

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

Есть несколько вариантов активации плагина в зависимости от требуемой изоляции от хоста:

  1. Без изоляции: плагиныи хост запускаются в одном процессе и одном домене приложения.

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

  2. Средняя изоляция: каждый плагин запускается в своём домене приложения.

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

  3. Высокая изоляция: каждый плагин запускается в своём процессе.

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

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

varplugin =addInTokens.First().Activate<IExportPluginView>(AddInSecurityLevel.FullTrust);

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

MessageBox.Show($"Plugin '{plugin.DisplayName}' has been activated","MAF Demo message",MessageBoxButton.OK);

Расширение функционала

Реализация обратного взаимодействия от плагина к хосту

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

Пусть пока API для плагинов содержит метод получения даты последнего изменения файла. Создадим IPluginApi интерфейс в сегментах HostView, AddInView и Contracts (здесь не забываем отметить атрибутом AddInContract и отнаследоваться от IContract так же, как и в случае с IPluginView):

[AddInContract]public interface IPluginApi : IContract{DateTime GetLastModifiedDate(string path);}

Для него нужны адаптеры, так же как и для IPluginView, но в обратную сторону. Соответственно, на уровне адаптеров хоста это будет адаптер из представления хоста в представление контракта, а на уровне адаптеров плагина это будет адаптер из представления контракта в представление плагина:

[HostAdapter]public class PluginApiHostViewToContractAdapter : ContractBase, IPluginApi{  private readonly HostViews.IPluginApi view;    public PluginApiHostViewToContractAdapter(HostViews.IPluginApi view)  {  this.view = view;  }    public DateTime GetLastModifiedDate(string path)  {  return view.GetLastModifiedDate(path);  }}

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

Адаптерхоста:

public void Initialize(IPluginApi api){  contract.Initialize(new PluginApiHostViewToContractAdapter(api));}

Адаптерплагина:

public void Initialize(IPluginApi api){  view.Initialize(new PluginApiContractToPluginViewAdapter(api));}

В качестве финального штриха реализуем интерфейс Pipeline.HostView.IPluginApi на стороне хоста и инициализируем им наш плагин:

var plugin = addInTokens.First().Activate<IExportPluginView>(AddInSecurityLevel.FullTrust); plugin.Initialize(new PluginApi()); 

ДобавляемUIдляплагина

Для отображения пользовательского интерфейса плагина в хост-приложении в MAF существует специальный интерфейс INativeHandle, который предоставляет доступ к дескриптору окна (Hwnd). INativeHandle, получив дескриптор окна из ресурсов, передаётся между доменами приложений, таким образом хост может показать объект пользовательского интерфейса плагина.

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

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

FrameworkElementGetPanelUI();

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

INativeHandleContractGetPanelUI();

Для его создания в адаптереплагинаиспользуется конвертер, интегрированный вMAF:

public INativeHandleContract GetPanelUI(){  FrameworkElement frameworkElement = view.GetPanelUI();  return FrameworkElementAdapters.ViewToContractAdapter(frameworkElement);}

А для получения элемента интерфейса из дескриптора используется обратный метод конвертера:

public FrameworkElement GetPanelUI() {  INativeHandleContract handleContract = contract.GetPanelUI();  return FrameworkElementAdapters.ContractToViewAdapter(handleContract); } 

Для использования класса FrameworkElementAdapters необходимо подключить библиотеку System.Windows.Presentation.

Сделав всё это, остаётся только нарисовать контрол, который плагин будет отдавать хост-приложению по запросу:

public FrameworkElement GetPanelUI() {  return new PanelUI(); } 

Хост, в свою очередь, может использовать этот контрол внутри любого ContentControl'а. Получается примерно так:

С использованием пользовательского интерфейса, переданного таким образом, есть нюансы:

  • кконтролу плагина невозможно применить триггеры;

  • контрол плагина будет отображаться поверх других контролов этого окна вне зависимости от ZIndex, но другие окна приложения (диалоги, всплывающие окна) будут отображаться как положено (нам из-за этого пришлось переделать реализацию тост-сообщений в виде popup-окон);

  • контрол плагина не будет отображаться, если к окну применена прозрачность; кроме того, свойство AllowTransparenсy должно иметь значение false. (В нашем случае отображению контрола мешало свойство WindowChrome.GlassFrameThickness);

  • хост-приложение не может получить события от действий мышки, так же как и события GotFocus и LostFocus, а свойство IsMouseOver для контрола плагина всегда будет false;

  • в контроле плагина нельзя использовать VisualBrush, а также проигрывать медиа в MediaElement;

  • к контролу плагина не применяются трансформации: поворот, масштабирование, наклон;

  • и конечно, такой контрол плагина не поддерживает изменения его XAML на лету изменения не подтянутся во время исполнения;

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

Стилизация плагина

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

Для этого необходимо добавить новый проект типа Class library, в который добавить словари ресурсов (ResourceDictionary) с необходимыми стилями, а также создать один словарь, который будет агрегировать их все:

Для того чтобы использовать эти стили в плагине, в проект плагина нужно добавить созданный проект со стилями и потом указать ссылку на сводный словарь ресурсов в XAML-файле, где необходимо применить общие стили:

<UserControl.Resources>     <ResourceDictionary>        <ResourceDictionary.MergedDictionaries>             <ResourceDictionary Source="pack://application:,,,/Demo.Styles;component/AllStyles.xaml"/>          </ResourceDictionary.MergedDictionaries>     </ResourceDictionary> </UserControl.Resources> 

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

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

Деактивация и выгрузка плагинов

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

Выгрузка плагина состоит из двух этапов:

1. Закрыть ссылку на контракт.

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

Для этого в адаптере плагина необходимо создать метод, в котором будет вызвано удаление ссылки:

[AddInAdapter] public class PluginApiContractToPluginViewAdapter : AddInViews.IPluginApi{  public void Unload()  {  handle.Dispose();  }} 

Чтобы этот метод можно было вызвать из хост-приложения, добавим его и в интерфейс IPluginHostView.

2. Выключить плагин.

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

var controller = AddInController.GetAddInController(plugin); controller.Shutdown();  

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

Заключение

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

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

P. S. Полный код демо-приложения можно найти на github.

Подробнее..

Представляем Windows Package Manager 1.0

02.06.2021 10:16:01 | Автор: admin

Мы начали путь к созданию собственного диспетчера пакетов для Windows 10, когда анонсировали предварительную версию диспетчера пакетов Windows на Microsoft Build 2020. Мы выпустили проект на GitHub как совместный с открытым исходным кодом, и участие сообщества было очень важным аспектом! И вот недавно прошла конференция Microsoft Build 2021.

И мы рады объявить о выпуске Windows Package Manager 1.0! Подробности под катом!

Клиент

Клиент winget - это основной инструмент, который вы будете использовать для управления пакетами на вашем компьютере. На изображении ниже показан winget, выполненный в Терминале Windows через PowerShell. Вы можете увидеть список доступных команд, используемых для управления пакетами и работы с манифестами. Вы можете искать пакет (поиск находит по имени, моникеру и тегам) с помощью winget search vscode. Установить что-либо на свой компьютер так же просто, как winget installPowerToys. Вы можете проверить обновления пакетов с помощью winget upgrade или просто обновить все с помощью winget upgrade --all. Вы настраиваете новую машину? Убедитесь, что winget export packages.json на вашем текущем компьютере (и скопируйте файл на новый компьютер), чтобы вы могли импортировать файл packages.json на новом компьютере. С winget list вы можете увидеть все, что установлено, в Установка и удаление программ, и вы можете winget uninstall , чтобы удалить его из вашей системы. Вы можете узнать больше о командах и синтаксисе в нашей документации.

Как мне это получить?

Если вы используете любую текущую сборку Windows Insider или подписались на группу Windows Package Manager Insider, возможно, она у вас уже есть. Диспетчер пакетов Windows распространяется вместе с установщиком приложений из Microsoft Store. Вы также можете загрузить и установить диспетчер пакетов Windows со страницы выпусков GitHub или просто установить последнюю доступную версию.

Версия 1.0 диспетчера пакетов Windows скоро будет поставляться в виде автоматического обновления через Microsoft Store для всех устройств под управлением Windows 10 версии 1809 и более поздних версий. Если вы являетесь ИТ-специалистом, мы опубликовали информацию об управлении диспетчером пакетов Windows с помощью групповой политики. Пользователи смогут определить, какие политики действуют, выполнив winget --info.

Репозиторий сообщества Microsoft

Сообщество внесло более 1400 уникальных пакетов в репозиторий сообщества Microsoft! Вы можете winget search , чтобы узнать, доступен ли пакет. Нас до сих пор поражает, сколько замечательных программ для Windows 10 есть в репозитории. Если поиск не дает никаких результатов, вы можете выполнить процесс, чтобы запустить Edge и выполнить поиск загрузки установщика программного обеспечения. Как только вы найдете его, вы можете добавить его в репозиторий сообщества, чтобы вам не пришлось снова проходить этот процесс. Написав десятки манифестов вручную, мы поняли, что для этого должен быть инструмент.

Windows Package Manager Manifest Creator Preview

Мы также выпускаем еще один инструмент с открытым исходным кодом, который поможет отправлять пакеты в репозиторий сообщества Microsoft. Откройте свой любимый интерфейс командной строки и выполните winget install wingetcreate, чтобы установить создатель манифеста диспетчера пакетов Windows (Windows Package Manager Manifest Creator Preview). После установки инструмента выполните wingetcreate new и укажите URL-адрес установщика. Затем инструмент загрузит установщик, проанализирует его, чтобы определить любые значения манифеста, доступные в установщике, и проведет вас через процесс создания действительного манифеста. Если вы предоставите свои учетные данные GitHub при появлении запроса, он даже создаст ветвь репозитория, создаст новую ветку, отправит pull request и предоставит вам URL-адрес для отслеживания его прогресса. На изображении ниже показано, как wingetcreate выполняется в Терминале Windows через PowerShell.

Приватные репозитории

И последнее, но не менее важное: мы выпустили эталонную реализацию для источника REST API, чтобы вы могли разместить свой собственный частный репозиторий. Это новый тип источника для диспетчера пакетов Windows. Нашим источником по умолчанию является пакет PreIndexed, поставляемый через Microsoft Store, но вы можете добавить дополнительные источники на основе REST, если они правильно реализуют схему REST API на основе JSON.

Подробнее..

Jupyter в Visual Studio Code июньский релиз

17.06.2021 10:17:44 | Автор: admin

Мы рады сообщить, что стал доступен июньский релиз расширения Jupyter для Visual Studio Code. Если вы работаете с Python, мы рекомендуем загрузить расширение Python из Marketplace или установить его прямо из галереи расширений в Visual Studio Code. Если у вас уже установлено расширение Python, вы также можете получить последнее обновление, перезапустив Visual Studio Code. Узнайте больше о поддержке Python в Visual Studio Code в документации.

В этом релизе основное внимание уделялось:

  • Усилению мер безопасности

  • Дополнительным настройкам макета Native Notebook

  • Улучшению средств Data Viewer и Variable Explorer

Если вам интересно, вы можете изучить полный список улучшений в нашем журнале изменений.

Подробнее о самых интересных новинках под катом.

Workspace Trust

Команда Visual Studio Code серьезно относится к безопасности. Функция Workspace Trust позволяет определить, каким папкам и содержимому проекта вы доверяете, а какие остаются в ограниченном режиме.

Что это значит для notebooks?

При открытии папки в VS Code вас спросят, доверяете ли вы авторам и содержимому папок.

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

** Примечание. Важно понимать, что notebooks могут содержать вредоносный код, встроенный в их выходные данные, и могут работать без выполнения вами ячеек. VS Code будет подавлять выходные данные до тех пор, пока вы явно не доверяете записной книжке. Важно определить, доверяете ли вы notebooks, загруженным из внешних источников.

Дополнительные сведения и сведения о доверии рабочей области см. в разделе Visual Studio Code - доверие рабочей области.

Улучшенная фильтрация в средстве просмотра данных

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

Сортировка в проводнике переменных

По многочисленным просьбам переменные в вашем notebook теперь можно сортировать. Щелкните заголовок Имя или Тип в проводнике переменных для сортировки. Направление стрелки в заголовке будет указывать, по какому заголовку сортируются переменные и в каком порядке выполняется сортировка: в алфавитном или обратном алфавитном порядке.

Новый взгляд на нативные notebooks

Чтобы опробовать нативные notebooks сегодня, загрузите VS Code Insiders и расширение Jupyter. Расширение Python настоятельно рекомендуется для работы с записными книжками Python.

  • Панель инструментов notebook теперь находится в верхнем левом углу со всеми вашими любимыми действиями и функциями, связанными с notebook

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

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

Кастомизируемые нативные notebooks

Хотя приведенные выше основные моменты показывают, что уже работает в notebooks из коробки, вы всегда можете настроить все по своему вкусу. Мы добавили ряд настроек, чтобы по-настоящему сделать ваш notebook идеальным. Чтобы изучить настройки макета notebook, щелкните значок Дополнительные действия в конце панели инструментов и выберите Настроить макет notebook.

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

Полный список настроек компоновки notebook:

  • notebook.insertToolbarLocation

  • notebook.consolidatedRunButton

  • notebook.cellFocusIndicator

  • notebook.cellToolbarVisibility

  • notebook.compactView

  • notebook.consolidatedOutputButton

  • notebook.dragAndDropEnabled

  • notebook.globalToolbar

  • notebook.showCellStatusBar

  • notebook.showFoldingControls

  • notebook.editorOptionsCustomizations

Прочие изменения и улучшения

Мы также добавили небольшие улучшения и исправили проблемы, запрошенные пользователями, которые должны улучшить ваш опыт работы с notebooks в Visual Studio Code. Некоторые заметные изменения включают:

  • Ограничение количества языков, отображаемых в средстве выбора языка ячейки, языками, поддерживаемыми ядром

  • Добавление ABCMeta и ввод в список исключений проводника переменных

  • Настройка размера и вида переменных в соответствии с VS Code

  • Скрытие ядер, принадлежащих удаленным средам Python, из средства выбора ядра

Загрузите расширение Python и расширение Jupyter для Visual Studio Code сейчас, чтобы опробовать вышеуказанные улучшения. Если у вас возникнут какие-либо проблемы или у вас есть предложения, сообщите о проблеме на странице Jupyter VS Code GitHub.

Подробнее..

Powershell настоящий язык программирования. Скрипт оптимизации рутины в техподдержке

20.06.2021 14:08:21 | Автор: admin

Работая в компании IT-аутсорса в качестве руководителя 3 линии поддержки, задумался, как автоматизировать подключение сотрудников по RDP, через VPN к серверам десятков клиентов.

Таблички с адресами, паролями и прочими настройками серверов, конечно, хорошо, но поиск клиента и вбивание адресов с аккаунтами занимает довольно существенное время.
Держать все подключения к VPN в Windows не самая лучшая идея, да и при переустановке оного, создавать VPNы тоже не доставляет удовольствие.
Плюс к тому, в большинстве случаев, требуется установить VPN подключение к клиенту без использования шлюза. дабы не гонять весь интернет-трафик через клиента.
Задача, к тому же, осложняется тем, что у некоторых клиентов pptp, у кого-то l2tp, у некоторых несколько подсетей, туннели и т.п.

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

До написания этого скрипта-приложения программированием не занимался вообще, разве что лет 20 назад что-то пописывал на VBS в MS Excel и MS Access, поэтому не гарантирую красивость кода и принимаю критику от опытных программистов, как можно было бы сделать красивее.

В Powershell, начиная с Windows 8 и, конечно в Windows 10, появилась прекрасная возможность создавать VPN подключения командой Add-VpnConnection и указывать какие маршруты использовать с этими соединениями командой Add-VpnConnectionRoute, для использования VPN без шлюза.

На основании этих команд и создано данное приложение. Но, обо всем по порядку.

Для начала, создаем в Google Disk таблицу с именованными столбцами:
Number; Name; VPNname; ServerAddress; RemoteNetwork; VPNLogin; VPNPass; VPNType; l2tpPsk; RDPcomp; RDPuser; RDPpass; DefaultGateway; PortWinbox; WinboxLogin; WinboxPwd; Link; Inform

  • VPNname произвольное имя для VPN соединения

  • ServerAddress адрес VPN сервера

  • RemoteNetwork адреса подсети или подсетей клиента, разделенные ;

  • VPNLogin; VPNPass учетная запись VPN

  • VPNType -тип VPN (пока используется pptp или l2tp)

  • l2tpPsk PSK для l2tp, в случае pptp оставляем пустым

  • RDPcomp адрес сервера RPD

  • RDPuser; RDPpass учетная запись RPD

  • DefaultGateway принимает значение TRUE или FALSE и указывает на то, использовать ли Шлюз по умолчанию для этого соединения. В 90% случаев = FALSE

  • PortWinbox; WinboxLogin; WinboxPwd порт, логин и пароль для Winbox, поскольку у нас большинство клиентов использует Mikrotik)

  • Link ссылка на расширенную информацию о компании, например, на диске Google, или в любом другом месте, будет выводиться в информационном поле для быстрого доступа к нужной информации

Inform примечание

Пример таблицы доступен по ссылке

Number

Name

VPNname

ServerAddress

RemoteNetwork

VPNLogin

VPNPass

VPNType

l2tpPsk

RDPcomp

RDPuser

RDPpass

DefaultGateway

PortWinbox

WinboxLogin

WinboxPwd

Link

Inform

1

Тест1

Test1

a.b.c.d

192.168.10.0/24: 10.10.0.0/24

vpnuser

passWord

pptp

none

192.168.10.1

user

passWord

TRUE

8291

Admin

Admin

http://yandex.ru

тест

2

Тест2

Test2

e.f.j.k

192.168.2.0/24

vpnuser

passWord

l2tp

KdoSDtdP

192.168.2.1

user

passWord

FALSE

8291

Admin

Admin

Скриншот работающего приложения с затертыми данными:

Далее следует листинг приложения с комментариями и пояснениями. Если интересно, но непонятно, задавайте вопросы, постараюсь прокомментировать

function Get-Clients #Функция принимает строку адреса файла в Google Drive и возвращает в виде массива данных о клиентах{param([string]$google_url = "")[string]$xlsFile = $google_url$csvFile = "$env:temp\clients.csv"$Comma = ','Invoke-WebRequest $xlsFile -OutFile $csvFile$clients = Import-Csv -Delimiter $Comma -Path "$env:temp\clients.csv"Remove-Item -Path $csvFilereturn $clients}function Main {<#    Функция, срабатываемая при запуске скрипта#>Param ([String]$Commandline)#Иннициализируем переменные и присваиваем начальные значения. Здесь же, указываем путь к таблице с клиентами$Global:Clients = $null$Global:Current$Global:CurrentRDPcomp$Global:google_file = "https://docs.google.com/spreadsheets/d/1O-W1YCM4x3o5W1w6XahCJZpkTWs8cREXVF69gs1dD0U/export?format=csv" # Таблица скачивается сразу в виде csv-файла$Global:Clients = Get-Clients ($Global:google_file) # Присваиваем значения из таблицы массиву #Скачиваем Winbox64 во временную папку$download_url = "https://download.mikrotik.com/winbox/3.27/winbox64.exe"$Global:local_path = "$env:temp\winbox64.exe"If ((Test-Path $Global:local_path) -ne $true){$WebClient = New-Object System.Net.WebClient$WebClient.DownloadFile($download_url, $Global:local_path)}  #Разрываем все текущие VPN соединения (на всякий случай)foreach ($item in get-vpnconnection | where { $_.ConnectionStatus -eq "Connected" }){Rasdial $item.Name /disconnect}  #Удаляем все, ранее созданные программой временные соединения, если вдруг не удалились при некорректном закрытии приложенияget-vpnconnection | where { $_.Name -match "tmp" } | Remove-VpnConnection -Force#Запускаем приложениеShow-MainForm_psf}#Собственно, само приложениеfunction Show-MainForm_psf{[void][reflection.assembly]::Load('System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089')[void][reflection.assembly]::Load('System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a')#Создаем форму и объекты формы[System.Windows.Forms.Application]::EnableVisualStyles()$formКлиентыАльбус = New-Object 'System.Windows.Forms.Form'$statusbar1 = New-Object 'System.Windows.Forms.StatusBar'$groupboxTools = New-Object 'System.Windows.Forms.GroupBox'$buttonPing = New-Object 'System.Windows.Forms.Button'$buttonВыход = New-Object 'System.Windows.Forms.Button'$buttonWindox = New-Object 'System.Windows.Forms.Button'$buttonПеречитатьДанные = New-Object 'System.Windows.Forms.Button'$buttonPingAll = New-Object 'System.Windows.Forms.Button'$groupboxRDP = New-Object 'System.Windows.Forms.GroupBox'$comboboxRDP = New-Object 'System.Windows.Forms.ComboBox'$textboxRDPLogin = New-Object 'System.Windows.Forms.TextBox'$textboxRdpPwd = New-Object 'System.Windows.Forms.TextBox'$buttonПодключитьRDP = New-Object 'System.Windows.Forms.Button'$groupboxVPN = New-Object 'System.Windows.Forms.GroupBox'$buttonПодключитьVPN = New-Object 'System.Windows.Forms.Button'$buttonОтключитьVPN = New-Object 'System.Windows.Forms.Button'$checkboxШлюзПоумолчанию = New-Object 'System.Windows.Forms.CheckBox'$richtextboxinfo = New-Object 'System.Windows.Forms.RichTextBox'$listbox_clients = New-Object 'System.Windows.Forms.ListBox'$InitialFormWindowState = New-Object 'System.Windows.Forms.FormWindowState'  #----------------------------------------------# Обработчики событий#----------------------------------------------$formКлиентыАльбус_Load = {#При загрузке формы очистить поле информации и заполнить поле с клиентами (их названиями) $richtextboxinfo.Clear()$Global:Clients | ForEach-Object {[void]$listbox_clients.Items.Add($_.Name)} # В листбокс добавляем всех наших клиентов по именам и массива при загрузке формы}$listbox_clients_SelectedIndexChanged = {#Прочитать из массива информацию о клиенте при выборе его в поле listbox_clients (массив, как мы помним считан из файла с диска Google)$statusbar1.Text = 'Выбран клиент: ' + $listbox_clients.SelectedItem.ToString() # Пишем клиента в статусбар$Global:Current = $Global:Clients.Where({ $_.Name -eq $listbox_clients.SelectedItem.ToString() })If ($Current.PortWinbox -ne 0) # Если порт Winbox указан, то у клиента Mikrotik, включаем соответствующую кнопку{$buttonWindox.Enabled = $true$buttonWindox.Text = "Winbox"}$VPNname = $Global:Current.VPNname + "-tmp" #Добавляем к имени VPN соединения "-tmp" для указания метки временного соединения, чтобы при выходе удалить только ихswitch ($Global:Current.VPNType) #В зависимости от типа VPN пишем на кнопке "Подключить pptp VPN" или "Подключить l2tp VPN", если у клиента нет VPN, то пишем "Здесь нет VPN"{"pptp" {$buttonПодключитьVPN.Enabled = $true$buttonПодключитьVPN.Text = "Подключить pptp VPN"}"l2tp" {$buttonПодключитьVPN.Enabled = $true$buttonПодключитьVPN.Text = "Подключить l2tp VPN"}DEFAULT{$buttonПодключитьVPN.Enabled = $false$buttonПодключитьVPN.Text = "Здесь нет VPN"}}switch ($Global:Current.DefaultGateway) #Смотрим в массиве, используется ли у клиента "Шлюз по-умолчанию" и заполняем соответствующий чекбокс{"FALSE"{ $checkboxШлюзПоумолчанию.Checked = $false }"Нет"{ $checkboxШлюзПоумолчанию.Checked = $false }"TRUE"{ $checkboxШлюзПоумолчанию.Checked = $true }"Да"{ $checkboxШлюзПоумолчанию.Checked = $true }DEFAULT{ $checkboxШлюзПоумолчанию.Checked = $false }}$VPNStatus = (ipconfig | Select-String $VPNname -Quiet) #Проверяем, не установлено ли уже это VPN соединение?If ($VPNStatus) #Если установлено, то разблокируем кнопку "Подключить RDP"{$buttonПодключитьRDP.Enabled = $true}else{$buttonПодключитьRDP.Enabled = $false}$richtextboxinfo.Clear() #Очищаем информационное поле # И заполняем информацией о клиенте из массива$richtextboxinfo.SelectionColor = 'Black'$richtextboxinfo.Text = "Клиент: " + $Global:Current.Name + [System.Environment]::NewLine + `"Имя VPN: " + $Global:Current.VPNname + [System.Environment]::NewLine + `"Тип VPN: " + $Global:Current.VPNType + [System.Environment]::NewLine + `"Адрес сервера: " + $Global:Current.ServerAddress + [System.Environment]::NewLine + `"Подсеть клиента: " + $Global:Current.RemoteNetwork + [System.Environment]::NewLine + `"Адрес сервера RDP: " + $Global:Current.RDPcomp + [System.Environment]::NewLine + [System.Environment]::NewLine + `"DefaultGateway: " + $Global:Current.DefaultGateway + [System.Environment]::NewLine + [System.Environment]::NewLine + `"Примечание: " + [System.Environment]::NewLine + $Global:Current.Inform + [System.Environment]::NewLine + `"Connection '" + $VPNname + "' status is " + $buttonПодключитьRDP.Enabled + [System.Environment]::NewLine$richtextboxinfo.AppendText($Global:Current.Link)$RDPServers = $Global:Current.RDPcomp.Split(';') -replace '\s', '' #Считываем и разбираем RDP серверы клиента из строки с разделителем в массив#Добавляем из в выпадающее поле выбора сервера$comboboxRDP.Items.Clear()$comboboxRDP.Text = $RDPServers[0]foreach ($RDPServer in $RDPServers){$comboboxRDP.Items.Add($RDPServer)}#Заполняем поля имени и пароля RDP по умолчанию из таблицы о клиенте (при желании, их можно поменять в окне программы)$textboxRdpPwd.Text = $Global:Current.RDPpass$textboxRdpLogin.Text = $Global:Current.RDPuser} # Форма заполнена, при смене выбранного клиента произойдет перезаполнение полей в соответствии с выбранным клиентом$buttonWindox_Click = {#Обработка нажатия кнопки WinboxIf ($Global:Current.PortWinbox -ne 0) #Если порт Winbox заполнен, то открываем скачанный ранее Winbox, подставляем туда имя и пароль к нему и запускаем{$runwinbox = "$env:temp\winbox64.exe"$ServerPort = $Global:Current.ServerAddress + ":" + $Global:Current.PortWinbox$ServerLogin = " """ + $Global:Current.WinboxLogin + """"$ServerPass = " """ + $Global:Current.WinboxPwd + """"$Arg = "$ServerPort $ServerLogin $ServerPass "Start-Process -filePath $runwinbox -ArgumentList $Arg}}$buttonПодключитьVPN_Click = {#Обработка нажатия кнопки ПодключитьVPN$VPNname = $Global:Current.VPNname + "-tmp" #Добавляем к имени VPN соединения "-tmp" для указания метки временного соединения, чтобы при выходе удалить только их$richtextboxinfo.Clear() #Очищаем информационное поля для вывода туда информации о процессе подключения$richtextboxinfo.Text = "Клиент: " + $Global:Current.Name + [System.Environment]::NewLineforeach ($item in get-vpnconnection | where { $_.ConnectionStatus -eq "Connected" }) #Разрываем все установленные соединения{$richtextboxinfo.Text = $richtextboxinfo.Text + "Обнаружено активное соединение " + $item.Name + " разрываем его" + [System.Environment]::NewLineRasdial $item.Name /disconnect}Remove-VpnConnection $VPNname -Force #Удаляем соединение, если ранее оно было создано$RemoteNetworks = $Global:Current.RemoteNetwork.Split(';') -replace '\s', '' #Считываем и разбираем по строкам в массив список подсетей клиента разделенный ;switch ($Global:Current.VPNType) #В зависимости от типа VPNа создаем pptp или l2tp соединение{"pptp" {$richtextboxinfo.Text = $richtextboxinfo.Text + "Создаем pptp подключение " + $VPNname + [System.Environment]::NewLineIf ($checkboxШлюзПоумолчанию.Checked -eq $false) #Если не используется "Шлюз по-умолчанию", то создаем VPN соединение без него и прописываем маршруты{$Errcon = (Add-VpnConnection -Name $VPNname -ServerAddress $Global:Current.ServerAddress -TunnelType $Global:Current.VPNType -SplitTunneling -Force -RememberCredential -PassThru) #Здесь происходит создание VPNforeach ($RemoteNetwork in $RemoteNetworks) #Добавляем все подсети клиента к этому VPN{$richtextboxinfo.AppendText('Добавляем маршрут к ' + $RemoteNetwork + [System.Environment]::NewLine)Add-VpnConnectionRoute -ConnectionName $VPNname -DestinationPrefix $RemoteNetwork -PassThru}}else #Если используется "Шлюз по-умолчанию", то создаем VPN соединение с ним и маршруты к клиенту не нужны{$Errcon = (Add-VpnConnection -Name $VPNname -ServerAddress $Global:Current.ServerAddress -TunnelType $Global:Current.VPNType -Force -RememberCredential -PassThru)}}"l2tp" {$richtextboxinfo.Text = $richtextboxinfo.Text + "Создаем l2tp подключение " + $Global:Current.VPNname + [System.Environment]::NewLineIf ($checkboxШлюзПоумолчанию.Checked -eq $false) #Если не используется "Шлюз по-умолчанию", то создаем VPN соединение без него и прописываем маршруты{$Errcon = (Add-VpnConnection -Name $VPNname -ServerAddress $Global:Current.ServerAddress -TunnelType $Global:Current.VPNType -L2tpPsk $Global:Current.l2tpPsk -SplitTunneling -Force -RememberCredential -PassThru) #Здесь происходит создание VPNforeach ($RemoteNetwork in $RemoteNetworks) #Добавляем все подсети клиента к этому VPN{$richtextboxinfo.AppendText('Добавляем маршрут к ' + $RemoteNetwork + [System.Environment]::NewLine)Add-VpnConnectionRoute -ConnectionName $VPNname -DestinationPrefix $RemoteNetwork -PassThru}}else #Если используется "Шлюз по-умолчанию", то создаем VPN соединение с ним и маршруты к клиенту не нужны{$Errcon = (Add-VpnConnection -Name $VPNname -ServerAddress $Global:Current.ServerAddress -TunnelType $Global:Current.VPNType -L2tpPsk $Global:Current.l2tpPsk -Force -RememberCredential -PassThru)}}}$richtextboxinfo.AppendText("Устанавливаем " + $Global:Current.VPNType + " подключение к " + $VPNname + [System.Environment]::NewLine)$Errcon = Rasdial $VPNname $Global:Current.VPNLogin $Global:Current.VPNPass #Устанавливаем созданное VPN подключение и выводим информацию в поле$richtextboxinfo.Text = $richtextboxinfo.Text + [System.Environment]::NewLine + $Errcon + [System.Environment]::NewLineIf ((ipconfig | Select-String $VPNname -Quiet)) #Проверяем успешность соединения и, если все удачно, разблокируем кнопку RDP  и кнопку "Отключить VPN"{$buttonПодключитьRDP.Enabled = $true$buttonОтключитьVPN.Visible = $true$buttonОтключитьVPN.Enabled = $true$statusbar1.Text = $Global:Current.Name + ' подключен'}}$formКлиентыАльбус_FormClosing = [System.Windows.Forms.FormClosingEventHandler]{#При закрытии формы подчищаем за собой. Разрываем и удаляем все созданные соединения. foreach ($item in get-vpnconnection | where { $_.ConnectionStatus -eq "Connected" }){$richtextboxinfo.Text = $richtextboxinfo.Text + "Обнаружено активное соединение " + $item.Name + " разрываем его" + [System.Environment]::NewLineRasdial $item.Name /disconnect}$richtextboxinfo.Text = $richtextboxinfo.Text + "Удаляем все временные соединения" + [System.Environment]::NewLineget-vpnconnection | where { $_.Name -match "tmp" } | Remove-VpnConnection -Force#Удаляем информацию о RPD-серверах из реестра$Global:Clients | ForEach-Object {$term = "TERMSRV/" + $_.RDPcompcmdkey /delete:$term}}$buttonПодключитьRDP_Click = {#Обработка кнопки ПодключитьRDP$RDPcomp = $comboboxRDP.Text$RDPuser = $textboxRDPLogin.Text$RDPpass = $textboxRdpPwd.Textcmdkey /generic:"TERMSRV/$RDPcomp" /user:"$RDPuser" /pass:"$RDPpass"mstsc /v:$RDPcomp}$buttonОтключитьVPN_Click = {#При отключении VPN подчищаем за собой и оповещаем о процессе в поле информацииforeach ($item in get-vpnconnection | where { $_.ConnectionStatus -eq "Connected" }){$richtextboxinfo.Text = $richtextboxinfo.Text + "Обнаружено активное соединение " + $item.Name + " разрываем его" + [System.Environment]::NewLineRasdial $item.Name /disconnect}$richtextboxinfo.Text = $richtextboxinfo.Text + "Удаляем все временные соединения" + [System.Environment]::NewLineget-vpnconnection | where { $_.Name -match "tmp" } | Remove-VpnConnection -Force$buttonОтключитьVPN.Visible = $false$buttonПодключитьRDP.Enabled = $false$statusbar1.Text = $Global:Current.Name + ' отключен'}$buttonPingAll_Click={#Пингуем всех клиентов и оповещаем о результатах$I=0$richtextboxinfo.Clear()$richtextboxinfo.SelectionColor = 'Black'$clientscount = $Global:Clients.count$Global:Clients | ForEach-Object {if ((test-connection -Count 1 -computer $_.ServerAddress -quiet) -eq $True){$richtextboxinfo.SelectionColor = 'Green'$richtextboxinfo.AppendText($_.Name +' ('+ $_.ServerAddress +') доступен' + [System.Environment]::NewLine)}else{$richtextboxinfo.SelectionColor = 'Red'$richtextboxinfo.AppendText($_.Name + ' (' + $_.ServerAddress + ')  недоступен (или закрыт ICMP)' + [System.Environment]::NewLine)}$richtextboxinfo.ScrollToCaret()$I = $I + 1Write-Progress -Activity "Ping in Progress" -Status "$i clients of $clientscount pinged" -PercentComplete ($i/$clientscount*100)}$richtextboxinfo.SelectionColor = 'Black'Write-Progress -Activity "Ping in Progress" -Status "Ready" -Completed}$buttonПеречитатьДанные_Click={#Перечитываем данные из таблицы Google$Global:Clients = Get-Clients ($Global:google_file)$listbox_clients.Items.Clear()$Global:Clients | ForEach-Object {[void]$listbox_clients.Items.Add($_.Name)}}$buttonВыход_Click = {#Выход$formКлиентыАльбус.Close()}$richtextboxinfo_LinkClicked=[System.Windows.Forms.LinkClickedEventHandler]{#Обработка нажатия на ссылку в окне информацииStart-Process $_.LinkText.ToString()}$buttonPing_Click={#Пингуем ip текущего клиента и выводим результат в поле информацииif ((test-connection -Count 1 -computer $Global:Current.ServerAddress -quiet) -eq $True){$richtextboxinfo.AppendText([System.Environment]::NewLine)$richtextboxinfo.SelectionColor = 'Green'$richtextboxinfo.AppendText($Global:Current.Name + ' (' + $Global:Current.ServerAddress + ') доступен' + [System.Environment]::NewLine)}else{$richtextboxinfo.AppendText([System.Environment]::NewLine)$richtextboxinfo.SelectionColor = 'Red'$richtextboxinfo.AppendText($Global:Current.Name + ' (' + $Global:Current.ServerAddress + ')  недоступен (или закрыт ICMP)' + [System.Environment]::NewLine)}}#----------------------------------------------#Описание объектов формы#----------------------------------------------## formКлиентыАльбус#$formКлиентыАльбус.Controls.Add($statusbar1)$formКлиентыАльбус.Controls.Add($groupboxTools)$formКлиентыАльбус.Controls.Add($groupboxRDP)$formКлиентыАльбус.Controls.Add($groupboxVPN)$formКлиентыАльбус.Controls.Add($richtextboxinfo)$formКлиентыАльбус.Controls.Add($listbox_clients)$formКлиентыАльбус.AutoScaleDimensions = '6, 13'$formКлиентыАльбус.AutoScaleMode = 'Font'$formКлиентыАльбус.AutoSize = $True$formКлиентыАльбус.ClientSize = '763, 446'$formКлиентыАльбус.FormBorderStyle = 'FixedSingle'$formКлиентыАльбус.MaximizeBox = $False$formКлиентыАльбус.Name = 'formКлиентыАльбус'$formКлиентыАльбус.SizeGripStyle = 'Hide'$formКлиентыАльбус.StartPosition = 'CenterScreen'$formКлиентыАльбус.Text = 'Клиенты Альбус'$formКлиентыАльбус.add_FormClosing($formКлиентыАльбус_FormClosing)$formКлиентыАльбус.add_Load($formКлиентыАльбус_Load)## statusbar1#$statusbar1.Location = '0, 424'$statusbar1.Name = 'statusbar1'$statusbar1.Size = '763, 22'$statusbar1.TabIndex = 17## groupboxTools#$groupboxTools.Controls.Add($buttonPing)$groupboxTools.Controls.Add($buttonВыход)$groupboxTools.Controls.Add($buttonWindox)$groupboxTools.Controls.Add($buttonПеречитатьДанные)$groupboxTools.Controls.Add($buttonPingAll)$groupboxTools.Location = '308, 258'$groupboxTools.Name = 'groupboxTools'$groupboxTools.Size = '147, 163'$groupboxTools.TabIndex = 10$groupboxTools.TabStop = $False$groupboxTools.Text = 'Tools'$groupboxTools.UseCompatibleTextRendering = $True## buttonPing#$buttonPing.Location = '7, 44'$buttonPing.Name = 'buttonPing'$buttonPing.Size = '133, 23'$buttonPing.TabIndex = 12$buttonPing.Text = 'Ping'$buttonPing.UseCompatibleTextRendering = $True$buttonPing.UseVisualStyleBackColor = $True$buttonPing.add_Click($buttonPing_Click)## buttonВыход#$buttonВыход.Location = '7, 125'$buttonВыход.Name = 'buttonВыход'$buttonВыход.Size = '133, 23'$buttonВыход.TabIndex = 15$buttonВыход.Text = 'Выход'$buttonВыход.UseCompatibleTextRendering = $True$buttonВыход.UseVisualStyleBackColor = $True$buttonВыход.add_Click($buttonВыход_Click)## buttonWindox#$buttonWindox.Enabled = $False$buttonWindox.Location = '7, 17'$buttonWindox.Name = 'buttonWindox'$buttonWindox.Size = '133, 23'$buttonWindox.TabIndex = 11$buttonWindox.Text = 'Windox'$buttonWindox.UseCompatibleTextRendering = $True$buttonWindox.UseVisualStyleBackColor = $True$buttonWindox.add_Click($buttonWindox_Click)## buttonПеречитатьДанные#$buttonПеречитатьДанные.Location = '7, 98'$buttonПеречитатьДанные.Name = 'buttonПеречитатьДанные'$buttonПеречитатьДанные.Size = '133, 23'$buttonПеречитатьДанные.TabIndex = 14$buttonПеречитатьДанные.Text = 'Перечитать данные'$buttonПеречитатьДанные.UseCompatibleTextRendering = $True$buttonПеречитатьДанные.UseVisualStyleBackColor = $True$buttonПеречитатьДанные.add_Click($buttonПеречитатьДанные_Click)## buttonPingAll#$buttonPingAll.Location = '7, 71'$buttonPingAll.Name = 'buttonPingAll'$buttonPingAll.Size = '133, 23'$buttonPingAll.TabIndex = 13$buttonPingAll.Text = 'Ping All'$buttonPingAll.UseCompatibleTextRendering = $True$buttonPingAll.UseVisualStyleBackColor = $True$buttonPingAll.add_Click($buttonPingAll_Click)## groupboxRDP#$groupboxRDP.Controls.Add($comboboxRDP)$groupboxRDP.Controls.Add($textboxRDPLogin)$groupboxRDP.Controls.Add($textboxRdpPwd)$groupboxRDP.Controls.Add($buttonПодключитьRDP)$groupboxRDP.Location = '308, 128'$groupboxRDP.Name = 'groupboxRDP'$groupboxRDP.Size = '147, 126'$groupboxRDP.TabIndex = 5$groupboxRDP.TabStop = $False$groupboxRDP.Text = 'RDP'$groupboxRDP.UseCompatibleTextRendering = $True## comboboxRDP#$comboboxRDP.FormattingEnabled = $True$comboboxRDP.Location = '7, 17'$comboboxRDP.Name = 'comboboxRDP'$comboboxRDP.Size = '133, 21'$comboboxRDP.TabIndex = 6$comboboxRDP.Text = 'IP RDP сервера'## textboxRDPLogin#$textboxRDPLogin.Location = '7, 44'$textboxRDPLogin.Name = 'textboxRDPLogin'$textboxRDPLogin.Size = '133, 20'$textboxRDPLogin.TabIndex = 7$textboxRDPLogin.Text = 'RDP-login'## textboxRdpPwd#$textboxRdpPwd.Location = '7, 69'$textboxRdpPwd.Name = 'textboxRdpPwd'$textboxRdpPwd.PasswordChar = '*'$textboxRdpPwd.Size = '133, 20'$textboxRdpPwd.TabIndex = 8$textboxRdpPwd.Text = 'RDP-Password'## buttonПодключитьRDP#$buttonПодключитьRDP.Enabled = $False$buttonПодключитьRDP.Location = '7, 94'$buttonПодключитьRDP.Name = 'buttonПодключитьRDP'$buttonПодключитьRDP.Size = '133, 20'$buttonПодключитьRDP.TabIndex = 9$buttonПодключитьRDP.Text = 'Подключить RDP'$buttonПодключитьRDP.UseCompatibleTextRendering = $True$buttonПодключитьRDP.UseVisualStyleBackColor = $True$buttonПодключитьRDP.add_Click($buttonПодключитьRDP_Click)## groupboxVPN#$groupboxVPN.Controls.Add($buttonПодключитьVPN)$groupboxVPN.Controls.Add($buttonОтключитьVPN)$groupboxVPN.Controls.Add($checkboxШлюзПоумолчанию)$groupboxVPN.Location = '308, 27'$groupboxVPN.Name = 'groupboxVPN'$groupboxVPN.Size = '147, 98'$groupboxVPN.TabIndex = 1$groupboxVPN.TabStop = $False$groupboxVPN.Text = 'VPN'$groupboxVPN.UseCompatibleTextRendering = $True## buttonПодключитьVPN#$buttonПодключитьVPN.Enabled = $False$buttonПодключитьVPN.Location = '7, 45'$buttonПодключитьVPN.Name = 'buttonПодключитьVPN'$buttonПодключитьVPN.Size = '133, 20'$buttonПодключитьVPN.TabIndex = 3$buttonПодключитьVPN.Text = 'Подключить VPN'$buttonПодключитьVPN.UseCompatibleTextRendering = $True$buttonПодключитьVPN.UseVisualStyleBackColor = $True$buttonПодключитьVPN.add_Click($buttonПодключитьVPN_Click)## buttonОтключитьVPN#$buttonОтключитьVPN.Enabled = $False$buttonОтключитьVPN.Location = '7, 67'$buttonОтключитьVPN.Name = 'buttonОтключитьVPN'$buttonОтключитьVPN.Size = '133, 20'$buttonОтключитьVPN.TabIndex = 4$buttonОтключитьVPN.Text = 'Отключить VPN'$buttonОтключитьVPN.UseCompatibleTextRendering = $True$buttonОтключитьVPN.UseVisualStyleBackColor = $True$buttonОтключитьVPN.Visible = $False$buttonОтключитьVPN.add_Click($buttonОтключитьVPN_Click)## checkboxШлюзПоумолчанию#$checkboxШлюзПоумолчанию.Location = '7, 19'$checkboxШлюзПоумолчанию.Name = 'checkboxШлюзПоумолчанию'$checkboxШлюзПоумолчанию.Size = '133, 24'$checkboxШлюзПоумолчанию.TabIndex = 2$checkboxШлюзПоумолчанию.Text = 'Шлюз по-умолчанию'$checkboxШлюзПоумолчанию.TextAlign = 'MiddleRight'$checkboxШлюзПоумолчанию.UseCompatibleTextRendering = $True$checkboxШлюзПоумолчанию.UseVisualStyleBackColor = $True## richtextboxinfo#$richtextboxinfo.Cursor = 'Default'$richtextboxinfo.ForeColor = 'WindowText'$richtextboxinfo.HideSelection = $False$richtextboxinfo.Location = '461, 27'$richtextboxinfo.Name = 'richtextboxinfo'$richtextboxinfo.ReadOnly = $True$richtextboxinfo.ScrollBars = 'ForcedVertical'$richtextboxinfo.ShowSelectionMargin = $True$richtextboxinfo.Size = '290, 394'$richtextboxinfo.TabIndex = 16$richtextboxinfo.Text = ''$richtextboxinfo.add_LinkClicked($richtextboxinfo_LinkClicked)## listbox_clients#$listbox_clients.FormattingEnabled = $True$listbox_clients.Location = '12, 27'$listbox_clients.Name = 'listbox_clients'$listbox_clients.Size = '290, 394'$listbox_clients.TabIndex = 0$listbox_clients.add_SelectedIndexChanged($listbox_clients_SelectedIndexChanged)#Save the initial state of the form$InitialFormWindowState = $formКлиентыАльбус.WindowState#Init the OnLoad event to correct the initial state of the form$formКлиентыАльбус.add_Load($Form_StateCorrection_Load)#Clean up the control events$formКлиентыАльбус.add_FormClosed($Form_Cleanup_FormClosed)#Store the control values when form is closing$formКлиентыАльбус.add_Closing($Form_StoreValues_Closing)#Show the Formreturn $formКлиентыАльбус.ShowDialog()}#Запуск приложения!Main ($CommandLine) 

Скрипт можно запускать как скрипт ps1 или скомпилировать в exe через ps2exe и использовать как полноценное приложение

Подробнее..

Ещё один модуль рисования графиков

25.05.2021 20:20:29 | Автор: admin
Лет пятнадцать назад потребовалось мне в программе для диплома отобразить график. Была бы программа на Builder или Delphi, всё было бы ничего, но только писал я для Windows на MFC, а там с классами графиков как-то не очень. И написал я тогда собственный модуль построения графиков. Три пятилетки прошло, а модуль остался, был переработан и я его иногда использую в своих поделках в QNX, Linux и Windows. Быть может, он пригодится чем-либо и вам.

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

Функции растеризации вынесены в подключаемые классы. Всего возможно на текущий момент три варианта: рисование штатными функциями Windows-GUI из MFC (класс CVideo_Windows), рисование штатными функциями Qt (класс CVideo_Qt) и программная растеризация (класс CVideo_Software с доработкой этот модуль можно использовать на микроконтроллерах). Перекодировку символов в требуемый для классов растеризации формат осуществляет класс CTranslator.

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

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

Выглядят нарисованные графики вот так:



Создание класса графика, например, для Windows в MFC выполняется следующим образом:

 CWnd_Graphics cWnd_Graphics;//класс графиков CGrData *cGrData_SinPtr;//указатель на данные графика синуса CGrData *cGrData_CosPtr;//указатель на данные графика косинуса//создаём графикCRect cRect_Client;((CStatic*)GetDlgItem(IDC_STATIC_MAIN_MAP))->GetClientRect(&cRect_Client);cWnd_Graphics.Create(WS_VISIBLE,cRect_Client,(CStatic*)GetDlgItem(IDC_STATIC_MAIN_MAP));  //настраиваем график cWnd_Graphics.GetIGraphicsPtr()->SetBackGroundColor(CGrColor(192,192,192)); cWnd_Graphics.GetIGraphicsPtr()->SetLegendBackGroundColor(CGrColor(230,230,230)); cWnd_Graphics.GetIGraphicsPtr()->SetAxisColor(CGrColor(0,0,0),CGrColor(0,0,0)); cWnd_Graphics.GetIGraphicsPtr()->SetTextValueColor(CGrColor(0,0,0),CGrColor(0,0,0),CGrColor(0,0,0)); cWnd_Graphics.GetIGraphicsPtr()->SetSelectRectangleColor(CGrColor(0,0,255));  cWnd_Graphics.GetIGraphicsPtr()->SetName("Графики функций"); //создаём графики cGrData_SinPtr=cWnd_Graphics.GetIGraphicsPtr()->AddNewGraphic(); cGrData_SinPtr->SetEnable(true); cGrData_SinPtr->SetGrColor(CGrColor(255,0,0)); cGrData_SinPtr->SetGrLineStyle(CGrLineStyle(IVideo::LINE_TYPE_SOLID,1,false,false)); cGrData_SinPtr->SetName("График синуса");  for(size_t n=0;n<1024;n++) {  double x=n;  double y=sin(x*0.01);  cGrData_SinPtr->AddPoint(x,y); } cGrData_CosPtr=cWnd_Graphics.GetIGraphicsPtr()->AddNewGraphic(); cGrData_CosPtr->SetEnable(true); cGrData_CosPtr->SetGrColor(CGrColor(0,0,255)); cGrData_CosPtr->SetGrLineStyle(CGrLineStyle(IVideo::LINE_TYPE_SOLID,3,false,false)); cGrData_CosPtr->SetName("График косинуса"); for(size_t n=0;n<1024;n++) {  double x=n;  double y=cos(x*0.01);  cGrData_CosPtr->AddPoint(x,y); } //приводим масштаб, чтобы отображались все графики CGrRect cGrRect; cWnd_Graphics.GetIGraphicsPtr()->FindViewRectangle(cGrRect); cWnd_Graphics.GetIGraphicsPtr()->SetRectangle(cGrRect); cWnd_Graphics.GetIGraphicsPtr()->OnMagnify(); cWnd_Graphics.GetIGraphicsPtr()->GetRectangle(cGrRect); cWnd_Graphics.GetIGraphicsPtr()->SetOriginalScale(cGrRect);

Здесь класс cWnd_Graphics обеспечивает связку класса графиков CGraphics с Windows, перенаправляя в класс CGraphics события, возникающие в Windows и обеспечивая отображение графика в событии перерисовки ON_WM_PAINT. Для других ОС эту связку потребуется переписать, с учётом используемой ОС. В данном примере через cWnd_Graphics.GetIGraphicsPtr() можно непосредственно обратиться к классу графиков CGraphics и настроить параметры отображения графиков, а так же попросить класс графиков создать новый график и вернуть на него указатель AddNewGraphic (будет получен указатель на класс CGrData). Удалять этот указатель самостоятельно нельзя график можно удалить только через функцию DeleteGraphic. В дальнейшем работа с графиком выполняется через полученный указатель.

Всего доступны следующие функции управления графиками:

CGrData* AddNewGraphic(void);//добавить график и получить указатель на созданный график  void DeleteAllGraphics(void);//удалить все графики из памяти  void DeleteGraphic(CGrData *cGrDataPtr);//удалить график из памяти  void FindRectangle(CGrRect &cGrRect) const;//найти описывающий прямоугольник всех активных графиков  void FindRectangleOfEndPoints(CGrRect &cGrRect,size_t points) const;//найти описывающий прямоугольник всех активных графиков за последние points точек  void FindRectangleOfEndTime(CGrRect &cGrRect,long double time) const;//найти описывающий прямоугольник всех активных графиков за последние time время  void FindViewRectangle(CGrRect &cGrRect) const;//найти описывающий прямоугольник всех активных графиков с запасом по высоте  void FindViewRectangleOfEndPoints(CGrRect &cGrRect,size_t points) const;//найти описывающий прямоугольник всех активных графиков с запасом по высоте за последние points точек  void FindViewRectangleOfEndTime(CGrRect &cGrRect,long double time) const;//найти описывающий прямоугольник всех активных графиков с запасом по высоте за последнее time время  void SetTimeDivider(double value);//установить временной делитель  double GetTimeDivider(void) const;//получить временной делитель  //функции отображения  void CancelSelect(void);//убрать выделение  void Redraw(void);//перерисовать изображение  void RedrawAll(void);//перерисовать всё  void OnOriginalScale(void);//перейти в режим оригинального масштаба  //функции цветовых настроек  void SetBackGroundColor(const CGrColor &cGrColor);//задать цвет фона  void SetLegendBackGroundColor(const CGrColor &cGrColor);//задать цвет фона легенды  void SetAxisColor(const CGrColor &cGrColor_AxisX,const CGrColor &cGrColor_AxisY);//задать цвет осей  void SetGridColor(const CGrColor &cGrColor_GridX,const CGrColor &cGrColor_GridY);//задать цвет сетки  void SetSelectRectangleColor(const CGrColor &cGrColor);//задать цвет прямоугольника выделения  void SetTextValueColor(const CGrColor &cGrColor_TextX,const CGrColor &cGrColor_TextY,const CGrColor &cGrColor_TextLegend);//задать цвет текста  //функции настройки стиля линий  void SetAxisLineStyle(const CGrLineStyle &cGrLineStyle_AxisX,const CGrLineStyle &cGrLineStyle_AxisY);//задать стиль осей  void SetGridLineStyle(const CGrLineStyle &cGrLineStyle_GridX,const CGrLineStyle &cGrLineStyle_GridY);//задать стиль сетки  //функции настройки системы координат  void SetRectangle(const CGrRect &cGrRect);//задать прямоугольник системы координат  void SetGridStep(long double step_x,long double step_y);//задать шаг сетки  void GetRectangle(CGrRect &cGrRect) const;//получить прямоугольник системы координат  void GetGridSize(long double &step_x,long double &step_y) const;//получить оптимальный шаг сетки  //функции дополнительных настроек  void SetEnableMagnify(bool enable);//разрешить увеличение  void SetEnableValue(bool x_value,bool y_value);//разрешить отображение значений  void SetOriginalScale(const CGrRect &cGrRect);//задать оригинальный масштаб  void SetMoveMode(bool inversion);//задать режим инверсии мышки  bool GetSelectedRectangle(CGrRect &cGrRect) const;//вернуть прямоугольник выделения  void GetClientRectangle(CGrRect &cGrRect) const;//вернуть прямоугольник графика  void SetName(const std::string &name);//установить название графика  bool GetUserMode(void) const;//получить режим управления пользователем  void SetUserMode(bool state);//установить режим управления пользователем

В принципе, настроить отображение можно достаточно гибко.

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

Пример программы, использующей этот модуль (там и исходники модуля).

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

Управляем звуком ПК от активности пользователя с помощью Python

17.06.2021 14:15:17 | Автор: admin

Настройка программного обеспечения

Без промедления начнём. Нам нужно установить следующее ПО:

  • Windows 10

  • Anaconda 3 (Python 3.8)

  • Visual Studio 2019 (Community) - объясню позже, зачем она понадобится.

Открываем Anaconda Prompt (Anaconda3) и устанавливаем следующие пакеты:

pip install opencv-pythonpip install dlibpip install face_recognition

И уже на этом моменте начнутся проблемы с dlib.

Решаем проблему с dlib

Я перепробовал все решения, что нашёл в интернете и они оказались неактуальными - раз, два, три, официальное руководство и видео есть. Поэтому будем собирать пакет вручную.

Итак, первая же ошибка говорит о том, что у нас не установлен cmake.

ERROR: CMake must be installed to build dlib
ERROR: CMake must be installed to build dlibERROR: CMake must be installed to build dlib

Не закрывая консоль, вводим следующую команду:

pip install cmake
Проблем при установке быть не должно

Пробуем установить пакет той же командой (pip install dlib), но на этот раз получаем новую ошибку:

Отсутствуют элементы Visual Studio

Ошибка явно указывает, что у меня, скорее всего, стоит студия с элементами только для C# - и она оказывается права. Открываем Visual Studio Installer, выбираем "Изменить", в вкладке "Рабочие нагрузки" в разделе "Классические и мобильные приложения" выбираем пункт "Разработка классических приложений на С++":

Пошагово
"Изменить""Изменить"Разработка классических приложений на С++Разработка классических приложений на С++Ждем окончания установкиЖдем окончания установки

Почему важно оставить все галочки, которые предлагает Visual Studio. У меня с интернетом плоховато, поэтому я решил не скачивать пакет SDK для Windows, на что получил следующую ошибку:

Не нашли компилятор

Я начал искать решение этой ошибки, пробовать менять тип компилятора (cmake -G " Visual Studio 16 2019"), но только стоило установить SDK, как все проблемы ушли.

Я пробовал данный метод на двух ПК и отмечу ещё пару подводных камней. Самое главное - Visual Studio должна быть 2019 года. У меня под рукой был офлайн установщик только 2017 - я мигом его поставил, делаю команду на установку пакета и получаю ошибку, что нужна свежая Microsoft Visual C++ версии 14.0. Вторая проблема была связана с тем, что даже установленная студия не могла скомпилировать проект. Помогла дополнительная установка Visual C++ 2015 Build Tools и Microsoft Build Tools 2015.

Открываем вновь Anaconda Prompt, используем ту же самую команду и ждём, когда соберется проект (около 5 минут):

Сборка
Всё прошло успешноВсё прошло успешно

Управляем громкостью

Вариантов оказалось несколько (ссылка), но чем проще - тем лучше. На русском язычном StackOverflow предложили использовать простую библиотеку от Paradoxis - ей и воспользуемся. Чтобы установить её, нам нужно скачать архив, пройти по пути C:\ProgramData\Anaconda3\Lib и перенести файлы keyboard.py, sound.py из архива. Проблем с использованием не возникало, поэтому идём дальше

Собираем события мыши

Самым популярным модулем для автоматизации управления мышью/клавиатурой оказался pynput. Устанавливаем так же через (pip install dlib). У модуля в целом неплохое описание - https://pynput.readthedocs.io/en/latest/mouse.html . Но у меня возникли сложности при получении событий. Я написал простую функцию:

from pynput import mousedef func_mouse():        with mouse.Events() as events:            for event in events:                if event == mouse.Events.Scroll or mouse.Events.Click:                    #print('Переместил мышку/нажал кнопку/скролл колесиком: {}\n'.format(event))                    print('Делаю половину громкости: ', time.ctime())                    Sound.volume_set(volum_half)                    break

Самое интересное, что если раскомментировать самую первую строчку и посмотреть на событие, которое привело выходу из цикла, то там можно увидеть Move. Если вы заметили, в условии if про него не слово. Без разницы, делал я только скролл колесиком или только нажатие любой клавиши мыши - все равно просто движение мыши приводит к выходу из цикла. В целом, мне нужно все действия (Scroll, Click, Move), но такое поведение я объяснить не могу. Возможно я где-то ошибаюсь, поэтому можете поправить.

А что в итоге?

Adam Geitgey, автор библиотеки face recognition, в своём репозитории имеет очень хороший набор примеров, которые многие используют при написании статей: https://github.com/ageitgey/face_recognition/tree/master/examples

Воспользуемся одним из них и получим следующий код, который можно скачать по ссылке: Activity.ipynb, Activity.py

Код
# Подключаем нужные библиотекиimport cv2import face_recognition # Получаем данные с устройства (веб камера у меня всего одна, поэтому в аргументах 0)video_capture = cv2.VideoCapture(0) # Инициализируем переменныеface_locations = []from sound import SoundSound.volume_up() # увеличим громкость на 2 единицыcurrent = Sound.current_volume() # текущая громкость, если кому-то нужноvolum_half=50  # 50% громкостьvolum_full=100 # 100% громкостьSound.volume_max() # выставляем сразу по максимуму# Работа со временем# Подключаем модуль для работы со временемimport time# Подключаем потокиfrom threading import Threadimport threading# Функция для работы с активностью мышиfrom pynput import mousedef func_mouse():        with mouse.Events() as events:            for event in events:                if event == mouse.Events.Scroll or mouse.Events.Click:                    #print('Переместил мышку/нажал кнопку/скролл колесиком: {}\n'.format(event))                    print('Делаю половину громкости: ', time.ctime())                    Sound.volume_set(volum_half)                    break# Делаем отдельную функцию с напоминаниемdef not_find():    #print("Cкрипт на 15 секунд начинается ", time.ctime())    print('Делаю 100% громкости: ', time.ctime())    #Sound.volume_set(volum_full)    Sound.volume_max()        # Секунды на выполнение    #local_time = 15    # Ждём нужное количество секунд, цикл в это время ничего не делает    #time.sleep(local_time)        # Вызываю функцию поиска действий по мышке    func_mouse()    #print("Cкрипт на 15 сек прошел")# А тут уже основная часть кодаwhile True:    ret, frame = video_capture.read()        '''    # Resize frame of video to 1/2 size for faster face recognition processing    small_frame = cv2.resize(frame, (0, 0), fx=0.50, fy=0.50)    rgb_frame = small_frame[:, :, ::-1]    '''    rgb_frame = frame[:, :, ::-1]        face_locations = face_recognition.face_locations(rgb_frame)        number_of_face = len(face_locations)        '''    #print("Я нашел {} лицо(лица) в данном окне".format(number_of_face))    #print("Я нашел {} лицо(лица) в данном окне".format(len(face_locations)))    '''        if number_of_face < 1:        print("Я не нашел лицо/лица в данном окне, начинаю работу:", time.ctime())        '''        th = Thread(target=not_find, args=()) # Создаём новый поток        th.start() # И запускаем его        # Пока работает поток, выведем на экран через 10 секунд, что основной цикл в работе        '''        #time.sleep(5)        print("Поток мыши заработал в основном цикле: ", time.ctime())                #thread = threading.Timer(60, not_find)        #thread.start()                not_find()        '''        thread = threading.Timer(60, func_mouse)        thread.start()        print("Поток мыши заработал.\n")        # Пока работает поток, выведем на экран через 10 секунд, что основной цикл в работе        '''        #time.sleep(10)        print("Пока поток работает, основной цикл поиска лица в работе.\n")    else:        #все хорошо, за ПК кто-то есть        print("Я нашел лицо/лица в данном окне в", time.ctime())        Sound.volume_set(volum_half)            for top, right, bottom, left in face_locations:        cv2.rectangle(frame, (left, top), (right, bottom), (0, 0, 255), 2)        cv2.imshow('Video', frame)        if cv2.waitKey(1) & 0xFF == ord('q'):        breakvideo_capture.release()cv2.destroyAllWindows()

Суть кода предельно проста: бегаем в цикле, как только появилось хотя бы одно лицо (а точнее координаты), то звук делаем 50%. Если не нашёл никого поблизости, то запускаем цикл с мышкой.

Тестирование в бою

Ожидание и реальность

Если вы посмотрели видео, то поняли, что результат ещё далёк от реальной эксплуатации.

Признаю честно - до этого момента никогда не сталкивался с многопоточностью на Python, поэтому "с наскоку" тему взять не удалось и результат по видео понятен. Есть неплохая статья на Хабре, описывающая различные методы многопоточности, применяемые в языке. Пока у меня решения нету по этой теме нету - будет повод разобраться лучше и дописать код/статью с учетом этого.

Так же возникает закономерный вопрос - а если вместо живого человека поставить перед монитором картинку? Да, она распознает, что, скорее всего, не совсем верно. Мне попался очень хороший материал по поводу определения живого лица в реальном времени - https://www.machinelearningmastery.ru/real-time-face-liveness-detection-with-python-keras-and-opencv-c35dc70dafd3/ , но это уже немного другой уровень и думаю новичкам это будет посложнее. Но эксперименты с нейронными сетями я чуть позже повторю, чтобы тоже проверить верность и повторяемость данного руководства.

Немаловажным фактором на качество распознавания оказывает получаемое изображение с веб-камеры. Предложение использовать 1/4 изображения (сжатие его) приводит только к ухудшению - моё лицо алгоритм распознать так и не смог. Для повышения качества предлагают использовать MTCNN face detector (пример использования), либо что-нибудь посложнее из абзаца выше.

Другая интересная особенность - таймеры в Питоне. Я, опять же, признаю, что ни разу до этого не было нужды в них, но все статьях сводится к тому, чтобы ставить поток в sleep(кол-во секунд). А если мне нужно сделать так, чтобы основной поток был в работе, а по истечению n-ое количества секунд не было активности, то выполнялась моя функция? Использовать демонов (daemon)? Так это не совсем то, что нужно. Писать отдельную программу, которая взаимодействует с другой? Возможно, но единство программы пропадает.

Заключение

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

P.S. Предлагаю вам, читатели, обсудить в комментариях статью - ваши идеи, замечания, уточнения.

Подробнее..

Перевод Показания загруженности процессора в диспетчере задач ни о чем на самом деле не говорят

17.04.2021 22:08:49 | Автор: admin

Когда диспетчер задач сообщает вам, что процессор загружен на все 100 %, вы можете подумать, что система работает изо всех сил и не имеет дополнительных вычислительных мощностей. А когда диспетчер задач сообщает о том, что ЦП загружен на 40 %, вы интуитивно полагаете, что он потребляет 40 % от всей общей доступной мощности. То было достаточно точное толкование как в Windows 7, так и Windows Server 2008 R2. Но с выходом нового дизайна, впервые представленного в Windows 8, вкладки Процессы и Производительность диспетчера задач теперь полагаются на разные метрики и показывают цифры, которые вводят в заблуждение и которые без дополнительного контекста совершенно бессмысленны.

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

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

Чтобы попытаться учесть эти сложности, Windows ввела понятие служебной программы процессора вместе со счетчиками производительности, которые измеряют эту служебную программу. Windows определяет полезность процессора как объем выполненной работы в процентах от объема работы, которую он мог бы выполнить, если бы он постоянно работал на своем номинальном уровне производительности и никогда не включал режим повышенной производительности. Когда процессор включает режим повышенной производительности обычное вообще явление его процент полезности может легко превысить 100 %. Возможная верхняя граница неопределенна и зависит от множества факторов, которые Windows не может измерить напрямую. Вкладки Процессы и Производительность диспетчера задач теперь используют счетчик процент полезности процессора в качестве основы для показания их загруженности, а не счетчик процент загруженности процессора, на который полагался Диспетчер задач, который по-прежнему используется вкладкой Подробности диспетчера задач и Process Explorer, входящий в комплект Sysinternals.

В результате, поскольку динамически настраиваемые процессоры автоматически переходят в расширенный режим под нагрузкой, процент загруженности ЦП диспетчера задач на процесс, который активно выполняет код, обычно будет значительно больше, чем время его процессора. Но поскольку это число находится на шкале, которая ограничена 100 %, диспетчер задач создает впечатление, что процесс занимает больше вычислительных мощностей системы, чем есть на самом деле. Некоторое время сам диспетчер задач сообщал об общей загрузке ЦП более 100 %. Поскольку это противоречит здравому смыслу даже для людей, которые понимают концепцию полезности, диспетчер задач теперь ограничивает сообщаемые значения на отметке в 100 %. Это приводит в дальнейшем еще к большему искажению. Я покажу.

На приведенных ниже скриншотах Process Explorer и вкладка Производительность диспетчера задач подтверждают, что ровно половина из двенадцати потоков полностью загружены, а остальные процессоры почти полностью простаивают. Однако обратите внимание, что Process Explorer сообщает об общем проценте загруженности ЦП чуть более 50, а диспетчер задач сообщает о 83. Возможно, это должно быть 83 % по шкале 166 %. Но что это вообще значит? Процент буквально означает на сотню. Вкладка Производительность диспетчера задач также показывает, что базовая частота процессоров составляет 2,40 ГГц, но во время этого измерения скорость составляет 3,94 ГГц. Получается, что 83 % умножить на 2,40/3,94 это 50,56%, что близко к фактическому использованию. 83 процента в данном случае фактически находятся во временной шкале 164,17 (100 %*3,94/2,40). Это объясняет, как получаются числа, но не делает их интуитивно понятными или значимыми.

Диалоговое окно "Информация о системе" в Process ExplorerДиалоговое окно "Информация о системе" в Process ExplorerВкладка "Производительность" диспетчера задачВкладка "Производительность" диспетчера задач

На вкладке Процессы диспетчера задач указано, что моя тестовая программа потребляет 82,8 %, то есть почти все из 83 % общего использования, о которых она сообщает. В то же время на вкладке Подробности диспетчера задач указано, что моя программа потребляет 50 % ресурсов ЦП. Цифры на вкладке "Подробности", основанные на "проценте загруженности процессора", а не на "процент полезности процессора" и более понятны, чем числа на вкладке "Процессы".

Вкладка "Процессы" диспетчера задачВкладка "Процессы" диспетчера задачИ его вкладка "Подробности"И его вкладка "Подробности"

Давайте возьмем его на ступеньку выше и запустим восемь потоков, привязанных к потокам ЦП, задействуя две трети процессора, оставив остальные простаивать. Вкладка Производительность диспетчера задач сообщает о 100 % загрузке ЦП, подразумевая, что система работает на максимальной мощности, в то время, как и она, и Process Explorer ясно показывают, что четыре из двенадцати процессоров простаивают и готовы к работе. На вкладке Процессы диспетчера задач указано, что тестовая программа использует 100 % ЦП, а на вкладке Подробности указано, что она потребляет только 67 % две трети доступной вычислительной мощности.

Process ExplorerProcess ExplorerВкладка "Производительность" диспетчера задачВкладка "Производительность" диспетчера задачВкладка "Процессы" диспетчера задачВкладка "Процессы" диспетчера задачВкладка "Сведения" диспетчера задачВкладка "Сведения" диспетчера задач

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

Здесь можно взглянуть на точку зрения моего коллеги Джеффа Стоукса по поводу данного вопроса.

Подробнее..

Представляем Windows Terminal Preview 1.8

22.04.2021 10:12:30 | Автор: admin

Добро пожаловать в очередной выпуск Windows Terminal! Этот выпуск обновляет Windows Terminal Preview до версии 1.8 и основной Windows Terminal до версии 1.7. Обе сборки можно установить в Microsoft Store, а также со страницы выпусков GitHub. Давайте узнаем, что нового!

Пользовательский интерфейс настроек по умолчанию в стабильной версии

Пользовательский интерфейс настроек теперь входит в стабильную сборку Windows Terminal! Вы можете получить доступ к пользовательскому интерфейсу настроек, нажав кнопку настроек в раскрывающемся меню или нажав Ctrl+,. Мы все еще работаем над улучшением интерфейса настроек, поэтому следите за обновлениями по мере выхода новых версий!

Shift + щелчок для открытия профиля в новом окне

Теперь вы можете удерживать Shift и щелкнуть профиль в раскрывающемся меню, чтобы открыть этот профиль в новом окне. Эта функция добавлена к уже существующей функции удерживания Alt и щелчка по профилю, чтобы открыть его на новой панели.

Параметры внешнего вида без фокусировки

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

// Устанавливает непрозрачность фонового изображения профиля на 0,3 без фокусировки "unfocusedAppearance": {    "backgroundImageOpacity": 0.3},

Именование окон

Мы добавили возможность давать имена окнам терминала. Это упрощает идентификацию ваших окон при использовании аргументов командной строки wt, которые определяют определенное окно с аргументом --window, -w . Если вы хотите легко идентифицировать свое окно, вы можете использовать действие identifyWindow. Вы можете назвать новое окно с помощью командной строки или переименовать существующее окно с помощью палитры команд. Дополнительную информацию об этих командах нового окна можно найти на нашем сайте документации.

Обновления пользовательского интерфейса настроек

Раскрывающийся список шрифтов

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

Удаление базового слоя

Команда терминала решила удалить страницу базового уровня из пользовательского интерфейса настроек. Это связано с некоторыми архитектурными конфликтами с расширениями фрагментов JSON. В настоящее время мы планируем альтернативные способы одновременного редактирования всех ваших профилей с помощью пользовательского интерфейса настроек. Вы по-прежнему можете использовать раздел defaults в файле settings.json в качестве временного решения. Мы собираем отзывы об этом изменении в этом разделе и хотели бы услышать ваше мнение.

Новые аргументы командной строки

Скрыть заголовок приложения

Аргумент suppressApplicationTitle и флаги --suppressApplicationTitle/--useApplicationTitle были добавлены в список аргументов командной строки wt. Эти новые дополнения позволяют указать, хотите ли вы, чтобы определенный профиль подавлял изменение заголовка приложения при запуске при использовании командной строки. Более подробную информацию о настройке подавления заголовка приложения можно найти на нашем сайте документации.

Разные улучшения

  • tabSwitcherMode был добавлен в действия nextTab и prevTab

  • Палитра команд стала еще более доступной благодаря дополнительным объявлениям МАУ

  • Предупреждение о многострочной вставке теперь отключено в режиме вставки в скобках

  • Подкоманда split-pane теперь принимает -D, - duplicate для дублирования профиля текущей активной панели

  • Теперь вы можете использовать параметр --colorScheme в командах new-tab и split-pane

Багфиксы

  • Выбор в несколько кликов снова работает должным образом

  • Наведение указателя мыши на URL-адреса теперь работает в режиме мыши VT

  • Команда scrollToBottom теперь работает должным образом

  • Терминал теперь правильно распознает файлы шейдеров и шрифты.

Подробнее..

Настройки Windows 10 часть III, или куда приводят скрипты

23.04.2021 14:18:12 | Автор: admin

Здравствуйте, товарищи! Прошло чуть больше полугода после выхода предыдущей статьи о Windows 10 Sophia Script скрипте, который за прошедшие годы стал самым крупным (а их осталось всего два) опенсорс-проектом по персонализации и настройке Windows 10, а также автоматизации рутинных задач. В статье я расскажу, что изменилось с момента релиза версии, описываемой в статье от 29.09.2020, с какими трудностями мы столкнулись, и куда всё движется.


Как всё начиналось

Разработка наброска скрипта берёт своё начало в те далёкие времена, когда после года работы экономистом в отделе проектирования птицефабрики в одной организации я решил перейти в местный отдел IT.

Отдел ITОтдел IT

Перейдя на новое место, я предложил автоматизировать настройку пользовательских ОС. Так, через пару недель, появился первый прародитель данного модуля примитивный .reg-файл для настройки Windows 8.1. Но я и ему был рад, так как всё было в новинку.

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

В первый день выхода Windows 10 я сразу же "пересел" на неё, поняв, что Windows 8.1 осталась для Microsoft в прошлом. Со временем же "батник" рос, "мужал", разрастался и в какой-то момент даже стал дёргать другой интерпретатор, powershell.exe. Скорость работы падала, и я понимал, что придётся учить PowerShell, так как batch уже не удовлетворяет моим маниакальным запросам автоматизировать всё при настройке ОС.

Как сейчас помню, в феврале 2017 года я сел читать первую статью по запросу "как внести данные в реестр с помощью PowerShell". Уже к лету 2017 года я значительно продвинулся в переписывании всех имеющих функций из "батника" в новый скрипт.

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

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

В таком неспешном темпе разработка шла до августа 2019 года, когда я решил поделиться своими наработками здесь. Хотя я читаю Хабр с года эдак 2007-го, зарегистрировался лишь в 2016-м.

Немного облагородил код (ага, 10 раз), добавил описания на английском языке и накатал крохотную статью о своей pet-разработке. Удивительно, но статью пропустили, она попала в бездну, и я сел ждать.

Мой лик, когда ожидаю приглашенияМой лик, когда ожидаю приглашения

Как сейчас помню: сижу на сеансе в кинотеатре, и приходит уведомление на почту о новом комментарии к моей статье. Так, стоп! Её одобрили?!

Я не успевал отвечать на комментарии! Это была какая-то эйфория. Какой там фильм?! меня на Хабр пригласили прямым инвайтом! Фурор! Даже код не обо...ли (а там был кровавый мрак) и вообще любезно приняли.

Мой лик, когда получил приглашениеМой лик, когда получил приглашение

Самым неожиданным поворотом стало то, что через 5 дней после публикации мне написал некий Дмитрий (@oz-zo), прочитавший моё сетование на то, что у меня не хватает знаний, чтобы сделать графическую версию скрипта, даже хотя бы на Windows.Forms. Я был приятно удивлён, что есть ещё один старый безумец. Как выяснилось, старый, но не бесполезный!

Познакомившись, мы запланировали всё сделать примерно за 3 месяца на Windows.Forms, но наше приключение затянулось больше чем на 1,5 года: лишь в этом месяце мы вышли на финишную прямую по созданию графической версии моего скрипта SophiApp. Но это уже другая история, и, когда будет что показать, я обязательно расскажу, что мы пережили за время разработки, поделившись нашими инфернальными набросками и наработками.

С того времени как я познакомился с Дмитрием, разработка пошла быстрее: он внёс огромный вклад в создание новых функций, которых не было ни у кого: все графические функции с использованием WPF и логику к ним написал именно он; я лишь объяснил, как получать данные.

Иконка Sophia ScriptИконка Sophia Script

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

Хотя Дэвиду уже нормально так, программировать он сел лишь недавно, окончив курсы. Но его программа выполняет ту задачу, для которой её и написали. Одним словом, люди пользуются.

Sophia Script WrapperSophia Script Wrapper

Что поменялось в скрипте

За время, прошедшее с момента выхода прошлой статьи в сентябре, много воды утекло. Скрипт уже и не узнать. Больше 12 000 строк кода Самые интересные изыскания пришлись на удаление UWP-приложений и закрепление ярлыков на начальный экран.

Напомню, какие версии Windows 10 поддерживает скрипт на данный момент.

Версия

Маркетинговое название

Билд

Архитектура

Издания

21H1

Spring 2021 Update

19043

x64

Home/Pro/Enterprise

20H2

October 2020 Update

19042

x64

Home/Pro/Enterprise

2004

May 2020 Update

19041

x64

Home/Pro/Enterprise

1809

LTSC Enterprise 2019

17763

x64

Enterprise

А теперь пройдёмся по всем доработанным и новым функциям.

Функции касающиеся манипуляций с UWP-приложениями

Было/СталоБыло/Стало

Наверное, вы уже заметили, что список стал локализованным. Также хочется добавить, что список генерируется динамически, загружая лишь установленные пакеты UWP-приложений в соответствии с текущей локализацией. Как это реализовано?

Свойство DysplayName, которое содержит локализованное имя пакета, находится лишь в одном классе (Get-AppxPackage вам никак тут не может, к сожалению):

"Windows.Management.Deployment.PackageManager"

[Windows.Management.Deployment.PackageManager, Windows.Web, ContentType = WindowsRuntime]::new().FindPackages() | Select-Object -Property DisplayName -ExpandProperty Id | Select-Object -Property Name, DisplayName
На выходе вы получите что-то вроде этого (простыню кода прячу под спойлер)
Name                                        DisplayName                                          ----                                        -----------                                          1527c705-839a-4832-9118-54d4Bd6a0c89                                                             c5e2524a-ea46-4f67-841f-6a9465d9d515        Проводник                                            E2A4F912-2574-4A75-9BB0-0D023378592B        Сопоставитель приложений                             F46D4000-FD22-4DB4-AC8E-4E1DDDE828FE        Диалоговое окно "Добавить рекомендованные папки"     Microsoft.AAD.BrokerPlugin                  Учетная запись компании или учебного заведения       Microsoft.AccountsControl                   Электронная почта и учетные записи                   Microsoft.AsyncTextService                  AsyncTextService                                     Microsoft.BioEnrollment                     Настройка Windows Hello                              Microsoft.CredDialogHost                    Диалоговое окно учетных данных                       Microsoft.ECApp                             Управление глазами                                   Microsoft.LockApp                           Экран блокировки Windows по умолчанию                Microsoft.MicrosoftEdgeDevToolsClient       Клиент средств разработчика для Microsoft Edge       Microsoft.MicrosoftEdge                                                                          Microsoft.Win32WebViewHost                  Веб-средство просмотра классических приложений       Microsoft.Windows.Apprep.ChxApp             SmartScreen Защитника Windows                        Microsoft.Windows.AssignedAccessLockApp     Приложение "Блокировка" при ограниченном доступе     Microsoft.Windows.CallingShellApp           Видеозвонки                                          Microsoft.Windows.CapturePicker             CapturePicker                                        Microsoft.Windows.CloudExperienceHost       Ваша учетная запись                                  Microsoft.Windows.ContentDeliveryManager    Содержимое, предоставленное корпорацией Майкрософт   Microsoft.Windows.NarratorQuickStart        Экранный диктор                                      Microsoft.Windows.OOBENetworkCaptivePortal  Поток портала авторизации                            Microsoft.Windows.OOBENetworkConnectionFlow Последовательность действий при сетевом подключении  Microsoft.Windows.ParentalControls          Функции семьи учетных записей Майкрософт             Microsoft.Windows.PeopleExperienceHost      Windows Shell Experience                             Microsoft.Windows.PinningConfirmationDialog PinningConfirmationDialog                            Microsoft.Windows.Search                    Windows Search                                       Microsoft.Windows.SecHealthUI               Безопасность Windows                                 Microsoft.Windows.SecureAssessmentBrowser   Тестирование                                         Microsoft.Windows.ShellExperienceHost       Windows Shell Experience                             Microsoft.Windows.StartMenuExperienceHost   Запустить                                            Microsoft.Windows.XGpuEjectDialog           Безопасное извлечение устройства                     Microsoft.XboxGameCallableUI                Xbox Game UI                                         MicrosoftWindows.Client.CBS                 Windows Feature Experience Pack                      MicrosoftWindows.UndockedDevKit             UDK Package                                          NcsiUwpApp                                  NcsiUwpApp                                           Windows.CBSPreview                          Предварительный просмотр штрихкодов Windows          windows.immersivecontrolpanel               Параметры                                            Windows.PrintDialog                         PrintDialog                                          Microsoft.Services.Store.Engagement         Microsoft Engagement Framework                       Microsoft.Services.Store.Engagement         Microsoft Engagement Framework                       Microsoft.UI.Xaml.2.0                       Microsoft.UI.Xaml.2.0                                Microsoft.VCLibs.140.00                     Microsoft Visual C++ 2015 UWP Runtime Package        Microsoft.Advertising.Xaml                  Microsoft Advertising SDK for XAML                   Microsoft.NET.Native.Framework.2.2          Microsoft .Net Native Framework Package 2.2          Microsoft.NET.Native.Framework.2.2          Microsoft .Net Native Framework Package 2.2          Microsoft.VCLibs.140.00                     Microsoft Visual C++ 2015 UWP Runtime Package        Microsoft.VCLibs.140.00                     Microsoft Visual C++ 2015 UWP Runtime Package        Microsoft.NET.Native.Runtime.2.2            Microsoft .Net Native Runtime Package 2.2            Microsoft.NET.Native.Runtime.2.2            Microsoft .Net Native Runtime Package 2.2            Microsoft.VCLibs.140.00.UWPDesktop          Microsoft Visual C++ 2015 UWP Desktop Runtime PackageMicrosoft.VCLibs.140.00.UWPDesktop          Microsoft Visual C++ 2015 UWP Desktop Runtime PackageMicrosoft.UI.Xaml.2.1                       Microsoft.UI.Xaml.2.1                                Microsoft.UI.Xaml.2.1                       Microsoft.UI.Xaml.2.1                                Microsoft.UI.Xaml.2.0                       Microsoft.UI.Xaml.2.0                                Microsoft.UI.Xaml.2.3                       Microsoft.UI.Xaml.2.3                                Microsoft.UI.Xaml.2.3                       Microsoft.UI.Xaml.2.3                                Microsoft.UI.Xaml.2.4                       Microsoft.UI.Xaml.2.4                                Microsoft.UI.Xaml.2.4                       Microsoft.UI.Xaml.2.4                                Microsoft.ScreenSketch                      Набросок на фрагменте экрана                         Microsoft.NET.Native.Framework.1.7          Microsoft .Net Native Framework Package 1.7          Microsoft.NET.Native.Framework.1.7          Microsoft .Net Native Framework Package 1.7          Microsoft.NET.Native.Runtime.1.7            Microsoft .Net Native Runtime Package 1.7            Microsoft.NET.Native.Runtime.1.7            Microsoft .Net Native Runtime Package 1.7            Microsoft.VCLibs.120.00                     Microsoft Visual C++ Runtime Package                 Microsoft.VCLibs.120.00                     Microsoft Visual C++ Runtime Package                 Microsoft.VCLibs.140.00.UWPDesktop          Microsoft Visual C++ 2015 UWP Desktop Runtime PackageMicrosoft.VCLibs.140.00.UWPDesktop          Microsoft Visual C++ 2015 UWP Desktop Runtime PackageMicrosoft.VCLibs.140.00                     Microsoft Visual C++ 2015 UWP Runtime Package        Microsoft.VCLibs.140.00                     Microsoft Visual C++ 2015 UWP Runtime Package        Microsoft.WebpImageExtension                Расширения для изображений Webp                      Microsoft.DesktopAppInstaller               Установщик приложения                                Microsoft.NET.Native.Framework.2.2          Microsoft .Net Native Framework Package 2.2          Microsoft.NET.Native.Framework.2.2          Microsoft .Net Native Framework Package 2.2          AppUp.IntelGraphicsExperience               Центр управления графикой Intel                     Microsoft.Windows.StartMenuExperienceHost   Запустить                                            Microsoft.Windows.ShellExperienceHost       Windows Shell Experience                             Microsoft.Windows.AssignedAccessLockApp     Приложение "Блокировка" при ограниченном доступе     Microsoft.WindowsTerminal                   Windows Terminal                                     Microsoft.AV1VideoExtension                 AV1 Video Extension                                  Microsoft.HEIFImageExtension                Расширения для изображений HEIF                      Microsoft.Windows.Photos                    Фотографии (Майкрософт)                              Microsoft.UI.Xaml.2.5                       Microsoft.UI.Xaml.2.5                                Microsoft.UI.Xaml.2.5                       Microsoft.UI.Xaml.2.5                                Microsoft.WindowsStore                      Microsoft Store                                      Microsoft.StorePurchaseApp                  Узел для покупок в Store                             Microsoft.LanguageExperiencePackru-RU       Пакет локализованного интерфейса на русском          Microsoft.MicrosoftEdge                     Microsoft Edge                                       Microsoft.VP9VideoExtensions                Расширения для VP9-видео                             MicrosoftWindows.Client.WebExperience       Windows Web Experience Pack                          Microsoft.WebMediaExtensions                Расширения для интернет-мультимедиа                  Microsoft.HEVCVideoExtension                Расширения для видео HEVC от производителя устройстваMicrosoftWindows.Client.CBS                 Windows Feature Experience Pack                      Microsoft.MicrosoftEdge.Stable              Microsoft Edge     
Первые попытки переписать функцию удаления UWP-пакетовПервые попытки переписать функцию удаления UWP-пакетов

Хоть на картинке и не видно, но кнопка "Для всех пользователей" была тоже полностью переписана. Раньше она совершенно неправильно работала. Сейчас же её логика приведена к должному функционалу. По умолчанию при загрузке формы отображается список приложений для текущего пользователя (все системные пакеты и Microsoft Store исключены из списка, так что удалить хоть что-то важное не получится никак, в отличие, кстати, от всех других скриптов в Интернете). При нажатии на кнопку "Для всех пользователей" происходит динамическая перегенерация списка с учётом установленных пакетов во всех учётных записях. То есть вы можете удалить все приложения для текущего пользователя, и форма отобразится пустой, но при запуске функции с ключом "-ForAllUsers" отобразится список пакетов для всех учётных записей.

Как-то меня попросили добавить поддержку PowerShell 7. И всё это было бы смешно, когда бы не было так грустно

Во-первых, ни для кого не будет секретом, что, хотя в PowerShell 7 исправили очень много багов, в нынешнем виде очень далёк от финальной версии, ведь там до сих пор даже не работает командлет Get-ComputerRestorePoint из коробки. И (в качестве временного решения) Microsoft предложил загружать в сессию недостающие модули из папки PowerShell 5.1, используя аргумент -UseWindowsPowerShell.

Таким образом, мне приходится загружать модули Microsoft.PowerShell.Management, PackageManagement, Appx, чтобы воссоздать работоспособность скрипта на PowerShell 7:

Import-Module -Name Microsoft.PowerShell.Management, PackageManagement, Appx -UseWindowsPowerShell

Во-вторых, код для получения локализованных имен UWP-пакетов не работает в PowerShell 7 вообще, так как Microsoft решил не включать библиотеки WinRT в релизы PowerShell 7, но вынес разработку на отдельные ресурсы: WinRT и Windows.SDK. Это упомянул и Steven Lee в обсуждении на GitHub, а также уведомил, что команда PowerShell решила не включать в дальнейшем в релизы эти библиотеки. Поэтому, чтобы вызвать необходимые API, мне приходится хранить в папке две библиотеки по 26 МБ и 284 КБ. Тут остаётся лишь поставить мем с пингвином.

Код для получения локализованных имен UWP-пакетов на PowerShell 7 выглядит так:

Add-Type -AssemblyName "$PSScriptRoot\Libraries\WinRT.Runtime.dll"Add-Type -AssemblyName "$PSScriptRoot\Libraries\Microsoft.Windows.SDK.NET.dll"$AppxPackages = Get-AppxPackage -PackageTypeFilter Bundle -AllUsers$PackagesIds = [Windows.Management.Deployment.PackageManager]::new().FindPackages().AdditionalTypeData[[Collections.IEnumerable].TypeHandle] | Select-Object -Property DisplayName -ExpandProperty Id | Select-Object -Property Name, DisplayNameforeach ($AppxPackage in $AppxPackages){$PackageId = $PackagesIds | Where-Object -FilterScript {$_.Name -eq $AppxPackage.Name}if (-not $PackageId){continue}[PSCustomObject]@{Name = $AppxPackage.NamePackageFullName = $AppxPackage.PackageFullNameDisplayName = $PackageId.DisplayName}}

На выходе будет что-то вроде:

Name                                         PackageFullName                                                                DisplayName----                                         ---------------                                                                -----------RealtekSemiconductorCorp.RealtekAudioControl RealtekSemiconductorCorp.RealtekAudioControl_1.1.137.0_neutral_~_dt26b99r8h8gj Realtek Audio ControlMicrosoft.MicrosoftStickyNotes               Microsoft.MicrosoftStickyNotes_3.7.142.0_neutral_~_8wekyb3d8bbwe               Microsoft Sticky NotesMicrosoft.ScreenSketch                       Microsoft.ScreenSketch_2020.814.2355.0_neutral_~_8wekyb3d8bbwe                 Набросок на фрагменте экранаMicrosoft.WindowsCalculator                  Microsoft.WindowsCalculator_2020.2008.2.0_neutral_~_8wekyb3d8bbwe              Windows CalculatorAppUp.IntelGraphicsExperience                AppUp.IntelGraphicsExperience_1.100.3282.0_neutral_~_8j3eq9eme6ctt             Центр управления графикой IntelMicrosoft.MicrosoftSolitaireCollection       Microsoft.MicrosoftSolitaireCollection_4.7.10142.0_neutral_~_8wekyb3d8bbwe     Microsoft Solitaire CollectionMicrosoft.DesktopAppInstaller                Microsoft.DesktopAppInstaller_2020.1112.20.0_neutral_~_8wekyb3d8bbwe           Установщик приложенияMicrosoft.WindowsStore                       Microsoft.WindowsStore_12101.1001.1413.0_neutral_~_8wekyb3d8bbwe               Microsoft StoreMicrosoft.Windows.Photos                     Microsoft.Windows.Photos_2020.20120.4004.0_neutral_~_8wekyb3d8bbwe             Фотографии (Майкрософт)Microsoft.WebMediaExtensions                 Microsoft.WebMediaExtensions_1.0.40471.0_neutral_~_8wekyb3d8bbwe               Расширения для интернет-мультимMicrosoft.WindowsCamera                      Microsoft.WindowsCamera_2021.105.10.0_neutral_~_8wekyb3d8bbwe                  Камера WindowsMicrosoft.StorePurchaseApp                   Microsoft.StorePurchaseApp_12103.1001.813.0_neutral_~_8wekyb3d8bbwe            Узел для покупок в StoreMicrosoft.WindowsTerminal                    Microsoft.WindowsTerminal_2021.413.2245.0_neutral_~_8wekyb3d8bbwe              Windows TerminalMicrosoft.WindowsTerminalPreview             Microsoft.WindowsTerminalPreview_2021.413.2303.0_neutral_~_8wekyb3d8bbwe       Windows Terminal Preview

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

Восстановление удаленных UWP-приложений для текущего пользователяВосстановление удаленных UWP-приложений для текущего пользователя

Код для получения общего списка всех манифестов выглядит так (можете даже выполнить, если, конечно, не удалили все UWP-приложения):

$Bundles = (Get-AppXPackage -PackageTypeFilter Framework -AllUsers).PackageFullNameGet-ChildItem -Path "HKLM:\SOFTWARE\Classes\Local Settings\Software\Microsoft\Windows\CurrentVersion\AppModel\PackageRepository\Packages" | ForEach-Object -Process {Get-ItemProperty -Path $_.PSPath} | Where-Object -FilterScript {$_.Path -match "Program Files"} | Where-Object -FilterScript {$_.PSChildName -notin $Bundles} | Where-Object -FilterScript {$_.Path -match "x64"} | ForEach-Object -Process {"$($_.Path)\AppxManifest.xml"}
И вы увидите что-то вроде:
C:\Program Files\WindowsApps\A025C540.Yandex.Music_4.40.7713.0_x64__vfvw9svesycw6\AppxManifest.xmlC:\Program Files\WindowsApps\Microsoft.549981C3F5F10_2.2103.17603.0_x64__8wekyb3d8bbwe\AppxManifest.xmlC:\Program Files\WindowsApps\Microsoft.BingNews_1.0.6.0_x64__8wekyb3d8bbwe\AppxManifest.xmlC:\Program Files\WindowsApps\Microsoft.BingWeather_1.0.6.0_x64__8wekyb3d8bbwe\AppxManifest.xmlC:\Program Files\WindowsApps\Microsoft.DesktopAppInstaller_1.11.10771.0_x64__8wekyb3d8bbwe\AppxManifest.xmlC:\Program Files\WindowsApps\Microsoft.GamingApp_1.0.1.0_x64__8wekyb3d8bbwe\AppxManifest.xmlC:\Program Files\WindowsApps\Microsoft.GetHelp_10.2102.40951.0_x64__8wekyb3d8bbwe\AppxManifest.xmlC:\Program Files\WindowsApps\Microsoft.Getstarted_10.2.40751.0_x64__8wekyb3d8bbwe\AppxManifest.xmlC:\Program Files\WindowsApps\Microsoft.HEIFImageExtension_1.0.40978.0_x64__8wekyb3d8bbwe\AppxManifest.xmlC:\Program Files\WindowsApps\Microsoft.MicrosoftOfficeHub_18.2008.12711.0_x64__8wekyb3d8bbwe\AppxManifest.xmlC:\Program Files\WindowsApps\Microsoft.MicrosoftSolitaireCollection_4.9.4072.0_x64__8wekyb3d8bbwe\AppxManifest.xmlC:\Program Files\WindowsApps\Microsoft.MicrosoftStickyNotes_1.8.15.0_x64__8wekyb3d8bbwe\AppxManifest.xmlC:\Program Files\WindowsApps\Microsoft.Paint_10.2103.1.0_x64__8wekyb3d8bbwe\AppxManifest.xmlC:\Program Files\WindowsApps\Microsoft.People_10.1909.12456.0_x64__8wekyb3d8bbwe\AppxManifest.xmlC:\Program Files\WindowsApps\Microsoft.PowerAutomateDesktop_1.0.31.0_x64__8wekyb3d8bbwe\AppxManifest.xmlC:\Program Files\WindowsApps\Microsoft.ScreenSketch_11.2103.13.0_x64__8wekyb3d8bbwe\AppxManifest.xmlC:\Program Files\WindowsApps\Microsoft.SkypeApp_14.53.77.0_x64__kzf8qxf38zg5c\AppxManifest.xmlC:\Program Files\WindowsApps\Microsoft.StorePurchaseApp_12103.1001.8.0_x64__8wekyb3d8bbwe\AppxManifest.xmlC:\Program Files\WindowsApps\Microsoft.Todos_0.41.4902.0_x64__8wekyb3d8bbwe\AppxManifest.xmlC:\Program Files\WindowsApps\Microsoft.VP9VideoExtensions_1.0.40631.0_x64__8wekyb3d8bbwe\AppxManifest.xmlC:\Program Files\WindowsApps\Microsoft.WebMediaExtensions_1.0.40831.0_x64__8wekyb3d8bbwe\AppxManifest.xmlC:\Program Files\WindowsApps\Microsoft.WebpImageExtension_1.0.32731.0_x64__8wekyb3d8bbwe\AppxManifest.xmlC:\Program Files\WindowsApps\Microsoft.Windows.Photos_2021.21030.17018.0_x64__8wekyb3d8bbwe\AppxManifest.xmlC:\Program Files\WindowsApps\Microsoft.WindowsAlarms_1.0.38.0_x64__8wekyb3d8bbwe\AppxManifest.xmlC:\Program Files\WindowsApps\Microsoft.WindowsCalculator_10.2103.8.0_x64__8wekyb3d8bbwe\AppxManifest.xmlC:\Program Files\WindowsApps\Microsoft.WindowsCamera_2020.503.58.0_x64__8wekyb3d8bbwe\AppxManifest.xmlC:\Program Files\WindowsApps\microsoft.windowscommunicationsapps_16005.13426.20688.0_x64__8wekyb3d8bbwe\AppxManifest.xmlC:\Program Files\WindowsApps\Microsoft.WindowsFeedbackHub_1.2009.10531.0_x64__8wekyb3d8bbwe\AppxManifest.xmlC:\Program Files\WindowsApps\Microsoft.WindowsMaps_1.0.27.0_x64__8wekyb3d8bbwe\AppxManifest.xmlC:\Program Files\WindowsApps\Microsoft.WindowsNotepad_10.2103.6.0_x64__8wekyb3d8bbwe\AppxManifest.xmlC:\Program Files\WindowsApps\Microsoft.WindowsSoundRecorder_1.0.42.0_x64__8wekyb3d8bbwe\AppxManifest.xmlC:\Program Files\WindowsApps\Microsoft.WindowsStore_12103.1001.11.0_x64__8wekyb3d8bbwe\AppxManifest.xmlC:\Program Files\WindowsApps\Microsoft.WindowsTerminal_1.6.10571.0_x64__8wekyb3d8bbwe\AppxManifest.xmlC:\Program Files\WindowsApps\Microsoft.Xbox.TCUI_1.23.28002.0_x64__8wekyb3d8bbwe\AppxManifest.xmlC:\Program Files\WindowsApps\Microsoft.XboxGameOverlay_1.54.4001.0_x64__8wekyb3d8bbwe\AppxManifest.xmlC:\Program Files\WindowsApps\Microsoft.XboxGamingOverlay_5.621.4072.0_x64__8wekyb3d8bbwe\AppxManifest.xmlC:\Program Files\WindowsApps\Microsoft.XboxIdentityProvider_12.67.21001.0_x64__8wekyb3d8bbwe\AppxManifest.xmlC:\Program Files\WindowsApps\Microsoft.XboxSpeechToTextOverlay_1.21.13002.0_x64__8wekyb3d8bbwe\AppxManifest.xmlC:\Program Files\WindowsApps\Microsoft.YourPhone_1.21022.202.0_x64__8wekyb3d8bbwe\AppxManifest.xmlC:\Program Files\WindowsApps\Microsoft.ZuneMusic_10.21012.10511.0_x64__8wekyb3d8bbwe\AppxManifest.xmlC:\Program Files\WindowsApps\Microsoft.ZuneVideo_10.21021.10311.0_x64__8wekyb3d8bbwe\AppxManifest.xmlPS C:\Windows\system32\WindowsPowerShell\v1.0> 

Но нам надо сопоставить имя пакета, его локализованное имя в системе и путь до манифеста. Искать будем среди пакетов, которые имеют статус "Staged", то есть готовы к восстановлению.

# Тут нельзя напрямую вписать -PackageTypeFilter Bundle, так как иначе не выдается нужное свойство InstallLocation. Только сравнивать с $Bundles$Bundles = (Get-AppXPackage -PackageTypeFilter Bundle -AllUsers).Name$AppxPackages = Get-AppxPackage -AllUsers | Where-Object -FilterScript {$_.PackageUserInformation -match "Staged"} | Where-Object -FilterScript {$_.Name -in $Bundles}$PackagesIds = [Windows.Management.Deployment.PackageManager, Windows.Web, ContentType = WindowsRuntime]::new().FindPackages() | Select-Object -Property DisplayName -ExpandProperty Id | Select-Object -Property Name, DisplayNameforeach ($AppxPackage in $AppxPackages){$PackageId = $PackagesIds | Where-Object -FilterScript {$_.Name -eq $AppxPackage.Name}if (-not $PackageId){continue}[PSCustomObject]@{Name            = $AppxPackage.NamePackageFullName = $AppxPackage.PackageFullNameDisplayName     = $PackageId.DisplayNameAppxManifest    = "$($AppxPackage.InstallLocation)\AppxManifest.xml"}}

Ну, а дальше уже дело техники. Кстати, если вам надо восстановить все возможные пакеты без разбора, то в этом вам поможет.

# Re-register all UWP apps$Bundles = (Get-AppXPackage -PackageTypeFilter Framework -AllUsers).PackageFullNameGet-ChildItem -Path "HKLM:\SOFTWARE\Classes\Local Settings\Software\Microsoft\Windows\CurrentVersion\AppModel\PackageRepository\Packages" | ForEach-Object -Process {Get-ItemProperty -Path $_.PSPath} | Where-Object -FilterScript {$_.Path -match "Program Files"} | Where-Object -FilterScript {$_.PSChildName -notin $Bundles} | Where-Object -FilterScript {$_.Path -match "x64"} | ForEach-Object -Process {"$($_.Path)\AppxManifest.xml"} | Add-AppxPackage -Register -ForceApplicationShutdown -ForceUpdateFromAnyVersion -DisableDevelopmentMode -Verbose# Check for UWP apps updatesGet-CimInstance -Namespace "Root\cimv2\mdm\dmmap" -ClassName "MDM_EnterpriseModernAppManagement_AppManagement01" | Invoke-CimMethod -MethodName UpdateScanMethod

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

Точно таким же способом можно выводить список локализованных имён компонентов Windows и дополнительных компонентов.w

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

Get-WindowsOptionalFeature -Online | ForEach-Object -Process {Get-WindowsOptionalFeature -FeatureName $_.FeatureName -Online} | Select-Object -Property FeatureName, DisplayName | Format-Table -AutoSize
БылоБылоСталоСталоНаши лики, когда, наконец, всё заработалоНаши лики, когда, наконец, всё заработало

Интернационализация скрипта

На необходимость этой фичи обратил внимание @FrankSinatraв комментариях. Интернационализация позволяет избавиться от страшной конструкции вида

if ($RU){}else{}

Отныне в корне папки скрипта находятся папки с названием кода локализации: например, ru-RU, en-US и так далее, где внутри находится файл локализации вида UnsupportedOSBitness = The script supports Windows 10 x64 only. Получить значение локализации можно командой $PSUICulture.

Соответственно, чтобы это всё заработало, мы импортируем указанные локализационные файлы, сохраняя строки в переменную так, чтобы можно было вызывать их в скрипте:

# Sophia.psd1ConvertFrom-StringData -StringData @'UnsupportedOSBitness = The script supports Windows 10 x64 only'@# Sophia.ps1Import-LocalizedData -BindingVariable Global:Localization -FileName Sophia$Localization.UnsupportedOSBitness

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

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

Закрепление ярлыков на начальном экране

Используется при этом чистый PowerShell. Изначально я использовал стороннюю программу syspin, но возникло желание всё-таки избавиться от неё. Как вы знаете, с выходом Windows 10 October 2018 Microsoft без шума закрыл доступ к API открепления (закрепления) ярлыков от начального экрана и панели задач: отныне это можно сделать лишь вручную.

Ниже приведён пример кода для закрепления (открепления) ярлыка на начальный экран, который когда-то работал. Как можете видеть, в коде используется метод получения локализованной строки, и для этого нам необходимо знать код строки, чтобы вызвать соответствующий пункт контекстного меню. В данном примере, чтобы закрепить ярлык командной строки, мы вызываем строку с кодом 51201, Закрепить на начальном экране, из библиотеки %SystemRoot%\system32\shell32.dll.

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

Попытка закрепить ярлык командной строки устаревшим методом:

# Extract a localized string from shell32.dll$Signature = @{Namespace = "WinAPI"Name = "GetStr"Language = "CSharp"MemberDefinition = @"[DllImport("kernel32.dll", CharSet = CharSet.Auto)]public static extern IntPtr GetModuleHandle(string lpModuleName);[DllImport("user32.dll", CharSet = CharSet.Auto)]internal static extern int LoadString(IntPtr hInstance, uint uID, StringBuilder lpBuffer, int nBufferMax);public static string GetString(uint strId){IntPtr intPtr = GetModuleHandle("shell32.dll");StringBuilder sb = new StringBuilder(255);LoadString(intPtr, strId, sb, sb.Capacity);return sb.ToString();}"@}if (-not ("WinAPI.GetStr" -as [type])){Add-Type @Signature -Using System.Text}# Pin to Start: 51201# Unpin from Start: 51394$LocalizedString = [WinAPI.GetStr]::GetString(51201)# Trying to pin the Command Prompt shortcut to Start$Target = Get-Item -Path "$env:APPDATA\Microsoft\Windows\Start Menu\Programs\System Tools\Command Prompt.lnk"$Shell = New-Object -ComObject Shell.Application$Folder = $Shell.NameSpace($Target.DirectoryName)$file = $Folder.ParseName($Target.Name)$Verb = $File.Verbs() | Where-Object -FilterScript {$_.Name -eq $LocalizedString}$Verb.DoIt()

Сейчас консоль вываливается с ошибкой Access is denied. (Exception from HRESULT: 0x80070005 (E_ACCESSDENIED).)

Хотя, как можно заметить, API, конечно, отдаёт глагол контекстного меню Закрепить на начальном &экране, но не может его выполнить.

Мы знаем, что текущий макет начального экрана можно выгрузить в формате XML. Но, даже если его настроить должным образом, импортировать макет в профиль текущего пользователя не получится: Import-StartLayout -LayoutPath D:\Layout.xml импортирует макеты начального экрана и панели задач только для новых пользователей.

Идея заключается в том, чтобы использовать политику Макет начального экрана (Prevent users from customizing their Start Screen), отвечающую за подгрузку предзаготовленного макета в формате XML из определённого места. Таким образом, наш хак будет состоять из следующих пунктов:

  • Выгружаем текущий макет начального экрана.

  • Парсим XML, добавляя необходимые нам ярлыки (ссылки должны вести на реально существующие ярлыки), и сохраняем.

  • С помощью политики временно выключаем возможность редактировать макет начального экрана.

  • Перезапускаем меню Пуск.

  • Программно открываем меню Пуск, чтобы в реестре сохранился его макет.

  • Выключаем политику, чтобы можно было редактировать макет начального экрана.

  • И открываем меню Пуск опять.

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

Код целиком:
<#.SYNOPSISConfigure the Start tiles.PARAMETER ControlPanelPin the "Control Panel" shortcut to Start.PARAMETER DevicesPrintersPin the "Devices & Printers" shortcut to Start.PARAMETER PowerShellPin the "Windows PowerShell" shortcut to Start.PARAMETER UnpinAllUnpin all the Start tiles.EXAMPLE.\Pin.ps1 -Tiles ControlPanel, DevicesPrinters, PowerShell.EXAMPLE.\Pin.ps1 -UnpinAll.EXAMPLE.\Pin.ps1 -UnpinAll -Tiles ControlPanel, DevicesPrinters, PowerShell.EXAMPLE.\Pin.ps1 -UnpinAll -Tiles ControlPanel.EXAMPLE.\Pin.ps1 -Tiles ControlPanel -UnpinAll.LINKhttps://github.com/farag2/Windows-10-Sophia-Script.NOTESSeparate arguments with commaCurrent user#>[CmdletBinding()]param([Parameter(Mandatory = $false,Position = 0)][switch]$UnpinAll,[Parameter(Mandatory = $false,Position = 1)][ValidateSet("ControlPanel", "DevicesPrinters", "PowerShell")][string[]]$Tiles,[string]$StartLayout = "$PSScriptRoot\StartLayout.xml")begin{# Unpin all the Start tilesif ($UnpinAll){Export-StartLayout -Path $StartLayout -UseDesktopApplicationID[xml]$XML = Get-Content -Path $StartLayout -Encoding UTF8 -Force$Groups = $XML.LayoutModificationTemplate.DefaultLayoutOverride.StartLayoutCollection.StartLayout.Groupforeach ($Group in $Groups){# Removing all groups inside XML$Group.ParentNode.RemoveChild($Group) | Out-Null}$XML.Save($StartLayout)}}process{# Extract strings from shell32.dll using its' number$Signature = @{Namespace = "WinAPI"Name = "GetStr"Language = "CSharp"MemberDefinition = @"[DllImport("kernel32.dll", CharSet = CharSet.Auto)]public static extern IntPtr GetModuleHandle(string lpModuleName);[DllImport("user32.dll", CharSet = CharSet.Auto)]internal static extern int LoadString(IntPtr hInstance, uint uID, StringBuilder lpBuffer, int nBufferMax);public static string GetString(uint strId){IntPtr intPtr = GetModuleHandle("shell32.dll");StringBuilder sb = new StringBuilder(255);LoadString(intPtr, strId, sb, sb.Capacity);return sb.ToString();}"@}if (-not ("WinAPI.GetStr" -as [type])){Add-Type @Signature -Using System.Text}# Extract the localized "Devices and Printers" string from shell32.dll$DevicesPrinters = [WinAPI.GetStr]::GetString(30493)# We need to get the AppID because it's auto generated$Script:DevicesPrintersAppID = (Get-StartApps | Where-Object -FilterScript {$_.Name -eq $DevicesPrinters}).AppID$Parameters = @(# Control Panel hash table@{# Special name for Control PanelName = "ControlPanel"Size = "2x2"Column = 0Row = 0AppID = "Microsoft.Windows.ControlPanel"},# "Devices & Printers" hash table@{# Special name for "Devices & Printers"Name = "DevicesPrinters"Size   = "2x2"Column = 2Row    = 0AppID  = $Script:DevicesPrintersAppID},# Windows PowerShell hash table@{# Special name for Windows PowerShellName = "PowerShell"Size = "2x2"Column = 4Row = 0AppID = "{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\WindowsPowerShell\v1.0\powershell.exe"})# Valid columns to place tiles in$ValidColumns = @(0, 2, 4)[string]$StartLayoutNS = "http://schemas.microsoft.com/Start/2014/StartLayout"# Add pre-configured hastable to XMLfunction Add-Tile{param([string]$Size,[int]$Column,[int]$Row,[string]$AppID)[string]$elementName = "start:DesktopApplicationTile"[Xml.XmlElement]$Table = $xml.CreateElement($elementName, $StartLayoutNS)$Table.SetAttribute("Size", $Size)$Table.SetAttribute("Column", $Column)$Table.SetAttribute("Row", $Row)$Table.SetAttribute("DesktopApplicationID", $AppID)$Table}if (-not (Test-Path -Path $StartLayout)){# Export the current Start layoutExport-StartLayout -Path $StartLayout -UseDesktopApplicationID}[xml]$XML = Get-Content -Path $StartLayout -Encoding UTF8 -Forceforeach ($Tile in $Tiles){switch ($Tile){ControlPanel{$ControlPanel = [WinAPI.GetStr]::GetString(12712)Write-Verbose -Message ("The `"{0}`" shortcut is being pinned to Start" -f $ControlPanel) -Verbose}DevicesPrinters{$DevicesPrinters = [WinAPI.GetStr]::GetString(30493)Write-Verbose -Message ("The `"{0}`" shortcut is being pinned to Start" -f $DevicesPrinters) -Verbose# Create the old-style "Devices and Printers" shortcut in the Start menu$Shell = New-Object -ComObject Wscript.Shell$Shortcut = $Shell.CreateShortcut("$env:APPDATA\Microsoft\Windows\Start menu\Programs\System Tools\$DevicesPrinters.lnk")$Shortcut.TargetPath = "control"$Shortcut.Arguments = "printers"$Shortcut.IconLocation = "$env:SystemRoot\system32\DeviceCenter.dll"$Shortcut.Save()Start-Sleep -Seconds 3}PowerShell{Write-Verbose -Message ("The `"{0}`" shortcut is being pinned to Start" -f "Windows PowerShell") -Verbose}}$Parameter = $Parameters | Where-Object -FilterScript {$_.Name -eq $Tile}$Group = $XML.LayoutModificationTemplate.DefaultLayoutOverride.StartLayoutCollection.StartLayout.Group | Where-Object -FilterScript {$_.Name -eq "Sophia Script"}# If the "Sophia Script" group exists in Startif ($Group){$DesktopApplicationID = ($Parameters | Where-Object -FilterScript {$_.Name -eq $Tile}).AppIDif (-not ($Group.DesktopApplicationTile | Where-Object -FilterScript {$_.DesktopApplicationID -eq $DesktopApplicationID})){# Calculate current filled columns$CurrentColumns = @($Group.DesktopApplicationTile.Column)# Calculate current free columns and take the first one$Column = (Compare-Object -ReferenceObject $ValidColumns -DifferenceObject $CurrentColumns).InputObject | Select-Object -First 1# If filled cells contain desired ones assign the first free columnif ($CurrentColumns -contains $Parameter.Column){$Parameter.Column = $Column}$Group.AppendChild((Add-Tile @Parameter)) | Out-Null}}else{# Create the "Sophia Script" group[Xml.XmlElement]$Group = $XML.CreateElement("start:Group", $StartLayoutNS)$Group.SetAttribute("Name","Sophia Script")$Group.AppendChild((Add-Tile @Parameter)) | Out-Null$XML.LayoutModificationTemplate.DefaultLayoutOverride.StartLayoutCollection.StartLayout.AppendChild($Group) | Out-Null}}$XML.Save($StartLayout)}end{# Temporarily disable changing the Start menu layoutif (-not (Test-Path -Path HKCU:\SOFTWARE\Policies\Microsoft\Windows\Explorer)){New-Item -Path HKCU:\SOFTWARE\Policies\Microsoft\Windows\Explorer -Force}New-ItemProperty -Path HKCU:\SOFTWARE\Policies\Microsoft\Windows\Explorer -Name LockedStartLayout -Value 1 -ForceNew-ItemProperty -Path HKCU:\SOFTWARE\Policies\Microsoft\Windows\Explorer -Name StartLayoutFile -Value $StartLayout -ForceStart-Sleep -Seconds 3# Restart the Start menuStop-Process -Name StartMenuExperienceHost -Force -ErrorAction IgnoreStart-Sleep -Seconds 3# Open the Start menu to load the new layout$wshell = New-Object -ComObject WScript.Shell$wshell.SendKeys("^{ESC}")Start-Sleep -Seconds 3# Enable changing the Start menu layoutRemove-ItemProperty -Path HKCU:\SOFTWARE\Policies\Microsoft\Windows\Explorer -Name LockedStartLayout -Force -ErrorAction IgnoreRemove-ItemProperty -Path HKCU:\SOFTWARE\Policies\Microsoft\Windows\Explorer -Name StartLayoutFile -Force -ErrorAction IgnoreRemove-Item -Path $StartLayout -ForceStop-Process -Name StartMenuExperienceHost -Force -ErrorAction IgnoreStart-Sleep -Seconds 3# Open the Start menu to load the new layout$wshell = New-Object -ComObject WScript.Shell$wshell.SendKeys("^{ESC}")}

Создаваемые задания в планировщике заданий

Для начала разберём две задачи по автоматизации очистки папок %TEMP% и %SystemRoot%\SoftwareDistribution\Download. Эти папки полезно очищать по расписанию, чтобы они не разрастались. На текущий момент папка временных файлов самоочищается раз в 60 дней, а папка, куда скачиваются установочные файлы для обновлений, раз в 90 дней.

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

Windows 10 позволяет генерировать такие тосты очень просто. Пример всплывающего тоста, как на картинке выше:

[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null[xml]$ToastTemplate = @"<toast duration="Long"><visual><binding template="ToastGeneric"><text>Уведомление</text><group><subgroup><text hint-style="body" hint-wrap="true">Кэш обновлений Windows успешно удален</text></subgroup></group></binding></visual><audio src="ms-winsoundevent:notification.default" /></toast>"@$ToastXml = [Windows.Data.Xml.Dom.XmlDocument]::New()$ToastXml.LoadXml($ToastTemplate.OuterXml)$ToastMessage = [Windows.UI.Notifications.ToastNotification]::New($ToastXML)[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier("windows.immersivecontrolpanel_cw5n1h2txyewy!microsoft.windows.immersivecontrolpanel").Show($ToastMessage)

В вызове CreateToastNotifier можно указывать приложение, иконка которого будет отображаться в верхнем левом углу тоста и которое будет открываться при нажатии на тост. В данном случае я использовал windows.immersivecontrolpanel_cw5n1h2txyewy!microsoft.windows.immersivecontrolpanel "Настройки". Но вы вольны указать любые приложения. Узнать список всех установленных приложений и их AppID нам поможет команда Get-StartApps.

Напомню, что задача по очистке папки для временных файлов удаляет лишь файлы старше суток:

Get-ChildItem -Path $env:TEMP -Recurse -Force | Where-Object {$_.CreationTime -lt (Get-Date).AddDays(-1)} | Remove-Item -Recurse -Force

А задача по очистке папки %SystemRoot%\SoftwareDistribution\Download ждёт остановку службы wuauserv (Центр обновления Windows), чтобы в дальнейшем очистить папку

(Get-Service -Name wuauserv).WaitForStatus('Stopped', '01:00:00')

С заданием по запуску очистки диска и DISM с аргументами всё гораздо веселее. Изначально стояла задача просто запускать предзаготовленный пресет настроек для очистки диска и очистку ненужных обновлений, используя DISM: dism.exe /Online /English /Cleanup-Image /StartComponentCleanup /NoRestart.

Но пришлось решать, как заставить задание запускать очистку диска, сворачивать его окно, а потом также минимизировать окно консоли с запущенным DISM.

Сложность состоит в том, что при запуске очистки диска сначала открывается первое окошко со сканированием того, что можно очистить, потом оно закрывается, и только после этого открывается новое окошко (с новым MainWindowHandle) уже непосредственно с очисткой.

Если первое окошко достаточно легко свернуть:

$ProcessInfo = New-Object -TypeName System.Diagnostics.ProcessStartInfo$ProcessInfo.FileName = "$env:SystemRoot\system32\cleanmgr.exe"$ProcessInfo.Arguments = "/sagerun:1337"$ProcessInfo.UseShellExecute = $true$ProcessInfo.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Minimized

То над тем, как свернуть второе, я поломал голову, конечно. После многих попыток хоть за что-то зацепиться, я понял, что:

только MainWindowHandle окна может помочь
Get-Process -Name cleanmgr | Stop-Process -ForceGet-Process -Name Dism | Stop-Process -ForceGet-Process -Name DismHost | Stop-Process -Force$ProcessInfo = New-Object -TypeName System.Diagnostics.ProcessStartInfo$ProcessInfo.FileName = "$env:SystemRoot\system32\cleanmgr.exe"$ProcessInfo.Arguments = "/sagerun:1337"$ProcessInfo.UseShellExecute = $true$ProcessInfo.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Minimized$Process = New-Object -TypeName System.Diagnostics.Process$Process.StartInfo = $ProcessInfo$Process.Start() | Out-NullStart-Sleep -Seconds 3[int]$SourceMainWindowHandle = (Get-Process -Name cleanmgr | Where-Object -FilterScript {$_.PriorityClass -eq "BelowNormal"}).MainWindowHandlefunction MinimizeWindow{[CmdletBinding()]param([Parameter(Mandatory = $true)]$Process)$ShowWindowAsync = @{Namespace = "WinAPI"Name = "Win32ShowWindowAsync"Language = "CSharp"MemberDefinition = @'[DllImport("user32.dll")]public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);'@}if (-not ("WinAPI.Win32ShowWindowAsync" -as [type])){Add-Type @ShowWindowAsync}$MainWindowHandle = (Get-Process -Name $Process | Where-Object -FilterScript {$_.PriorityClass -eq "BelowNormal"}).MainWindowHandle[WinAPI.Win32ShowWindowAsync]::ShowWindowAsync($MainWindowHandle, 2)}while ($true){[int]$CurrentMainWindowHandle = (Get-Process -Name cleanmgr | Where-Object -FilterScript {$_.PriorityClass -eq "BelowNormal"}).MainWindowHandleif ($SourceMainWindowHandle -ne $CurrentMainWindowHandle){MinimizeWindow -Process cleanmgrbreak}Start-Sleep -Milliseconds 5}$ProcessInfo = New-Object -TypeName System.Diagnostics.ProcessStartInfo$ProcessInfo.FileName = "$env:SystemRoot\system32\dism.exe"$ProcessInfo.Arguments = "/Online /English /Cleanup-Image /StartComponentCleanup /NoRestart"$ProcessInfo.UseShellExecute = $true$ProcessInfo.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Minimized$Process = New-Object -TypeName System.Diagnostics.Process$Process.StartInfo = $ProcessInfo$Process.Start() | Out-Null

Как видно из кода, $SourceMainWindowHandle первого окна ждёт, пока появится $CurrentMainWindowHandle второго окна, и, если, они не равны, то можно минимизировать новое окно. Дальше уже можно запускать DISM с ключами.

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

Как видно на скриншоте, пользователю предоставляются на выбор 3 варианта развития событий: отложить вопрос на 1, 30 минут или 4 часа, полностью отклонить предложение (тогда задача запустится через 30 дней) или запустить.

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

if (-not (Test-Path -Path Registry::HKEY_CLASSES_ROOT\WindowsCleanup)){New-Item -Path Registry::HKEY_CLASSES_ROOT\WindowsCleanup -Force}New-ItemProperty -Path Registry::HKEY_CLASSES_ROOT\WindowsCleanup -Name "(default)" -PropertyType String -Value "URL:WindowsCleanup" -ForceNew-ItemProperty -Path Registry::HKEY_CLASSES_ROOT\WindowsCleanup -Name "URL Protocol" -Value "" -ForceNew-ItemProperty -Path Registry::HKEY_CLASSES_ROOT\WindowsCleanup -Name EditFlags -PropertyType DWord -Value 2162688 -Forceif (-not (Test-Path -Path Registry::HKEY_CLASSES_ROOT\shell\open\command)){New-item -Path Registry::HKEY_CLASSES_ROOT\WindowsCleanup\shell\open\command -Force}# If "Run" clicked run the "Windows Cleanup" taskNew-ItemProperty -Path Registry::HKEY_CLASSES_ROOT\WindowsCleanup\shell\open\command -Name "(default)" -PropertyType String -Value 'powershell.exe -Command "& {Start-ScheduledTask -TaskPath ''\Sophia Script\'' -TaskName ''Windows Cleanup''}"' -Force

А потом привязываю его на кнопку запуска:

<action arguments="WindowsCleanup:" content="$($Localization.Run)" activationType="protocol"/>

Всё вместе выглядит так:
# Persist the Settings notifications to prevent to immediately disappear from Action Centerif (-not (Test-Path -Path "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Notifications\Settings\windows.immersivecontrolpanel_cw5n1h2txyewy!microsoft.windows.immersivecontrolpanel")){New-Item -Path "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Notifications\Settings\windows.immersivecontrolpanel_cw5n1h2txyewy!microsoft.windows.immersivecontrolpanel" -Force}New-ItemProperty -Path "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Notifications\Settings\windows.immersivecontrolpanel_cw5n1h2txyewy!microsoft.windows.immersivecontrolpanel" -Name ShowInActionCenter -PropertyType DWord -Value 1 -Force# Register the "WindowsCleanup" protocol to be able to run the scheduled task upon clicking on the "Run" buttonif (-not (Test-Path -Path Registry::HKEY_CLASSES_ROOT\WindowsCleanup)){New-Item -Path Registry::HKEY_CLASSES_ROOT\WindowsCleanup -Force}New-ItemProperty -Path Registry::HKEY_CLASSES_ROOT\WindowsCleanup -Name "(default)" -PropertyType String -Value "URL:WindowsCleanup" -ForceNew-ItemProperty -Path Registry::HKEY_CLASSES_ROOT\WindowsCleanup -Name "URL Protocol" -Value "" -ForceNew-ItemProperty -Path Registry::HKEY_CLASSES_ROOT\WindowsCleanup -Name EditFlags -PropertyType DWord -Value 2162688 -Forceif (-not (Test-Path -Path Registry::HKEY_CLASSES_ROOT\shell\open\command)){New-item -Path Registry::HKEY_CLASSES_ROOT\WindowsCleanup\shell\open\command -Force}# If "Run" clicked run the "Windows Cleanup" taskNew-ItemProperty -Path Registry::HKEY_CLASSES_ROOT\WindowsCleanup\shell\open\command -Name "(default)" -PropertyType String -Value 'powershell.exe -Command "& {Start-ScheduledTask -TaskPath ''\Sophia Script\'' -TaskName ''Windows Cleanup''}"' -Force[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null[xml]$ToastTemplate = @"<toast duration="Long" scenario="reminder"><visual><binding template="ToastGeneric"><text>$($Localization.CleanupTaskNotificationTitle)</text><group><subgroup><text hint-style="title" hint-wrap="true">$($Localization.CleanupTaskNotificationEventTitle)</text></subgroup></group><group><subgroup><text hint-style="body" hint-wrap="true">$($Localization.CleanupTaskNotificationEvent)</text></subgroup></group></binding></visual><audio src="ms-winsoundevent:notification.default" /><actions><input id="SnoozeTimer" type="selection" title="$($Localization.CleanupTaskNotificationSnoozeInterval)" defaultInput="1"><selection id="1" content="$($Localization.Minute)" /><selection id="30" content="$($Localization.Minute)" /><selection id="240" content="$($Localization.Minute)" /></input><action activationType="system" arguments="snooze" hint-inputId="SnoozeTimer" content="" id="test-snooze"/><action arguments="WindowsCleanup:" content="$($Localization.Run)" activationType="protocol"/><action arguments="dismiss" content="" activationType="system"/></actions></toast>"@$ToastXml = [Windows.Data.Xml.Dom.XmlDocument]::New()$ToastXml.LoadXml($ToastTemplate.OuterXml)$ToastMessage = [Windows.UI.Notifications.ToastNotification]::New($ToastXML)[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier("windows.immersivecontrolpanel_cw5n1h2txyewy!microsoft.windows.immersivecontrolpanel").Show($ToastMessage)

Функция ассоциации файлов

Как известно, начиная с Windows 8 невозможно самостоятельно ассоциировать какое-либо расширение с программой, не вычислив правильный хэш. Как выяснилось, Microsoft проводит манипуляции с захардоженной строкой "User Choice set via Windows User Experience {D18B6DD5-6124-4341-9318-804003BAFA0B}".

Пользователь Danyfirex смог реализовать правильное вычисление хэш-суммы на чистом PowerShell, но, к сожалению, после проведённых тестов выяснилось, что сам PowerShell 5.1 считает его неправильно, поэтому я вынужден был использовать код другого разработчика с алгоритмом, реализованным на чистом C#. Крайне быстро! Функция огромная, поэтому просто оставлю просто ссылку на код.

Автоматизация установки бесплатного расширения для встроенного UWP-приложения Фотографии

Еще одна забавная, но крайне полезная функция даёт возможность открывать файлы формата .heic и .heif.

Расширение крайне полезное, так как все современные телефоны умеют делать фотографии в формате HEIC, но по умолчанию Windows 10 не умеет открывать такие файлы, предлагая купить расширение в Microsoft Store по цене в 0,99 $. Но мало кто знает, что есть скрытая страница от поиска этого же самого расширения, но предназначенного для OEM-производителей. Эту же страницу можно открыть вручную, выполнив через Win+R или через PowerShell: ms-windows-store://pdp/?ProductId=9n4wgh0z6vhq

Для скачивания установочного пакета на помощь приходит всеми известный сайт https://store.rg-adguard.net. Он позволяет, зная ID страницы, получать временные прямые ссылки на установочные пакеты. Значит, можно распарсить.

$API = "https://store.rg-adguard.net/api/GetFiles"# HEVC Video Extensions from Device Manufacturer$ProductURL = "https://www.microsoft.com/store/productId/9n4wgh0z6vhq"$Body = @{"type" = "url""url"  = $ProductURL"ring" = "Retail""lang" = "en-US"}$Raw = Invoke-RestMethod -Method Post -Uri $API -ContentType 'application/x-www-form-urlencoded' -Body $Body# Parsing the page$Raw | Select-String -Pattern '<tr style.*<a href=\"(?<url>.*)"\s.*>(?<text>.*)<\/a>' -AllMatches | ForEach-Object -Process {$_.Matches} | ForEach-Object -Process {$TempURL = $_.Groups[1].Value$Package = $_.Groups[2].Valueif ($Package -like "Microsoft.HEVCVideoExtension_*_x64__8wekyb3d8bbwe.appx"){[PSCustomObject]@{PackageName = $PackagePackageURL  = $TempURL}}}

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

Автопродление имен функций по введённым буквам, содержащимся в названии функции или её аргумента

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

Проблема в том, что до этого была лишь возможность вручную указывать функции и их аргументы а-ля .\Sophia.ps1 -Functions "DiagTrackService -Disable", "DiagnosticDataLevel -Minimal", UninstallUWPApps.

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

Чтобы заработала сия шайтан-машина, пришлось прибегнуть к Register-ArgumentCompleter.

Весь код сосредоточен в отдельном файле, и не получится его поместить в текущий пресет-файл: файл необходимо вызывать с использованием dot sourcing. Одним словом, пришлось в ScriptBlock для argumentcompleter перебирать все возможные варианты конструкций вида "функция-аргумент" и просто "функция", если у последней нет собственного аргумента.

Теперь, чтобы вызвать, допустим, функцию по удалению UWP-приложений, можно ввести (после загрузки Functions.ps1) So<tab> -Fu<tab> uwp<tab>.

Крайне жутко выглядит, но стало гораздо удобнее.

Ну, а закончу рассказ на том, что даже сборка прикрепляемых архивов на странице релизов стала осуществляться с помощью конфига Github Actions. Как можно заметить, для создания архива под версию для PowerShell 7 приходится выкачивать две библиотеки с ресурсов Microsoft, так как загрузить файлы больше 25 МБ в репозиторий невозможно. Автоматизируй автоматизацию!

Итоги

Это были крайне плодотворные полгода. У нас такое ощущение, что мы прошли PowerShell на уровне "Ultra Violence". Ну, а что дальше? Параллельно я прорабатываю вариант, как реализовать, используя текущий паттерн взаимодействия пользователя со скриптом, настройку офлайновых образов WIM. Но главный приоритет для нас сейчас, конечно, разработка SophiApp.

Цель проекта показать, как, по нашему мнению, должен выглядеть, чувствоваться и каким функционалом обладать так называемый твикер для Windows 10. Идей просто огромное количество! Хотя у нас нет опыта в разработке и нас всего лишь двое, а весь код на SophiApp пишет в одиночку Дмитрий, возможно, летом уже появится первый рабочий билд. Но это уже совсем другая история.

Хочу выразить огромную благодарность также пользователям forum.ru-board westlife и iNNOKENTIY21: ребят, без вашей помощи и подсказок, всё было бы по-другому! А логотип нарисовала художница tea_head, за что ей тоже спасибо. Скрины, использованные в материале, взяты из мультфильма Коргот-варвар. Любите Windows 10, настраивайте её с умом и до новых встреч!

А если хотите прокачать себя, например получить навыки пентестера и зарабатывать на уязвимостях, или подтянуть знания алгоритмов и структур данных приходите учиться, будет сложно, но интересно!

Узнайте, как прокачаться и в других специальностях или освоить их с нуля:

Другие профессии и курсы
Подробнее..

Представляем Windows Terminal Preview 1.9

10.06.2021 10:07:14 | Автор: admin

По следам Microsoft Build 2021 у нашего Windows Terminal второй день рождения! Этот релиз впервые представляет версию 1.9 для Windows Terminal Preview и переносит основной Windows Terminal в версию 1.8. Как всегда, вы можете установить обе сборки в Microsoft Store, а также на странице выпусков GitHub. Под катом расскажем, что нового!

Терминал по умолчанию

Теперь вы можете установить Windows Terminal Preview в качестве эмулятора терминала по умолчанию в Windows! Это означает, что любое приложение командной строки будет запускаться внутри выбранного эмулятора терминала (т.е. дважды щелкните PowerShell, и по умолчанию оно откроется в Windows Terminal Preview). Этот параметр в настоящее время находится в сборке Windows Insider Program Dev Channel и может быть найден на странице свойств консоли. Мы также добавили этот параметр в пользовательский интерфейс настроек в Windows Terminal Preview.

Quake mode

Терминал Windows теперь поддерживает quake mode! Quake Mode позволяет быстро открыть новый экземпляр терминала из любого места в Windows, нажав Win+`. Окно quake появится в верхней половине экрана, и его можно легко закрыть с помощью того же сочетания клавиш. Если вы хотите дополнительно настроить способ вызова терминала, ознакомьтесь с новыми функциями, которые мы добавили для на нашем сайте документации.

Обновления шрифта Cascadia Code

Cascadia Code Italic

У Cascadia Code теперь есть курсивный вариант. Этот вариант по умолчанию используется в терминале, а также может быть загружен с GitHub. Версии шрифтов, в названии которых отсутствует Курсив, будут иметь стандартный шрифт без курсивных букв. Огромное спасибо Аарону Беллу за разработку Cascadia Code Italic и Виктории Грабовской за разработку курсивных символов кириллицы!

Арабские и ивритские символы

Мы также добавляем арабские символы и символы иврита в Cascadia Code в середине июня. Они добавляются к существующему набору шрифтов Cascadia Code, но пока не будут иметь курсивного шрифта. Огромное спасибо Мохамаду Дакаку за разработку арабских символов и Лирону Лави Тюркеничу за разработку ивритских символов! Если вы хотите узнать больше о дизайне арабских символов, ознакомьтесь с этим документом.

Обновления UI настроек

Редактируемая страница действий

Теперь вы можете редактировать существующие действия на странице Действия в пользовательском интерфейсе настроек. Это значительно упрощает настройку сочетаний клавиш, которые вы хотите использовать в Windows Terminal.

Добавление нового профиля

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

Окно предварительного просмотра внешнего вида профиля

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

Подробнее..

О параметре компилятора SAFESEH

16.06.2021 18:22:08 | Автор: admin

Введение

В этой истории я расскажу вам об увлекательном приключении, которое привело меня к решению одной загадки, которую я сам себе загадал. Разгадка являет собой небольшую подробность в механизме загрузчика 32-х разрядных приложений в системе Windows 7 и выше, а процесс разгадки - длинное путешествие воина, который следует по пути сердца.

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

Поле boundImport, которое содержится в структуре dataDirectories, указывает на структуру IMAGE_LOAD_CONFIG_DIRECTORY32. Эта структура содержит поле SEHandlerTable. Если оно равно нулю, то параметр /SAFESEH выключен. Если оно является виртуальным указателем на таблицу безопасных обработчиков, то будут работать только те обработчики, которые указаны в таблице. Таблица представляет из себя список смещений, относительно виртуального адреса секции .text . Количество обработчиков задаётся в поле SEHandlerCount.

С чего всё началось

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

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

Ближе к делу

Рассмотрим маленький пример (я пользуюсь компилятором MicrosoftVisual C++ )

int main(){ __asm { mov eax, DWORD PTR SS : [0] }}

Ничего особенного, пытаемся положить в регистр eax содержимое по адресу 0.

Компилируем, запускаем под отладчиком OllyDbg.

Получаем грустное сообщение access violation when reading 0x00000000.

Берем на заметку, что размер инструкции, которая пытается прочитать по адресу 0 - равен 6 байтам. Обратим также внимание на регистр Eip, который указывает на проблемную инструкцию (совпадает с её адресом слева). Сама же инструкция выглядит как последовательность из 6 байт: 36 A1 00 00 00 00.

Теперь я хочу обработать это исключение, не используя ключевых слов __try, __except т.к. мне показалось это интересной и заманчивой идеей.

Напишем же свой собственный обработчик для обработки этого исключения!

Есть информация, что при возникновении исключения в функцию-обработчик система отправляет следующие аргументы:

    struct _EXCEPTION_RECORD* exceptionRecord,    void* establisherFrame,    struct _CONTEXT* contextRecord,    void* dispatcherContext

где exceptionRecord структура, содержащая код ошибки, адрес исключения, а contextRecord структура содержащая контекст регистров (включая регистр Eip). Соглашение о вызове функции-обработчика должно быть __cdecl, а возвратить она должна одно из следующих значений:

typedef enum _EXCEPTION_DISPOSITION{ ExceptionContinueExecution, ExceptionContinueSearch, ExceptionNestedException, ExceptionCollidedUnwind} EXCEPTION_DISPOSITION;

Я решил обработать это исключение таким нехитрым образом:

EXCEPTION_DISPOSITION __cdecl ExceptionHanler(    struct _EXCEPTION_RECORD* exceptionRecord,    void* establisherFrame,    struct _CONTEXT* contextRecord,    void* dispatcherContext){    contextRecord->Eip += 6;    MessageBoxA(0, "Exteption was handled", "Success!", 0);    return ExceptionContinueExecution;}

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

Просто поместить код обработчика в программе - этого мало. Необходимо дать понять системе, что нужно вызывать именно его. И ещё я хочу, чтобы он вызвался самым первым, среди прочих. Для этого нам необходимо где-то создать структуру, состоящую из двух полей: Next и Handler. Поле Next будет содержать указатель на такую же структуру, но с предыдущим обработчиком (с тем, который мы потесним). Поле Handler будет содержать указатель на нашу функцию-обработчик. Указатель на текущую такую структуру находится в TIB (thread information block), иначе говоря, по адресу fs:[0]. Создадим её в стеке.

Преобразуем нашу функцию main:

int main(){__asm{    push ExceptionHanler //положим в стек адрес функции-обработчикаpush fs:[0]          //положим в стек указатель на структуру текущего обработчика                     //теперь в стеке лежит структура, содержащая 2 поля: указатель на     //следующий обработчик и адрес нашего обработчикаmov fs:[0] , esp   //положим указатель на свою структуру, вместо текущего                                                                                                                                                   //обработчика по адресу fs:[0]mov eax, DWORD PTR SS:[0] //пытаемся прочитать по адресу 0    add esp, 8          //чистим стек, удалив 8 байт (размер структуры)}}

По незнанию, я компилирую это с настройками по умолчанию - с флагом /SAFESEH и игнорирую непонятные рекомендации компилятора о том, что мне неплохо было бы отключить этот флаг, раз я записываю что-то по адресу fs:[0]. Да что там говорить, я далеко не сразу обратил на это внимание.

Результат все та же ошибка чтения по адресу 0 и приложение падает как ни в чем не бывало!

Хорошо, что все позади. Теперь я знаю об этом флаге. Пробую компилировать с настройкой /SAFESEH:NO.

Компилируем, выполняем наш код, видим наше развесёлое сообщение:

Жмем ок программа успешно завершается. Фух.

Переломный момент

Но нет, подождите-ка, но что ИМЕННО сделал в тот раз компилятор, чтобы проигнорировать мой обработчик исключения? Что это за фокусы? И как мне быть, если вдруг ну чисто теоретически, мне вдруг однажды захочется сделать так, чтобы моё приложение имело флаг /SAFESEH, но при этом, чтобы оно заработало, я должен буду встроить в него код mov fs:[0], esp, с указателем на свою функцию-обработчик, вызвать в произвольном месте исключение и обработать его так, как моей душе угодно? Не считаю, что флаг /SAFESEH должен быть помехой для моих фантазий.

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

Не падая духом, в надежде разгадать загадку

Полистав срабатывающие обработчики, я увидел те, которые пушит компилятор на стадии выполнения crt0 (код до функции main), а также обработчик из библиотеки ntdll.dll. Все эти обработчики исправно срабатывают в порядке своей очереди, если я не помещаю свой обработчик по адресу fs:[0]! Но как система узнает, какая функция безопасный обработчик, а какая нет?

В своем путешествии я провёл серию экспериментов. Например, компилируя с флагом /SAFESEH, ставил точку останова перед проблемной инструкцией, брал первый обработчик, на который указывала первая структура по адресу fs:[0] и менял начало функции прямо в отладчике на JMP ExceptionHandler (мой самописный обработчик). Срабатывает! Срабатывает именно мой обработчик! И к чему тогда эта неведомая таблица безопасных, когда я могу легко подставить вместо любого безопасного прыжок на свой опасный? Впрочем, не важно. Это не даёт ответа на мои изначальные вопросы.

Теперь я пробую записать лог от entry point до функции main двух EXE файлов, скомпилированных с флагом /SAFESEH и /SAFESEH:NO, наивно предполагая, что компилятор на этой стадии вызывает некие WinApi функции, с помощью которых можно добавить в систему адреса безопасных обработчиков. Все тщетно. Два лога практически не отличаются, разве что серией дополнительных невнятных инструкций, не имеющих ничего общего с адресами безопасных обработчиков. Но подозрения все равно остались.

Чтобы окончательно исключить crt0 из списка подозреваемых мне необходимо выяснить еще кое-что. Я придумал следующий эксперимент, результат которого даст мне точный ответ на вопрос! Меняю инструкцию точки входа в программу на следующие команды push main, ret. Таким образом, первой командой в стек попадает адрес функции main, а вторая команда ret возвращает регистр указатель на выполнение функции main и функция main выполняется сразу, без crt0! (можно было обойтись и простым JMP, но его немного сложнее реализовать на лету в отладчике)

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

Может быть, они спрятали этот флаг в PE хедере? В каком-нибудь поле LoaderFlags? И тут неудача У обоих файлов абсолютно идентичные структуры, за исключением разницы в размере секции rdata. Но даже если в секции rdata и лежит несколько дополнительных байт, разве это может быть способом взаимодействия с загрузчиком, кардинально изменяющим работу программы? Я был уверен, что нет

Ведь в секциях можно размещать что угодно, главное - соблюсти ключевые правила, думаю я. Там ведь полно мусора! Всякие строки, цифры, дни недели, это ведь не монолитная структура какая-то, а блоки данных, на которые указывают указатели. Эти блоки располагаются в произвольном месте, но в диапазоне секции. При сравнении двух файлов, никаких указателей на область rdata я не увидел, кроме известной importTable, debug и неизвестного указателя на boundImport. Кроме того, такой же указатель был и в файле без флага /SAFESEH! Его размер в таблице dataDirectories был очень маленьким и он был заполнен нулями. Это не вызывало моих подозрений, потому что я был уверен, что если бы это каким-то образом включало в себя таблицу обработчиков, то в файле без флага /SAFESEH это поле просто напросто отсутствовало бы, что было бы знаком для загрузчика у этого EXE таблицы нет.

Победа близка

Отчаявшись, я беру адрес первого безопасного обработчика, который подсовывает в мой EXE компилятор. А точнее - первые два младших байта его адреса и прошелся поиском по всему файлу. То, что я решил взять именно 2 байта, включая те значения, которые не являются виртуальными адресами, является везением. И вот, я выписал все сырые адреса, по которым располагаются эти значения, преобразовал адреса в виртуальные (их было около 3-х) и приступил к задуманному.

Имея несколько адресов в памяти, предположительно намекающих на законный обработчик от компилятора, я ставлю точку останова перед командой mov eax, DWORD PTR SS : [0] и жму play. Срабатывает. Я расставляю точки останова на чтение памяти по записанным адресам и снова жму play.

Удивительно! О брекпоинт споткнулась некая безымянная функция в библиотеке ntdll.dll.

Смотрим. Функция пытается считать по адресу 0x1341d20 из диапазона rdata число 0x1be0, а затем сравнить его с числом, находящимся в EBX 0x1000. Так, а ведь по адресу imageBase + 0x1000 расположился мой подставной обработчик! Он - то сейчас и находится в числе первых обработчиков. А по адресам 0x1be0, 0x2080 и 0x2351 располагаются безопасные обработчики от компилятора. Вот она эта таблица! Она всё это время была в моём файле!

В отладчике я меняю содержимое адреса 0x1341d20 с 0x1be0 на 0x1000 и мой обработчик стал вдруг безопасным и послушно обработал исключение!

Реализация проверки безопасных обработчиков в открытом виде! Она не находится внутри ядра Windows, как я ожидал. Так что можно даже подкорректировать код в ntdll.dll, чтобы в нужное время он разрешал или запрещал выбранные нами обработчики!

Вернёмся к нашему файлу еще раз внимательно посмотрим, что находится в окрестностях.

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

Попробую-ка я заполнить нулями этот адрес-указатель.

Успех! Программа, которую я скомпилировал с флагом /SAFESEH теперь выполняется так, как будто бы был установлен флаг флага /SAFESEH:NO!

А сравним с файлом, который был скомпилирован без этого флага?

Чёрт ни адреса-указателя на таблицу в этом месте, ни таблицы обработчиков И как я сразу не додумался сравнить два файла скомпилированных с разными флагами в Total Commander?!

Тем не менее, что указывает на это место в PE хедере? Самым близким является boundImport. И вот только теперь я открываю сайт Microsoft с описанием PE формата.

В общем я предлагаю вам самим ознакомиться с тем, что парни из Microsoft пишут там про boundImport. Что-то о таблице каталога отложенной загрузки на этапе связывания пост-обработки? Что?

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

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

Смотрю дальше... The Load Configuration Structure. Судя по описанию очень похоже! Они говорят, что структура IMAGE_LOAD_CONFIG_DIRECTORY где-то присутствует и описывают ее предназначение, связанное с SEH.

Думаю, а попробую-ка. Взял, да и написал, что по адресу boundImport находится структура IMAGE_LOAD_CONFIG_DIRECTORY32 и рассмотрел её поля. Все сошлось как пазл! Это опять же было большим везением, что я решился на это.

Вот и всё, весь секрет заключался в том, что boundImport в PE хедере (разложенном по таблице из Википедии) указывает на структуру IMAGE_LOAD_CONFIG_DIRECTORY32. Эта структура содержит поля, которые кардинальным образом меняют работу программы! Теперь мы с уверенностью можем сами создать свою таблицу обработчиков и добавить в неё только то, что посчитаем нужным!

Может быть, где-то в недрах документации есть информация о том, что boundImport уже называется не так на моей платформе? Или информация о том, что его назначение изменилось? Может быть, boundImport оказался исключен или сдвинут?

Подробнее..

Как WCF сам себе в ногу стреляет посредством TraceSource

21.06.2021 14:12:41 | Автор: admin

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

Предыстория

В дистрибутиве PVS-Studio есть одна утилита под названием CLMonitor.exe, или система мониторинга компиляции. Она предназначена для "бесшовной" интеграции статического анализа PVS-Studio для языков C и C++ в любую сборочную систему. Сборочная система должна использовать для сборки файлов один из компиляторов, поддерживаемых анализатором PVS-Studio. Например: gcc, clang, cl, и т.п.

Стандартный сценарий работы данной Windows утилиты очень простой, всего 3 шага:

  1. Выполняем 'CLMonitor.exe monitor';

  2. Выполняем сборку проекта;

  3. Выполняем 'CLMonitor.exe analyze'.

Первый шаг запускает 'сервер', который начинает отслеживать все процессы компиляторов в системе до тех пор, пока его не остановят. Как только мы запустили сервер выполняем сборку проекта, который мы хотим проанализировать. Если сборка прошла, то нужно запустить анализ. Для этого мы исполняем третий шаг. 'CLMonitor.exe analyze' запускает 'клиент', который говорит серверу: "Всё, хватит, выключайся и давай сюда результаты мониторинга за процессами". В этот момент сервер должен завершить свою работу, а клиент запустить анализ. Подробнее о том, как внутри работает система мониторинга и зачем сервер вообще собирает процессы, мы поговорим чуть позже.

И вот в один прекрасный момент описанный сценарий не заработал, анализ просто-напросто не запустился. Ну и чтобы было поинтереснее, возникла эта проблема не у нас, а у пользователя, который обратился к нам в поддержку. У него стабильно после запуска анализа происходило десятиминутное ожидание ответа от сервера с дальнейшим выходом из программы по timeout'у. В чём причина непонятно. Проблема не воспроизводится. Да... беда. Пришлось запросить дампфайл для процесса нашей утилиты, чтобы посмотреть, что там происходит внутри.

Примечание. Проблема у пользователя возникла при использовании Windows утилиты CLMonitor.exe. Поэтому все дальнейшие примеры будут актуальны именно для Windows.

Как работает CLMonitor.exe

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

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

Зачем мы вообще отлавливаем процессы

Как вы поняли, история начинается с того, что нужно запустить сервер, который будет отлавливать все процессы. Делаем мы это не просто так. Вообще, более удобный способ проанализировать C++ проект это прямой запуск анализатора через утилиту командной строки PVS-Studio_Cmd. У неё, однако, есть существенное ограничение она может проверять только проекты для Visual Studio. Дело в том, что для анализа требуется вызывать компилятор, чтобы он препроцессировал проверяемые исходные файлы, ведь анализатор работает именно с препроцессированными файлами. А чтобы вызвать препроцессор, нужно знать:

  • какой конкретно компилятор вызывать;

  • какой файл препроцессировать;

  • параметры препроцессирования.

Утилита PVS-Studio_Cmd узнает все необходимое из проектного файла (*.vcxproj). Однако это работает только для "обычных" MSBuild проектов Visual Studio. Даже для тех же NMake проектов мы не можем получить необходимую анализатору информацию, потому что она не хранится в самом проектном файле. И это несмотря на то, что NMake также является .vcxproj. Сам проект при этом является как бы обёрткой для другой сборочной системы. Тут в игру и вступают всяческие ухищрения. Например, для анализа Unreal Engine проектов мы используем прямую интеграцию с *Unreal Build Tool * сборочной системой, используемой "под капотом". Подробнее здесь.

Поэтому, для того чтобы можно было использовать PVS-Studio независимо сборочной системы, даже самой экзотической, у нас и появилась утилита CLMonitor.exe. Она отслеживает все процессы во время сборки проекта и отлавливает вызовы компиляторов. А уже из вызовов компиляторов мы получаем всю необходимую информацию для дальнейшего препроцессирования и анализа. Теперь вы знаете, зачем нам нужно мониторить процессы.

Как клиент запускает анализ

Для обмена данными между сервером и клиентом мы используем программный фреймворк WCF (Windows Communication Foundation). Давайте далее кратко опишем, как мы с ним работаем.

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

static ErrorLevels PerformMonitoring(....) {  using (ServiceHost host = new ServiceHost(                       typeof(CLMonitoringContract),                          new Uri[]{new Uri(PipeCredentials.PipeRoot)}))   {    ....    host.AddServiceEndpoint(typeof(ICLMonitoringContract),                             pipe,                             PipeCredentials.PipeName);    host.Open();         ....  }}

Обратите тут внимание на две вещи: *CLMonitoringContract *и ICLMonitoringContract.

*ICLMonitoringContract * это сервисный контракт. *CLMonitoringContract * реализация сервисного контракта. Выглядит это так:

[ServiceContract(SessionMode = SessionMode.Required,                  CallbackContract = typeof(ICLMonitoringContractCallback))]interface ICLMonitoringContract{  [OperationContract]  void StopMonitoring(string dumpPath = null);} [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]class CLMonitoringContract : ICLMonitoringContract{  public void StopMonitoring(string dumpPath = null)  {    ....    CLMonitoringServer.CompilerMonitor.StopMonitoring(dumpPath);  } }

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

public void FinishMonitor(){  CLMonitoringContractCallback сallback = new CLMonitoringContractCallback();  var pipeFactory = new DuplexChannelFactory<ICLMonitoringContract>(           сallback,            pipe,            new EndpointAddress(....));  ICLMonitoringContract pipeProxy = pipeFactory.CreateChannel();  ((IContextChannel)pipeProxy).OperationTimeout = new TimeSpan(24, 0, 0);  ((IContextChannel)pipeProxy).Faulted += CLMonitoringServer_Faulted;  pipeProxy.StopMonitoring(dumpPath);}

Когда клиент выполняет метод StopMonitoring, он на самом деле выполняется у сервера и вызывает его остановку. А клиент получает данные для запуска анализа.

Всё, теперь вы, хоть немного, имеете представление о внутренней работе утилиты CLMonitor.exe.

Просмотр дамп файла и осознание проблемы

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

**Интересный факт. **Откуда вообще взялись эти 10 минут? Дело в том, что мы задаем время ожидания ответа от сервера намного больше, а именно - 24 часа, как видно в примере кода, приведённом выше. Однако для некоторых операций фреймворк сам решает, что это слишком много и он успеет быстрее. Поэтому берет только часть от изначального значения.

Мы попросили у пользователя снять дамп с двух процессов (клиент и сервер) минуток через 5 после запуска клиента, чтобы посмотреть, что там происходит.

Тут небольшая пауза. Хочется быть честным по отношению к моему коллеге Павлу и упомянуть, что это именно он разобрался в данной проблеме. Я же ее просто чинил, ну и вот сейчас описываю :) Конец паузы.

Дамп 'клиента'

Так вот, когда мы открыли дамп файл клиента, перед глазами предстал следующий список потоков:

Нас интересует главный поток. Он висит на методе, отвечающем за запрос остановки сервера:

public void FinishMonitor(){  ....  ICLMonitoringContract pipeProxy = pipeFactory.CreateChannel();  ((IContextChannel)pipeProxy).OperationTimeout = new TimeSpan(24, 0, 0);  ((IContextChannel)pipeProxy).Faulted += CLMonitoringServer_Faulted;  pipeProxy.StopMonitoring(dumpPath);            // <=  ....}

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

Дамп 'сервера'

Открываем его и видим следующий список потоков:

Воу-воу, откуда так много TraceEvent'ов? Кстати, на скриншоте не уместилось, но всего их более 50. Ну давайте подумаем. Данный метод у нас используется, чтобы логировать различную информацию. Например, если отловленный процесс является компилятором, который не поддерживается, произошла ошибка считывания какого-либо параметра процесса и т.д. Посмотрев стеки данных потоков, мы выяснили, что все они ведут в один и тот же метод в нашем коде. А метод этот смотрит, является ли отловленный нашей утилитой процесс компилятором или это нечто иное и неинтересное, и, если мы отловили такой неинтересный процесс, мы это логируем.

Получается, что у пользователя запускается очень много процессов, которые, конкретно для нас, являются 'мусором'. Ну допустим, что это так. Однако данная картина все равно выглядит очень подозрительно. Почему же таких потоков так много? Ведь, по идее, логирование должно происходить быстро. Очень похоже на то, что все эти потоки висят на какой-то точке синхронизации или критической секции и чего-то ждут. Давайте зайдем на ReferenceSource и посмотрим исходный код метода TraceEvent.

Открываем исходники и действительно видим в методе TraceEvent оператор lock:

Мы предположили, что из-за постоянной синхронизации и логирования накапливается большое количество вызовов методов TraceEvent, ждущих освобождения TraceInternal.critSec. Хм, ну допустим. Однако это пока не объясняет, почему сервер не может ответить клиенту. Посмотрим еще раз в дамп файл сервера и заметим один одинокий поток, который висит на методе DiagnosticsConfiguration.Initialize:

В данный метод мы попадаем из метода NegotiateStream.AuthenticateAsServer, выполняющего проверку подлинности со стороны сервера в соединении клиент-сервер:

В нашем случае клиент-серверное взаимодействие происходит с помощью WCF. Плюс напоминаю, что клиент ждет ответ от сервера. По этому стеку очень похоже, что метод DiagnosticsConfiguration.Initialize был вызван при запросе от клиента и теперь висит и ждет. Хм... а давайте-ка зайдем в его исходный код:

И тут мы замечаем, что в данном методе имеется критическая секция, да еще и на ту же самую переменную. Посмотрев, что вообще такое этот critSec, увидим следующее:

Собственно, у нас уже есть достаточно информации, чтобы подвести итоги.

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

"Also one of the locks, TraceInternal.critSec, is only present if the TraceListener asks for it. Generally speaking such 'global' locks are not a good idea for a high performance logging system (indeed we don't recommend TraceSource for high performance logging at all, it is really there only for compatibility reasons)".

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

Итоги изучения дампов

Итак, что мы имеем:

  1. Клиент общается с сервером с помощью фреймворка WCF.

  2. Клиент не может получить ответа от сервера. После 10 минут ожидания он падает по тайм-ауту.

  3. На сервере висит множество потоков на методе TraceEvent и всего один - на Initialize.

  4. Оба метода зависят от одной и той же переменной в критической секции, притом это статическое поле.

  5. Потоки, в которых выполняется метод TraceEvent, бесконечно появляются и из-за lock не могут быстро сделать свое дело и исчезнуть. Тем самым они долго не отпускают объект в lock.

  6. Метод Initialize возникает при попытке клиента завершить работу сервера и висит бесконечно на lock.

Из этого можно сделать вывод, что сервер получил команду завершения от клиента. Чтобы начать выполнять метод остановки работы сервера, необходимо установить соединение и выполнить метод Initialize. Данный метод не может выполниться из-за того, что объект в критической секции держат методы TraceEvent, которые в этот момент выполняются на сервере. Появление новых TraceEvent'ов не прекратится, потому что сервер продолжает работать и отлавливать новые 'мусорные' процессы. Получается, что клиент никогда не получит ответа от сервера, потому что сервер бесконечно логирует отловленные процессы с помощью TraceEvent. Проблема найдена!

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

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

Воспроизведение проблемы

Само воспроизведение получается крайне простое. Все, что нам нужно, это сделать так, чтобы сервер постоянно что-то логировал. Потому создаем метод с говорящим названием CrazyLogging, который и будет это делать:

private void CrazyLogging(){  for (var i = 0; i < 30; i++)  {    var j = i;    new Thread(new ThreadStart(() =>    {      while (!Program.isStopMonitor)        Logger.TraceEvent(TraceEventType.Error, 0, j.ToString());    })).Start();  }}

За работу сервера у нас отвечает метод Trace, поэтому добавляем наше логирование в него. Например, вот сюда:

public void Trace(){  ListenersInitialization();  CrazyLogging();  ....}

Готово. Запускаем сервер (я буду это делать с помощью Visual Studio 2019), приостанавливаем секунд через 5 процесс и смотрим что у нас там с потоками:

Отлично! Теперь запускаем клиент (TestTraceSource.exe analyze), который должен установить связь с сервером и остановить его работу.

Запустив, мы увидим, что анализ не начинается. Поэтому опять останавливаем потоки в Visual Studio и видим ту же самую картину из дамп файла сервера. А именно появился поток, который висит на методе DiagnosticsConfiguration.Initialize. Проблема воспроизведена.

Как же её чинить? Ну для начала стоит сказать, что TraceSource это класс, который предоставляет набор методов и свойств, позволяющих приложениям делать трассировку выполнения кода и связывать сообщения трассировки с их источником. Используем мы его потому, что сервер может быть запущен не приаттаченным к консоли, и консольное логирование будет бессмысленно. В этом случае мы логировали всё в Event'ы операционной системы с помощью метода TraceSource.TraceEvent.

Проблему мы "решили" следующим образом. По умолчанию теперь вся информация логируется в консоль, используя метод Console.WriteLine. В большинстве случаев, даже если эта логируемая информация и будет потеряна из-за не приаттаченной консоли, она всё равно не требуется для решения задач в работе утилиты, а проблема исчезла. Плюс изменения заняли какие-то минуты. Однако мы оставили возможность старого способа логирования с помощью специального флага enableLogger.

Код, воспроизводящий проблему

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

Чтобы запустить имитирование работы сервера, запустите .exe с флагом trace. Чтобы запустить клиент, воспользуйтесь флагом analyze.

**Примечание: **количество потоков в методе CrazyLogging следует подбирать индивидуально. Если проблема у вас не воспроизводится, то попробуйте поиграться с этим значением. Также можете запустить данный проект в Visual Studio в режиме отладки.

Точка входа в программу:

using System.Linq;namespace TestTraceSource{  class Program  {    public static bool isStopMonitor = false;    static void Main(string[] args)    {      if (!args.Any())        return;      if (args[0] == "trace")      {        Server server = new Server();        server.Trace();      }      if (args[0] == "analyze")      {        Client client = new Client();        client.FinishMonitor();      }    }    }}

Сервер:

using System;using System.Diagnostics;using System.ServiceModel;using System.Threading;namespace TestTraceSource{  class Server  {    private static TraceSource Logger;    public void Trace()    {      ListenersInitialization();      CrazyLogging();      using (ServiceHost host = new ServiceHost(                          typeof(TestTraceContract),                           new Uri[]{new Uri(PipeCredentials.PipeRoot)}))      {        host.AddServiceEndpoint(typeof(IContract),                                 new NetNamedPipeBinding(),                                 PipeCredentials.PipeName);        host.Open();        while (!Program.isStopMonitor)        {          // We catch all processes, process them, and so on        }        host.Close();      }      Console.WriteLine("Complited.");    }    private void ListenersInitialization()    {      Logger = new TraceSource("PVS-Studio CLMonitoring");      Logger.Switch.Level = SourceLevels.Verbose;      Logger.Listeners.Add(new ConsoleTraceListener());      String EventSourceName = "PVS-Studio CL Monitoring";      EventLog log = new EventLog();      log.Source = EventSourceName;      Logger.Listeners.Add(new EventLogTraceListener(log));    }    private void CrazyLogging()    {      for (var i = 0; i < 30; i++)      {        var j = i;        new Thread(new ThreadStart(() =>        {          var start = DateTime.Now;          while (!Program.isStopMonitor)            Logger.TraceEvent(TraceEventType.Error, 0, j.ToString());        })).Start();      }    }   }}

Клиент:

using System;using System.ServiceModel;namespace TestTraceSource{  class Client  {    public void FinishMonitor()    {      TestTraceContractCallback сallback = new TestTraceContractCallback();      var pipeFactory = new DuplexChannelFactory<IContract>(                                сallback,                                new NetNamedPipeBinding(),                                new EndpointAddress(PipeCredentials.PipeRoot                                                   + PipeCredentials.PipeName));      IContract pipeProxy = pipeFactory.CreateChannel();      pipeProxy.StopServer();      Console.WriteLine("Complited.");        }  }}

Прокси:

using System;using System.ServiceModel;namespace TestTraceSource{  class PipeCredentials  {    public const String PipeName = "PipeCLMonitoring";    public const String PipeRoot = "net.pipe://localhost/";    public const long MaxMessageSize = 500 * 1024 * 1024; //bytes  }  class TestTraceContractCallback : IContractCallback  {    public void JobComplete()    {      Console.WriteLine("Job Completed.");    }  }  [ServiceContract(SessionMode = SessionMode.Required,                    CallbackContract = typeof(IContractCallback))]  interface IContract  {    [OperationContract]    void StopServer();  }  interface IContractCallback  {    [OperationContract(IsOneWay = true)]    void JobComplete();  }  [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]  class TestTraceContract : IContract  {    public void StopServer()    {      Program.isStopMonitor = true;    }  }}

Вывод

Будьте осторожны со стандартным методом TraceSource.TraceEvent. Если у вас теоретически возможно частое использование данного метода в программе, то вы можете столкнуться с подобной проблемой. Особенно если у вас высоконагруженная система. Сами разработчики в таком случае не рекомендуют использовать всё, что связано с классом TraceSource. Если вы уже сталкивались с чем-то подобным, то не стесняйтесь рассказать об этом в комментариях.

Спасибо за просмотр. Незаметно рекламирую свой Twitter.

Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Nikolay Mironov. How WCF Shoots Itself in the Foot With TraceSource.

Подробнее..

FlaNium как сделать тестирование Desktop-приложений под Windows проще

27.04.2021 10:04:26 | Автор: admin
На рынке так много программных продуктов для тестирования, что может показаться, будто для всего найдется готовое решение и нет необходимости тратить время и усилия на разработку инструментов тестирования. На самом деле это не так. Мы в ЛАНИТ Экспертизе убедились в этом, когда появилась задача тестирования Desktop-приложений, и теперь делимся с вами опытом.

Источник: kotomatrix.ru

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

В отличие от автоматизации WEB, API или мобильных приложений, тестирование Desktop в некоторой степени экзотика, и на это есть несколько причин:

  • Отсутствуют качественные Open source решения, наподобие Selenium для Web, а те, что есть, либо сильно устарели, либо неудобны в использовании. Иными словами, воспитывают в автоматизаторах смирение и обреченность.
  • Коммерческие продукты хоть и обладают широким функционалом и удобством в использовании, стоят зачастую дороже, чем вся автоматизация и используют vendor lock-in бизнес-модель, которая также накладывает дополнительные затраты и ограничения (переобучение персонала, отсутствие возможности использовать существующие наработки и решения, полная зависимость от поставщика ПО и т.п.).
  • Ввиду стремительного развития web-технологий, Desktop понемногу отмирает, что сильно сказывается на развитии и поддержке инструментов тестирования и наличии квалифицированных специалистов в этой области.

Несмотря на всю пессимистичность сложившейся ситуации с автоматизацией Desktop-приложений, можно выделить некоторые Open source продукты, достойные внимания. Они же в дальнейшем стали основой в разработке собственного инструмента тестирования.

Анализ готовых инструментов


Обзор предлагаемых решений показал, что Open source продуктов не так уж много. Сначала мы испытали Winium Automation framework for Windows platforms.

Winium это фреймворк для тестирования Desktop Windows приложений на базе Selenium. Он обладает всеми основными требованиями, которые были необходимы для работы с предстоящим проектом:

  • поддержка WinForms и WPF приложений;
  • REST-протокол взаимодействия между тестами и тестируемым приложением;
  • возможность взаимодействия с Selenium (а конкретно Java + Selenium).

Несмотря на все свои преимущества у Winium есть один существенный недостаток ограниченный функционал. Ядро Cruciatus, на базе которого построен Winium, поддерживает только AutomationElement Identifiersclass, и соответственно, доступа к некоторым основным параметрам элементов у него нет. Например, нет поддержки ValuePattern (нет возможности получить значение положения ProgressBar, Slider, ScrollBar, состояния трехрежимного CheckBox и др.),SelectionItemPattern (нет возможности получить данные о выбранном элементе ComboBox и т.п.) и других паттернов. Данный недостаток был критичным, и мы продолжили поиск.

Следующим, что показалось нам интересным в плане возможностей, оказалась библиотека FlaUI. Библиотека была написана на C# и не имела какого-либо API для взаимодействия извне. По сути это некая оболочка над библиотеками автоматизации Microsoft Windows Automation API, которая упрощает написание тестов. В отличие от Winium данная библиотека обладает полным функционалом взаимодействия с WinForms и WPF-приложениями, но требует написания тестов на языке C#. Поскольку у нас уже был готовый фреймворк тестирования, реализованный на языке Java, данная библиотека не удовлетворяла наши потребности.

Дальнейший анализ инструментов тестирования показал, что наиболее оптимальный вариант это реализация своего инструмента. Имеющиеся продукты обладали теми или иными серьезными недостатками, а комбинировать несколько решений не представлялось возможным. Так как абсолютно все Open source решения использовали взаимодействие со стандартной библиотекой Windows Automation API, то было принято решение взять за основу ядро FlaUI Core, построенное на основе взаимодействия с данной библиотекой, и обладающее полным функционалом взаимодействия с элементами тестируемого приложения. Затем добавить поддержку Selenium REST API, аналогично Winium. Так родился проект FlaNium.

Что представляет из себя FlaNium


На данный момент в состав проекта входят пока только два компонента.

FlaNium.Desktop.Driver основной компонент, представляющий из себя драйвер взаимодействия с тестируемым приложением посредством Windows Automation API и использующий протокол взаимодействия Selenium REST API.

FlaNium.WinAPI Java-библиотека, расширяющая протокол Selenium REST API и добавляющая дополнительные возможности по настройке и взаимодействию с FlaNium драйвером. Также данная библиотека позволяет типизировать стандартный Selenium WebElement и привести его к компонентам тестируемого приложения, добавляя дополнительные методы взаимодействия, характерные определенному типу элемента.

Рассмотрим пример работы с FlaNium драйвером на базе Selenium Java


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

Перед началом работы с FlaNium драйвером необходимо загрузить последнюю версию драйвера, а также прописать следующие зависимости в pom.xml:

...<dependency>  <groupId>com.github.lanit-exp</groupId>  <artifactId>FlaNium.WinAPI</artifactId>  <version>LATEST</version></dependency><dependency><groupId>org.seleniumhq.selenium</groupId><artifactId>selenium-java</artifactId><version>3.141.59</version></dependency>...

Далее производим настройку и инициализацию драйвера:

String DRIVER_PATH = "src/main/resources/driver/FlaNium.Desktop.Driver/FlaNium.Driver.exe";String APP_PATH = "С:/Test_app.exe";int driverPort = 9999;// Инициализация драйвера:FlaNiumDriverService service = new FlaNiumDriverService.Builder()    // Указание пути до драйвера    .usingDriverExecutable(new File(DRIVER_PATH).getAbsoluteFile())    // Установка порта (по умолчанию 9999)    .usingPort(driverPort)    // Включение режима отладки (вывод логов в консоль)    .withVerbose(true)    // Отключение логирования    .withSilent(false)    .buildDesktopService();// Инициализация приложения:DesktopOptions options = new DesktopOptions();// Указание пути до тестируемого приложенияoptions.setApplicationPath(new File(APP_PATH));// Задержка после запуска приложения (сек)options.setLaunchDelay(5);// Подключение к ранее запущенному экземпляру приложенияoptions.setDebugConnectToRunningApp(false);// Получение экземпляра драйвера приложенияFlaNiumDriver driver = new FlaNiumDriver(service, options);

После получения экземпляра FlaniumDriver можно осуществлять поиск контролов приложения и взаимодействовать с ними через стандартные методы библиотеки Selenium.

WebElement edit = driver.findElement(By.xpath("//*[(@ControlType = 'Edit')   and contains(@Name,'Text')]"));edit.sendKeys("Test text");

Есть возможность поиска элементов по XPath, Name, Id (AutomationId) и ClassName, а также поддерживаются пять параметров поиска с помощью XPath AutomationId, Name, ClassName, HelpText, ControlType.

driver.findElement(By.xpath("//*[(@AutomationId = '')]"));driver.findElement(By.xpath("//*[(@Name = '')]"));driver.findElement(By.xpath("//*[(@ClassName = '')]"));driver.findElement(By.xpath("//*[(@HelpText = '')]"));driver.findElement(By.xpath("//*[(@ControlType = '')]"));driver.findElement(By.name("Checkbox1"));driver.findElement(By.id("Form1"));driver.findElement(By.className("MenuItem"));

Для более комфортной работы лучше воспользоваться такими инструментами как FlaUInspect, UISpy и подобными, так как они значительно упрощают написание тестов приложения, позволяя визуально отобразить структуру и параметры элементов приложения. Данные инструменты также позволяют понять, как можно обратиться к различным элементам или какие паттерны поддерживает конкретный из них.

Благодаря расширению протокола Selenium есть возможность типизировать любой WebElement и получить дополнительные возможности для работы. Для этого необходимо создать экземпляр требуемого класса и передать WebElement в качестве параметра:

TextBox textBox = new TextBox(edit);// где edit  WebElement полученный ранееtextBox.setText("Test text2");

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

На рисунке ниже изображено тестируемое приложение (слева) и инспектор (справа):


У нас есть элемент ComboBox, согласно инспектору, доступа к элементам списка у нас нет, чтобы его получить необходимо раскрыть список нажав на кнопку Открыть.


После раскрытия списка мы получаем доступ ко всем вложенным элементам и можем осуществлять поиск и выбор необходимого элемента.

Вот так будет выглядеть код при использовании стандартных Selenium-методов:

// Находим элемент комбобокса по id элементаWebElement comboBox = driver.findElement(By.xpath("//*[@AutomationId =      'NonEditableCombo']"));// Находим кнопку раскрытия списка и кликаем по нейcomboBox.findElement(By.xpath(".//*[@ControlType = 'Button']")).click();// После раскрытия списка, нам становятся доступны варианты выбораList<WebElement> items = comboBox.findElements(By.xpath(".//*[@ControlType = 'ListItem']"));// Далее пробегаемся по всему списку элементов и сравниваем имена с нужным нам значениемitems.stream().filter(webElement -> webElement.getAttribute("Name").equals("Item 3")).findFirst().get().click();// И в конце кликаем на найденном элементе

А вот так будет выглядеть то же самое, но при использовании методов типизированных элементов библиотеки FlaNium.WinAPI:

// Находим элемент комбобокса по id элементаComboBox comboBox = new ComboBox(driver.findElement(By.xpath("//*[@AutomationId = 'NonEditableCombo']")));// Выбираем необходимое значениеcomboBox.select("Item 3");

Как мы видим, использование данных методов значительно упрощает взаимодействие и сокращает код. С полным списком поддерживаемых элементов и реализованных методов можно ознакомиться по ссылке.

Итог


Мы не изобретали ничего кардинально нового, но взяв в основу преимущества Winium и FlaUI, скомпоновали продукт с удобным универсальным интерфейсом и широкими возможностями взаимодействия с тестируемым приложением. Удалось объединить протокол Selenium REST и библиотеки Windows Automation API.

Давайте рассмотрим, чем же обязан FlaNium этим двум проектам:


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

Поскольку драйвер использует универсальный протокол взаимодействия, стало возможным использовать уже имеющийся фреймворк тестирования с некоторыми изменениями. Была добавлена работа с элементами посредством библиотеки FlaNium.WinAPI и реализована логика работы с конкретным тестируемым приложением. Также удалось избежать сложностей при работе с элементами приложения, поскольку используется полноценная библиотека взаимодействия и не возникало ситуаций, когда нет доступа к какому-либо параметру элемента.

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

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

Тулзы ручного тестировщика приложений на базе Windows

25.04.2021 20:19:15 | Автор: admin
Моё лицо, когда не удается найти причины появления очередного блокераМоё лицо, когда не удается найти причины появления очередного блокера

Я занимаюсь тестированием десктоп приложений на базе Windows и в данный момент слежу за качеством одной отечественной ММОРПГ. Со временем я оброс некоторыми тулзами, позволяющими улучшить процессы ручного тестирования. В данной статье не хочу трогать полноценную автоматизацию (зависящую от ЯП, игрового движка и желания куашника лезть в эти дебри), а больше рассказать про ПО, которым я сам пользуюсь и которое не так часто всплывает в подобных темах (зачастую упор на мобилках и вэбе).

Virtual Box

Когда использовать: инсталляционное тестирование.

По данным Steam на апрель 2021 наиболее популярной ОСью является Windows 10 64 bit (92.38%). Следом за ней идут Windows 7 64 bit (2.33%) и Windows 8.1 64 bit (1.08%). Среди языков наиболее популярные: английский (39.27%), упрощенный китайский (18.83%) и русский (11.18%). Если с языками в рамках одной ОСи и можно поиграться, то иметь несколько разных ОСей на одной машине и переключаться между ними, прогоняя тесты, удовольствие, скажем честно, на любителя.

Здесь нам и приходят на помощь средства виртуализации, где имхо самым простым и удобным будет Virtual Box. Позволяет прямо здесь и сейчас раскатать несколько образов систем разных разрядностей, языков, объемов ПЗУ и ОЗУ. Создать клон виртуальной машины или сделать снимок системы, к которому можно откатываться после прогона тестов. Образы виртуального диска можно копипастить и использовать на разных физических машинах. Из минусов, в виртуалку не прокинуть видеокарту, в Virtual Box данного функционала попросту нет. link_virtualbox

NetBalancer и Tmeter

Когда использовать: стресс тестирование.

То, что замечательно работает при 500 Мбит, может быть абсолютно неюзабельным при 1 Мбит. Как проводить подобные тесты? Можно, конечно, понизить пропускную способность сетевой карты средствами windows, урезав, скажем, значение до 10 Мбит и поиграть с дуплексом. Или потыкаться в настройках роутера. Но хочется все же более точных цифр и статистики.

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

Преимущество, на мой взгляд, у NetBalancer`а есть возможность выбрать запущенное приложение и сохранить всю его статистику по трафику с указанием времени. Но да, за него придется немного забашлять разрабам. Tmeter бесплатный, но со статистикой у него похуже: подробного и посекундного сбора у него нет, только общие цифры. Небольшая оговорка: следует помнить, что все эти проверки синтетические и могут отличаться от реальных условий.
link_netbalancer и link_tmeter

AutoHotKey

Когда использовать: когда угодно.

Простейшая автоматизация рутинных процессов. Как быстро заполнять поля или отправить в игровой чат тысячу сообщений нажатием одной кнопки? Или воспроизвести последовательность действий, не прожимая кучу кнопок? Ответ - быстрый и ч0ткий AutoHotkey. link_ahk Например, можно зафиксировать шаблон баг репорта с заранее подготовленным форматированием:

^+v::sendinput, *Краткое описание проблемы:*sendinput, {ENTER}sendinput, {ENTER}sendinput, *Шаги воспроизведения:*sendinput, {ENTER}sendinput, 1.sendinput, {ENTER}sendinput, *Фактический результат:*sendinput, {ENTER}sendinput, {ENTER}sendinput, *Ожидаемый результат:*sendinput, {ENTER}sendinput, {ENTER}sendinput, *Окружение:*sendinput, {ENTER}sendinput, {ENTER}sendinput, *Комментарий:*sendinput, {ENTER}sendinput, -return

При нажатии сочетания клавиш ctrl+shift+v поле будет выглядеть так:

Process monitor и DebugView

Когда использовать: когда угодно/анализ данных для баг репорта.

Допустим, вы запускаете игру и ловите синий экран смерти. С чего начать диагностику? Безусловно пойдем изучать журнал windows и тыкать клиентские логи приложения. Но в дополнение к этому я бы выделил сразу две тулзы: Process monitor и DebugView. Первая выводит список всех процессов с указанием времени, позволяет сохранить их в табличку в csv/xml. Быстро, просто и удобно. Вторая позволяет смотреть в риалтайме на события системы во время использования приложения. С какой-то стороны DebugView может ответить на вопрос, что думает винда о том, как ты бьешь орков в вове. link_prmonitor и link_debugview

MSI Afterburner

Когда использовать: когда угодно/нагрузочное тестирование.

Как быстро понять, привели ли изменения в проекте к улучшению производительности и увеличению кол-ва кадров в секунду? А если бы еще можно было бы подключить к этой программе свои скрипты так вообще была бы сказка. Ну так есть такое! MSI Afterburner. Можно вывести на экран не просто кол-во кадров, а всю инфу с нагрузкой на железо (ОЗУ, ЦПУ, ГПУ, МГУ). А также поиграться с настройкой видюхи! Вольтаж, память, регулировка скорости вентиляторов и кучу всякого разного. Еще раз отмечу, что MSI Afterburner позволяет запускать сторонние приложения на свои события. Так, например, я делаю питонячим скриптом скрины экрана в местах, где ФПС значительно проседает. link_msiab

Intel Graphics Perfomance Analyzers

Когда использовать: когда графическое приложение в конкретном месте безбожно тормозит.

Не совсем для всех и каждого. Приложение подойдет для мониторинга производительности программ с упором на графическую составляющую. Включает в себя целый список программных средств: GPA System Analyzer, Frame Analyzer, Trace Analyzer. Позволяет запустить прилажку, поставить ее на паузу, захватить фрейм, собрать по нему объемную информацию и передать на анализ, скажем, техартистам, которые уже и будут заниматься оптимизацией. Еще никогда процесс сбора подобной информации не был так прост. Кому захочется почитать подробнее, вот хорошая статья на русском. link_intel_gpanalyzers

Самописные тулзы

Когда использовать: когда угодно.

Не хотел трогать автоматизацию, но считаю, что стоит упомянуть о самописных тулзах, которые можно использовать каждый день и на всех этапах тестирования. Отправка запросов, работа с БД, парсинг json`ов, анализ любых внутренних данных в свои тулзы можно запихнуть все, что нужно и все, что угодно. Вариаций может быть масса. Я же использую Python и библиотеку pysimplegui, у которой есть отличный cookbook с примерами. А с помощью auto-py-to-exe легко и просто гуишка упаковывается в исполняемый файл.

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

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

Подробнее..

Запускаем Homebrew на Windows 10

08.06.2021 14:19:38 | Автор: admin

Коллеги, которые только начали погружение в мир Cloud Native, часто задаются вопросом, как установить необходимое ПО на Windows. Решение уже давно известно Windows Subsystem for Linux (WSL). Это действительно неплохой вариант для полноценной работы. И не забывайте, что установить все необходимые утилиты очень просто вам нужен Homebrew. Этот пакетный менеджер уже давно доступен не только для OS X, но и для Linux. Приступим!

Установка Ubuntu 20.04

Заходим в Microsoft Store и устанавливаем Ubuntu 20.04. Может потребоваться удалить старую версию:

CTRL + R -> CMD#Check installationC:\Users\suchak>wsl --listWindows Subsystem for Linux Distributions:Ubuntu-20.04 Legacy (Default)#Uninstalling older versionC:\Users\suchak>wsl --unregister Legacy#Check the default installation againC:\Users\suchak>wsl --listWindows Subsystem for Linux Distributions:Ubuntu-20.04 (Default)

Устанавливаем терминал для Windows

Iterm2 великолепный терминал для OS X. Работая в Linux я предпочитаю Tilix. А что же выбрать для работы с Windows? Неплохим решением может быть Terminus, хотя вы можете использовать и Power Shell.

Для установки Terminus загружаем инсталляционный файл для Windows из официального репозитория на GitHub.

Устанавливаем Homebrew

Запускаем терминал Terminus и приступаем к установке. Для работы Homebrew требуется gcc:

sudo apt install gcc

Теперь устанавливаем Homebrew:

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

Всё готово!

Установим необходимые программы:

brew install kind kubernetes-cli helm k9s kubectx kubecolor

Теперь мы можем без труда устанавливать дополнительные приложения, работая c Windows, как если бы мы работали с OS X или Linux.

Подробнее..

В закладки Кунг-фу на клавиатуре. Искусство сочетать клавиши

11.04.2021 14:15:43 | Автор: admin

Сочетания клавиш для тех, кто хочет войти в IT и не только.

Содержание

Сочетания клавиш - это простые команды, позволяющие удерживать пальцы на клавиатуре, а не прыгать вперед и назад к мыши. Вы, вероятно, уже знаете такие распространенные сочетания клавиш, как CTRL + C для копирования и CTRL + V для вставки, но есть множество других сочетаний клавиш, которые позволяют делать что угодно на вашем компьютере или в конкретной программе. Говорят, что знание только основных сочетаний экономит вам 8 рабочих дней каждый год. Цель этой статьи - сэкономить вам ГОРАЗДО больше.

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

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

В конце вы увидите, как все эти знания можно использовать при работе с самым популярным на данный момент текстовым редактором Visual Studio Code.

Не пытайтесь просто запомнить все, что написано в статье. Обязательно попробуйте понажимать все сочетания клавиш, о которых вы не знали! А потом возвращайтесь время от времени, чтобы освежить память.

Если вы пользователь системы Windows, то вы обязаны дочитать до конца.

Поехали.

Базовые сочетания

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

Esc- aka Escape или Эскейп или Эскейт или... ладно, ладно. Тут все просто, она в основном используется для выхода из чего-либо или отмены чего-либо. Если все пошло наперекосяк и "Я что-то нажала и все уронилось", то скорее всего вам нужно будет обратиться к этой клавише.

Tab. Таб это не только символ табуляции, но еще и полезнейший инструмент смены фокуса. Фокус здесь означает то место, на которое будут направлены действия пользователя в системе. Так, например, если у нас в приложении стоит фокус на текстовом поле, то все, что мы печатаем, будет вноситься именно в это поле. Если фокус на кнопке - тоSpaceили в худшем случаеEnterнажмут на эту кнопку. Так что совет тут такой. Когда заполняем формы, переключаемся между полями черезTab, отмечаем галочки черезSpace, решили вернуться -Shift+Tab.

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

Ctrl- работает похожим образом, но позволяет точечно выбирать, что именно вам нужно.

Shift+/<key/>- так исторически сложилось, что Shift часто используется для инвертирования действия, выполненного сочетанием клавиш без Shift. Например Shift+Tab совершает действие, противоположное Tab. Keep in mind!

F1- вызов справки. Просто для справки.

Ctrl+W- закрыть окно, вкладку. Почти всегда работает в приложениях, где есть вкладки.

Ctrl+F- поиск. F - Find.

Ctrl+P. В большинстве программ вызывает меню печати.

Ctrl+S- сохранить. Что тут еще говорить.

Ctrl+Z- отменить действие.

Ctrl+Y/Ctrl+Shift+Z- повторить отмененное действие.

Ctrl+N. Открыть новое окно/файл.

Ctrl+(+)- приблизить/увеличить масштаб.

Ctrl+(-)- отдалить/уменьшить масштаб.

Ctrl+(?shift)+[|Ctrl+(?shift)+]- уменьшить | увеличить размер шрифта.

Shift+Enter- перевести курсор на начало новой строки. Это сочетание бывает необходимо, если нажатиеEnterделает что-то другое.

Графика

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

Ctrl+D- создать копию объекта. От слова Duplicate.

Ctrl+(?shift)+[- Переместить элемент на задний план. В некоторых программах требуется клавиша модификатор Shift.

Ctrl+(?shift)+]- Переместить элемент на передний план.

Хотя использование мыши - дело, недостойное настоящего погромиста, но иногда без нее никуда.

Эти сочетания мыши критичны для тех, кто работает с графическими элементами.

Ctrl+MouseScroll- увеличить / уменьшить масштаб.

Shift+MouseScroll- переместиться по горизонтали. Иногда весь контент не помещается в окне, и появляется горизонтальная полоса прокрутки. Данное сочетание позволяет перемещаться вправо или влево в таких ситуациях.

Shift+Click- выбрать группу элементов, если возможно.

Ctrl+Click- выбрать элементы, точечно.

Ctrl+(Dragging)- копировать элемент или группу элементов и переместить на позицию курсора. Работает не везде, но полезно помнить.

Терминал Linux

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

Tab- автодополнение команд.

Tab+Tab- вывести список возможных команд автодополнения.

Ctrl+C- послать сигнал прерывания процесса.

Ctrl+Z- приостановить процесс и перевести в фон.

Ctrl+D- удалить символ после курсора. Если строка пустая, то эта команда завершит работу терминала.

Alt+D- удалить слово после курсора.

Ctrl+W- вырезать слово перед курсором.

Alt+Backspace- удалить слово перед курсором.

Ctrl+A- перейти в начало строки.

Ctrl+E- перейти в конец строки.

Ctrl+B- переместиться на символ назад.

Ctrl+F- переместиться на символ вперед.

Windows

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

Парочка полезностей:

  • Поиск через "Start" в Windows работает так, что он быстро находит файлы, находящиеся в системных папках. Поэтому, если вам лень ходить по файловой системе, нажимаемWinи вводим имя файла - система быстро нам его найдет.

  • Если открыть контекстное меню файла с зажатымShift, то появятся дополнительные опции. Одной из них будет "Скопировать как путь", очень удобно.

Сочетания

Win+D- скрыть все окна.

Win+M- свернуть все окна.

Win+Shift+M- развернуть все окна.

Win+X- открыть меню системных инструментов.

Win+P- открыть меню проекции на дополнительные дисплеи.

Win+Left/Right- переместить окно влево/вправо.

Win+Up- развернуть окно на весь экран.

Win+Down- открепить окно / свернуть.

Win+L- заблокировать систему.

Win+B- фокус на панель инструментов.

Win+Ctrl+Left/Right- переключиться между рабочими столами.

Win+(.)- открыть меню стикеров.

Alt+Tab- вы и так знаете. Переключаемся между активными окнами. Зажатый Shift двигает нас назад.

Alt+Enter- позволяет открыть некоторые окна в полноэкранном режиме.

Win+{1, 2, 3, ...}- открыть n-ное окно прикрепленное к панели задач. Обязательно к использованию!

Win+Shift+{1, 2, 3, ...}- открыть n-ное окно от имени администратора.

Win+Shift+S- сделать скриншот части экрана.

Ctrl+Shift+Esc- открыть диспетчер задач.

Alt+\<key\>- активирует навигацию в окнах многих программ. Например, сочетаниеAlt+V + H + Hпозволяет скрыть/показать скрытые файлы в проводнике.

PrintScreenилиPrtScn- сделать снимок экрана. Вроде бы вещь банальная, но, как показывает практика, все еще есть люди, которые фотографируют экран. Скриншот сохраняется в буфер обмена, поэтому, чтобы просмотреть картинку, вам нужно будет ее вставить в соответствующую программу, например Paint.

Alt+PrintScreen- сделать скриншот окна.

Win+PrintScreen- сделать скриншот экрана и сохранить в папку "Изображения".

Проводник

F2- переименовать файл/папку.

Win+E- открыть проводник.

Alt+Enter- открыть свойства файла.

Ctrl+Enter- открыть папку в новом окне.

Ctrl+Shift+N- создать новую папку в текущей директории в проводнике.

Alt+Left/Alt+Right- предыдущая папка / следующая папка в истории.

Alt+Up. Переместиться на уровень вверх.

Утилиты

Magnifier

Win+(+)- увеличить часть экрана.

Win+(-)- уменьшить часть экрана.

Win+Esc- закрыть Magnifier.

Xbox Game Bar

Win+G- открыть игровое меню XBox.

Win+Alt+R- Начать/остановить запись текущего окна. R - Record.

Chrome

В хроме исправно работают многие из сочетаний, описанных вБазовые сочетания.

Ctrl+Shift+W- брат Ctrl+W. Позволяет закрыть все вкладки сразу.

Ctrl+Shift+N- открыть новое окно в режиме инкогнито. Если вам часто нужно открыть "чистое" окно браузера, то запомните это сочетание.

Ctrl+T- открыть новую вкладку и перейти к ней.

Ctrl+Shift+T- открыть ранее закрытую вкладку. В некоторых случаях это поможет восстановить историю вкладок, если браузер был принудительно закрыт.

Ctrl+Tab- перейти к следующей вкладке на панели.

Ctrl+Shift+Tab- перейти к следующей вкладке на панели.

Ctrl+{1, 2, ..., 8}- перейти к n-ной вкладке.

Ctrl+9- перейти к последней вкладке.

Ctrl+R- перезагрузить страницу.

Ctrl+Shift+R- перезагрузить страницу, без использования данных в кэше.

Ctrl+D- сохранить текущую страницу в закладках.

Ctrl+Shift+D- сохранить все открытые вкладки в закладках. Поосторожнее с этим!

Ctrl+L- фокус на адресную строку.

Сочетания мыши

MiddleMouseButton- открыть страницу по ссылке в новой вкладке.

Ctrl+Click- открыть страницу по ссылке в новой вкладке.

Ctrl+Shift+Click- открыть ссылку в новой вкладке и перейти к ней. Обычно это именно то, что нужно, если вы хотите быстро просмотреть информацию по другой ссылке.

Shift+Click- открыть ссылку в новом окне.

Меню

Ctrl+H- открыть историю браузера.

Ctrl+J- открыть страницу загрузок.

Shift+Esc- открыть диспетчер задач Chrome. Если вы не понимаете, какая страница сильно нагружает ваши ресурсы, то стоит сюда заглянуть.

Ctrl+Shift+O- открыть страницу закладок.

Developer tools

Developer tools - один из самых полезных инструментов в арсенале Web-разработчика. Посмотрим, как можно немного ускорить работу с ним в браузере Chrome.

Ctrl+Shift+J- открыть вкладку Console в инструментах разработчика

Ctrl+(backtick)- фокус на консоли. При открытом меню инструментов разработчика.

Ctrl+[,Ctrl+]- сменить вкладку на панели.

F2- редактировать элемент как html.

Ctrl+Shift+P- открыть панель команд. Сочетание, которое может заменить их все.

Ctrl+Shift+I/F12- открыть последнюю использованную вкладку в инструментах разработчика.

Ctrl+Shift+C- открыть панель элементов страницы.

Этого немного, но достаточно, чтобы пользоваться и не перегружать мозг. Больше сочетанийздесь.

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

Vimium

Чего мы хотим?Чего мы хотим?

Vimium- расширение для браузера, которое предоставляет сочетания клавиш для навигации и управления в духе редактора Vim.

Для работы с русской раскладкойнужно будет добавить "мэпы" в настройки вимиума.

Вот что это нам дает.

Переход по ссылкам черезF+<key>.Больше не нужно тянуться за мышкой, чтобы кликнуть по ссылке. Это сделает за вас Vimium. Нажмите клавишуFи на странице к каждой ссылке прикрепится название клавиши, которую нужно нажать далее, чтобы перейти по ней.

Перемещение через сочетания как в виме

Используйте привычные клавишиhjklдля движения по странице.

Панель поиска

  • Ищем везде черезo

  • Ищем в закладках черезb

  • Ищем в открытых вкладках черезT

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

Vimium hotkeysVimium hotkeys

AutoHotkey

Вначале будет многа букофф...

Хакерский редактор

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

Vim (сокр. от Vi Improved, произносится вим) кошмар для случайного пользователя. Если его удаётся в конце концов закрыть, то вздыхаешь с облегчением и твёрдой уверенностью, что больше не будешь запускать эту программу ни разу в жизни. (c) Материал из Викиучебника

Так что же в нем особенного? На Хекслете есть неплохаястатья, в которой рассказано, почему Vim - это хорошо.

Я тоже считаю, что вим - это хорошо и знать основные его команды очень пригодится. Так, например, упоминание этого редактора при разговоре с разработчиками даст вам +1 к крутости. А на многих сайтах (например YouTube) используются сочетания именно из этого редактора. Однако реальность такова, что Vim - не современный инструмент, а популярность его поддерживается лишь благодаря старичкам и тем, кто любит выпендриваться.

О самом популярном в мире редакторе кода чуть позже.

Главный аргумент в пользу вима -Vim позволяет писать эффективно

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

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

Используем AHK

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

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

На обычной клавиатуре зачастую есть такая замечательная, очень бесполезная клавиша, какCapsLock. Почему бесполезная? Да потому что единственная ее функция - зажимать вместо вас клавишуShift. Ее любят в основном тролли в интернете и те, кто часто пишут SQL запросы. Зато неудобств от нее куча. Сколько раз вы проверяли, включен ли у васCapsLockперед вводом пароля? Как минимум раз так точно, а этого уже достаточно, чтобы ночью видеть кошмары.

Вердикт - "капсу" можно найти лучшее применение.

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

Вот как выглядит часть моегоскриптадля AutoHotkey

SetCapsLockState AlwaysOff; Basic movementCapsLock & j::Send {blind}{Left}CapsLock & l::Send {blind}{Right}CapsLock & i::Send {blind}{Up}CapsLock & k::Send {blind}{Down}; Fast moveCapsLock & u::Send {blind}{Up 5}CapsLock & n::Send {blind}{Down 5}; Fast deleteCapsLock & Backspace::Send {blind}{Backspace 5}CapsLock & Delete::Send {blind}{Delete 5}; Delete wordsCapsLock & w::Send {blind}^{Backspace}CapsLock & e::Send {blind}^{Delete}

А вот что он делает

capsKeys keyboard layoutcapsKeys keyboard layout

Если в Vim сочетания клавиш опираются на семантику, то в этом скрипте я опирался на удобство.

Чтобы попробовать, вы можете:

или

Если еще не поняли, то рассказываю в чем суть: когда этот скрипт работает, ничего не меняется до тех пор, пока вы не зажмете клавишу CapsLock. С этого момента начинается магия. Какая? Рассказываю.

Особенности

Никакого больше переключения регистра.Строчные буквы по умолчанию! Это которые маленькие.

Кнопки перемещения находятся в области правой руки.Мы привыкли, что стрелочки справа, поэтому и кнопки навигации - справа.

  • Caps+J- влево

  • Caps+i- вверх

  • Caps+L- вправо

  • Caps+K- вниз

  • Caps+U- вверх пять раз

  • Caps+N- вниз пять раз

  • Caps+Y- страница вверх (PgUp)

  • Caps+B- Страница вниз (PgDn)

Зажатый капс работает как Ctrl в случае с символами X, C, V.Так просто удобнее!

Вызов контекстного меню.Caps + P- позволяет вызвать контекстное меню. Очень полезная комбинация.

Перемещаемся по словамЧерезCaps+LeftAltиCaps+Space. Одно из самых часто используемых мной сочетаний, особенно вместе с зажатым Shift. Однако здесь есть недостаток. Так как мы затрагиваем функциональную клавишу, это меняет поведение Alt. Например, мы хотим использовать сочетаниеAlt+Upв VSCode и делаем это конечно же через AHK. То естьCaps+Alt+Up, но вот проблема, как только мы зажимаем первые две клавиши, все идет к чертям. Что делать? Тут есть два варианта. Можно использовать правый Alt то естьCaps+RightAlt+Upили сначала нажимать Alt а потом остальную часть сочетания.

Удаляем эффективноВы знали, что сочетаниеCtrl+Backspace,Ctrl+Deleteудаляет целые слова вместо символов? Я тоже, до недавнего времени. В скрипте есть целая линия клавиш выделенная для удаления.

  • Caps+Q- удалить все символы слева от курсора

  • Caps+W- удалить слово слева от курсора

  • Caps+E- удалить слово справа от курсора

  • Caps+R- удалить все символы справа от курсора

Фишки

  • Caps+A- Скопировать текущую строку и вставить снизу

  • Caps+S- Выделить слово на позиции курсора

  • Caps+D- Скопировать строку и удалить. Украдено прямиком из вима.

Работает вездеРаботает как в вашем редакторе кода, так и в любом другом текстовом поле. Запомнил один раз - пользуйся везде.

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

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

VSCode

Когда дело доходит до выбора редактора кода, то тут все решают личные предпочтения. Однако есть редактор, который придется по вкусу практически каждому, и это Visual Studio Code.

VSCode - опенсорсное творение Microsoft, написанное при помощи фреймворков для JavaScript/TypeScript. Из коробки это простенький красивый редактор кода, который позволит вам в считанные секунды начать писать что вздумается. А если прикрутить к нему расширения, которые регулярно пишутся и поддерживаются огромным сообществом, то то эта малышка даст жару любой профессиональной IDE.

Навигация

Ctrl+Bоткрыть / закрыть боковую панель.

Ctrl+Shift+E- открыть меню навигации.

Ctrl+Shift+D- открыть меню запуска. Используется во время дебаггинга.

Ctrl+Shift+Fменю поиска. Позволяет искать текст по всем файлам.

Ctrl+Shift+H- открыть меню замены. Брат Ctrl+Shift+F, но с функцией замены.

Ctrl+Shift+G- открыть меню контроля версий. Если у вас установлено расширение GitLens, то оно может изменить это сочетание.

Ctrl+J- открыть/закрыть панель.

Ctrl+Shift+X- открыть меню расширений.

Ctrl+(backtick)- открыть терминал.

Ctrl+Shift+M- открыть панель ошибок.

Ctrl+Shift+U- открыть консоль вывода.

Ctrl+Shift+Y- открыть консоль отладки.

Простые сочетания

F1- открыть окошко команд. Наше все для пользователя VSCode. Почти любое действие можно выполнить с помощью этого окна.

F2переименовать определение / переменную / функцию синхронно. Одна из самых приятных возможностей в VSCode. Если вы передумали с названием переменной, это позволит изменить все места где используется эта переменная, учитывая контекст.

F8- переместиться к следующему проблемному месту в коде.

Ctrl+.- быстрое исправление ошибки. Если вы допустили какую-то распространенную ошибку, VSCode может исправить ее за вас. Очень удобно использовать в сочетании сF8.

F9- поставить точку остановки для дебаггера.

Ctrl+P- перейти к файлу.

Ctrl+R- открыть недавний проект / файл.

Ctrl+(,)- открыть настройки редактора.

Ctrl+Q- перейти в другую панель.

Ctrl+/- закомментировать строку.

Ctrl+T- переместиться к функции или переменной во всех файлах.

Ctrl+Shift+Oпереместиться к функции или переменной в текущем файле.

Ctrl+Home- переместиться к началу файла.

Ctrl+End- переместиться к концу файла.

Ctrl+Shift+\переместиться к соответствующей скобке.

Ctrl+Shift+N- открыть новое окно VSCode.

Alt+Left/Alt+Rightпереместиться к предыдущей / следующей активной строке. Когда вы скачете со строки на строку, VSCode запоминает это в своей истории, и вы можете быстро переключаться между самыми горячими местами в коде.

Alt+Up/Down- переместить строку вверх/вниз. Must have!

Alt- ускорить прокрутку страницы. Если начать прокручивать страницу и зажать Alt, то процесс пойдет немного быстрее.

Alt+Z- переключить режим переноса строк.

Shift+Alt+F- автоматическое форматирование текста. VSCode поддерживает автоматическое форматирование для большинства языков. Нажатие этой клавиши подгонит ваш код под стандарты, принятые в Code style вашего языка.

Shift+Alt+O- упорядочить импорты в соответствии со стандартами вашего языка.

Ctrl+C- скопировать текущую строку, если нет выделения, иначе работает как обычное копирование.

Ctrl+X- вырезать строку, если нет выделения, иначе работает как обычная операция Cut.

Ctrl+Shift+Up/Down- скопировать выделенные строки вверх/вниз. Полезно, если вам нужно повторить какой-то блок кода несколько раз.

Ctrl+Alt+Right- переместить активный файл в соседнюю группу редактирования. Это позволит вам параллельно просматривать два или более файлов.Ctrl+Alt+Leftдвигает его обратно.

Ctrl+0- фокус на боковую панель.

Ctrl+{1, 2, 3, ..}- фокус на первую/вторую/третью группу редакторов. Если вы параллельно просматриваете два или больше файлов, это сочетание поможет вам переключаться между ними. Однако по своему опыту скажу, что больше двух редакторов никто обычно не открывает. Поэтому у себя я поменял сочетание для работы с двумя окнами редактирования и двумя терминалами.

Alt+{1, 2,..., 8}- переключиться между активными вкладками в окне редактирования. Обязательно к запоминанию!Alt+0открывает последнюю вкладку.

Ctrl+Space- активировать автоподстановку.

Shift+Ctrl+Space- открыть подсказку для параметров функции.

Посложнее

Alt+Ctrl+Up/Down- добавить курсор на верхнюю/нижнюю строку. Одна из особенностей современных редакторов. Позволяет редактировать текст одновременно в нескольких местах. Суперфича!

Ctrl+D- добавить курсор в конец следующего вхождения данного слова.

Alt+Click- добавить курсор в позицию указателя мыши.

Shift+Alt+(Dragging)- добавлять курсоры по пути следования указателя мыши.

Shift+Alt+Right- выделение с учетом контекста. Например, у нас есть длинное выражение внутри скобок, данное сочетание позволит нам выбрать все, что находится внутри них. Последовательные нажатия расширяют область выделения.

Shift+Alt+Left- действие, противоположное Shift+Alt+Right.

Ctrl+Shift+[- свернуть блок кода. Если файл стал слишком большим, и перемещаться стало слишком сложно, то данное сочетание позволит свернуть блоки текста, которые вам сейчас не нужны.

Ctrl+Shift+]- развернуть блок кода. Противоположно Ctrl+Shift+[.

Следующее сочетание отсутствует в сборке для Windows, но я рекомендую установить его вручную. У меня этоCtrl+Shift+J.

Нет(Win) /Ctrl+J(Mac) - присоединить следующую строку к текущей. По сути, все, что делает данная команда, так это удаляет символ переноса с текущей строки. Очень удобно, если вдруг нужно сжать html файл или еще что-то.

Ctrl+Shift+L- выделить все одинаковые слова и переместить курсор к концу каждого. Сильная штука, позволяет редактировать все вхождения определенного слова. Но стоит быть аккуратнее, если это слово встречается в качестве подстроки, то оно тоже будет изменено.

Ctrl+K Z- Включить Zen Mode. Для настоящих гуру.

Демонстрация

Ну и на десерт, небольшое видео, где я наглядно показываю, как можно совместить использование скрипта capsKeys и возможности редактора VSCode. Чудес не обещаю, но суть должна быть понятна.

Заключение

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

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

Пишите в комментариях, если у вас есть свои любимые сочетания, не описанные в статье.

Также, если у вас есть идеи по поводу улучшения скрипта для AutoHotkey, буду рад видеть ваши pull request-ы настранице репозитория.

Изначальную идею скрипта я почерпнул из этой замечательнойстатьив блоге Никиты Прокопова.

Полезные ссылки

Подробнее..

Книга Работа с ядром Windows

06.04.2021 14:13:41 | Автор: admin
image Привет, Хаброжители! Ядро Windows таит в себе большую силу. Но как заставить ее работать? Павел Йосифович поможет вам справиться с этой сложной задачей: пояснения и примеры кода превратят концепции и сложные сценарии в пошаговые инструкции, доступные даже начинающим.

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

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

Глава 8


Уведомления потоков и процессов


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

В этой главе:

  • Уведомления процессов
  • Реализация уведомления процессов
  • Передача данных в пользовательский режим
  • Уведомления потоков
  • Уведомления о загрузке образов
  • Упражнения

Уведомления процессов


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

При создании процесса драйвер также получает возможность остановить создание процесса и вернуть ошибку стороне, инициировавшей создание процесса. Эта возможность доступна только в режиме ядра.
Windows предоставляет другие механизмы уведомления о создании или уничтожении процессов. Например, с механизмом ETW (Event Tracing for Windows) такие уведомления могут приниматься процессами пользовательского режима (работающими с повышенными привилегиями). Впрочем, предотвратить создание процесса при этом не удастся. Более того, у ETW существует внутренняя задержка уведомлений около 13 секунд (по причинам, связанным с быстродействием), так что процесс с коротким жизненным циклом может завершиться до получения уведомления. Если в этот момент будет сделана попытка открыть дескриптор для созданного процесса, произойдет ошибка.

Основная функция API для регистрации уведомлений процессов PsCreateSetProcessNotifyRoutineEx определяется так:

NTSTATUSPsSetCreateProcessNotifyRoutineEx (    _In_ PCREATE_PROCESS_NOTIFY_ROUTINE_EX NotifyRoutine,    _In_ BOOLEAN Remove);

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

В первом аргументе передается функция обратного вызова драйвера, прототип которой выглядит так:

typedef void(*PCREATE_PROCESS_NOTIFY_ROUTINE_EX) (    _Inout_ PEPROCESS Process,    _In_ HANDLE ProcessId,    _Inout_opt_ PPS_CREATE_NOTIFY_INFO CreateInfo);

Второй аргумент PsCreateSetProcessNotifyRoutineEx указывает, что делает драйвер регистрирует обратный вызов или отменяет его регистрацию (FALSE первое). Обычно драйвер вызывает эту функцию с аргументом FALSE в своей функции DriverEntry, а потом вызывает ту же функцию с аргументом TRUE в своей функции выгрузки.

Аргументы функции уведомления:

  • Process объект создаваемого или уничтожаемого процесса.
  • ProcessId уникальный идентификатор процесса. Хотя аргумент объявлен с типом HANDLE, на самом деле это идентификатор.
  • CreateInfo структура с подробной информацией о создаваемом процессе. Если процесс уничтожается, то этот аргумент равен NULL.

При создании процесса функция обратного вызова драйвера выполняется создающим потоком. При выходе из процесса функция обратного вызова выполняется последним потоком, выходящим из процесса. В обоих случаях обратный вызов вызывается в критической секции (с блокировкой нормальных APC-вызовов режима ядра).
В Windows 10 версии 1607 появилась другая функция для уведомлений процессов: PsCreateSetProcessNotifyRoutineEx2. Эта расширенная функция создает обратный вызов, сходный с предыдущим, но обратный вызов также активизируется для процессов Pico. Процессы Pico используются хост-процессами Linux для WSL (Windows Subsystem for Linux). Если драйвер заинтересован в таких процессах, он должен регистрироваться с расширенной функцией.

У драйвера, использующего эти обратные вызовы, должен быть установлен флаг IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY в заголовке PE (Portable Executable). Без установки флага вызов функции регистрации возвращает STATUS_ACCESS_DENIED (значение не имеет отношения к режиму тестовой подписи драйверов). В настоящее время Visual Studio не предоставляет пользовательского интерфейса для установки этого флага. Он должен задаваться в параметрах командной строки компоновщика ключом /integritycheck. На рис. 8.1 показаны свойства проекта при указании этого ключа.

image

Структура данных, предоставляемая для создания процесса, определяется следующим образом:

typedef struct _PS_CREATE_NOTIFY_INFO {   _In_ SIZE_T Size;   union {       _In_ ULONG Flags;       struct {            _In_ ULONG FileOpenNameAvailable : 1;            _In_ ULONG IsSubsystemProcess : 1;            _In_ ULONG Reserved : 30;        };     };     _In_ HANDLE ParentProcessId;     _In_ CLIENT_ID CreatingThreadId;     _Inout_ struct _FILE_OBJECT *FileObject;     _In_ PCUNICODE_STRING ImageFileName;     _In_opt_ PCUNICODE_STRING CommandLine;     _Inout_ NTSTATUS CreationStatus;} PS_CREATE_NOTIFY_INFO, *PPS_CREATE_NOTIFY_INFO;

Описание важнейших полей этой структуры:

  • CreatingThreadId комбинация идентификаторов потока и процесса, вызывающего функцию создания процесса.
  • ParentProcessId идентификатор родительского процесса (не дескриптор). Этот процесс может быть тем же, который предоставляется CreateThreadId.UniqueProcess, но может быть и другим, так как при создании процесса может быть передан другой родитель, от которого будут наследоваться некоторые свойства.
  • ImageFileName имя файла с исполняемым образом; доступен при установленном флаге FileOpenNameAvailable.
  • CommandLine полная командная строка, используемая для создания процесса. Учтите, что он может быть равен NULL.
  • IsSubsystemProcess этот флаг устанавливается, если процесс является процессом Pico. Это возможно только в том случае, если драйвер регистрируется PsCreateSetProcessNotifyRoutineEx2.
  • CreationStatus статус, который будет возвращен вызывающей стороне. Драйвер может остановить создание процесса, поместив в это поле статус ошибки (например, STATUS_ACCESS_DENIED).

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

Реализация уведомлений процессов


Чтобы продемонстрировать, как работают уведомления процессов, мы построим драйвер, который будет собирать информацию о создании и уничтожении процессов и предоставлять эту информацию для потребления клиентом пользовательского режима. Как и программа Process Monitor из пакета Sysinternals, он использует уведомления процессов (и потоков) для передачи информации об активности процессов (и потоков). В процессе реализации этого драйвера будут использованы некоторые средства, описанные в предыдущих главах.

Наш драйвер будет называться SysMon (хотя он никак не связан с программой SysMon из пакета Sysinternals). Он будет хранить всю информацию о создании/уничтожении в связном списке (с использованием структур LIST_ENTRY). Так как к связному списку могут одновременно обращаться несколько потоков, необходимо защитить его мьютексом или быстрым мьютексом; мы воспользуемся быстрым мьютексом, так как он более эффективен.

Собранные данные должны быть переданы в пользовательский режим, поэтому мы должны объявить стандартные структуры, которые будут строиться драйвером и получаться клиентом пользовательского режима. Мы добавим в проект драйвера стандартный заголовочный файл с именем SysMonCommon.h и определим несколько структур. Начнем со стандартного заголовка для всех информационных структур, который определяется следующим образом:

enum class ItemType : short {    None,    ProcessCreate,    ProcessExit};struct ItemHeader {    ItemType Type;    USHORT Size;    LARGE_INTEGER Time;};

Приведенное выше определение перечисления ItemType использует новую возможность C++ 11 перечисления с областью видимости (scoped enums). В таких перечислениях значения имеют область видимости (ItemType в данном случае). Также размер этих перечислений может быть отличен от int short в данном случае. Если вы работаете на C, используйте классические перечисления или даже #define.

Структура ItemHeader содержит информацию, общую для всех типов событий: тип события, время события (выраженное в виде 64-разрядного целого числа) и размер полезных данных. Размер важен, так как каждое событие имеет собственную информацию. Если позднее вы захотите упаковать массив таких событий и (допустим) предоставить его клиенту пользовательского режима, клиент должен знать, где заканчивается каждое событие и начинается новое.

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

struct ProcessExitInfo : ItemHeader {    ULONG ProcessId;};

Для события выхода из процесса существует только один интересный фрагмент информации (кроме заголовка) идентификатор завершаемого процесса.
Если вы работаете на C, наследование вам недоступно. Впрочем, его можно имитировать создайте первое поле типа ItemHeader, а затем добавьте конкретные поля; структура памяти остается одинаковой.

struct ExitProcessInfo {    ItemHeader Header;    ULONG ProcessId;};

Для идентификатора процесса используется тип ULONG. Использовать тип HANDLE не рекомендуется, так как в пользовательском режиме он может создать проблемы. Кроме того, тип DWORD не используется, хотя в заголовках пользовательского режима тип DWORD (32-разрядное целое без знака) встречается часто. В заголовках WDK тип DWORD не определен. И хотя определить его явно нетрудно, лучше использовать тип ULONG он означает то же самое, но определяется в заголовках как пользовательского режима, так и режима ядра.


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

В новом файле с именем SysMon.h определяется параметризованная структура, в которой хранится поле LIST_ENTRY с основной структурой данных:

template<typename T>struct FullItem {    LIST_ENTRY Entry;    T Data;};

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

struct FullProcessExitInfo {     LIST_ENTRY Entry;     ProcessExitInfo Data;};

Также возможно наследовать от LIST_ENTRY, а затем добавить структуру ProcessExitInfo. Но такое решение менее элегантно, так как наши данные не имеют никакого отношения к LIST_ENTRY, поэтому расширение искусственный прием, которого следует избегать.

Тип FullItem избавляет от хлопот с созданием этих отдельных типов.
Если вы используете C, то, естественно, решение с шаблонами будет недоступно, поэтому вам придется применить представленный структурный подход. Не буду снова упоминать C всегда существует обходное решение, которым можно воспользоваться в случае необходимости.

Заголовок связанного списка должен где-то храниться. Мы создадим структуру данных для хранения всего глобального состояния драйвера (вместо набора отдельных переменных). Определение структуры выглядит так:

struct Globals {    LIST_ENTRY ItemsHead;    int ItemCount;    FastMutex Mutex;};


В определении используется тип FastMutex, который был разработан в главе 6. Также в определении встречается RAII-обертка AutoLock на C++ (тоже из главы 6).

Функция DriverEntry


Функция DriverEntry для драйвера SysMon похожа на одноименную функцию драйвера Zero из главы 7. В нее нужно добавить регистрацию уведомлений процессов и инициализацию объекта Globals:

Globals g_Globals;extern "C" NTSTATUSDriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING) {    auto status = STATUS_SUCCESS;    InitializeListHead(&g_Globals.ItemsHead);    g_Globals.Mutex.Init();    PDEVICE_OBJECT DeviceObject = nullptr;    UNICODE_STRING symLink = RTL_CONSTANT_STRING(L"\\??\\sysmon");    bool symLinkCreated = false;    do {        UNICODE_STRING devName = RTL_CONSTANT_STRING(L"\\Device\\sysmon");        status = IoCreateDevice(DriverObject, 0, &devName,            FILE_DEVICE_UNKNOWN, 0, TRUE, &DeviceObject);        if (!NT_SUCCESS(status)) {            KdPrint((DRIVER_PREFIX "failed to create device (0x%08X)\n",               status));               break;          }           DeviceObject->Flags |= DO_DIRECT_IO;          status = IoCreateSymbolicLink(&symLink, &devName);          if (!NT_SUCCESS(status)) {              KdPrint((DRIVER_PREFIX "failed to create sym link (0x%08X)\n",                 status));              break;          }          symLinkCreated = true;          // Регистрация для уведомлений процессов          status = PsSetCreateProcessNotifyRoutineEx(OnProcessNotify, FALSE);          if (!NT_SUCCESS(status)) {              KdPrint((DRIVER_PREFIX "failed to register process callback\ (0x%08X)\n",              status));          break;         }    } while (false);    if (!NT_SUCCESS(status)) {        if (symLinkCreated)             IoDeleteSymbolicLink(&symLink);        if (DeviceObject)             IoDeleteDevice(DeviceObject);     }     DriverObject->DriverUnload = SysMonUnload;     DriverObject->MajorFunction[IRP_MJ_CREATE] =     DriverObject->MajorFunction[IRP_MJ_CLOSE] = SysMonCreateClose;     DriverObject->MajorFunction[IRP_MJ_READ] = SysMonRead;     return status;}

Функция диспетчеризации для чтения позднее будет использоваться для возвращения информации о событиях пользовательскому режиму.

Обработка уведомлений о выходе из процессов


В приведенном выше коде функция уведомления процессов называется OnProcessNotify, а ее прототип был представлен ранее в этой главе. Эта функция обратного вызова обрабатывает события создания и завершения процессов. Начнем с выхода из процессов, так как это событие намного проще создания процесса (как вы вскоре увидите). Общая схема функции обратного вызова выглядит так:

void OnProcessNotify(PEPROCESS Process, HANDLE ProcessId,    PPS_CREATE_NOTIFY_INFO CreateInfo) {    if (CreateInfo) {       // Создание процесса    }    else {      // Завершение процесса    }}

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

auto info = (FullItem<ProcessExitInfo>*)ExAllocatePoolWithTag(PagedPool,   sizeof(FullItem<ProcessExitInfo>), DRIVER_TAG);if (info == nullptr) {   KdPrint((DRIVER_PREFIX "failed allocation\n"));   return;}

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

Затем нужно заполнить общую информацию: время, тип и размер элемента. Получить все эти данные несложно:

auto& item = info->Data;KeQuerySystemTimePrecise(&item.Time);item.Type = ItemType::ProcessExit;item.ProcessId = HandleToULong(ProcessId);item.Size = sizeof(ProcessExitInfo);PushItem(&info->Entry);

Сначала мы обращаемся к самому элементу данных (в обход LIST_ENTRY) через переменную info. Затем заполняется информация заголовка: тип элемента хорошо известен, так как текущей является ветвь, обрабатывающая уведомления о завершении процессов; время можно получить при помощи функции KeQuerySystemTimePrecise, возвращающей текущее системное время (UTC, не местное время) в формате 64-разрядного целого числа, с отчетом от 1 января 1601 года. Наконец, размер элемента величина постоянная, равная размеру структуры данных, предоставляемой пользователю (а не размеру FullItem).
Функция API KeQuerySystemTimePrecise появилась в Windows 8. В более ранних версиях следует использовать функцию API KeQuerySystemTime.

Дополнительные данные при завершении процесса состоят из идентификатора процесса. В коде используется функция HandleToULong для корректного преобразования объекта HANDLE в 32-разрядное целое без знака.

А теперь остается добавить новый элемент в конец связного списка. Для этого мы определим функцию с именем PushItem:

void PushItem(LIST_ENTRY* entry) {    AutoLock<FastMutex> lock(g_Globals.Mutex);    if (g_Globals.ItemCount > 1024) {       // Слишком много элементов, удалить самый старый       auto head = RemoveHeadList(&g_Globals.ItemsHead);       g_Globals.ItemCount--;       auto item = CONTAINING_RECORD(head, FullItem<ItemHeader>, Entry);       ExFreePool(item);     }     InsertTailList(&g_Globals.ItemsHead, entry);     g_Globals.ItemCount++;}

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

Кроме того, драйвер ограничивает количество элементов связного списка. Такая предосторожность необходима, потому что ничто не гарантирует, что клиент будет быстро потреблять эти события. Драйвер не должен допускать неограниченное потребление данных, так как это может повредить системе в целом. Значение 1024 выбрано совершенно произвольно. Правильнее было бы читать это число из раздела драйвера в реестре.
Реализуйте это ограничение с чтением из реестра в DriverEntry. Подсказка: используйте такие функции API, как ZwOpenKey или IoOpenDeviceRegistryKey, а также ZwQueryValueKey.

Если счетчик элементов превысил максимальное значение, самый старый элемент удаляется; фактически связанный список рассматривается как очередь (RemoveHeadList). При освобождении элемента его память должна быть освобождена. Указателем на элемент не обязательно должен быть указатель, изначально использованный для выделения памяти (хотя в данном случае это так, потому что объект LIST_ENTRY стоит на первом месте в структуре FullItem<>), поэтому для получения начального адреса объекта FullItem<> используется макрос CONTAINING_RECORD. Теперь элемент можно освободить вызовом ExFreePool.

На рис. 8.2 изображена структура объектов FullItem.

image


Наконец, драйвер вызывает InsertTailList, чтобы добавить элемент в конец списка, а счетчик элементов увеличивается на 1.
Использовать атомарные операции инкремента/декремента в функции PushItem не обязательно, потому что операции со счетчиком элементов всегда выполняются под защитой быстрого мьютекса.

Обработка уведомлений о создании процессов


Обработка уведомлений о создании процессов создает больше проблем из-за непостоянного объема информации. Например, длина командной строки изменяется в зависимости от процесса. Сначала необходимо решить, какая информация должна сохраняться для создания процесса. Первая попытка:

struct ProcessCreateInfo : ItemHeader {    ULONG ProcessId;    ULONG ParentProcessId;    WCHAR CommandLine[1024];};

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

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

А можно ли использовать решение следующего вида:

struct ProcessCreateInfo : ItemHeader {    ULONG ProcessId;    ULONG ParentProcessId;    UNICODE_STRING CommandLine; // Будет работать?};

Нет, такое решение работать не будет. Во-первых, UNICODE_STRING обычно не определяется в заголовках пользовательского режима. Во-вторых (что намного хуже), внутренний указатель на символы обычно будет указывать в системное пространство, недоступное для пользовательского режима.

Ниже приведен другой вариант, который мы используем в драйвере:

struct ProcessCreateInfo : ItemHeader {     ULONG ProcessId;     ULONG ParentProcessId;     USHORT CommandLineLength;     USHORT CommandLineOffset;};

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

С таким объявлением можно приступить к построению реализации для создания процесса:

USHORT allocSize = sizeof(FullItem<ProcessCreateInfo>);USHORT commandLineSize = 0;if (CreateInfo->CommandLine) {    commandLineSize = CreateInfo->CommandLine->Length;    allocSize += commandLineSize;}auto info = (FullItem<ProcessCreateInfo>*)ExAllocatePoolWithTag(PagedPool,    allocSize, DRIVER_TAG);if (info == nullptr) {    KdPrint((DRIVER_PREFIX "failed allocation\n"));return;}

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

auto& item = info->Data;KeQuerySystemTimePrecise(&item.Time);item.Type = ItemType::ProcessCreate;item.Size = sizeof(ProcessCreateInfo) + commandLineSize;item.ProcessId = HandleToULong(ProcessId);item.ParentProcessId = HandleToULong(CreateInfo->ParentProcessId);

Размер элемента должен вычисляться с учетом базовой структуры и длины командной строки.

Затем необходимо скопировать командную строку по адресу за базовой структурой, а также обновить длину и смещение:

if (commandLineSize > 0) {    ::memcpy((UCHAR*)&item + sizeof(item), CreateInfo->CommandLine->Buffer,        commandLineSize);    item.CommandLineLength = commandLineSize / sizeof(WCHAR); // Длина в WCHAR    item.CommandLineOffset = sizeof(item);}else {    item.CommandLineLength = 0;}PushItem(&info->Entry);

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

Передача данных в пользовательский режим


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

Начнем обработку запроса чтения с получения адреса пользовательского буфера с применением прямого ввода/вывода (настраивается в DriverEntry):

NTSTATUS SysMonRead(PDEVICE_OBJECT, PIRP Irp) {    auto stack = IoGetCurrentIrpStackLocation(Irp);    auto len = stack->Parameters.Read.Length;    auto status = STATUS_SUCCESS;    auto count = 0;    NT_ASSERT(Irp->MdlAddress); // Используем прямой ввод/вывод    auto buffer = (UCHAR*)MmGetSystemAddressForMdlSafe(Irp->MdlAddress,        NormalPagePriority);     if (!buffer) {        status = STATUS_INSUFFICIENT_RESOURCES;}else {

Теперь необходимо обратиться к связанному списку и извлечь элементы из заголовка:

AutoLock lock(g_Globals.Mutex); // C++ 17while (true) {    if (IsListEmpty(&g_Globals.ItemsHead)) // также можно проверить                                       // g_Globals.ItemCount        break;    auto entry = RemoveHeadList(&g_Globals.ItemsHead);    auto info = CONTAINING_RECORD(entry, FullItem<ItemHeader>, Entry);    auto size = info->Data.Size;    if (len < size) {        // Пользовательский буфер заполнен, вставить элемент обратно        InsertHeadList(&g_Globals.ItemsHead, entry);        break;    }    g_Globals.ItemCount--;    ::memcpy(buffer, &info->Data, size);    len -= size;    buffer += size;    count += size;    // Освободить данные после копирования    ExFreePool(info);}

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

Наконец, запрос завершается с текущим статусом, а в поле Information сохраняется значение переменной count:

Irp->IoStatus.Status = status;Irp->IoStatus.Information = count;IoCompleteRequest(Irp, 0);return status;

К функции выгрузки также стоит присмотреться повнимательнее. Если в связном списке присутствуют элементы, они должны быть освобождены явно; в противном случае возникнет утечка ресурсов:

void SysMonUnload(PDRIVER_OBJECT DriverObject) {    // Отмена регистрации уведомлений процессов    PsSetCreateProcessNotifyRoutineEx(OnProcessNotify, TRUE);    UNICODE_STRING symLink = RTL_CONSTANT_STRING(L"\\??\\sysmon");    IoDeleteSymbolicLink(&symLink);    IoDeleteDevice(DriverObject->DeviceObject);    // Освобождение оставшихся элементов    while (!IsListEmpty(&g_Globals.ItemsHead)) {       auto entry = RemoveHeadList(&g_Globals.ItemsHead);       ExFreePool(CONTAINING_RECORD(entry, FullItem<ItemHeader>, Entry));}}

Клиент пользовательского режима


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

Функция main вызывает ReadFile в цикле с небольшой приостановкой, чтобы поток не потреблял ресурсы процессора постоянно. Поступившие данные отправляются для вывода:

int main() {    auto hFile = ::CreateFile(L"\\\\.\\SysMon", GENERIC_READ, 0,         nullptr, OPEN_EXISTING, 0, nullptr);    if (hFile == INVALID_HANDLE_VALUE)         return Error("Failed to open file");    BYTE buffer[1 << 16]; // 64-килобайтный буфер    while (true) {         DWORD bytes;         if (!::ReadFile(hFile, buffer, sizeof(buffer), &bytes, nullptr))            return Error("Failed to read");         if (bytes != 0)            DisplayInfo(buffer, bytes);         ::Sleep(200);     }}

Функция DisplayInfo должна разобраться в структуре полученного буфера. Так как все события начинаются с общего заголовка, функция различает события по значению ItemType. После того как событие будет обработано, поле Size в заголовке указывает, где начинается следующее событие:

void DisplayInfo(BYTE* buffer, DWORD size) {    auto count = size;    while (count > 0) {        auto header = (ItemHeader*)buffer;        switch (header->Type) {        case ItemType::ProcessExit:        {             DisplayTime(header->Time);             auto info = (ProcessExitInfo*)buffer;             printf("Process %d Exited\n", info->ProcessId);             break;         }         case ItemType::ProcessCreate:         {              DisplayTime(header->Time);              auto info = (ProcessCreateInfo*)buffer;              std::wstring commandline((WCHAR*)(buffer +                                           info->CommandLineOffset),                     info->CommandLineLength);               printf("Process %d Created. Command line: %ws\n",                                               info->ProcessId,                      commandline.c_str());                break;           }           default:               break;     }     buffer += header->Size;     count -= header->Size;   }}

Для правильного извлечения командной строки в коде используется конструктор класса C++ wstring, который может построить строку по указателю и длине строки. Вспомогательная функция DisplayTime форматирует время в виде, удобном для чтения:

void DisplayTime(const LARGE_INTEGER& time) {    SYSTEMTIME st;    ::FileTimeToSystemTime((FILETIME*)&time, &st);    printf("%02d:%02d:%02d.%03d: ",           st.wHour, st.wMinute, st.wSecond, st.wMilliseconds);}

Драйвер устанавливается и запускается так, как было описано в главе 4.

sc create sysmon type= kernel binPath= C:\Book\SysMon.syssc start sysmon

Пример вывода, полученного при запуске SysMonClient.exe:

C:\Book>SysMonClient.exe12:06:24.747: Process 13000 Exited12:06:31.032: Process 7484 Created. Command line: SysMonClient.exe12:06:42.461: Process 3128 Exited12:06:42.462: Process 7936 Exited12:06:42.474: Process 12320 Created. Command line: "C:\$WINDOWS.~BT\                                                    Sources\mighost.\exe" {5152EFE5-97CA-4DE6-BBD2-4F6ECE2ABD7A} /InitDoneEvent:MigHost.                                                    {5152EFE5-97CA-4D\E6-BBD2-4F6ECE2ABD7A}.Event /ParentPID:11908 /LogDir:"C:\$WINDOWS.~BT\Sources\                                                     Panthe\r"12:06:42.485: Process 12796 Created. Command line: \??\C:\WINDOWS\system32\                                                     conhost.e\xe 0xffffffff -ForceV112:07:09.575: Process 6784 Created. Command line: "C:\WINDOWS\system32\cmd.exe"12:07:09.590: Process 7248 Created. Command line: \??\C:\WINDOWS\system32\                                                     conhost.ex\e 0xffffffff -ForceV112:07:11.387: Process 7832 Exited12:07:12.034: Process 2112 Created. Command line: C:\WINDOWS\system32\                                                     ApplicationFra\meHost.exe -Embedding12:07:12.041: Process 5276 Created. Command line: "C:\Windows\SystemApps\                                                     Microsoft.M\icrosoftEdge_8wekyb3d8bbwe\MicrosoftEdge.exe" -ServerName:MicrosoftEdge.                                                     AppXdnhjhccw\3zf0j06tkg3jtqr00qdm0khc.mca12:07:12.624: Process 2076 Created. Command line: C:\WINDOWS\system32\                                                     DllHost.exe /P\rocessid:{7966B4D8-4FDC-4126-A10B-39A3209AD251}12:07:12.747: Process 7080 Created. Command line: C:\WINDOWS\system32\                                                     browser_broker\.exe -Embedding12:07:13.016: Process 8972 Created. Command line: C:\WINDOWS\System32\                                                        svchost.exe -k\LocalServiceNetworkRestricted12:07:13.435: Process 12964 Created. Command line: C:\WINDOWS\system32\                                                     DllHost.exe /\Processid:{973D20D7-562D-44B9-B70B-5A0F49CCDF3F}12:07:13.554: Process 11072 Created. Command line: C:\WINDOWS\system32\                                                     Windows.WARP.\JITService.exe 7f992973-8a6d-421d-b042-6afd93a19631S-1-15-2-3624051433-2125758914-1\423191267-1740899205-1073925389-3782572162-737981194S-1-5-21-4017881901-586210945-2\666946644-1001 51612:07:14.454: Process 12516 Created. Command line: C:\Windows\System32\RuntimeBroker.exe -Embedding12:07:14.914: Process 10424 Created. Command line: C:\WINDOWS\system32\                                                    MicrosoftEdge\SH.exe SCODEF:5276 CREDAT:9730 APH:1000000000000017 JITHOST /prefetch:212:07:14.980: Process 12536 Created. Command line: "C:\Windows\System32\                                                    MicrosoftEdg\eCP.exe" -ServerName:Windows.Internal.WebRuntime.ContentProcessServer12:07:17.741: Process 7828 Created. Command line: C:\WINDOWS\system32\                                                    SearchIndexer.\exe /Embedding12:07:19.171: Process 2076 Exited12:07:30.286: Process 3036 Created. Command line: "C:\Windows\System32\                                                    MicrosoftEdge\CP.exe" -ServerName:Windows.Internal.WebRuntime.ContentProcessServer12:07:31.657: Process 9536 Exited

Уведомления потоков


Ядро предоставляет обратные вызовы создания и уничтожения потоков, аналогичные обратным вызовам процессов. Для регистрации используется функция API PsSetCreateThreadNotifyRoutine, а для ее отмены другая функция, PsRemoveCreateThreadNotifyRoutine. В аргументах функции обратного вызова передается идентификатор процесса, идентификатор потока, а также флаг создания/уничтожения потока.

Расширим существующий драйвер SysMon, чтобы он получал не только уведомления процессов, но и уведомления потоков. Начнем с добавления значений перечисления и структуры, представляющей информацию, все это добавляется в заголовочный файл SysMonCommon.h:

enum class ItemType : short {    None,    ProcessCreate,    ProcessExit,    ThreadCreate,    ThreadExit};struct ThreadCreateExitInfo : ItemHeader {    ULONG ThreadId;    ULONG ProcessId;};

Затем можно добавить вызов регистрации в DriverEntry, непосредственно за вызовом регистрации уведомлений процессов:

status = PsSetCreateThreadNotifyRoutine(OnThreadNotify);if (!NT_SUCCESS(status)) {    KdPrint((DRIVER_PREFIX "failed to set thread callbacks (status=%08X)\n", status)\);   break;}

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

void OnThreadNotify(HANDLE ProcessId, HANDLE ThreadId, BOOLEAN Create) {   auto size = sizeof(FullItem<ThreadCreateExitInfo>);   auto info = (FullItem<ThreadCreateExitInfo>*)ExAllocatePoolWithTag(PagedPool,        size, DRIVER_TAG);    if (info == nullptr) {        KdPrint((DRIVER_PREFIX "Failed to allocate memory\n"));        return;    }    auto& item = info->Data;    KeQuerySystemTimePrecise(&item.Time);    item.Size = sizeof(item);    item.Type = Create ? ItemType::ThreadCreate : ItemType::ThreadExit;    item.ProcessId = HandleToULong(ProcessId);    item.ThreadId = HandleToULong(ThreadId);    PushItem(&info->Entry);}

Большая часть кода выглядит довольно знакомо.

Чтобы завершить реализацию, мы добавим в клиент код для вывода информации о создании и уничтожении потоков (в DisplayInfo):

case ItemType::ThreadCreate:{    DisplayTime(header->Time);    auto info = (ThreadCreateExitInfo*)buffer;    printf("Thread %d Created in process %d\n",         info->ThreadId, info->ProcessId);    break;}case ItemType::ThreadExit:{     DisplayTime(header->Time);     auto info = (ThreadCreateExitInfo*)buffer;     printf("Thread %d Exited from process %d\n",          info->ThreadId, info->ProcessId);     break;}

Пример вывода с обновленным драйвером и клиентом:

13:06:29.631: Thread 12180 Exited from process 1197613:06:29.885: Thread 13016 Exited from process 882013:06:29.955: Thread 12532 Exited from process 856013:06:30.218: Process 12164 Created. Command line: SysMonClient.exe13:06:30.219: Thread 12004 Created in process 1216413:06:30.607: Thread 12876 Created in process 10728...13:06:33.260: Thread 4524 Exited from process 448413:06:33.260: Thread 13072 Exited from process 448413:06:33.263: Thread 12388 Exited from process 448413:06:33.264: Process 4484 Exited13:06:33.264: Thread 4960 Exited from process 577613:06:33.264: Thread 12660 Exited from process 577613:06:33.265: Process 5776 Exited13:06:33.272: Process 2584 Created. Command line: "C:\$WINDOWS.~BT\Sources\                                                      mighost.e\xe" {CCD9805D-B15B-4550-94FB-B2AE544639BF} /InitDoneEvent:MigHost.                                                     {CCD9805D-B15B-455\0-94FB-B2AE544639BF}.Event /ParentPID:11908 /LogDir:"C:\$WINDOWS.~BT\Sources\                                                      Panther\"13:06:33.272: Thread 13272 Created in process 258413:06:33.280: Process 12120 Created. Command line: \??\C:\WINDOWS\system32\                                                               conhost.e\xe 0xffffffff -ForceV113:06:33.280: Thread 4200 Created in process 1212013:06:33.283: Thread 4400 Created in process 1212013:06:33.284: Thread 9632 Created in process 1212013:06:33.284: Thread 6064 Created in process 1212013:06:33.289: Thread 2472 Created in process 12120

Добавьте в клиент код вывода имени образа процесса при создании и завершении потока.

Уведомления о загрузке образов


Последний механизм обратного вызова, который будет рассмотрен в этой главе, уведомления о загрузке образов. Каждый раз, когда в системе загружается файл образа (EXE, DLL, драйвер), драйвер может получать уведомление.

Функция API PsSetLoadImageNotifyRoutine регистрируется для получения этих уведомлений, а функция PsRemoveImageNotifyRoutine отменяет регистрацию. Функция обратного вызова имеет следующий прототип:

typedef void (*PLOAD_IMAGE_NOTIFY_ROUTINE)(    _In_opt_ PUNICODE_STRING FullImageName,    _In_ HANDLE ProcessId, // pid, с которым связывается образ    _In_ PIMAGE_INFO ImageInfo);

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

Аргумент FullImageName не так прост. Как указывает аннотация SAL, он необязателен и может содержать NULL. Но даже если он отличен от NULL, он не всегда содержит точное имя файла образа.

Причины кроются глубоко в ядре и выходят за рамки книги. В большинстве случаев решение работает нормально, а путь использует внутренний формат NT, начинающийся с \Device\HadrdiskVolumex\ вместо c:\. Преобразование может быть выполнено разными способами. Тема более подробно рассматривается в главе 11.

Аргумент ProcessId содержит идентификатор процесса, в котором загружается образ. Для драйверов (образов режима ядра) это значение равно нулю.

Аргумент ImageInfo содержит дополнительную информацию об образе; его объявление выглядит так:

#define IMAGE_ADDRESSING_MODE_32BIT 3typedef struct _IMAGE_INFO {    union {      ULONG Properties;      struct {         ULONG ImageAddressingMode : 8; // Режим адресации         ULONG SystemModeImage : 1; // Образ системного режима         ULONG ImageMappedToAllPids : 1; // Образ отображается во все процессы         ULONG ExtendedInfoPresent : 1; // Доступна структура IMAGE_INFO_EX         ULONG MachineTypeMismatch : 1; // Несоответствие типа архитектуры         ULONG ImageSignatureLevel : 4; // Уровень цифровой подписи         ULONG ImageSignatureType : 3; // Тип цифровой подписи         ULONG ImagePartialMap : 1; // Не равно 0 при частичном                                                       отображении         ULONG Reserved : 12;     };   };   PVOID ImageBase;   ULONG ImageSelector;   SIZE_T ImageSize;   ULONG ImageSectionNumber;} IMAGE_INFO, *PIMAGE_INFO;

Краткая сводка важных полей структуры:

  • SystemModeImage флаг устанавливается для образа режима ядра и сбрасывается для образа пользовательского режима.
  • ImageSignatureLevel уровень цифровой подписи (Windows 8.1 и выше). См. описание констант SE_SIGNING_LEVEL_ в WDK.
  • ImageSignatureType тип сигнатуры (Windows 8.1 и выше). См. описание перечисления SE_IMAGE_SIGNATURE_TYPE в WDK.
  • ImageBase виртуальный адрес, по которому загружается образ.
  • ImageSize размер образа.
  • ExtendedInfoPresent если флаг установлен, IMAGE_INFO является частью большей структуры IMAGE_INFO_EX:

typedef struct _IMAGE_INFO_EX {    SIZE_T Size;    IMAGE_INFO ImageInfo;    struct _FILE_OBJECT *FileObject;} IMAGE_INFO_EX, *PIMAGE_INFO_EX;

Для обращения к большей структуре драйвер использует макрос CONTAINING_RECORD:

if (ImageInfo->ExtendedInfoPresent) {    auto exinfo = CONTAINING_RECORD(ImageInfo, IMAGE_INFO_EX, ImageInfo);    // Обращение к FileObject}

В расширенной структуре добавляется всего одно осмысленное поле объект файла, используемый для управления образом. Драйвер может добавить ссылку на объект (ObReferenceObject) и использовать его в других функциях по мере надобности.
Добавьте в драйвер SysMon уведомления о загрузке образов; драйвер должен собирать информацию только для образов пользовательского режима. Клиент должен выводить путь образа, идентификатор процесса и базовый адрес образа.

Упражнения


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

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

Итоги


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

Более подробно с книгой можно ознакомиться на сайте издательства
Оглавление
Отрывок

Для Хаброжителей скидка 25% по купону Windows

По факту оплаты бумажной версии книги на e-mail высылается электронная книга.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru