PowerShell: расширение оболочки на примере Windows Shell Libraries

в 15:04, , рубрики: .net, c++, powershell, WinAPI, windows, метки: , , , , ,

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

  • вспомним, что огромная часть Windows API реализована COM-объектами;
  • вспомним про C++/CLI, который поможет нам связать unmanaged- и manager-миры;
  • и наконец напишем расширение (.NET Framework 4.5, C#).

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

Введение

Когда-то Microsoft приступила к разработке новой командной оболочки, которая должна была заменить cmd.exe — оболочку, возможности которой, мягко говоря, никогда особо не впечатляли. Так появлился PowerShell. Сейчас на дворе 2013-й год, и у нас есть уже 4-я версия не очередного интерпретатора командной строки, но очень мощного, расширяемого средства автоматизации и администрирования.

Shell Libraries?

Windows 7 среди прочего привнёс в систему новшество — библиотеки. По замыслу разработчиков они должны были стать основным хранилищем пользовательских данных — документов, музыки, картинок, видео. Сами по себе библиотеки не хранят никаких файлов. Вместо этого они в одном месте показывают файлы, которые физически находятся в разных местах. Например, если часть музыки хранится в папке My Music, а часть — на внешнем жёстком диске, через библиотеку «Музыка» можно просмотреть всю коллекцию целиком.

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

Постановка задачи

Мы заглянем внутрь PowerShell, посмотрим, как он устроен, и на примере библиотек напишем небольшое расширение.

Почему библиотеки?

Во-первых, один из лучших способов научиться чему-то — как известно, научить этому другого. У меня давно на слуху всякие Shell Extensions, COM-составляющая Windows и пр., теперь пришла пора построить пару велосипедов и систематизировать знания.

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

В-третьих, удобных средств работы с библиотеками из командной строки нет. Или, по крайней мере, я плохо искал и не нашёл. То есть, конечно, есть утилиты, например, в тех же Shell SDK Samples: можно создавать библиотеки, переименовывать их, удалять, но самого главного они не умеют. Я хочу просматривать библиотеки и работать с файлами в них так же прозрачно как и в Проводнике. Я хочу делать вот так:

cd Libraries:Documents

Windows PowerShell
Copyright (C) 2012 Microsoft Corporation. All rights reserved.

C:WindowsSystem32WindowsPowerShellv1.0$ cd Libraries:Documents
Libraries:Documents$ ls

    Directory: Libraries:Documents

Mode                LastWriteTime     Length Name
----                -------------     ------ ----
d----        07.10.2013     17:51            Bootstrap
d----        14.08.2013     19:42            Custom Office Templates
d----        30.08.2013     12:36            OpenTTD
d----        24.09.2013     11:58            Outlook Files
d----        08.10.2013     18:28            vfw4
d----        29.04.2013     13:59            Visual Studio 2008
d----        05.08.2013     18:05            Visual Studio 2010
d----        09.10.2013     16:48            Visual Studio 2012
d----        11.10.2013     13:27            Visual Studio 2013
d----        17.10.2013     18:40            WindowsPowerShell
-a---        18.10.2013     16:29          0 1.txt
-a---        18.10.2013     16:29          0 10.txt
-a---        18.10.2013     16:29          0 2.txt
-a---        28.06.2013     16:54      18480 wix.png

Libraries:Documents$ new-item 3.txt -type file

    Directory: Libraries:Documents

Mode                LastWriteTime     Length Name
----                -------------     ------ ----
-a---        18.10.2013     19:32          0 3.txt

Видите? Мы как будто бы находимся в обычной папке. cmd.exe так не умеет.

В-четвёртых, просто так.

Лирическое отступление

Важно понимать, что возможности расширения PowerShell в большóй степени ограничиваются лишь вашей фантазией. Вы вспомнили, как в старых-добрых файловых навигаторах можно было «зайти» в архив и гулять в нём как в обычной папке? Пожалуйста:

PS> Get-Item D:Temparchive.zipFolderfile.xmlroot*
PS> Copy-Item D:Temparchive.zipfile.txt D:NewFolderfile.7zFolder
PS> Delete-Item D:Temparchive.7zFolder*.txt
PS> Move-Item D:Tempfile.txt D:TempexistingArchive.7zFolder
PS> Get-ChildItem D:Temparchive.7z

Взято отсюда.

Или можно «зайти» в sqlite базу данных, набрать dir (или ls, или Get-ChildItem, или… в общем, как больше нравится) и посмотреть список таблиц. Давайте посмотрим, сколько стоят наушники?

PS C:$ cd Market:Электроника и ФотоПортативная аудиотехникаНаушникиAKG
PS Market:Электроника и ФотоПортативная аудиотехникаНаушникиAKG$ ls

В общем… думаю, понятно.

Приступим!

Сердце объектной модели PowerShell — сборка System.Management.Automation.dll. Она не отображается в окне Reference Manager студии, там что искать её нам предстоит самостоятельно. Что, на самом деле, не совсем тривиально. У меня эта сборка лежит в %windir%Microsoft.NetassemblyGAC_MSILSystem.Management.Automationv4.0_3.0.0.0__31bf3856ad364e35. Обратите внимание, что с .NET Framework 4 GAC переехал в подпапку Microsoft.Net.

Прежде всего

Создаём проект типа Class Library и добавляем ссылку на вышеуказанную сборку. Также для удобства отладки сразу поменяем некоторые параметры. В окне Project Properties › Debug:

  • Start external program: C:WindowsSystem32WindowsPowerShellv1.0Powershell.exe
  • Command line arguments: -noexit -command "[Reflection.Assembly]::LoadFrom('LibrariesProvider.dll') | Import-Module"
также можно воспользоваться ConEmu:

  • Start external program: C:Program FilesConEmuConEmu64.exe
  • Command line arguments: /Single /cmd C:WindowsSystem32WindowsPowerShellv1.0PowerShell.exe -noexit -command "[Reflection.Assembly]::LoadFrom('PowerShell.Libraries.dll') | Import-Module"

В любом случае, в появившейся консоли наберите

[Appdomain]::CurrentDomain.GetAssemblies() | where { $_.FullName -match "LibrariesProvider" }

— если вы увидели вашу сборку, значит, всё нормально и можно продолжать.

NavigationCmdletProvider

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

PowerShell: расширение оболочки на примере Windows Shell Libraries

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

  • CmdletProvider — в конечном счёте этот класс является базовым для любых поставщиков. Как самый простой он даже не предоставляет возможности взаимодействовать с данными. Всё, что вы можете сделать, это узнать, когда ваш поставщик инициализировали и когда он завершает работу.
  • DriveCmdletProvider добавляет поддержку устройств, которые логически или физически делят данные на разделы. Например, ветки реестра HKCU и HKLM представлены PowerShell'ом двумя разными устройствами (или дисками, по аналогии с C:, D: и т.д.).
  • ItemCmdletProvider добавляет поддержку путей и и объектов. Данные-объекты можно получать, очищать, вызывать.
  • ContainerCmdletProvider добавляет поддержку объектов-контейнеров, то есть таких данных, которые сами содержат данные. Данные можно копировать, удалять и переименовывать. Важно одно обстоятельство: контейнеры не могут содержать другие контейнеры, только простые данные. Один уровень вложенности.
  • NavigationCmdletProvider — самый «навороченный» тип поставщиков, который предоставляет полную поддержку иерархии. Очевидный пример — файловая система, тут вам и папки (контейнеры), и подпапки («подконтейнеры»), и всё что угодно.

Поскольку библиотеки — это всё та же файловая система, думаю, очевидно, что нам нужен тип NavigationCmdletProvider.

Libraries:Documentsthatlongpathexiststoo$

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

[CmdletProvider("Libraries", ProviderCapabilities.None)] ①
public class LibrariesProvider : NavigationCmdletProvider
{
    protected override bool IsValidPath(string path) ②
    {
        return false;
    }

    protected override bool ItemExists(string path) ③
    {
        return true;
    }

    protected override bool IsItemContainer(string path) ④
    {
        return true;
    }

    protected override void GetChildItems(string path, bool recurse) ⑤
    {
            
    }

    protected override Collection<PSDriveInfo> InitializeDefaultDrives() ⑥
    {
        var driveInfos = new Collection<PSDriveInfo>();
        driveInfos.Add(new PSDriveInfo("Libraries", this.ProviderInfo, string.Empty, "Description", null));
        return driveInfos;
    }
}

CmdletProviderAttribute — это атрибут, без которого PowerShell не найдёт нашего поставщика. Мы указываем название поставщика и его возможности. Так, самой частой возможностью является ShouldProcess: многие команды (командлеты) поддерживают параметр -WhatIf, который отменяет непосредственный эффект (удаление, перемещение объекта и т.п.) и вместо этого делает вид, что эффект произошёл, то есть просто выводит строку вида

What if: Performing operation "Create directory" on Target "Destination: XXX".

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

Метод ⑤ позволяет вернуть содержимое контейнера. Пока что все наши подпапки будут пустыми.

Метод ⑥ позволяет на месте создать один или несколько устройств-дисков. Например, при подключении поставщика реальной файловой системы, можно сразу же создать устройства по количеству логических разделов на жёстких дисках. В нашем случае будет одно устройство (месторасположение «Библиотеки» единственно). А можно было бы насоздавать дисков по количеству библиотек (Music:, Video: и т.п.) — дело вкуса.

Убедимся, что приведённого кода достаточно; запускаем отладку и набираем в консоли:

PS C:LibrariesProviderbinDebug$ cd Libraries:
PS Libraries:$ cd Documents
PS Libraries:Documents$ dir ①
PS Libraries:Documents$ cd thatlongpathexiststoo
PS Libraries:Documentsthatlongpathexiststoo$ ②

① Действительно пусто.
② Работает.

Что дальше?

Дальше всё только интереснее.

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

Исходники предоставлю по первой просьбе.

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

Автор: withkittens

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js