Веб-сервер на power shell

Время на прочтение

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

Если объём корзины (C:\$Recycle.Bin) превышает 500МБ, то на панели появляется кнопка CR, она запускает скрипт для удаления из корзины файлов старше двух месяцев, таким образом пользователь не потеряет файл, недавно перемещённый в корзину.

Скрипт поддерживает работу с доменом и запрашивает имена пользователей у домена.

Также скрипт показывает информацию об устройствах и сетевых папках на удалённом ПК, кнопка Devices. Если устройство было подключено или отключено, то при следующем сканировании оно будет выделено цветом.

Запросы к удалённому ПК осуществляются командами Get-WmiObject, Invoke-Command и через доступ к папкам по SMB. Если скрипт не может получить информацию, то возможно доступ блокирует фаервол, либо на ПК отключены необходимые службы(WinRM), либо пользователь не добавлен в группу администраторы на удалённом ПК.

Скриншот 1
Веб-сервер на power shell
Скриншот 2
Веб-сервер на power shell
Вывод Devices с подсветкой подключенного устройства
Веб-сервер на power shell

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

Для сканирования удалённого ПК используется команда Invoke-Command, для её работы требуется работа службы WinRM на удалённом ПК, если она остановлена, то служба запускается на время работы скрипта.

Скриншот
Веб-сервер на power shell

Ссылка на GitHub

Если вы знаете программы с подобным функционалом, напишите о них в комментариях, особенно если они лучше, возможно я о них просто не знал и потратил время зря 🙂

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

RunOnce

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

[+] Сегодня в программе

Проблема

На самом деле слева не системный интерфейс, а магазинное приложение “Безопасность Windows”. Локализация интерфейса этих приложений определяется по первому языку в списке предпочитаемых языков. И он действительно был английским. После наводящих вопросов выяснилось, что выполнялась установка русской Windows 11.

Тут я вспомнил, как в обсуждении моего простого файла ответов читатель сетовал на ту же проблему – после установки первым в списке идет английский. Язык ОС он не сообщил, но предположим тоже русский.

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

Возможно, проблема возникает при каком-то сочетании региональных параметров файла ответов и языка дистрибутива. Кроме того, разработчики вносят изменения в языковые аспекты установки ОС. Про нововведения в 20H1 я рассказывал. А сейчас в документации DISM для параметра /set-uilang добавилось примечание. Начиная с Windows 11 заданный язык установки ОС не переопределяется пользователем при выборе языка на этапе OOBE (за исключением домашних изданий).

Задача

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

В формулировке кроются два нюанса:

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

Решение

Параметры только для первого пользователя

В общем случае настраивается так:

#список языков (русский первый)
Set-WinUserLanguageList -LanguageList ru-RU, en-US -Force
#английский метод ввода по умолчанию
Set-WinDefaultInputMethodOverride -InputTip 0409:00000409

Эти команды можно выполнить из FirstLogonCommands файла ответов.

<RunSynchronous> <RunSynchronousCommand wcm:action="add"> <Path>powershell -noprofile -command Set-WinUserLanguageList -LanguageList ru-RU, en-US -Force</Path> <Order>15</Order> <Description>LangList</Description> </RunSynchronousCommand> <RunSynchronousCommand wcm:action="add"> <Path>powershell -noprofile -command Set-WinDefaultInputMethodOverride -InputTip "0409:00000409"</Path> <Order>20</Order> <Description>DefaultInputLang</Description> </RunSynchronousCommand>
</RunSynchronous>

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

Параметры для всех пользователей

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

Можно настроить однократное выполнение команд из раздела реестра RunOnce или Run!

Настройка пользовательских региональных параметров не требует прав администратора. Поэтому при первом входе каждого нового пользователя в систему можно однократно выполнить команды PowerShell из реестра. Формально способ RunOnce не поддерживается для обычного пользователя, но работает, что я разбирал в канале Telegram.

  1. В раздел реестра RunOnce или Run профиля Default прописываются команды.
  2. При первом входе каждого нового пользователя они выполняются и…
  3. У этого пользователя при использовании:
    • RunOnce — команды удаляются автоматически
    • Run — необходимо удалить их самостоятельно отдельной командой

В файле ответов импорт тех же команд PowerShell в раздел RunOnce выглядят так:

<RunSynchronous> <RunSynchronousCommand wcm:action="add"> <Path>reg load HKEY_USERS\Custom %systemdrive%\Users\Default\NTUSER.DAT</Path> <Order>10</Order> <Description>Load NTUSER.DAT</Description> </RunSynchronousCommand> <RunSynchronousCommand wcm:action="add"> <Path>reg add HKEY_USERS\Custom\Software\Microsoft\Windows\CurrentVersion\RunOnce /v LangList /t REG_SZ /d "powershell -noprofile -command Set-WinUserLanguageList -LanguageList ru-RU, en-US -Force" /f</Path> <Order>15</Order> <Description>LangList</Description> </RunSynchronousCommand> <RunSynchronousCommand wcm:action="add"> <Path>reg add HKEY_USERS\Custom\Software\Microsoft\Windows\CurrentVersion\RunOnce /v LangList /t REG_SZ /d "powershell -noprofile -command Set-WinDefaultInputMethodOverride -InputTip \"0409:00000409\"" /f</Path> <Order>20</Order> <Description>DefaultInputLang</Description> </RunSynchronousCommand> <RunSynchronousCommand wcm:action="add"> <Path>reg unload HKEY_USERS\Custom</Path> <Order>30</Order> <Description>Unload NTUSER.DAT</Description> </RunSynchronousCommand>
</RunSynchronous>

Напомню, что эти команды должны выполняться на этапе specialize.

Бонус: настройка пользовательского профиля с помощью Active Setup

Есть еще один древний, но эффективный и в некоторых случаях необходимый способ — Active Setup! Я написал о нем в каналe Telegram.

Заключение

Конечно, ничего нового тут нет. Но даже очень древние способы могут быть эффективны в новой обертке. Более того, этот метод (за вычетом файла ответов) до сих пор использует Microsoft для установки клиента OneDrive даже в Windows 11. Там задействован раздел Run нежели RunOnce. Но очевидно установщик OneDrive в конце просто подчищает за собой команду в HKCU, которая его запускает.

To search by value you’ll have to get all keys/subkeys in your specified path first. Once you get the keys you’ll have to go through each one and get the key values and data. The whole key (and subkeys) well be removed if any of it’s value data contain the specified search term.

$searchRegKeyValue = "*test123*"
$myPath = "REGISTRY::HKCR\"
# $mypath = "REGISTRY::HKLM:\Software\"
$childItems = Get-ChildItem -Path $myPath -Recurse -ErrorAction SilentlyContinue
# go through each key in the reg path specified
foreach ($key in $childItems){ # for each value of the key get it's value and compare against the search term foreach ($name in $key.GetValueNames()){ if ($key.GetValue($name) -like $searchRegKeyValue){ # Get-Item -Path $key.pspath Remove-Item -Path $key.pspath -Recurse -WhatIf } }
}

The above solution works well if there are not a lot of keys/subkeys to go through but can get slow especially if you specify a top level path ("REGISTRY::HKCR\" vs "REGISTRY::HKCR\CLSID").

I found a really interesting solution to speed up registry search in powershell here. So I modified the solution a bit to work with this use case. The speed up using this solution is more noticeable with a larger search set. I saw 50%+ speedup on the larger search set but on smaller it was closer. For my orginal solution it took 293 seconds to search HKCR:\ while using the solution linked it took 101 seconds.
enter image description here

Bellow is from the linked solution modified to remove the keys that have values that match $search. I had to change some things around in the search to account for an empty $subKey to search from root level but it’s mostly the same other than that.

# [email protected]# reference: https://msdn.microsoft.com/de-de/vstudio/ms724875(v=vs.80)
cls
remove-variable * -ea 0
$ErrorActionPreference = "stop"
$signature = @'
[DllImport("advapi32.dll")]
public static extern Int32 RegOpenKeyEx( UInt32 hkey, StringBuilder lpSubKey, int ulOptions, int samDesired, out IntPtr phkResult );
[DllImport("advapi32.dll")]
public static extern Int32 RegQueryInfoKey( IntPtr hKey, StringBuilder lpClass, Int32 lpCls, Int32 spare, out int subkeys, out int skLen, int mcLen, out int values, out int vNLen, out int mvLen, int secDesc, out System.Runtime.InteropServices.ComTypes.FILETIME lpftLastWriteTime
);
[DllImport("advapi32.dll", CharSet = CharSet.Unicode)]
public static extern Int32 RegEnumValue( IntPtr hKey, int dwIndex, IntPtr lpValueName, ref IntPtr lpcchValueName, IntPtr lpReserved, out IntPtr lpType, IntPtr lpData, ref int lpcbData
);
[DllImport("advapi32.dll", CharSet = CharSet.Unicode)]
public static extern Int32 RegEnumKeyEx( IntPtr hKey, int dwIndex, IntPtr lpName, ref int lpcName, IntPtr lpReserved, IntPtr lpClass, int lpcClass, out System.Runtime.InteropServices.ComTypes.FILETIME lpftLastWriteTime
);
[DllImport("advapi32.dll")]
public static extern Int32 RegCloseKey(IntPtr hkey);
'@
$reg = add-type $signature -Name reg -Using System.Text -PassThru
$marshal = [System.Runtime.InteropServices.Marshal]
function search-RegistryTree($path) { if(($path -ne "") -and !($path.EndsWith("\"))){ $path += "\" } # open the key: [IntPtr]$hkey = 0 $result = $reg::RegOpenKeyEx($global:hive, $path, 0, 25 ,[ref]$hkey) if ($result -eq 0) { # get details of the key: $subKeyCount = 0 $maxSubKeyLen = 0 $valueCount = 0 $maxNameLen = 0 $maxValueLen = 0 $time = $global:time $result = $reg::RegQueryInfoKey($hkey,$null,0,0,[ref]$subKeyCount,[ref]$maxSubKeyLen,0,[ref]$valueCount,[ref]$maxNameLen,[ref]$maxValueLen,0,[ref]$time) if ($result -eq 0) { $maxSubkeyLen += $maxSubkeyLen+1 $maxNameLen += $maxNameLen +1 $maxValueLen += $maxValueLen +1 } # enumerate the values: if ($valueCount -gt 0) { $type = [IntPtr]0 $pName = $marshal::AllocHGlobal($maxNameLen) $pValue = $marshal::AllocHGlobal($maxValueLen) foreach ($index in 0..($valueCount-1)) { $nameLen = $maxNameLen $valueLen = $maxValueLen $result = $reg::RegEnumValue($hkey, $index, $pName, [ref]$nameLen, 0, [ref]$type, $pValue, [ref]$valueLen) if ($result -eq 0) { $name = $marshal::PtrToStringUni($pName) $value = switch ($type) { 1 {$marshal::PtrToStringUni($pValue)} 2 {$marshal::PtrToStringUni($pValue)} 3 {$b = [byte[]]::new($valueLen) $marshal::Copy($pValue,$b,0,$valueLen) if ($b[1] -eq 0 -and $b[-1] -eq 0 -and $b[0] -ne 0) { [System.Text.Encoding]::Unicode.GetString($b) } else { [System.Text.Encoding]::UTF8.GetString($b)} } 4 {$marshal::ReadInt32($pValue)} 7 {$b = [byte[]]::new($valueLen) $marshal::Copy($pValue,$b,0,$valueLen) $msz = [System.Text.Encoding]::Unicode.GetString($b) $msz.TrimEnd(0).split(0)} 11 {$marshal::ReadInt64($pValue)} } # if ($name -match $global:search) { # write-host "$path\$name : $value" # $global:hits++ # } elseif ($value -match $global:search) { # write-host "$path\$name : $value" # $global:hits++ # } # only find keys based on matched value data if ($value -match $global:search) { write-host "$path$name : $value" $item = "$path : $value" $keyList.Add($item) $global:hits++ } } } $marshal::FreeHGlobal($pName) $marshal::FreeHGlobal($pValue) } # enumerate the subkeys: if ($subkeyCount -gt 0) { $subKeyList = @() $pName = $marshal::AllocHGlobal($maxSubkeyLen) $subkeyList = foreach ($index in 0..($subkeyCount-1)) { $nameLen = $maxSubkeyLen $result = $reg::RegEnumKeyEx($hkey, $index, $pName, [ref]$nameLen,0,0,0, [ref]$time) if ($result -eq 0) { $marshal::PtrToStringUni($pName) } } $marshal::FreeHGlobal($pName) } # close: $result = $reg::RegCloseKey($hkey) # get Tree-Size from each subkey: $subKeyValueCount = 0 if ($subkeyCount -gt 0) { foreach ($subkey in $subkeyList) { if (!($subkey.EndsWith("\"))){ $subKeyValueCount += search-RegistryTree "$path$subkey\" } else{ $subKeyValueCount += search-RegistryTree "$path$subkey" } } } return ($valueCount+$subKeyValueCount) }
}
function remove-keys ($keyList){ Write-Output "The following KEYS will be removed: " foreach ($key in $keyList){ $value = $key.split(":")[1] $key = $key.split(":")[0].TrimEnd() Get-Item -Path "$root\$key" } foreach ($key in $keyList){ $value = $key.split(":")[1] $key = $key.split(":")[0].TrimEnd() # Get-Item -Path "$root\$key" Remove-Item -Path "$root\$key" -Recurse -WhatIf }
}
$timer = [System.Diagnostics.Stopwatch]::new()
$timer.Start()
# setting global variables:
$search = "test123"
#needs to change depending on the location you are searching
# $hive = [uint32]"0x80000002" #HKLM
$hive = [uint32]"0x80000000" #HKCR
# $root = "REGISTRY::HKEY_LOCAL_MACHINE"
$root = "REGISTRY::HKEY_CLASSES_ROOT"
# to search from the root level of the hive you selected
$subkey = ""
# $subkey = "CLSID"
$time = New-Object System.Runtime.InteropServices.ComTypes.FILETIME
$hits = 0
$keyList = [System.Collections.Generic.List[string]]::new()
write-host "We start searching for pattern '$search' in Registry-Path '$subkey' ...`n"
$count = search-RegistryTree $subkey
Write-Output $values
$timer.stop()
$sec = [int](100 * $timer.Elapsed.TotalSeconds)/100
write-host "`nWe checked $count reg-values in $sec seconds. Number of hits = $hits."
remove-keys $keyList

In both solutions you’ll need to remove -whatIf when using remove-item to actually remove the item. I just left it here for safety. It’ll remove the key and any subkeys of that key if there is a match. I’d recommend testing with your search term first before removing -whatIf to make sure it matching with what you want to remove.

:/>  windows - Full screen through batch command? - Stack Overflow

Время на прочтение

Все привет! В продолжение статьи о возможностях PowerShell, хочу поделиться несложной реализацией создания REST API и простого Web-сервера, используя только PowerShell на базе класса .NET HttpListener. Такая реализация позволяет настроить endpoint-ы (конечные точки) для обработки GET и POST запросов, которые принимают параметры в заголовке запроса, использовать Basic-авторизацию (на основе Base64), обрабатывать любые коды возврата, а так же без лишнего кода, отдавать информацию в разных форматах: json, xml, html, csv. Хочу акцентировать, что данное решение, возможно, не является самым правильным, тем не менее успешно помогло мне покрыть потребности нескольких задач и хотел лишний раз продемонстрировать возможности языка.

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

Кому и зачем данная реализация может понадобиться? Приведу свой пример, у меня была задача удаленно реализовать доступ к десктопному приложению, у которого для специфического взаимодействия с ним была возможность выполнять только локальные команды через консоль. Из условий, не было возможности настроить и использовать на машинах WinRM и OpenSSH, ввиду ограничений в компании со стороны ИБ, в то же самое время HTTP был валидным и стандартизированным (после HTTPS) решением. В результате, выполнение и вывод команд получилось автоматизировать, а в дополнение к этому, добавить настройки реестра, чистку temp и логов, что расширило возможности и позволило инженеру DevOps внедрить их в свой Pipeline, используя привычный интерфейс.

Во-вторых, работая системным администратором, в качестве интерфейса для автоматизации задач я использовал WinForms, редко Telegram. Тогда мне очень хотелось попробовать реализовать свой Web-интерфейс для визуализации подобных задач. Важно заметить, что не обладаю сильными познаниями в области систем CI/CD, и конечно, рациональнее использовать, например, интерфейс Jenkins для подобных целей. Тем не менее подобное решение имеет место, т.к. Jenkins все таки не содержит такой кастомизации, как собственный интерфейс.

В-третьих. У меня есть небольшой проект, целью которого является поиск и доставка контента из конкретного torrent-трекера Кинозал до телевизора с Plex (на эту тему у меня есть отдельная статья на Habr). Так сложилось, что за основу я выбрать язык Bash, т.к. планировал запускать бота удаленно и использовать только REST API интерфейс для взаимодействия со всеми сервисами. В течении всего времени эксплуатации, мне не хватало такого функционала, как остановка и повторный запустить torrent-клиента (qBittorrent), или просматривать свободное место на диске, узнать размер конкретных директорий и файлов, а так же возможности их удаления. По аналогии с другими сервисами, мне хотелось использовать единый интерфейс (REST API), в попытках найти готовое решение в виде десктопного приложения для Windows, вспоминая, что уже взаимодействовал с Open Hardware Monitor, используя его как клиент (в режиме HTTP), уже писал модуль для получения метрик через REST API (с возможностью отправки их в InfluxDB и визуализацией в Grafana). Но этого было мало, например для просмотра и удаления файлов можно настроить сервер Everything (который тоже имеет HTTP-сервер). Тут я понял, для покрытия нескольких специфических и не сложных задачи устанавливать дополнительно 2-3 сервиса нерационально, по этому решил написать отдельное решение.

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

Как это выглядит на практике. Для установки и запуска данного сервиса я попробовал реализовать два решения. Запуск в виде исполняемого файла (используя модуль ps2exe), в таком варианте можно запускается отдельная консоль, где можно наблюдать весь лог и завершать процесс при закрытии консоли. Второй вариант, это запуск фонового процесса, в таком случае для чтения лога используется файл. Но такое решение не самое удачное, т.к. модуль имеет ограничение, которое позволяет запускать любой скрипт только в PowerShell 5.1 и командлеты из версии Core попросту не буду работать.

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

:/>  How to Run Command Prompt as an Administrator
Пример удаленной остановки используя PowerShell и запуска службы из Linux (последним запросом я проверил статус службы самого WinAPI).
Пример удаленной остановки используя PowerShell и запуска службы из Linux (последним запросом я проверил статус службы самого WinAPI).
lifailon@hv-devops-01:~$ user="rest"
lifailon@hv-devops-01:~$ pass="api"
lifailon@hv-devops-01:~$ curl -s -X GET -u $user:$pass http://192.168.3.100:8443/api/service/winrm # запрашиваем статус службы WinRM
{ "Name": "WinRM", "DisplayName": "Служба удаленного управления Windows (WS-Management)", "Status": "Stopped", "StartType": "Automatic"
}
lifailon@hv-devops-01:~$ curl -s -X POST -u $user:$pass --data '' http://192.168.3.100:8443/api/service/winrm -H "Status: Start" # запускаем службу
{ "Name": "winrm", "DisplayName": "Служба удаленного управления Windows (WS-Management)", "Status": "Running", "StartType": "Automatic"
}

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

Список служб с отображением в формате HTML и возможностью их остановки и запуска.
Список служб с отображением в формате HTML и возможностью их остановки и запуска.

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

# Типовое условие для проверки вхождения на соответствие метода (GET) и конечной точки (/service)
elseif ($context.Request.HttpMethod -eq "GET" -and $context.Request.RawUrl -eq "/service") { # Получаем массив из списока служб, используя кастомную функцию для вывода с подробной информацией $Services = Get-ServiceDescription * # Формируем текст HTML-документа, задаем заголовок страницы и открываем тело страницы $GetService = "<html><head><title>Service</title></head><body>" # Добавляем заготовленные кнопки, которые перенаправляет на другие url $GetService += $BodyButtons # Указываем на создание таблицы и задаем имена столбцов $GetService += "<table border='1'>" $GetService += "<tr><th>Name</th><th>Status</th><th>Action</th><th>Start Type</th></tr>" # Передаем в цикл список служб и забираем значения foreach ($Service in $Services) { $name = "<b>$($Service.Name)</b>" $status = $Service.Status # Проверяем статус службы, если работает, красим в зеленый цвет if ($status -eq "Running") { $status = "<font color='green'><b>$status</b></font>" } else { $status = "<font color='red'><b>$status</b></font>" } $StartType = $Service.StartType # Заполняем значения столбцов, по анологии с наименованием столбцов (в блоке <tr>) $GetService += "<tr><td>$name</td><td>$status</td>" # Создаем кпноки, которые при нажатии ссылаются на функции startService и stopService, которые в качестве параметра передают наименование службы $GetService += "<td><button onclick='startService(""$($Service.Name)"")'>Start</button> " $GetService += "<button onclick='stopService(""$($Service.Name)"")'>Stop</button></td>" $GetService += "<td>$StartType</td></tr>" } $GetService += "</table>" $GetService += ' # Формируем в блоке <script> функции, для обработки нажатия на кнопки <script> function startService(serviceName) { sendServiceAction("Start", serviceName); } function stopService(serviceName) { sendServiceAction("Stop", serviceName); } # Данная функция принимает действие и отправляет соответствующий POST-запрос, для его обработки другой конечной точкой function sendServiceAction(action, serviceName) { var request = new XMLHttpRequest(); request.open("POST", "/api/service/" + serviceName, true); # В заголовок запроса передаем статус с содержимым действия (Status: <Stop/Start>) и обновляем страницу (reload) request.setRequestHeader("Status", action); request.onreadystatechange = function () { if (request.readyState === 4 && request.status === 200) { console.log("True"); location.reload(); } }; request.send(); } </script> </body></html> ' # Передаем сформированные данные и код ответа в функцию, для отправки ответва клиенту Send-Response -Data $GetService -Code 200 -v2
}

Для сравнения интерфейса, приведу пример управления службами, используя простой Jenkins Pipeline. Из явных преимуществ, такой интерфейс универсален для обеих систем (Windows и Linux), логика преимущественно на PowerShell и Bash (в моем случае), а доступ настраивается централизованно через Ansible, где в свою очередь используя ssh и winrm. Такой доступ можно заменить на REST-запросы, при наличии подобного сервера на каждой удаленной машине (например, в виде установленной службы). Безусловно, это более современное и правильное решение, но не взаимозаменяемое, речь только про интерфейс взаимодействия, где мы можем в одном интерфейсе управлять сразу с несколькими машинами.

Jenkins Pipeline для запуска и остановки служб
Jenkins Pipeline для запуска и остановки служб

По аналогии со службами, обработал остановку и запуск процессов.

Список процессов с возможностью их завершения и запуска по имени.
Список процессов с возможностью их завершения и запуска по имени.

Из интересного на мой взгляд, написал простую функцию для поиска исполняемого файла в системе, который отвечает за запуск процесса конкретного приложения. Если такой процесс не получается найти, то мы получим в ответ код 400: Bad Request. Process <$ProcessName> could not be found. В таком случае, можно воспользоваться заголовком Path, который принимает путь до исполняемого файла.

function Find-Process { param ( $ProcessName ) $ProcessPath = (Get-ChildItem "C:\Program Files" | Where-Object Name -match $ProcessName).FullName if ($null -eq $ProcessPath) { $ProcessPath = (Get-ChildItem "C:\Program Files (x86)" | Where-Object Name -match $ProcessName).FullName } if ($null -eq $ProcessPath) { $ProcessPath = (Get-ChildItem "$home\AppData\Roaming" | Where-Object Name -match $ProcessName).FullName } $ProcessNameExec = "$ProcessName"+".exe" (Get-ChildItem $ProcessPath -Recurse | Where-Object Name -eq $ProcessNameExec).FullName
}
> Find-Process qbittorrent
C:\Program Files\qBittorrent\qbittorrent.exe
> Find-Process nmap
C:\Program Files (x86)\Nmap\nmap.exe
Find-Process telegram
C:\Users\lifailon\AppData\Roaming\Telegram Desktop\Telegram.exe

Для сбора метрик используется CIM (Common Information Model). Сам скрипт сервера, описание с примерами, как и набор функций опубликованы на GitHub.

Получение информации о системе через CIM.
Получение информации о системе через CIM.

Так как PowerShell Core является кросс-платформенным решением, class System.Net.HttpListener работает и в системе Linux, используя такую же логику и возможности сразу нескольких языков (например, Bash), можно управлять службами на платформе Windows через systemctl используя REST API.

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

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

# Заполняем переменны с номером порта и данными для авторизации
$port = 8443
$user = "rest"
$pass = "api"
# Формируем строку Base64 из данных логина и пароля
$cred = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes("${user}:${pass}"))
# Функция для логирования запросов
function Get-Log { ### Debug (Get all Request, Headers and Response parameters):	# Используя содержимое запросов (Request), чтение передаваемых заголовков и ответа (Response), можно расширить возможности логирования для отладки процесса # $context.Request | Out-Default # foreach ($header in $context.Request.Headers) { # Write-Host "$header = $($context.Request.Headers[$header])" # } # $context.Response | Out-Default	# Забираем содержимое из запроса: адрес клиента, наименование агента, метод и url конечной точки $remote_host = $context.Request.RemoteEndPoint $client_agent = $context.Request.UserAgent $method = $context.Request.HttpMethod $endpoint = $context.Request.RawUrl $response_code = $context.Response.StatusCode $date = Get-Date -Format "dd.MM.yyyy hh:mm:ss"	# Выводим в консоль или в файл "$date $remote_host $client_agent => $method $endpoint => $response_code" # "$date $remote_host $client_agent => $method $endpoint => $response_code" | Out-File $Log_Path -Encoding utf8 -Append
}
# Функция для ответа клиенту
function Send-Response { param ( $Data, [int]$Code ) # Проверяем код ответа, если он равен 200 (успех), то конвертируем данные перед отправкой клиенту if ($Code -eq 200) { # Дополнительно можем проверить название агента на клиентской стороне, который может выступать в роли браузера или явно задан тип данных HTML if (($context.Request.UserAgent -match "Chrome") -or ($context.Request.ContentType -match "html")) { # Конвертируем полученные данные в HTML и указываем тип контента в ответе	$Data = $Data | ConvertTo-Html $context.Response.ContentType = "text/html; charset=utf-8" }	# Далее проверяем только тип контента из заголовка (если он задан явным образом), и конвертируем вывод в соответствующий тип данных elseif ($context.Request.ContentType -match "xml") { $Data = ($Data | ConvertTo-Xml).OuterXml $context.Response.ContentType = "text/xml; charset=utf-8" } elseif ($context.Request.ContentType -match "csv") { $Data = $Data | ConvertTo-Csv $context.Response.ContentType = "text/csv; charset=utf-8" }	# По умолчанию, конвертируем в JSON else { $Data = $Data | ConvertTo-Json $context.Response.ContentType = "text/json; charset=utf-8" } } # Указываем код статуса для ответа $context.Response.StatusCode = $Code # Преобразуем данные в массив байтов, используя кодировку UTF-8 (особенно важно, при передачи в формате HTML) $buffer = [System.Text.Encoding]::UTF8.GetBytes($Data) # Выполняем функцию логирования Get-Log # Забираем число количества байт буффера для записи в поток, который передается в параметр ответа. Это является важным условием, что все даныне были переданы и прочитаны на стороне клиента. $context.Response.ContentLength64 = $buffer.Length # Передаем массив байтов (наш буффер ответа с данными) в поток ответа, обязательно нужно передать параметры смешения (если бы нужно было начать запись с определенного места в массиве) и длинны буффера $context.Response.OutputStream.Write($buffer, 0, $buffer.Length)	# Данный метод обновляет буфер вывода, убеждаясь, что все данные из буфера отправлены клиенту $context.Response.OutputStream.Flush()	# Закрываем поток ответа $context.Response.OutputStream.Close()
}
# Создаем сокет слушателя
Add-Type -AssemblyName System.Net.Http
$http = New-Object System.Net.HttpListener
# Указываем адрес слушателя (+ что бы слушать на всех интерфейсах) и порт
$http.Prefixes.Add("http://+:$port/")
# Указываем использование базового метода аутентификации
$http.AuthenticationSchemes = [System.Net.AuthenticationSchemes]::Basic
# Запускаем сокет (начинаем слушать запросы на указанном порту)
$http.Start()
# Обработчик try-finally нужен для закрытия сокета в случае его непредвиденного завершения
try { # Отправляем в бесконечный цикл прослушивание входящих запросов, пока свойство IsListening объекта $http равно true while ($http.IsListening) { # Используем асинхронный режим, для ожидания новых запросов $contextTask = $http.GetContextAsync() # Синхронно ожидает завершения асинхронной задачи, чтобы дождаться завершения асинхронной операции, прежде чем продолжить выполнение кода while (-not $contextTask.AsyncWaitHandle.WaitOne(200)) { }	# Получение результата асинхронной задачи $context = $contextTask.GetAwaiter().GetResult() # Проверяем полученные данные авторизации (в формате Base64) из заголовка запроса на соответветствие переменной $cred $CredRequest = $context.Request.Headers["Authorization"] # Write-Host $CredRequest $CredRequest = $CredRequest -replace "Basic\s" if ( $CredRequest -ne $cred ) { # Если авторизационные данные не прошли проверку (неверно передан логин или пароль), передаем в функцию ответа параметры с текстом ошибки и кодом возравата 401 $Data = "Unauthorized (login or password is invalid)" Send-Response -Data $Data -Code 401 } else { # Если авторизация прошла, проверяем метод и url конечной точки на соответветствие, что бы его обработать if ($context.Request.HttpMethod -eq "GET" -and $context.Request.RawUrl -eq "/api/service") { $GetService = Get-Service -ErrorAction Ignore Send-Response -Data $GetService -Code 200 } # Дальше по аналогии дополнительными условиями (elseif) добавляем обработку других конечных точек elseif ($context.Request.HttpMethod -eq "GET" -and $context.Request.RawUrl -eq "/api/process") { $GetService = Get-Process Send-Response -Data $GetService -Code 200 } # Если не одно из методов не прошел соответветствие, отправляем ответ с кодом 405 elseif ($context.Request.HttpMethod -ne "GET") { $Data = "Method not allowed" Send-Response -Data $Data -Code 405 } # Если не одно из условий не подошло, отправляем ответ с кодом 404 else { $Data = "Not found endpoint" Send-Response -Data $Data -Code 404 } } }
}
finally { # Освобождаем сокет $http.Stop()
}
Пример работы базового REST API сервера с обработкой ошибок.
Пример работы базового REST API сервера с обработкой ошибок.

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

:/>  PING — сетевая диагностика на IP-уровне

>Просмотр истории установленных обновлений в Windows

С помощью команды Get-WUHistory вы можете получить список обновлений, установленных на компьютере ранее автоматически или вручную.

Get-WUHistory - история установки обновлений

Можно получить информацию о дате установки конкретного обновления:

Get-WUHistory найти установленные обновления

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

Get-WULastResults время последней установки обновлений в Windows

Управление обновлениями Windows на удаленных компьютерах через PowerShell

Практически все командлеты модуля PSWindowsUpdate позволяют управлять обновлеями на удаленных компьютерах. Для этого используется атрибут
-Computername Host1, Host2, Host3
. На удаленных компьютерах должен быть включен и настроен WinRM (вручную или через GPO). Модуль PSWindowsUpdate можно использовать для удаленного управлений обновлениями Windows как на компьютерах в домене AD, так и в рабочей группе (потребует определенной настройки PowerShell Remoting).

Для удаленного управления обновлениями компьютерах, нужно добавить имена компьютеров доверенных хостов winrm, или настроить удаленное управление PSRemoting через WinRM HTTPS:

Или с помощью PowerShell:
Set-Item wsman:\localhost\client\TrustedHosts -Value wsk-w10BO1 -Force

С помощью Invoke-Command можно разрешить использовать модуль PSWindowsUpdate на удаленных компьютерах и открыть необходимые порты в Windows Defender Firewall (команда
Enable-WURemoting
):

Проверить список доступных обновлений на удаленном компьютере:

Get-WUList –ComputerName server2

Командлет Invoke-WUJob (ранее командлет назывался Invoke-WUInstall) создаст на удаленном компьютере задание планировщика, запускаемое от SYSTEM. Можно указать точное время для установки обновлений Windows:

Проверить статус задания установки обновлений:

Get-WUJob -ComputerName $ServerNames

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

Проверьте наличие обновления на нескольких удаленных компьютерах:

Получить дату последней установки обновлений на всех компьютерах в домене можно с помощью командлета Get-ADComputer из модуля AD PowerShell:

PowerShell модуль PSWindowsUpdate удобно использовать для загрузки и установки обновлений Windows из командной строки (единственный доступны вариант в случае установки обновлений на хосты без графического интерфейса: Windows Server Core и Hyper-V Server). Также этот модуль незаменим, когда нужно одновременно запустить и проконтролировать установку обновлений сразу на множестве серверов/рабочих станциях Windows.

Установка обновлений Windows с помощью команды Install-WindowsUpdate

Install-WindowsUpdate -MicrosoftUpdate -AcceptAll -AutoReboot

Ключ AcceptAll включает одобрение установки для всех пакетов, а AutoReboot разрешает автоматическую перезагрузку Windows после завершения установки обновлений.

Также можно использовать следующе параметры:

  • IgnoreReboot – запретить автоматическую перезагрузку;
  • ScheduleReboot – задать точное время перезагрузки компьютера.

Можете сохранить историю установки обновлений в лог файл (можно использовать вместо WindowsUpdate.log).

Можно установить только конкретные обновления по номерам KB:

Get-WindowsUpdate -KBArticleID KB2267602, KB4533002 -Install

Install-WindowsUpdate установка обновлений windows с помощью powershell

Если вы хотите пропустить некоторые обновления при установке, выполните:

Install-WindowsUpdate -NotCategory "Drivers" -NotTitle OneDrive -NotKBArticleID KB4011670 -AcceptAll -IgnoreReboot

Проверить, нужна ли перезагрузка компьютеру после установки обновления (атрибуты RebootRequired и RebootScheduled):

Get-WURebootStatus нужна ли перезагрузка Windows после установки обновлений

Скрыть ненужные обновления Windows с помощью PowerShell

Вы можете скрыть определенные обновления, чтобы они никогда не устанавливались службой обновлений Windows Update на вашем компьютер (чаще всего скрывают обновления драйверов). Например, чтобы скрыть обновления KB2538243 и KB4524570, выполните такие команды:

$HideList = "KB2538243", "KB4524570"
Get-WindowsUpdate -KBArticleID $HideList -Hide

или используйте alias:

Hide-WindowsUpdate -KBArticleID $HideList -Verbose

Hide-WindowsUpdate - скрыть обновление, запретить установку

Теперь при следующем сканировании обновлений с помощью команды Get-WindowsUpdate скрытые обновления не будут отображаться в списке доступных для установки.

Вывести список скрытых обновлений:

Обратите внимание, что в колонке Status у скрытых обновлений появился атрибут H (Hidden).

Get-WindowsUpdate –IsHidden отобразить скрытые обновления windows

Отменить скрытие обновлений можно так:

Get-WindowsUpdate -KBArticleID $HideList -WithHidden -Hide:$false

Show-WindowsUpdate -KBArticleID $HideList

Сканировать и загрузить обновления Windows с помощью PowerShell

Чтобы просканировать компьютер на сервере обновлений и вывести список обновлений, которые ему требуется, выполните команду:

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

Поиск (сканирование) доступных обновлений windows: get-windowsupdate

Команда Get-WindowsUpdate при первом запуске может вернуть ошибку:

Value does not fall within the expected range.

Ошибка Get-WindowsUpdate - Value does not fall within the expected range.

Reset-WUComponent сбросить настройки windows update

Чтобы проверить, откуда получает ли Windows обновлений с серверов Windows Update в Интернете или локального WSUS, выполните команду:

Get-WUServiceManager - источникиа обновлений

В этом примере вы видите, компьютер настроен на получение обновлений с локального сервера WSUS (Windows Server Update Service = True). В этом случае вы должны увидеть список обновлений, одобренных для вашего компьютера на WSUS.

Если вы хотите просканировать ваш компьютер на серверах Microsoft Update в Интернете (кроме обновлений Windows на этих серверах содержатся обновления Office и других продуктов), выполните команду:

Вы получаете предупреждение:

Get-WUlist : Service Windows Update was not found on computer

Чтобы разрешить сканирование на Microsoft Update, выполните команду:

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

  • Категории (-NotCategory);
  • Названию (-NotTitle);
  • Номеру обновления (-NotKBArticleID).

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

Get-WUlist -NotCategory "Drivers" -NotTitle OneDrive -NotKBArticleID KB4533002

Get-WindowsUpdate -Download -AcceptAll

Windows загрузит все доступные патчи сервера обновлений (MSU и CAB файлы) в локальный каталог обновлений, но не запустит их автоматическую установку.

Get-WindowsUpdate скачать доступные обновления на диск

Удаление обновлений в Windows с помощью PowerShell

Для корректного удаления обновления Windows используется командлет Remove-WindowsUpdate. Вам достаточно указать номер KB в качестве аргумента параметра KBArticleID.

Remove-WindowsUpdate -KBArticleID KB4011634

Установка модуля управления обновлениями PSWindowsUpdate

В современных версиях Windows 10/11 и Windows Server 2022/2019/2016 модуль PSWindowsUpdate можно установить из онлайн репозитория PowerShell Gallery с помощью команды:

Install-Module -Name PSWindowsUpdate

Подтвердите добавление репозитариев, нажав Y. Проверьте, что модуль управлениям обновлениями установлен в Windows:

Get-Package -Name PSWindowsUpdate

Установить powershell модуль PSWindowsUpdate

Можно удаленно установить PSWindowsUpdate на другие компьютеры в сети. Следующая команда скопирует файлы модуля на указанные компьютеры (для доступа к удаленным компьютерам используется WinRM).

$Targets = "srv1.winitpro.loc", "srv2.winitpro.loc"
Update-WUModule -ComputerName $Targets -local

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

Set-ExecutionPolicy –ExecutionPolicy RemoteSigned -force

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

Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process

Импортируйте модуль в сессию PowerShell:

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

Get-command -module PSWindowsUpdate

список командлетов модуля pswindowupdate

Проверить текущие настройки клиента Windows Update:

ComputerName : WKS22122
WUServer : http://MS-WSUS:8530
WUStatusServer : http://MS-WSUS:8530
AcceptTrustedPublisherCerts : 1
ElevateNonAdmins : 1
DoNotConnectToWindowsUpdateInternetLocations : 1 TargetGroupEnabled : 1
TargetGroup : WorkstationsProd
NoAutoUpdate : 0
AUOptions : 3 - Notify before installation
ScheduledInstallDay : 0 - Every Day
ScheduledInstallTime : 3
UseWUServer : 1
AutoInstallMinorUpdates : 0
AlwaysAutoRebootAtScheduledTime : 0
DetectionFrequencyEnabled : 1
DetectionFrequency : 4Вывести текущие настройки windows update - Get-WUSettings

В данном примере клиент Windows Update на компьютере настроен с помощью GPO на получение обновлений с локального сервера обновлений WSUS.