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

Powershell

Правильно пишем командлеты на Powershell и заодно симулируем парадокс Монти Холла

25.12.2020 16:21:43 | Автор: admin
Хабр точно знаком с парадоксом, а вот с некоторыми фичами павершелла, вероятно, нет, поэтому тут больше про него.




Используем пайплайн в Powershell


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

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

New-Doors|Select-Door|Open-Door|Invoke-UserAction

New-Doors генерирует новые двери, в команде Select-Door игрок выбирает одну из дверей, в Open-Door ведущий открывает дверь в которой точно нет козы и которая не была выбрана игроком, а в Invoke-UserAction мы симулируем разное поведение пользователя.

Объект, описывающий двери, подается слева направо постепенно преобразовываясь.

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

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

Делаем двери


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

Дверь содержит либо козу, либо автомобиль. Дверь может быть выбрана игроком или открыта ведущим.

class Door {    <#    Модель данных, где описана каждая дверь.     Выбрана ли она игроком и открыта ли она ведущим.    #>    [string]$Contains = "Goat"    [bool]$Selected = $false    [bool]$Opened = $false}

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

class Doors {    <#    Модель данных, где описаны 3 двери    #>    [Door]$DoorOne     [Door]$DoorTwo     [Door]$DoorThree}

Можно было их поместить все двери в массив, но чем подробнее все будет описано, тем, лучше. Кстати в Powershell 7, классы, их конструкторы, методы и все остальное ООП, которое работает почти как надо, но об этом в другой раз.

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

function New-Doors {    <#    Генератор случайных дверей.    #>    $i = [Doors]::new()     $i.DoorOne = [Door]::new()    $i.DoorTwo = [Door]::new()    $i.DoorThree = [Door]::new()     switch ( Get-Random -Maximum 3 -Minimum 0 ) {        0 {             $i.DoorOne.Contains = "Car"        }        1 {             $i.DoorTwo.Contains = "Car"        }        2 {             $i.DoorThree.Contains = "Car"        }        Default {            Write-Error "Something in door generator went wrong"            break        }    }        return $i

Наш пайп выглядит так:

New-Doors

Игрок выбирает дверь


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

[Parameter(Mandatory)][ValidateSet("First", "Second", "Third", "Random")]$Principle

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

[parameter(ValueFromPipeline)][Doors]$i

Можно писать ValueFromPipeline без True.

Вот так выглядит законченный блок выбора двери:

function Select-Door {    <#    Игрок выбирает дверь.    #>    Param (        [parameter(ValueFromPipeline)]        [Doors]$i,        [Parameter(Mandatory)]        [ValidateSet("First", "Second", "Third", "Random")]        $Principle    )        switch ($Principle) {        "First" {            $i.DoorOne.Selected = $true        }        "Second" {            $i.DoorTwo.Selected = $true        }        "Third" {            $i.DoorThree.Selected = $true        }        "Random" {            switch ( Get-Random -Maximum 3 -Minimum 0 ) {                0 {                     $i.DoorOne.Selected = $true                }                1 {                     $i.DoorTwo.Selected = $true                }                2 {                     $i.DoorThree.Selected = $true                }                Default {                    Write-Error "Something in door selector went wrong"                    break                }            }        }        Default {            Write-Error "Something in door selector went wrong"            break        }    }     return $i 

Наш пайп выглядит так:

New-Doors | Select-Door -Principle Random

Ведущий открывает дверь


Тут все очень просто. Если дверь не была выбрана игроком и если за ней коза, то меняем поле Opened на True. Конкретно в это случае называть команду словом Open не корректно, вызываемый ресурс не читается, а изменяется. В подобных случаях используйте Set, а Open оставим для наглядности.

function Open-Door {    <#    Ведущий открывает дверь с козой, но не ту, что выбрал игрок.    #>    Param (        [parameter(ValueFromPipeline)]        [Doors]$i    )    switch ($false) {        $i.DoorOne.Selected {            if ($i.DoorOne.Contains -eq "Goat") {                $i.DoorOne.Opened = $true                continue            }                   }        $i.DoorTwo.Selected {             if ($i.DoorTwo.Contains -eq "Goat") {                $i.DoorTwo.Opened = $true                continue            }                   }        $i.DoorThree.Selected {             if ($i.DoorThree.Contains -eq "Goat") {                $i.DoorThree.Opened = $true                continue            }                    }    }    return $i

Для пущей убедительности нашей симуляции мы открываем эту дверь, меняя поле .opened на $true, а не удаляем объект из массива дверей.

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

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

New-Doors|Select-Door-Principle Random |Open-Door

Игрок меняет выбор


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

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

function Invoke-UserAction {    <#    Ситуация, где игрок менят или не меняет свой выбор.    #>    Param (        [parameter(ValueFromPipeline)]        [Doors]$i,        [Parameter(Mandatory)]        [bool]$SwitchDoor    )     if ($true -eq $SwitchDoor) {        switch ($false) {            $i.DoorOne.Opened {                  if ( $i.DoorOne.Selected ) {                    $i.DoorOne.Selected = $false                }                else {                    $i.DoorOne.Selected = $true                }            }            $i.DoorTwo.Opened {                if ( $i.DoorTwo.Selected ) {                    $i.DoorTwo.Selected = $false                }                else {                    $i.DoorTwo.Selected = $true                }            }            $i.DoorThree.Opened {                if ( $i.DoorThree.Selected ) {                    $i.DoorThree.Selected = $false                }                else {                    $i.DoorThree.Selected = $true                }            }        }      }     return $i

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

Еще одна функция в пайплайн.

New-Doors|Select-Door-PrincipleRandom |Open-Door|Invoke-UserAction-SwitchDoor$True

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

Поведение игрока


Как часто игрок меняет дверь. Предусмотрены 5 линий поведения:

  1. Never игрок никогда не меняет свой выбор
  2. Fifty-Fifty 50 на 50. Количество симуляций делится на два прохода. Первый проход игрок не меняет дверь, второй проход меняет.
  3. Random в каждой новой симуляции игрок подкидывает монетку
  4. Always игрок всегда меняет свой выбор.
  5. Ration игрок меняет выбор в N% случаях.

switch ($SwitchDoors) {        "Never" {             0..$Count | ForEach-Object {                $Win += Invoke-Simulation -Door $Door -SwitchDoors $false            }            continue        }        "FiftyFifty" {            $Fifty = [math]::Round($Count / 2)             0..$Fifty | ForEach-Object {                $Win += Invoke-Simulation -Door $Door -SwitchDoors $false            }             0..$Fifty | ForEach-Object {                $Win += Invoke-Simulation -Door $Door -SwitchDoors $true            }            continue        }        "Random" {            0..$Count | ForEach-Object {                [bool]$Random = Get-Random -Maximum 2 -Minimum 0                $Win += Invoke-Simulation -Door $Door -SwitchDoors $Random            }            continue        }        "Always" {            0..$Count | ForEach-Object {                $Win += Invoke-Simulation -Door $Door -SwitchDoors $true            }            continue        }        "Ratio" {            $TrueRatio = $Ratio / 100 * $Count             $FalseRatio = $Count - $TrueRatio             0..$TrueRatio | ForEach-Object {                $Win += Invoke-Simulation -Door $Door -SwitchDoors $true            }             0..$FalseRatio | ForEach-Object {                $Win += Invoke-Simulation -Door $Door -SwitchDoors $false            }            continue        }    }

ForEach-Object в Powershell 7 работает значительно быстрее цикла for, плюс, может быть распараллелен, поэтому тут используется вместо цикла for.

Оформляем командлет


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

Так выглядит код в блоке параметров:

param (        [Parameter(Mandatory = $false,            HelpMessage = "How often the player changes his choice.")]        [ValidateSet("Never", "FiftyFifty", "Random", "Always", "Ratio")]        $SwitchDoors = "Random"    )

Так выглядит подсказка:


Перед блоком параметров можно сделать comment based help. Вот так выглядит код перед блоком параметров:

  <#      .SYNOPSIS         Performs monty hall paradox simulation.         .DESCRIPTION         The Invoke-MontyHallParadox.ps1 script invoke monty hall paradox simulation.         .PARAMETER Door      Specifies door the player will choose during the entire simulation         .PARAMETER SwitchDoors      Specifies principle how the player changes his choice.         .PARAMETER Count      Specifies how many times to run the simulation.         .PARAMETER Ratio      If -SwitchDoors Ratio, specifies how often the player changes his choice. As a percentage."         .INPUTS         None. You cannot pipe objects to Update-Month.ps1.         .OUTPUTS         None. Update-Month.ps1 does not generate any output.         .EXAMPLE         PS> Invoke-MontyHallParadox -SwitchDoors Always -Count 10000         #>

Вот так выглядит сама подсказка:


Запускаем симуляцию


Результаты симуляции:


Если человек никогда не меняет свой выбор, то он побеждает в 33,37% случаев.

В случае двух проходов, в половине которых мы отказываемся менять свой выбор, шансы на победу составляют 49.9134%, что очень близко к ровным 50%.

В случае подкидывания монетки ничего не меняется, шанс на победу остается в районе 50,131%.

Ну а если игрок всегда меняет свой выбор, шанс на победу повышается до 66,6184%, иными словами, скучно и ничего нового.

Производительность:

Что касается производительности. Скрипт кажется не оптимальным. String вместо Bool, много разных функций со свитчаим внутри, передающих друг другу объект, но тем не менее, вот результаты Measure-Command по этому скрипту и скрипту от другого автора.

Сравнение проводилось на двух системах, везде стоял pwsh 7.1, 100 000 проходов.

I5-5200u


Этот алгоритм:

Days              : 0Hours             : 0Minutes           : 0Seconds           : 4Milliseconds      : 581Ticks             : 45811819TotalDays         : 5,30229386574074E-05TotalHours        : 0,00127255052777778TotalMinutes      : 0,0763530316666667TotalSeconds      : 4,5811819TotalMilliseconds : 4581,1819

Тот алгоритм:

Days              : 0Hours             : 0Minutes           : 0Seconds           : 5Milliseconds      : 104Ticks             : 51048392TotalDays         : 5,9083787037037E-05TotalHours        : 0,00141801088888889TotalMinutes      : 0,0850806533333333TotalSeconds      : 5,1048392TotalMilliseconds : 5104,8392

I9-9900K


Этот алгоритм:

Days              : 0Hours             : 0Minutes           : 0Seconds           : 1Milliseconds      : 891Ticks             : 18917629TotalDays         : 2,18954039351852E-05TotalHours        : 0,000525489694444444TotalMinutes      : 0,0315293816666667  TotalSeconds      : 1,8917629TotalMilliseconds : 1891,7629

Тот алгоритм:

Days              : 0Hours             : 0Minutes           : 0Seconds           : 1Milliseconds      : 954Ticks             : 19543236TotalDays         : 2,26194861111111E-05TotalHours        : 0,000542867666666667TotalMinutes      : 0,03257206TotalSeconds      : 1,9543236TotalMilliseconds : 1954,3236

Преимущество 63 мс, но результаты все равно очень странные, учитывая сколько раз в скрипте сравниваются строки.

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

Весь код
class Doors {
<#
Модель данных, где описаны 3 двери
#>
[Door]$DoorOne
[Door]$DoorTwo
[Door]$DoorThree
}

class Door {
<#
Модель данных, где описана каждая дверь.
Выбрана ли она игроком и открыта ли она ведущим.
#>
[string]$Contains = Goat
[bool]$Selected = $false
[bool]$Opened = $false
}

function New-Doors {
<#
Генератор случайных дверей.
#>
$i = [Doors]::new()

$i.DoorOne = [Door]::new()
$i.DoorTwo = [Door]::new()
$i.DoorThree = [Door]::new()

switch ( Get-Random -Maximum 3 -Minimum 0 ) {
0 {
$i.DoorOne.Contains = Car
}
1 {
$i.DoorTwo.Contains = Car
}
2 {
$i.DoorThree.Contains = Car
}
Default {
Write-Error Something in door generator went wrong
break
}
}

return $i
}

function Select-Door {
<#
Игрок выбирает дверь.
#>
Param (
[parameter(ValueFromPipeline)]
[Doors]$i,
[Parameter(Mandatory)]
[ValidateSet(First, Second, Third, Random)]
$Principle
)

switch ($Principle) {
First {
$i.DoorOne.Selected = $true
continue
}
Second {
$i.DoorTwo.Selected = $true
continue
}
Third {
$i.DoorThree.Selected = $true
continue
}
Random {
switch ( Get-Random -Maximum 3 -Minimum 0 ) {
0 {
$i.DoorOne.Selected = $true
continue
}
1 {
$i.DoorTwo.Selected = $true
continue
}
2 {
$i.DoorThree.Selected = $true
continue
}
Default {
Write-Error Something in selector generator went wrong
break
}
}
continue
}
Default {
Write-Error Something in door selector went wrong
break
}
}

return $i
}

function Open-Door {
<#
Ведущий открывает дверь с козой, но не ту, что выбрал игрок.
#>
Param (
[parameter(ValueFromPipeline)]
[Doors]$i
)
switch ($false) {
$i.DoorOne.Selected {
if ($i.DoorOne.Contains -eq Goat) {
$i.DoorOne.Opened = $true
continue
}
}
$i.DoorTwo.Selected {
if ($i.DoorTwo.Contains -eq Goat) {
$i.DoorTwo.Opened = $true
continue
}
}
$i.DoorThree.Selected {
if ($i.DoorThree.Contains -eq Goat) {
$i.DoorThree.Opened = $true
continue
}
}
}
return $i
}

function Invoke-UserAction {
<#
Ситуация, где игрок менят или не меняет свой выбор.
#>
Param (
[parameter(ValueFromPipeline)]
[Doors]$i,
[Parameter(Mandatory)]
[bool]$SwitchDoor
)

if ($true -eq $SwitchDoor) {
switch ($false) {
$i.DoorOne.Opened {
if ( $i.DoorOne.Selected ) {
$i.DoorOne.Selected = $false
}
else {
$i.DoorOne.Selected = $true
}
}
$i.DoorTwo.Opened {
if ( $i.DoorTwo.Selected ) {
$i.DoorTwo.Selected = $false
}
else {
$i.DoorTwo.Selected = $true
}
}
$i.DoorThree.Opened {
if ( $i.DoorThree.Selected ) {
$i.DoorThree.Selected = $false
}
else {
$i.DoorThree.Selected = $true
}
}
}
}

return $i
}

function Get-Win {
Param (
[parameter(ValueFromPipeline)]
[Doors]$i
)
switch ($true) {
($i.DoorOne.Selected -and $i.DoorOne.Contains -eq Car) {
return $true
}
($i.DoorTwo.Selected -and $i.DoorTwo.Contains -eq Car) {
return $true
}
($i.DoorThree.Selected -and $i.DoorThree.Contains -eq Car) {
return $true
}
default {
return $false
}
}
}

function Invoke-Simulation {
param (
[Parameter(Mandatory = $false,
HelpMessage = Which door the player will choose during the entire simulation.)]
[ValidateSet(First, Second, Third, Random)]
$Door = Random,

[bool]$SwitchDoors
)
return New-Doors | Select-Door -Principle $Door | Open-Door | Invoke-UserAction -SwitchDoor $SwitchDoors | Get-Win
}

function Invoke-MontyHallParadox {
<#
.SYNOPSIS

Performs monty hall paradox simulation.

.DESCRIPTION

The Invoke-MontyHallParadox.ps1 script invoke monty hall paradox simulation.

.PARAMETER Door
Specifies door the player will choose during the entire simulation

.PARAMETER SwitchDoors
Specifies principle how the player changes his choice.

.PARAMETER Count
Specifies how many times to run the simulation.

.PARAMETER Ratio
If -SwitchDoors Ratio, specifies how often the player changes his choice. As a percentage."

.INPUTS

None. You cannot pipe objects to Update-Month.ps1.

.OUTPUTS

None. Update-Month.ps1 does not generate any output.

.EXAMPLE

PS> Invoke-MontyHallParadox -SwitchDoors Always -Count 10000

#>
param (
[Parameter(Mandatory = $false,
HelpMessage = Which door the player will choose during the entire simulation.)]
[ValidateSet(First, Second, Third, Random)]
$Door = Random,

[Parameter(Mandatory = $false,
HelpMessage = How often the player changes his choice.)]
[ValidateSet(Never, FiftyFifty, Random, Always, Ratio)]
$SwitchDoors = Random,

[Parameter(Mandatory = $false,
HelpMessage = How many times to run the simulation.)]
[uint32]$Count = 10000,

[Parameter(Mandatory = $false,
HelpMessage = How often the player changes his choice. As a percentage.)]
[uint32]$Ratio = 30
)

[uint32]$Win = 0

switch ($SwitchDoors) {
Never {
0..$Count | ForEach-Object {
$Win += Invoke-Simulation -Door $Door -SwitchDoors $false
}
continue
}
FiftyFifty {
$Fifty = [math]::Round($Count / 2)

0..$Fifty | ForEach-Object {
$Win += Invoke-Simulation -Door $Door -SwitchDoors $false
}

0..$Fifty | ForEach-Object {
$Win += Invoke-Simulation -Door $Door -SwitchDoors $true
}
continue
}
Random {
0..$Count | ForEach-Object {
[bool]$Random = Get-Random -Maximum 2 -Minimum 0
$Win += Invoke-Simulation -Door $Door -SwitchDoors $Random
}
continue
}
Always {
0..$Count | ForEach-Object {
$Win += Invoke-Simulation -Door $Door -SwitchDoors $true
}
continue
}
Ratio {
$TrueRatio = $Ratio / 100 * $Count
$FalseRatio = $Count $TrueRatio

0..$TrueRatio | ForEach-Object {
$Win += Invoke-Simulation -Door $Door -SwitchDoors $true
}

0..$FalseRatio | ForEach-Object {
$Win += Invoke-Simulation -Door $Door -SwitchDoors $false
}
continue
}
}

Write-Output (Player won in + $Win + " times out of " + $Count)
Write-Output (Whitch is + ($Win / $Count * 100) + "%")

return $Win
}

#Invoke-MontyHallParadox -SwitchDoors Always -Count 500000




Подробнее..

Пишем юзабельную оболочку для FFMPEG на Powershell

28.01.2021 16:20:38 | Автор: admin

Нормальный вывод для ffmpeg

Наверное, вы, как и я, слышали про ffmpeg, но боялись его использовать. Респект таким парням, программа целиком выполнена на C ( си, без # и ++ ).

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

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

Делаем объект под пайп


class VideoFile {    $InputFileLiteralPath    $OutFileLiteralPath    $Arguments}

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

Begin, process, end


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

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

Думайте о конструкции Begin-Process как о foreach, где begin выполняется раньше, чем вызывается сама функция и задаются параметры, а End выполняется в последнюю очередь, после foreach.

Вот так бы выглядел код, если бы конструкции Begin, Process, End не было. Это пример плохого кода, так писать не надо.

# это begin$InputColection = Get-ChildItem -Path C:\file.txt function Invoke-FunctionName {    param (        $i    )    # это process    $InputColection | ForEach-Object {        $buffer = $_ | ConvertTo-Json     }        # это end    return $buffer} Invoke-FunctionName -i $InputColection

Что нужно класть в Begin блок?


Счетчики, составлять пути до исполняемых файлов и делать приветствие. Вот так выглядит Begin блок у меня:

 begin {        $PathToModule = Split-Path (Get-Module -ListAvailable ConvertTo-MP4).Path        $FfmpegPath = Join-Path (Split-Path $PathToModule) "ffmpeg"        $Exec = (Join-Path -Path $FfmpegPath -ChildPath "ffmpeg.exe")        $OutputArray = @()         $yesToAll = $false        $noToAll = $false         $Location = Get-Location    }

Хочу обратить внимание на строчку, это настоящий лайфхак:

$PathToModule = Split-Path (Get-Module -ListAvailable ConvertTo-MP4).Path

С помощью Get-Module мы получаем путь до папки с модулем, а Split-Path берет входное значение и возвращает папку уровнем ниже. Таким образом, можно хранить исполняемые файлы рядом с папкой с модулями, но не в самой этой папке.

Вот так:

PSffmpeg/ ConvertTo-MP4/    ConvertTo-MP4.psm1    ConvertTo-MP4.psd1    Readme.md ffmpeg/     ffmpeg.exe     ffplay.exe     ffprobe.exe

А еще с помощью Split-Path можно со стилем спускаться на уровень ниже.

Set-Location ( Get-Location | Split-Path )

Как сделать правильный Param блок?


Сразу после Begin идет Process вместе с Param блоком. Param блок сам проводит null чеки, и валидирует аргументы. К примеру:

Валидация по списку:

[ValidateSet("libx264", "libx265")]$Encoder

Тут все просто. Если значение не похоже на одно из списка, то возвращается False, а затем вызывается исключение.

Валидация по диапазону:

[ValidateRange(0, 51)][UInt16]$Quality = 21

Можно валидировать по диапазону, указав цифры от и до. Crf у ffmpeg поддерживает числа от 0 до 51, поэтому тут указан такой диапазон.

Валидация по скрипту:

[ValidateScript( { $_ -match "(?:(?:([01]?\d|2[0-3]):)?([0-5]?\d):)?([0-5]?\d)" })][timespan]$TrimStart

Сложный инпут можно валидировать регулярками или целыми скриптами. Главное, чтобы валидирующий скрипт возвращал true или false.

SupportsShouldProcess и force


Итак, вам нужно пачкой перекодировать файлы другим кодеком, но с тем же именем. Классический интерфейс ffmpeg предлагает пользователям нажимать y/N, чтобы перезаписать файл. И так для каждого файла.

Оптимальным вариантом является стандартный Yes to all, Yes, No, No to all.

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

function ConvertTo-WEBM {    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'high')]    param ( #все остальные параметры были удалены для наглядности  [switch]$Force     )

Так выглядит голый Param блок здорового человека. С помощью SupportsShouldProcess функция сможет спрашивать, прежде чем выполнять деструктивное действие, а свитч force полностью игнорирует его.

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

# Если указан параметр Force, то все файлы молча перезаписываются
if ($Force) {
$continue = $true
$yesToAll = $true
}

$Verb = "Overwrite file: " + $Arguments.OutFileLiteralPath # формируем строчку, которую отправим пользователю в слукчае вызова ShouldContinue    # Проверяем, не перезапишем ли мы файл.if (Test-Path $Arguments.OutFileLiteralPath) {    #Если файл вот вот будет перезаписан, срашиваем, перезаписывать ли все файлы в дальнейшем или нет    $continue = $PSCmdlet.ShouldContinue($OutFileLiteralPath, $Verb, [ref]$yesToAll, [ref]$noToAll)            #Если было выбрано А - Да, для всех, то продолжаем игнорируя факт того, что файл уже существует    if ($continue) {        Start-Process $Exec -ArgumentList $Arguments.Arguments -NoNewWindow -Wait                    }    #Если было выбрано Нет - То завершаем работу скрипта    else {        break    }}# Если файл не сущесвует, создаем новыйelse {    Start-Process $Exec -ArgumentList $Arguments.Arguments -NoNewWindow -Wait    }


Делаем нормальный пайп


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

function New-FfmpegArgs {            $VideoFile = $InputObject            | Join-InputFileLiterallPath             | Join-Preset -Preset $Preset            | Join-ConstantRateFactor -ConstantRateFactor $Quality            | Join-VideoScale -Height $Height -Width $Width            | Join-Loglevel -VerboseEnabled $PSCmdlet.MyInvocation.BoundParameters["Verbose"]            | Join-Trim -TrimStart $TrimStart -TrimEnd $TrimEnd -FfmpegPath "C:\Users\nneeo\Documents\lib.Scripts\PSffmpeg\ConvertTo-WEBM\ffmpeg\" -SourceVideoPath ([IO.Path]::GetFullPath($InputObject))            | Join-Codec -Encoder $Encoder -FfmpegPath "C:\Users\nneeo\Documents\lib.Scripts\PSffmpeg\ConvertTo-WEBM\ffmpeg\" -SourceVideoPath ([IO.Path]::GetFullPath($InputObject))            | Join-OutFileLiterallPath -OutFileLiteralPath $OutFileLiteralPath -SourceVideoPath ([IO.Path]::GetFullPath($InputObject))             return $VideoFile        }

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

function Invoke-FunctionName  {    $ParentVar = "Hello"    function Invoke-NetstedFunctionName {        Write-Host $ParentVar    }    Invoke-NetstedFunctionName}

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

Если бы я использовал вложенные функции это все выглядело так:

$VideoFile = $InputObject| Join-InputFileLiterallPath | Join-Preset | Join-ConstantRateFactor | Join-VideoScale | Join-Loglevel | Join-Trim | Join-Codec | Join-OutFileLiterallPath 

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

Делаем нормальные функции


Нам нужно составить аргументы для ffmpeg.exe, и для этого нет ничего лучше пайплайна. Как же я люблю пайплайны!

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

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

1. Measure-VideoResolution

function Measure-VideoResolution {    param (        $SourceVideoPath,        $FfmpegPath    )    Set-Location $FfmpegPath      .\ffprobe.exe -v error -select_streams v:0 -show_entries stream=height -of csv=s=x:p=0 $SourceVideoPath | ForEach-Object {        return $_    }}

h265 экономит битрейт начиная от 1080 и выше, при меньшем разрешении видео он не так важен, поэтому, для кодирования больших видео следует указывать h265 в качестве дефолта.
Return в Foreach-Object выглядит очень странно. Но тут ничего не поделаешь. FFmpeg пишет все в stdout и это самый простой способ выцепить значение из подобных программ. Используйте этот трюк, если вам нужно вытащить что-то из stdout. Не используйте Start-Process, чтобы вытащить stdout нужно вызвать исполняемый файл прямо так, как в этом примере.

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

  begin {        $Location = Get-Location    }

Эта функция хорошо смотрелась бы как отдельный командлет, пригодилась бы, но это на будущее.

2. Join-VideoScale

function Join-VideoScale {    param(        [Parameter(Mandatory = $true,            ValueFromPipeline = $true,            ValueFromPipelineByPropertyName = $true)]        [ValidateNotNullOrEmpty()]        [SupportsWildcards()]        [psobject]$InputObject,        $Height,        $Width    )     switch ($true) {        ($null -eq $Height -and $null -eq $Width) {            return $InputObject        }        ($null -ne $Height -and $null -ne $Width) {            $InputObject.Arguments += " -vf scale=" + $Width + ":" + $Height            return $InputObject        }        ($null -ne $Height) {             $InputObject.Arguments += " -vf scale=" + $Height + ":-2"             return $InputObject         }        ($null -ne $Width) {             $InputObject.Arguments += " -vf scale=" + "-2:" + $Width             return $InputObject         }    }}
Один из моих любимых приколов вывернутый наизнанку switch. Паттерн матчинга в Powershell нет, но такие конструкции заменяют его, по большей части.
В круглых скобках находится выполняемая функция. И если результат выполнения этой функции равен условию в свитче, то скриптблок в нем выполняется.

3. Join-Trim

function Join-Trim {    param(        [Parameter(Mandatory = $true,            ValueFromPipeline = $true,            ValueFromPipelineByPropertyName = $true)]        [ValidateNotNullOrEmpty()]        [SupportsWildcards()]        [psobject]$InputObject,        $TrimStart,        $TrimEnd,        $FfmpegPath,        $SourceVideoPath    )    if ($null -ne $TrimStart) {        $TrimStart = [timespan]::Parse($TrimStart)    }    if ($null -ne $TrimEnd) {        $TrimEnd = [timespan]::Parse($TrimEnd)    }        if ($TrimStart -gt $TrimEnd -and $null -ne $TrimEnd) {        Write-Error "TrimStart can not be equal to TrimEnd" -Category InvalidArgument        break    }    if ($TrimStart -ge $TrimEnd -and $null -ne $TrimEnd) {        Write-Error "TrimStart can not be greater than TrimEnd" -Category InvalidArgument        break    }    $ActualVideoLenght = Measure-VideoLenght -SourceVideoPath $SourceVideoPath -FfmpegPath $FfmpegPath       if ($TrimStart -gt $ActualVideoLenght) {        Write-Error "TrimStart can not be greater than video lenght" -Category InvalidArgument        break    }     if ($TrimEnd -gt $ActualVideoLenght) {        Write-Error "TrimEnd can not be greater than video lenght" -Category InvalidArgument        break    }     switch ($true) {        ($null -eq $TrimStart -and $null -eq $TrimEnd) {            return $InputObject        }        ($null -ne $TrimStart -and $null -ne $TrimEnd) {                        $ss = " -ss " + ("{0:hh\:mm\:ss}" -f $TrimStart)            $to = " -to " + ("{0:hh\:mm\:ss}" -f $TrimEnd)            $InputObject.Arguments += $ss + $to            return $InputObject         }        ($null -ne $TrimStart) {             $ss = " -ss " + ("{0:hh\:mm\:ss}" -f $TrimStart)            $InputObject.Arguments += $ss            return $InputObject        }        ($null -ne $TrimEnd) {             $to = " -to " + ("{0:hh\:mm\:ss}" -f $TrimEnd)            $InputObject.Arguments += $to            return $InputObject        }    }}

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

Выводим новые объекты


Чтобы этот скрипт можно было встраивать в другие пайплайны, нужно сделать так, чтобы он что-нибудь возвращал. У нас есть InputObject взятый из Get-ChildItem, но поле Name доступно только для чтения, просто поменять имена файлов нельзя.

Чтобы вывод команды был похож на системный, нужно сохранить имена перекодированных объектов и с помощью Get-Chilitem добавить их в массив и вывести его.

1. В Begin блоке объявляем массив

begin {        $OutputArray = @()}

2. В Process блоке заносим перекодированные файлы:

Не забываем про null чеки, даже в функциональном программировании они нужны.

process {       if (Test-Path $Arguments.OutFileLiteralPath) {      $OutputArray += Get-Item -Path $Arguments.OutFileLiteralPath  }}

3. В End блоке возвращаем полученный массив

end {        return $OutputArray    }

Ура, закончили end блок, пора использовать скрипт как надо.

Используем скрипт


Пример 1

Эта команда выберет все файлы в папке, перекодирует их в формат mp4 и тут же отправит эти файлы на сетевой диск.

Get-ChildItem | ConvertTo-MP4 -Width 320 -Preset Veryslow | Copy-Item Destination '\\local.smb.server\videofiles'

Пример 2

Перекодируем все свои игровые видео в указанной папке, а исходники удаляем.

ConvertTo-MP4 -Path  "C:\Users\Administrator\Videos\Escape From Tarkov\" | Remove-Item -Exclude $_

Пример 3

Кодирование всех файлов из папки и перемещение новых файлов в другую папку.

Get-ChildItem | ConvertTo-WEBM | Move-Item -Destination D:\OtherFolder

Заключение


Вот мы и пофиксили ffmpeg, вроде бы ничего критичного не упустили. Но что это получается, ffmpeg нельзя было использовать без нормальной оболочки?
Получается, да.
Но впереди еще очень много работы. Полезно было бы иметь в качестве модулей такие командлеты как Measure-videoLenght, возвращающий длительность видео в виде Timespan, с их помощью можно будет упростить пайп и сделать код компактнее.
Еще, нужно сделать команды ConvertTo-Webp и все в этом духе. Нужно бы еще создавать папку за пользователя, если она не существует, рекурсивно. И проверку доступа на запись и чтение тоже неплохо было бы сделать.

Ну а пока что так, следите за проектом на гитхабе.

Подробнее..

Перевод PowerShell это язык программирования?

27.02.2021 12:14:40 | Автор: admin
Является ли PowerShell языком программирования? Совершенно определённо является. И не обращайте внимание на тех, кто говорит, что это не так. Многие, работающие в сфере программирования, могут просто посмеяться над мыслью о том, что код, написанный для PowerShell это нечто большее, чем обычные скрипты. Такие люди категорически неправы. Здесь мы поговорим о том, почему это так. Но если вы читаете этот текст в поиске чёткого ответа, то знайте PowerShell это язык программирования. Более того, PowerShell это поразительный инструмент, который позволяет решать практически любые задачи. С помощью PowerShell можно сделать что-то простое, такое, что обычно делают в командной строке Windows (CMD), а можно, используя Windows Forms, построить полномасштабное приложение. Границы того, что можно создать с помощью PowerShell, ограничены лишь фантазией разработчика и его навыками поиска в интернете.



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

Зачем вообще учиться программировать?


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

Благодаря поисковику Google и видеохостингу YouTube (это, соответственно, первый и второй по посещаемости сайты в мире) в нашем распоряжении имеются нескончаемые запасы бесплатных и хорошо подготовленных учебных материалов. Если тому, кто хочет изучить некий язык программирования, попадётся хороший курс, то он меньше чем за час (в буквальном смысле) сможет настроить среду разработки и написать свой первый Hello World. На следующем рисунке можно видеть Hello World на PowerShell.


Hello World на PowerShell это очень просто

Может, изучая программирование, вы подумываете о продвижении по карьерной лестнице и считаете, что знания из сферы DevOps это как раз то, что вам нужно. Ведущие языки программирования для DevOps это Golang, Python, Ruby, C# Данный список можно продолжать и продолжать (и в этот список, пожалуй, можно включить практически все актуальные языки программирования).

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

Что такое язык программирования?


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

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

Существуют разные виды языков программирования. Кратко опишем их:

  • Процедурные языки программирования. В них код выполняется линейно и пошагово. Это означает, что если некто создал приложение, то код этого приложения должен выполниться от начала до конца, после чего его работа завершается. А если при выполнении этого кода возникнут какие-то проблемы его выполнение должно быть прервано.
  • Функциональные языки программирования. Тут используются символические представления вычислений. В таких языках применяются конструкции, которые не особенно сильно отличаются от математических функций.
  • Объектно-ориентированные языки программирования. В этих языках типы данных могут содержать как данные, в форме атрибутов и свойств объектов, так и функции, манипулирующие этими данными.
  • Скриптовые языки программирования. Для запуска кода, написанного на таких языках, не нужен компилятор. Они используют так называемые интерпретаторы. При написании программ на этих языках применяются предварительно подготовленные команды и функции, хотя подход к созданию программ на таких языках похож на подход, используемый при работе со стандартными языками.
  • Логические языки программирования. В логических языках программирования используются (вот уж неожиданность) правила логики. Они позволяют, оперируя фактами и заданными правилами, автоматически выводить результат.

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

Суть PowerShell


До PowerShell в мире Microsoft Windows существовал VBScript. Используя этот язык программирования можно решить очень большое количество задач, но в наше время, в 2021 году, вероятно, это количество примерно равно количеству задач, нерешаемых с помощью VBScript. Инструмент PowerShell создавался как средство, которое поможет использовать всё лучшее, что есть в платформе .NET, и при этом будет иметь доступ ко встроенным возможностям ОС. Джеффри Сновер, человек, который придумал PowerShell, на самом деле, очень подробно об этом рассказывает в работе Monad Manifesto, которую он написал в 2002 году. Это захватывающее чтение, в этом материале чётко выражено его видение проекта, который в итоге и стал тем, что получило название PowerShell.

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

И всё же, PowerShell это язык программирования или нет?


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

PowerShell полагается на системные хуки ОС Windows для выполнения множества встроенных команд, или, как их называют в PowerShell-сообществе, командлетов (так что этот термин придумал не я). Каждый из командлетов способен выполнить некую очень полезную, или, в некоторых случаях, очень мощную операцию.

В PowerShell для организации обмена переменными и данными используются объекты. Поэтому можно сказать, что тут используются идеи, роднящие PowerShell с объектно-ориентированными языками программирования. Многие считают PowerShell скриптовым языком программирования. А это, что невозможно не признать, во многих отношениях так и есть. Но PowerShell-код может работать и как код обычных приложений. Этот код, как и программы, написанные на том же Python, или на ещё каком-то распространённом языке, может включать в себя циклы, вроде foreach и while, условные конструкции и многое другое.

Всё это позволяет создавать весьма мощные приложения, которые могут быть запущены в Windows с помощью планировщика заданий, могут быть, ради упрощения их выполнения, преобразованы в .EXE-файлы, могут быть запущены в фоне, в виде служб Windows. Я регулярно пользуюсь всеми этими методами работы с PowerShell-скриптами и могу с уверенностью заявить о том, что они избавили меня от месяцев ручного труда и помогли мне оставаться на хорошем счету у начальства и коллег. Я, правда, шучу, никто этого не замечает, но я-то всё это вижу.

Если говорить серьёзнее, то PowerShell может оказать компьютерщику огромную помощь, и речь идёт не только о решении рабочих задач. Например, я хочу написать цикл материалов о том, как я создал простое Windows Form-приложение для моей мамы, которой уже за 70. Это приложение позволяет ей переключать настройки почтового сервера в путешествиях (до того, как мир охватило то, что происходит сейчас) буквально несколькими щелчками мыши.

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

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

Итоги


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

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

Какие задачи вы решаете с помощью PowerShell?

Подробнее..

Хождения по собеседованиям, или Как полтора года искать работу в ИТ самоучке

14.06.2021 14:06:00 | Автор: admin

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

Знаете, есть такие особые признаки, известные всем, когда точно понимаешь, что надо линять с текущей работы, ибо уже вилы. Как раз такое приключилось у меня с прошлой работой. В совсем уж подробности вдаваться не буду, но немного все же порефлексирую на тему своих скитаний по рынку труда. Опустим, что происходило уже на прошлой работе (спойлер: новое место работы я нашел), так как меня смело можно укорить в том, что я точно задержался, постоянно откладывая уход, и что все то, что происходило там, вымысел и быть не может. Но, к сожалению, происходило. В общем не будем о плохом. Как написал А. С. Пушкин в стихотворении "Если жизнь тебя обманет", "Что пройдет, то будет мило".


Предыстория

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

Но сразу поступило предложение от ребят из отдела IT от другой ветки бизнеса, которых я знал много лет. У них стала вакантно место сисадмина (или что-то в этом роде, так как по сути ИТ там и не было никогда). Решив все поставить на кон, я согласился по сути сменить профессию на 180 градусов.

Проработав там предостаточно лет (сейчас уже звучит дико), понял, что надо покупать весла и грести оттуда, так как местный "совок" был уже в печенке, и ощущалась постоянная депрессия от нереализованного моего потенциала. На дворе был ноябрь 2019 года, ни про "какой-то там вирус в Китае" никто не был в курсе, и, если бы мне тогда сказали, что поиски новой работы по тем или иным причинам растянутся на полтора года, я нет, не рассмеялся бы, а, подняв бровь, удивился: я настолько никчемен?

Вояж, вояж

Высунув язык, состряпал первое резюме и стал ждать. Вскоре со мной связалась какая-то финансовая контора, которая искала что-то среднее между менеджером, техподдержкой и системным администратором. Даже понимая, что это не мое, решил поглядеть, как же они проводят собеседование. Вся странность данного собеседования заключалась в том, что по телефону сказали, что командировок не будет, но по приезду в их офис в "Москва Сити" (+100500 к пафосу рекрутера), выяснилось, что командировки еще как будут (каждые три недели), и не по Золотому Кольцу, а во Владивосток!

Мое ошарашенное возражение "Но я не хочу ездить во Владивосток", рекрутерша парировала чудесной репликой: "Ну, почему же, город посмотрите!". Пока мы шли по офису, она все жужжала рядом, что вакансия актуальна только, пока я не ушел, "уйдете и можете не возвращаться"! Я был шокирован, что, оказывается, люди так могут врать. Какой в этом смысл? Первый блин комом

Подозрительные запросы

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

В начале 2020 уже вовсю началась пандемия, многие компании закрыли набор и погрузились в тотальный перевод всего и вся на удаленку. Рынок труда ушел в спячку до сентября 2020. И я понял, что попал! Очень скоро я уже нисколько не удивлялся тому, что топовые и небольшие конторы на отклики вообще не реагировали, даже не заходя (а иногда и заходя) в учетки на сайтах типа хаха.ру, а рекрутеры не стучались в Telegram.

Изгнание бесов

Так как у меня свободный английский, то еще я искал возможность работы с экспатами. И тут позвонили из знаменитой антивирусной компании, основанной выходцем из школы КГБ, пригласив меня на должность "младший системный администратор". Я весь такой в волнении, на кураже нельзя же ударить в грязь лицом.

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

Что будете делать, если проектор сломался, а Евгений Валентинович спешит к нам в офис?

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

Ой, как мало, брезгливо произнесла рекрутер, у нас по 18 идей люди предлагают, а вы всего 3. Некоторые даже предложили изгнать бесов из проектора...

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

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

Фэшн нот май профэшн

Другой примечательный случай произошел в известной французской компании из мира моды. Вакансия была достаточно типичная: админить центральный офис в Москве, где работают экспаты. Но тоже начали за здравие, а закончили за упокой.Поиск соискателей французы отдали на откуп сторонней компании, к которой претензий вроде и нет, не считая того, что у меня было 6 собеседований (в сумме все заняло 27 дней). На пятом собеседовании было знакомство с непосредственным руководителем. Все прошло гладко, и я уж было подумал, что вот оно счастье. Но на последнем шестом собеседовании было общение непосредственно с руководителем центрального офиса. С пары слов она дала понять, что ей ничего не надо, не интересно, а я ну давай рассказывай говорящая голова, отнимающая мое время. Диалог был примерно такой:

Чего умеешь?

Ну, например, автоматизировать люблю на PowerShell.

И чего же ты автоматизировал, дружок-пирожок?

<Пытаюсь рассказать о Windows 10 Sophia Script, и на кой ляд там 12 000+ строк кода>

Нет, а на работе, что конкретно автоматизировал?

Могу показать пример (сейчас уже в разы лучше выглядит) автоматизации выкачивания Adobe Acrobat Pro DC с патчем с FTP-сервера Adobe и распаковки. Просто с FTP-сервера у них маленькая скорость скачивания, ляпнул я зачем-то (это была фатальная ошибка).

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

На этом моменте у меня в голове уже началась предзагрузка экрана смертииз Dark Souls.

Дальше собеседование кубарем покатилось под откос. Мне, конечно же, отказали, хоть до этого на всех 5 собесах их все абсолютно устраивало. Зачем они меня тестировали вдоль и поперек, завалив по сути на 6 собеседовании, которое ничего уже и не решало, остается тайной. Ах, да, открыта ли та вакансия от ноября 2020 года? Да, она все еще открыта.

У нас такой офис!

Поступает звонок от "дочки" зеленого банка. Мило побеседовали, обсудив приблизительно, что я хочу от нового места работы. Дальше хантер начинает рассуждать вслух, что их офис на Кутузовском будет удобен для меня, так как не так далеко ехать. Я еще поддакнул, что видел обзор их офиса на YouTube. На том и порешили, что местоположение офиса мне подходит.

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

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

Я что-то пропустил, но в вакансии нет ни слова о разъездном характере работы. Или я не прав? И я не хочу ездить настраивать ПК; я уже не в том возрасте.

<сопит, открывая вакансию>
Да, и правда нет ни слова. Ну, и что?
Как что? Ничего, я откликнулся на вакансию, которую бы, зная такие подробности, даже бы не открыл. Разве это не так работает?
Вообще-то вы первый, кто обратил внимание, что описание не точное. Всех все устраивало.
Да позвольте! Вы неточное описание даете и завлекаете людей. Вы можете исправить, дополнив пункт о разъездах, чтобы будущих соискателей не вводить в заблуждение?
Не считаю нужным.
Почему?
Потому что.
Посыл понял. Всего хорошего.

Отдельно хочется вспомнить забавный момент собеседования в офисе одного модного строительного девелопера в Москве. Я не мог попасть в их собственном центральном офис 7 (семь) минут, стоя под дверью на этаже. Никто ко мне просто не шел, а HR заверяла по телефону, что вот-вот кто-то да и откроет мне. Собеседование прошло нормально, но HR не посчитал нужным, не только впускать соискателя в офис, но и уведомлять его при отказе.

Собеседований было достаточно много, в сумме около 25. На некоторых мне откровенно хамили и оскорбляли (до сих пор не понимаю, какие цели они этим преследовали), на других предлагали работать, выполняя ресурсоемкие задачи на ПК года эдак 2010 даже без SSD-накопителя. На что я только ни насмотрелся. И все это были крупные организации, считающие себя чуть не ИТ-гигантами.

Оглядываясь назад, я ощущаю, что как будто мне не хватало удачи. Вот прямо самую малость: то на 6 собеседовании отказывают, то уже, знакомясь с командой в офисе крупной компании, один из присутствующих начинает зачем-то задавать тупые вопросы, а потом сообщает своим, чтобы меня не брали, так как "я увиливаю от ответов". Периодически вижу в сети посты от рекрутеров, возмущающихся все возрастающими запросами инженеров, мол "совсем обнаглели, печенек и кипяточка на кофепойнте им уже мало". Думаю, здесь можно провести параллель с совершенно дикими райдерами рок-звезд, с помощью которых они "отыгрываются" за все то время, когда бедствовали, будучи никому не известными и унижались перед продюсерами. А рецепт лечения всего этого просто нужно быть людьми и относиться к друг-другу по-человечески. Как рекрутерам к соискателям, так и наоборот. Главное, что я нашел ту компанию, которую и искал.

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

Подробнее..

11 команд PowerShell для Office 365, которые полезно знать

12.01.2021 18:23:11 | Автор: admin


Использование PowerShell для управления Office 365 может сделать вашу работу быстрее, эффективнее и проще. PowerShell предоставляет доступ к информации о среде Office 365, к которой нельзя получить доступ через центр администрирования Microsoft 365, и позволяет выполнять массовые операции с помощью одной команды. Благодаря интеграции продуктов Office 365 в единый интерфейс, PowerShell также упрощает управление доступом пользователей и усиливает кибербезопасность.
Данная статья объясняет наиболее полезные для системных администраторов команды PowerShell для Office 365. Мы разделили эти команды на три категории автоматизация, отчетность и конфигурация, чтобы вы могли быстро найти то, что вам нужно.

Как PowerShell может помочь работе с Office 365




Центр администрирования Microsoft 365 отлично подходит для обычных пользователей. Используя эту систему, вы можете управлять своими учетными записями и лицензиями пользователей Office 365, а также такими службами, как Exchange Online, Teams и SharePoint Online. Вы также можете управлять всеми этими компонентами с помощью PowerShell. Его использование значительно упрощает автоматизацию и делает вашу работу более эффективной.
В частности, существует несколько ключевых факторов, которые упрощают управление Office 365 с помощью PowerShell:
  • PowerShell для Office 365 показывает дополнительную информацию, которую вы не можете увидеть в центре администрирования Microsoft 365;
  • PowerShell позволяет настраивать функции и параметры, недоступные в центре администрирования Office 365;
  • Если вы используете Office 365 для обмена файлами, PowerShell для Office 365 позволит быстро выполнять проверку и управлять доступом пользователей к общим дискам;
  • Через командную строку вы можете легко выполнять массовые операции;
  • В PowerShell для Office 365 вы можете использовать командлеты для фильтрации данных, полученных из вашей системы Office 365. Таким образом вы получите быстрый доступ к информации о пользователях и системах;
  • Его также можно использовать для автоматизации процесса сбора данных из Office 365 и их выгрузки в CSV-файл;
  • Благодаря возможности быстро проверять информацию о пользователях PowerShell является мощным инструментом для мониторинга и повышения кибербезопасности.

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

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

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

Команды PowerShell для автоматизации Office 365




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

Наиболее полезные команды PowerShell для автоматизации Office 365:

1. Подключение к приложению Office 365 с помощью PowerShell


Прежде чем начать использовать PowerShell для Office 365, необходимо скачать и установить модуль Office 365 для Windows PowerShell и подключить его к своему инстансу Office 365.

Вот как это сделать:
  • Cкачайте и установите Помощник по входу в Microsoft Online Services для ИТ-специалистов, RTW.
  • Импортируйте модуль PowerShell Online Services для Microsoft Azure Active Directory и Office 365, используя следующие команды в PowerShell:
    1.Install-Module -Name AzureAD2.3.Install-Module -Name MSOnline
    

  • Введите свои учетные данные администратора Office 365:
    $Cred = Get-Credential
    

    Теперь вам нужно создать сеанс PowerShell от имени удаленного пользователя. Это можно сделать с помощью следующей команды:
     $O365 = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://outlook.office365.com/powershell-liveid/ -Credential $Cred -Authentication Basic -AllowRedirection
    

  • Теперь импортируйте команды сеанса в локальный сеанс Windows PowerShell:
    Import-PSSession $O365
    

  • Наконец, подключите сеанс ко всем своим службам Office 365 с помощью этой команды:
    Connect-MsolService Credential $O365
    

    Это подключит PowerShell для Office 365 к вашему инстансу Office 365 и позволит управлять им.

2. Подключение к Exchange Online и SharePoint Online с помощью PowerShell


Вы можете подключиться к Microsoft Exchange Online и Microsoft SharePoint Online, чтобы управлять этими службами с помощью PowerShell.
  • Подключение к Exchange Online, по сути, происходит так же, как и подключение к Office 365. Вот соответствующие команды:
    1.$Cred = Get-Credential2.3.$Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://outlook.office365.com/powershell-liveid/ -Credential $Cred -Authentication Basic AllowRedirection
    

  • Подключение к SharePoint Online немного сложнее, и вам потребуется установить дополнительное программное обеспечение.
    Сначала установите компонент командной консоли SharePoint Online.

    Затем запустите из PowerShell следующую команду:
    1.$admin="Admin@enterprise.onmicrosoft.com"2.3.$orgname="enterprise"4.5.$userCred = Get-Credential -UserName $admin -Message "Укажите пароль."6.7.Connect-SPOService -Url https://$orgname-admin.sharepoint.com -Credential $userCred
    


3. Cписок доступных командлетов PowerShell для Office 365


Командлеты это основной тип команд PowerShell для Office 365, и вы будете использовать их чаще всего. PowerShell для Office 365, как и большинство интерфейсов командной строки, позволяет просмотреть список всех доступных командлетов для вашей системы.
  • Чтобы получить список всех доступных командлетов для MSOnline, выполните следующую команду:
    Get-Command -module MSOnline
    
  • Вы также можете запустить ту же команду, чтобы увидеть список всех доступных командлетов для Azure Active Directory, просто заменив переменную -module:
    Get-Command -module AzureAD
    

4. Cписок всех пользователей Office 365


Одно из наиболее распространенных применений PowerShell получение списка всех пользователей Office 365. В PowerShell это можно сделать с помощью всего одного командлета: Get-msoluser.
Этот командлет покажет вам всех пользователей Office 365 с действующей лицензией и автоматически получит некоторую базовую информацию о каждом из них: параметры DisplayName, City, Department и ObjectID.
  • Для этого выполните команду:
    1.Get-MsolUser | Select DisplayName, City, Department, ObjectID 
    
  • Затем вы можете увидеть количество учетных записей, выполнив аналогичную команду:
    1.Get-MsolAccountSku
    
  • А для получения списка доступных вам служб выполните эту команду:
    1.Get-MsolAccountSku | select -ExpandProperty ServiceStatus
    
  • С помощью стандартной логики командной строки эти команды можно расширить для фильтрации получаемых результатов. Например, вы можете сгруппировать всех пользователей в зависимости от места, запустив:
    1.Get-MsolUser | Select DisplayName, UsageLocation | Sort UsageLocation, DisplayName
    

5. Создание нового пользователя в Office 365 с помощью PowerShell


PowerShell можно использовать для автоматизации процесса создания новых пользователей вашей системы. Для этого воспользуйтесь командлетом New-MsolUser:
Для автоматизации процесса создания пользователей используйте этот командлет с соответствующими переменными:
1.New-MsolUser -UserPrincipalName JSmith@enterprise.onmicrosoft.com -DisplayName "John Smith" -FirstName John -LastName Smith

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

6. Изменение пароля в Office 365 с помощью PowerShell


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

Для этого выполните команду:
1.Set-MsolUserPassword -UserPrincipalName JSmith@netwrixqcspa.onmicrosoft.com -NewPassword P@SSw0rd!

Вы также можете не использовать параметр -NewPassword, и в этом случае система автоматически сгенерирует случайный пароль:
1.Set-MsolUserPassword -UserPrincipalName JSmith@netwrixqcspa.onmicrosoft.com

Команды Windows PowerShell для отчетности




PowerShell для Office 365 это отличный инструмент для создания отчетов. Использование командлетов PowerShell позволяет быстро и легко получать доступ, сортировать и сопоставлять информацию о пользователях Office 365, а также информацию о том, как они используют систему.
Следует отметить, что большинство командлетов для создания отчетов устарело в январе 2018 года. Корпорация Microsoft заменила эти командлеты новым API отчетов Microsoft Graph. Это сократило возможности PowerShell по созданию отчетов в Office 365, но все старые функции по-прежнему доступны через центр безопасности и соответствия требованиям Office 365.

Тем не менее, в вопросах отчетности о пользователях и группах PowerShell для Office 365 по-прежнему является ключевым инструментом. Ниже мы приведем наиболее полезные отчеты, для которых можно использовать PowerShell.

1. Планы лицензирования


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

В результате вы получите отчет, содержащий несколько ключевых элементов информации:
  • AccountSkuld показывает доступные планы лицензирования для вашей организации;
  • ActiveUnits количество лицензий, приобретенных вами для определенного плана лицензирования;
  • WarningUnits количество непродленных лицензий в плане лицензирования, которые истекают по окончании 30-дневного льготного периода;
  • ConsumedUnits количество лицензий, которые вы назначили пользователям из определенного плана лицензирования.

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

2. Учетные записи пользователей


Еще один полезный командлет для создания отчетов Get-MsolUser, который возвращает список всех учетных записей пользователей Office 365. Вот как вы можете использовать эту команду:
Выполните команду:
1.Get-MsolUser

Вы увидите полный список учетных записей пользователей с соответствующими именами. Вы также можете добавить ряд параметров для фильтрации отображаемых учетных записей. Например, чтобы получить список нелицензированных пользователей (пользователей, которые были добавлены в Office 365, но еще не получили лицензии на использование какой-либо из служб), выполните следующую команду:
1.Get-MsolUser -UnlicensedUsersOnly

Для дальнейшего изучения конкретных учетных записей можно использовать командлет where.
Чтобы скомбинировать два командлета, воспользуйтесь вертикальной чертой |: Это означает, что PowerShell для Office 365 возьмет результаты первой команды и отправит их следующей команде. Например, если вы хотите отображать только те учетные записи пользователей, у которых не указано место использования, вы можете использовать такую команду:
1.Get-MsolUser | Where {{$_.UsageLocation -eq $Null}}

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

3. Отчеты электронной почты


PowerShell также является мощным инструментом для проверки использования электронной почты и пользователей. Фактически это одно из основных системных приложений, когда дело доходит до отчетности. Ниже перечислено несколько полезных отчетов, касающихся электронной почты:
  • Вы можете использовать PowerShell для получения информации о каждом почтовом ящике в вашей системе, используя следующую команду:
    1.Get-mailbox | get-MailboxStatistics
    

  • Вы также можете получить список всех почтовых ящиков, в которые не выполнялся вход в течение 30 дней (или любого другого нужного вам периода, означающего, что вам необходимо закрыть эти ящики). Для этого выполните команду:
    1.Get-Mailbox RecipientType 'UserMailbox' | Get-MailboxStatistics | Sort-Object LastLogonTime | Where {{$_.LastLogonTime lt ([DateTime]::Now).AddDays(-30) }} | Format-Table DisplayName, LastLogonTime
    

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


Командлеты PowerShell для настройки Office 365




PowerShell также чрезвычайно полезен для настройки среды Office 365. Как мы отмечаем в нашем бесплатном видеокурсе по скрытым параметрам Office 365, которые можно разблокировать с помощью PowerShell, существуют определенные параметры конфигурации, которые доступны только через интерфейс PowerShell.
Наиболее полезными и часто используемыми командами PowerShell для настройки являются те, которые относятся к управлению группами пользователей и созданию новых сайтов SharePoint. Зачастую эти задачи усложняют работу системных администраторов, и их автоматизация может сэкономить много времени.

1. Настройте скрытые параметры с помощью PowerShell для Office 365


Как мы упоминали ранее, к некоторым параметрам конфигурации Office 365 можно получить доступ только с помощью PowerShell.
Наиболее ярким примером являются параметры конфигурации Skype для бизнеса. Онлайн-центр администрирования этой службы содержит несколько параметров, позволяющих настроить способ ее работы для вашей организации. Однако с помощью PowerShell вы получите доступ к большему количеству параметров настройки. Например, стандартные конференции в Skype настроены так, что:
  • анонимные пользователи могут автоматически войти в каждую конференцию;
  • участники могут вести запись конференции;
  • все пользователи вашей организации могут быть назначены докладчиками.

Чтобы изменить эти стандартные настройки, вы можете использовать Powershell. Вот команда для отключения всех трех вышеперечисленных параметров:
1.Set-CsMeetingConfiguration -AdmitAnonymousUsersByDefault $False -AllowConferenceRecording $False -DesignateAsPresenter "None"


Если вы хотите сбросить настройки до значений по умолчанию, используйте следующую команду:
1.Set-CsMeetingConfiguration -AdmitAnonymousUsersByDefault $True -AllowConferenceRecording $True -DesignateAsPresenter "Company"


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

2. Управление членством в группах Office 365 с помощью PowerShell


PowerShell имеет несколько командлетов, специально предназначенными для работы с группами Office 365. Например:
Выполните команду, чтобы просмотреть список всех активных групп в Office 365.
1.Get-MsolGroup

Эта команда также предоставит вам шестнадцатеричный идентификатор для каждой группы, который вам понадобится для управления членством.
Для добавления и удаления членов группы вам также понадобится шестнадцатеричный идентификатор их учетных записей, который можно получить с помощью команды:
1.Get-MsolUser | Select ObjectID.

Затем вы можете запустить соответствующий командлет для добавления или удаления пользователей из определенных групп. Чтобы добавить пользователей, выполните следующую команду, заменив указанные в примере шестнадцатеричные идентификаторы на идентификаторы, относящиеся к вашей группе и нужному пользователю:
1.Add-MsolGroupMember -GroupObjectId 5b61d9e1-a13f-4a2d-b5ba-773cebc08eec -GroupMemberObjectId a56cae92-a8b9-

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


Как Varonis взаимодействует с PowerShell


Varonis дополняет использование PowerShell несколькими способами.

Мониторинг


Varonis отслеживает и проверяет активность в Office 365 (включая изменения конфигурации) и команды PowerShell. Это позволяет вам отслеживать любые изменения, которые администраторы или злоумышленники вносят с помощью PowerShell.
Администраторы должны вносить изменения в конфигурации или разрешения Office 365, имея действующий запрос на изменение. Этот дополнительный уровень проверки обеспечивает бесперебойную работу процессов и процедур.
Злоумышленники пытаются использовать PowerShell для отключения мер безопасности или повышения прав учетной записи. Varonis улавливает эти изменения и отслеживает любые другие действия, которые злоумышленники совершают в Office 365.

Получайте оповещения и принимайте меры


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

Заключение


PowerShell является эффективным инструментом для работы с Office 365. Он позволяет быстро получать доступ к информации из системы, составлять подробные отчеты и выполнять массовые действия. Кроме того, с его помощью можно получить доступ к определенным функциям Office 365, которые недоступны другим способом.
Подключить Powershell к Office 365 относительно просто, и в результате вы получите доступ ко всем перечисленным выше расширенным функциям. Это также позволит вам более легко интегрировать вашу среду Office 365 с платформой кибербезопасности Varonis и обеспечить безопасность конфиденциальных данных.
Подробнее..

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

15.04.2021 18:16:38 | Автор: admin

Pedro Tavares

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


Описание угрозы

Семейство вредоносного ПО "работает" с июня 2020 года и атакует организации из разных стран с применением фишинговых сообщений электронной почты, содержащих изменённый файл Excel. Чтобы фишинговые сообщения не попадали в папки со спамом и против них не срабатывали механизмы отсечения нежелательной почты, злоумышленники отправляют свои письма с официальных учётных записей организаций. Учётные данные таких организаций, как правило, попадают в руки злоумышленников в результате взлома. Злоумышленники с помощью сервиса проверки аккаунтов на утечки "Have I Been Pwned?" проверяют, были ли скомпрометированы учётные записи электронной почты, или просто взламывают такие записи до того, как приступить к вредоносной рассылке.

Рис. 1. Пример фишингового электронного письма, рассылаемого вредоносным ПО Epic Manchego.Рис. 1. Пример фишингового электронного письма, рассылаемого вредоносным ПО Epic Manchego.

Согласно данным NVISO, "через VirusTotal было пропущено около 200 вредоносных документов, и был составлен список из 27 стран, ранжированных по количеству отправленных документов. В списке не делалось различие, каким способом загружались такие файлы (возможно, через VPN)".

В ходе исследования выяснилось, что наибольшему риску рассылки вредоносных файлов подвергаются такие страны, как Соединённые Штаты Америки, Чешская Республика, Франция, Германия и Китай.

Рис. 2. Целевые регионы, выявленные в ходе исследования угроз с помощью VirusTotal.Рис. 2. Целевые регионы, выявленные в ходе исследования угроз с помощью VirusTotal.

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

Рис. 3. Другие шаблоны электронных писем, рассылаемых вредоносным ПО Epic Manchego.Рис. 3. Другие шаблоны электронных писем, рассылаемых вредоносным ПО Epic Manchego.

Как работает Epic Manchego

В некоторых рассылаемых документах Office содержатся нарисованные фигуры, например прямоугольники, как это показано на рисунке 4.

Рис. 4. Прямоугольник внутри файла Excel c вредоносной полезной нагрузкой.Рис. 4. Прямоугольник внутри файла Excel c вредоносной полезной нагрузкой.

Вредоносные документы Microsoft Office создаются не через Microsoft Office Excel, а с использованием .NET библиотеки EPPlus. Поскольку такие документы не являются стандартными документами Excel, они могут маскироваться и обходить защитные механизмы.

Документ на рисунке 4 содержит объект drawing1.xml (скруглённый прямоугольник) с именем name="VBASampleRect и создан с использованием исходного кода EPPLUS Wiki (справа), как это показано ниже.

Рис. 5. Код прямоугольника Excel и код прямоугольника EPPlus.Рис. 5. Код прямоугольника Excel и код прямоугольника EPPlus.

Если открыть окно макросов документа, в нём не будет ни одного макроса.

Рис. 6. На первый взгляд никаких макросов в документе нет.Рис. 6. На первый взгляд никаких макросов в документе нет.

Тем не менее вредоносный код существует и к тому же защищён паролем. Интересно отметить, что этот код VBA вообще не зашифрован, а представлен открытым текстом.

При открытии документа с VBA-проектом, защищённым паролем, макросы VBA будут запускаться без пароля. Пароль необходим только для просмотра проекта VBA внутри интегрированной среды разработки (IDE) VBA.

Рис. 7. Пароль необходим только для отображения кода VBA внутри вредоносного кода.Рис. 7. Пароль необходим только для отображения кода VBA внутри вредоносного кода.

Если внести изменения в строку DPB или дешифровать пароль, можно увидеть, что при запуске вредоносного файла Office на компьютере жертвы запускается и выполняется полезная нагрузка PowerShell.

Рис. 8. Строка DPB вредоносного файла .doc.Рис. 8. Строка DPB вредоносного файла .doc.

На приведённом ниже скриншоте продемонстрирован запуск полезной нагрузки PowerShell во время заражения.

Согласно результатам исследования NVISO Labs, чтобы загрузить полезную нагрузку в коде VBA, используются либо объекты PowerShell, либо объекты ActiveX, в зависимости от разновидности исходного вредоносного ПО.

Анализ завершающего этапа работы вредоносного ПО

Через вредоносный код VBA на втором этапе с различных интернет-сайтов загружается полезная нагрузка. Каждый исполняемый файл, создаваемый соответствующим вредоносным документом и запускаемый на втором этапе, выступает для конечной полезной нагрузки как носитель вируса (дроппер). После этого на втором этапе также загружается вредоносный файл DLL. Этот компонент DLL формирует дополнительные параметры и полезную нагрузку для третьего этапа, после чего запускает на выполнение конечную полезную нагрузку, которая, как правило, крадёт информацию.

Рис. 9. Действия Epic Manchego на последнем этапе.Рис. 9. Действия Epic Manchego на последнем этапе.

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

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

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

Чаще всего (более чем в 50% случаев) на компьютер жертвы устанавливается вредоносная программа AZORult, похищающая личные данные пользователей, программы для кражи информации называются инфостилерами. В качестве других полезных нагрузок могут использоваться трояны AgentTesla, Formbook, Matiex и njRat, причем Azorult и njRAT имеют высокий уровень повторного использования.

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

Обнаружение и действия

Для запуска вредоносных программ злоумышленники придумывают новые методы обхода систем обнаружения угроз и реакции на конечных точках (EDR) и антивирусных программ (AV). При использовании нового способа создания вредоносных документов Office механизмы обнаружения угроз не должны позволять вредоносному ПО переходить на следующий этап. Часто на этом этапе запускается скрипт PowerShell, который может выполняться в памяти без обращения к диску.

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

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

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

  • Использовать решения защиты конечных точек (Endpoint Protection) и обновлённое антивирусное ПО для предотвращения заражения вредоносными программами.

  • Использовать системы управления уязвимостями и мониторинга для выявления потенциальных неустранённых уязвимостей и инцидентов в режиме реального времени.

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

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

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

Другие профессии и курсы

Источники:

Подробнее..

Перевод Если вы пишете код в 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 можно здесь.

Подробнее..

Представляем 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.

Подробнее..

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 и использовать как полноценное приложение

Подробнее..

Извлечь максимум из новостей в интернете, часть 1

04.04.2021 22:19:50 | Автор: admin

Извлечь максимум из новостей в интернете

Навеяно статьей Почему я по-прежнему пользуюсь RSS

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

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

Будут следующие части:

  1. Часть 1:

  • Какую информацию я вообще потребляю через новости;

  • Чтение программами (rss-агрегаторами) - что использую лично я;

  • Форматы RSS и Atom, как их можно обрабатывать программами на локальном компьютере;

  1. Часть 2:

  • Автоматизаторы (zapier, ifttt);

  1. Часть 3:

  • Как я автоматизировал доставку аудио-подкастов на свой плеер.

Часть 1

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

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

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

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

Чтение программами (rss-агрегаторами)

Чтобы читать новости на планшете, можно использовать любую подходящую программу. Благо, их очень много - для Андроида, для Apple и для десктопов.

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

Для десктопа я раньше использовал RSS Owl:

RSS OwlRSS Owl

А потом с удивлением открыл для себя продукт от JetBrains: OMEA Reader:

JetBrains OMEA ReaderJetBrains OMEA Reader

Функционал у продукта JetBrains просто космический, поэтому как только я на него перешёл, то уже и не уходил:

  • можно ставить комментарии к новостям

  • можно фильтровать новости и по отборам назначать тэги, выделять цветом или выдавать оповещения на экране

  • иерархическая группировка лент новостей

  • и многое-многое другое.

Форматы RSS и Atom, как их можно обрабатывать программами на локальном компьютере

Новости, по сути - это простой xml-файл. Есть формат RSS (очень старый формат, непонятно кто его поддерживает и будет ли он развиваться). Мне он не очень нравится:

  • из-за формата дат - <pubDate>Sun, 28 May 2017 09:00:00 GMT</pubDate>, что затрудняло для меня автоматическую обработку

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

На замену RSS фирма Google придумала свой формат - Atom, и вроде бы даже поддерживает его.

Файлы из интернета, в том числе и новости, можно читать с помощью curl, wget и (почему-бы и нет?) с помощью Power-shell.

Пока писал статью, с удивлением узнал, что в Windows 10 curl уже есть из коробки и не надо ничего скачивать и настраивать. Приятно.

Curl:

chcp 65001    curl ^    --header "user-agent: cURL automated task" ^    --output "%TEMP%\updates.xml" ^    "https://news.webits.1c.ru/news/Updates/atom"

Если на компьютере включен Антивирус Касперского, то можно получить ошибку curl: (35) schannel: next InitializeSecurityContext failed: Unknown error (0x80092012) - Функция отзыва не смогла произвести проверку отзыва для сертификата. Я просто отключил антивирус на 5 минут.

Файл записывается без BOM, что может вызвать проблемы с дальнейшей обработкой. Наверное это как-то настроено на сервере.

Power-shell:

# file: Get-News-001.ps1  Clear-Host    $webClient = New-Object Net.WebClient  $webClient.UseDefaultCredentials = $true  $webClient.Proxy.Credentials = $webClient.Credentials  $webClient.Headers.Add("user-agent", "PowerShell automated task")    # Подозреваю, что из-за того, что данные передаются без BOM, то получение данных  # через DownloadString с последующим выводом выдаст на экран кракозябры.  # Поэтому в явном виде преобразуем в UTF8  $newsData = $webClient.DownloadData("https://news.webits.1c.ru/news/Updates/atom")    Write-Host ([System.Text.Encoding]::UTF8).GetString($newsData)

Не забываем, что в power-shell надо включить возможность запуска неподписанных макросов. Это делается или в настройках вашей IDE, или прямо в командной строке, параметром -ExecutionPolicy=RemoteSigned

powershell -file "Get-News-001.ps1" -ExecutionPolicy=RemoteSigned

Что нам это даст? Пока ничего интересного. Но так как новости - это структурированный текст в формате xml, почему-бы не обработать его? Например, найти новость по значению какой-нибудь категории?

Найдём в новостях от 1С новости со значениями категорий Вид новости обновления=Публикация новой версии и Продукт=Комплексная автоматизация

Пример кода:

# file: Get-News-002.ps1Clear-Host# Настройки отбора, в виде массива$CategoryProducts = @(    # "Продукт=1С:Библиотека стандартных подсистем", # Заполнить!    "Продукт=Комплексная автоматизация" # Заполнить!)$CategoryNewsTypes = @(    "Вид новости обновлений=Публикация новой версии")$webClient = New-Object Net.WebClient$webClient.UseDefaultCredentials = $true$webClient.Proxy.Credentials = $webClient.Credentials$webClient.Headers.Add("user-agent", "PowerShell automated task")# Т.к. данные без BOM, то лучше явно преобразовать.$newsData = $webClient.DownloadData("https://news.webits.1c.ru/news/Updates/atom")[xml]$news = ([System.Text.Encoding]::UTF8).GetString($newsData)#[xml]$news = Get-Content -Encoding UTF8 -LiteralPath "$($env:TEMP)\updates.xml"for($c1=0;$c1 -lt $news.feed.entry.Count;$c1++){    # Получим новость    $entry = $news.feed.entry[$c1]    $ProductName    = ""    $bFoundProduct  = $false    $bFoundNewsType = $false    for($c2=0;$c2 -lt $entry.category.Count;$c2++){        # Получим и проверим категории новости        $CategoryProducts | ForEach-Object {            if($entry.category[$c2].term -eq $_){                $ProductName = $entry.category[$c2].term                $bFoundProduct = $true            }        }        $CategoryNewsTypes | ForEach-Object {            if($entry.category[$c2].term -eq $_){                $bFoundNewsType = $true            }        }    }    if ($bFoundProduct -and $bFoundNewsType) {        Write-Host ("Найдена подходящая новость. УИН: {0}, Заголовок: {1}" -f ($entry.id, $entry.title))    }}

Уже интереснее. А что мы можем сделать, если нашли нужную категорию? Например, можно самому себе отправить письмо.

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

# file: Get-News-003.ps1Clear-Host# Файл с информацией об отправленных email.$sendedEmailsPath = "$($env:TEMP)\sended.csv" # Заполнить!if(Test-Path $sendedEmailsPath){    # Файл существует} else {    # Файла не существует - создать пустой файл    Add-Content -LiteralPath $sendedEmailsPath -Encoding UTF8 -Force -Value ""}$sendedEmails = Get-Content -LiteralPath $sendedEmailsPath -Encoding UTF8 -Force# Настройки почты$CurrentDate        = Get-Date $CurrentDate_String = Get-Date -Format "yyyy-MM-dd HH:mm:ss"$From               = "news_center_tester@mail.ru" # Заполнить!$To                 = "old-coder-75@mail.ru" # Заполнить!$EncodingUTF8       = [System.Text.Encoding]::UTF8$UserName           = "news_center_tester" # Заполнить!$Password           = "*****" # Заполнить!$Credential         = New-Object -TypeName System.Management.Automation.PSCredential($UserName, (ConvertTo-SecureString $Password -AsPlainText -Force))$SMTPServer         = "smtp.mail.ru" # Заполнить!$SMTPPort           = 587 # Заполнить!# Настройки отбора, в виде массива$CategoryProducts = @(    # "Продукт=1С:Библиотека стандартных подсистем", # Заполнить!    "Продукт=Комплексная автоматизация" # Заполнить!)$CategoryNewsTypes = @(    "Вид новости обновлений=Публикация новой версии")$webClient = New-Object Net.WebClient$webClient.UseDefaultCredentials = $true$webClient.Proxy.Credentials = $webClient.Credentials$webClient.Headers.Add("user-agent", "PowerShell automated task")$newsData = $webClient.DownloadData("https://news.webits.1c.ru/news/Updates/atom")[xml]$news = ([System.Text.Encoding]::UTF8).GetString($newsData)#[xml]$news = Get-Content -Encoding UTF8 -LiteralPath "$($env:TEMP)\updates.xml"for($c1=0;$c1 -lt $news.feed.entry.Count;$c1++){    # Получим новость    $entry = $news.feed.entry[$c1]    $ProductName    = ""    $bFoundProduct  = $false    $bFoundNewsType = $false    for($c2=0;$c2 -lt $entry.category.Count;$c2++){        # Получим и проверим категории новости        $CategoryProducts | ForEach-Object {            if($entry.category[$c2].term -eq $_){                $ProductName = $entry.category[$c2].term                $bFoundProduct = $true            }        }        $CategoryNewsTypes | ForEach-Object {            if($entry.category[$c2].term -eq $_){                $bFoundNewsType = $true            }        }    }    if ($bFoundProduct -and $bFoundNewsType) {           Write-Host ("Найдена подходящая новость. УИН: {0}, Заголовок: {1}" -f ($entry.id, $entry.title))        # Проверим, была ли информация уже отправлена по email?        # Информация об отправке email хранится по каждому id новости в лог-файле $sendedEmailsPath.        $bEmailWasSent = $false        foreach ($sendedEmail in $sendedEmails) {            if ( $sendedEmail.StartsWith($entry.id) ) {                $bEmailWasSent = $true                break            }        }        # По этой новости ещё не создавался email.        if ($bEmailWasSent -eq $false){            Write-Host "По подходящей новости отправляем емейл..."            # Отправить почту            $Subject = $entry.title            $Body = "<h1>Выход новой версии</h1>" + `                "<p>" + `                $entry.summary."#cdata-section" + `                "</p>"            Send-MailMessage `                -From $From `                -To $To `                -Body $Body `                -BodyAsHtml `                -Credential $Credential `                -Encoding $EncodingUTF8 `                -SmtpServer $SMTPServer `                -Subject $Subject `                -Priority High `                -UseSsl `                -Port $SMTPPort `                -Verbose                            # Записать в лог, чтобы повторно не отправлять почту по этой новости            $LogString = $entry.id + ";" + $CurrentDate_String + ";"            Add-Content -LiteralPath $sendedEmailsPath -Encoding UTF8 -Force -Value $LogString        } else {               Write-Host "По подходящей новости уже был отправлен емейл."        }    }}

Хм. Не хочется вызывать скрипт ручками. Было бы неплохо это вызывать регулярно, например каждые 2 часа. Настроим "Планировщик заданий". В окне запуска (Win+R) вызовем "taskschd.msc".

Добавим новое расписание:

Настроим частоту запуска каждые 4 часа (триггер):

Добавим запуск скрипта (Действие):

Путь к программе powershell.exe надо указывать полный. И в аргументах - или полный путь к скрипту в параметре -file, или заполнить Рабочая папка.

Немного тюнинга - не люблю всплывающие окна. А при выполнении задания по расписанию, каждые 4 часа вылезает на весь экран окно выполнения скрипта. Можно запускать код power-shell через SilentCMD или CreateProcessHidden. Правда, на последнюю ругается антивирус, но не сильно.

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

Что ж, надеюсь, что информация оказалась полезной.
С интересом почитаю комментарии.

Остальные главы постараюсь в ближайшее время выложить.

Подробнее..

Начало работы с Windows Terminal

22.12.2020 10:22:28 | Автор: admin
Привет, Хабр! Сегодня делимся гайдом по началу работы с Windows Terminal. Да, поскольку он о начале работы с инструментом, в основном в материале описываются какие-то базовые моменты. Но я думаю, что и профессионалы смогут подчерпнуть для себя что-то полезное, как минимум из списка полезных ссылок в конце статьи. Заглядывайте под кат!



Установка


Windows Terminal доступен в двух разных сборках: Windows Terminal и Windows Terminal Preview. Обе сборки доступны для загрузки в Microsoft Store и на странице выпусков GitHub.

Требования


Для запуска любой сборки Windows Terminal на вашем компьютере должна быть установлена Windows 10 1903 или более поздняя версия.

Windows Terminal Preview


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

Windows Terminal


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

Первый запуск


После установки терминала вы можете запустить приложение и сразу приступить к работе с командной строкой. По умолчанию терминал включает профили Windows PowerShell, Command Prompt и Azure Cloud Shell в раскрывающемся списке. Если на вашем компьютере установлены дистрибутивы Подсистемы Windows для Linux (WSL), они также должны динамически заполняться как профили при первом запуске терминала.

Профили


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



Дефолтный профиль


При первом запуске Windows Terminal в качестве профиля по умолчанию устанавливается Windows PowerShell. Профиль по умолчанию это профиль, который всегда открывается при запуске терминала, и это профиль, который открывается при нажатии кнопки новой вкладки. Вы можете изменить профиль по умолчанию, установив defaultProfile на имя вашего предпочтительного профиля в файле settings.json.

"defaultProfile": "PowerShell"

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


Новые профили можно добавлять динамически с помощью терминала или вручную. Терминал Windows автоматически создаст профили для распределений PowerShell и WSL. Эти профили будут иметь свойство source, которое сообщает терминалу, где он может найти соответствующий исполняемый файл.

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

Примечание. Вы не сможете скопировать свойство source из динамически созданного профиля. Терминал просто проигнорирует этот профиль. Вам нужно будет заменить source на commandline и предоставить исполняемый файл, чтобы дублировать динамически созданный профиль.

Структура Settings.json


В Терминал Windows включены два файла настроек. Один из них defaults.json, который можно открыть, удерживая клавишу Alt и нажав кнопку Настройки в раскрывающемся списке. Это неизменяемый файл, который включает в себя все настройки по умолчанию, которые поставляются с терминалом. Второй файл settings.json, в котором вы можете применить все свои пользовательские настройки. Доступ к нему можно получить, нажав кнопку Настройки в раскрывающемся меню.

Файл settings.json разделен на четыре основных раздела. Первый это объект глобальных настроек, который находится в верхней части файла JSON внутри первого {. Примененные здесь настройки повлияют на все приложение.

Следующим основным разделом файла является объект profiles. Объект profiles разделен на два раздела: defaults и list. Вы можете применить настройки профиля к объекту defaults, и они будут применяться ко всем профилям в вашем list. list содержит каждый объект профиля, который представляет профили, описанные выше, и это элементы, которые появляются в раскрывающемся меню вашего терминала. Настройки, примененные к отдельным профилям в списке, имеют приоритет над настройками, примененными в разделе defaults.

Далее в файле расположен массив schemes. Здесь можно разместить собственные цветовые схемы. Отличный инструмент, который поможет вам создать свои собственные цветовые схемы, это terminal.sexy.

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

Базовая кастомизация


Вот несколько основных настроек, которые помогут вам начать настройку вашего терминала.

Фон


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

"backgroundImage": "C:\Users\admin\background.png"

Параметр backgroundImage принимает расположение файла изображения, которое вы хотите использовать в качестве фона вашего профиля. Допустимые типы файлов: .jpg, .png, .bmp, .tiff, .ico и .gif.



Цветовая схема


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

"colorScheme": "COLOR SCHEME NAME"

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

Начертание шрифта


По умолчанию Windows Terminal использует Cascadia Mono в качестве шрифта. Начертание шрифта это настройка уровня профиля. Вы можете изменить шрифт, установив fontFace на имя шрифта, который вы хотите использовать.

"fontFace": "FONT NAME"`

Совет: Терминал Windows также поставляется с начертанием шрифта Cascadia Code, который включает программные лигатуры (см. Gif ниже). Если вы используете Powerline, Cascadia Code также поставляется в PL-версии, которую можно загрузить с GitHub.



Полезные ресурсы


Докуметация Windows Terminal
Скотт Хансельман: как сделать красивым Windows Terminal с помощью Powerline, шрифтов Nerd, кода Cascadia, WSL и oh-my-posh
Скотт Хансельман: Как настроить терминал с помощью Git Branch, Windows Terminal, PowerShell, + Cascadia Code!
Скотт Хансельман: Windows Terminal Feature PREVIEW Кастомизируйте свои привязки клавиш, цветовые схемы, панели, и многое другое!
>_TerminalSplash темы Windows Terminal
Подробнее..

Закрепляем ярлыки на начальном экране в Windows 10 у текущего пользователя

06.04.2021 10:06:21 | Автор: admin

Эта статья своего рода proof of concept, как можно закрепить программно (открепить) ярлык на начальном экране для текущего пользователя без перезапуска или выхода из учетной записи. Как вы знаете, с выходом 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, конечно, отдает глагол контекстного меню Закрепить на начальном &экране, но не может его выполнить.

Где-то читал, что, возможно, Microsoft заблокировал доступ в целях недопущения закрепления ярлыков bloatware. Звучит странно, но ладно

Я уже много лет поддерживаю крупнейший PowerShell-модуль для тонкой настройки Windows 10 и автоматизации рутинных задач. Подробнее можно почитать здесь. И встала задача из спортивного интереса обойти это ограничение и попробовать закрепить нужные мне ярлыки. Речь, конечно, идет по большей части о домашних пользователях, ведь в энтерпрайзе используется GPO для импорта предзаготовленного макета начального экрана и панели задач.

Мы знаем, что текущий макет начального экрана можно выгрузить в формате 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}")}

Страница GitHub Windows 10 Sophia Script, где в том числе используется данный метод.

Огромное спасибо iNNOKENTIY21 за помощь в реализации метода.

Подробнее..

Автоматизируем ведение большого количества пользователей в AD

21.04.2021 20:21:36 | Автор: admin
Автоматизируем ведение большого количества пользователей в AD:

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

Имеем два территориально распределённых домена AD по 10 000 человек, применённое решение по организации Веб-доступа к удаленным рабочим столам через приложения RemoteApp с несколькими интегрированными информационными системами и активно пополняющиеся база, человек так на 500 в месяц. На ~24 в рабочий день, на ~3 человека в час.

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

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

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

Но процесс можно немного автоматизировать, применив пару нехитрых скриптов. Логика сводится к обратному процессу:
1) Утверждаем стандарт внесения Учётных Записей в AD на предприятии
2) Запрашиваем у пользователя данные едином формате.
image
3) Вносим в таблицу основные данные, например:
4) Экспортируем из Excel в CSV файл, автоматически сгенерированную страницу, пригодную для автоматического занесения в AD при помощи скриптов
5) Экспортируем и вуаля! Остаётся передать логин и пароль пользователю.
Возможно описанные мной методы нельзя назвать best practice, однако они позволяют на практике решить существующую проблему без написания отдельно информационной системы и создания большого количества интеграций.

Далее я опишу пару технических моментов и опубликую скрипты которыми пользуюсь:
Так выглядит таблица пригодная для импорта в AD:

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

Как вы думаете какими будут разделители если открыть сгенерированный файл блокнотом? Неправильно. Такими ;

Отдельно в моей реализации следует остановиться на столбце транслит. В утверждённом нами стандарте часть полей заполняется транслитом по утверждённому образцу и чтобы не делать это каждый раз я использовал vba скрипт, вот он:
Function TranslitText(RusText As String) As String    Dim RusAlphabet As Variant 'массив из букв русского алфавита    RusAlphabet = Array("-", "а", "б", "в", "г", "д", "е", "ё", "ж", "з", "и", "й", "к", "л", "м", "н", "о", "п", "р", "с", "т", "у", "ф", "х", "ц", "ч", "ш", "щ", "ъ", "ы", "ь", "э", "ю", "я", "А", "Б", "В", "Г", "Д", "Е", "Ё", "Ж", "З", "И", "Й", "К", "Л", "М", "Н", "О", "П", "Р", "С", "Т", "У", "Ф", "Х", "Ц", "Ч", "Ш", "Щ", "Ъ", "", "Ь", "Э", "Ю", "Я")     Dim EngAlphabet As Variant 'массив из букв английского алфавита    EngAlphabet = Array("-", "a", "b", "v", "g", "d", "e", "yo", "zh", "z", "i", "y", "k", "l", "m", "n", "o", "p", "r", "s", "t", "u", "f", "kh", "ts", "ch", "sh", "sch", "", "y", "", "e", "yu", "ya", "A", "B", "V", "G", "D", "E", "Yo", "Zh", "Z", "I", "Y", "K", "L", "M", "N", "O", "P", "R", "S", "T", "U", "F", "Kh", "Ts", "Ch", "Sh", "Sch", "", "Y", "", "E", "Yu", "Ya")         Dim EngText As String, Letter As String, Flag As Boolean                 For i = 1 To Len(RusText) 'цикл по всем символам русского текста        Letter = Mid(RusText, i, 1)        Flag = 0        For j = 0 To 67 'цикл по всем буквам русского алфавита            If RusAlphabet(j) = Letter Then 'если символ из текста совпал с буквой из русского алфавита...                Flag = 1                If RusAlphabet(j) = Letter Then 'проверка на регистр (верхний или нижний)                    EngText = EngText & EngAlphabet(j) '... то добавляем соответствующую букву из английского алфавита                    Exit For                Else                    EngText = EngText & UCase(EngAlphabet(j))                    Exit For                End If            End If        Next j        If Flag = 0 Then EngText = EngText & Letter 'если символа из текста в алфавите нет (например, знаки препинания и т.п.), то добавляем символ без изменения    Next i    TranslitText = EngTextEnd Function


Не делайте как я, пожалуйста, используйте один из существующих стандартов транслитерации по ссылке habr.com/ru/post/499574

Следующий же скрипт помещённый в файл с расширением .ps1 позволит вам в пару кликов закинуть все учётные записи из сгенерированного на предыдущем шаге файла в AD, как бы много их там не было. А заодно и навесить на все созданные УЗ группу ad-group.
Import-Module activedirectory Import-Csv "C:\generated.csv" -Encoding default -Delimiter ';'| ForEach-Object {New-ADUser -Server DOMEN.RU -Name $_.FirstName `-DisplayName $_.DisplayName `-GivenName $_.GivenName `-Surname $_.LastName `-Initials $_.Initials `-OfficePhone $_.Phone `-Description $_.Description `-UserPrincipalName $_.UserPrincipalName `-SamAccountName $_.samAccountName `-Email $_.mail `-Path "OU=TEST_OU,OU=Guest,OU=Users,OU=DOMEN,DC=DOMEN,DC=RU" `-AccountPassword (ConvertTo-SecureString $_.Password -AsPlainText -force) -Enabled $true Set-ADuser $_.samAccountName -ChangePasswordAtLogon $True Add-AdGroupMember -Identity ad-group  -Members $_.samAccountName} 
Подробнее..

Настройки 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, настраивайте её с умом и до новых встреч!

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

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

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

Книга PowerShell для сисадминов

27.05.2021 16:21:56 | Автор: admin
image Привет, Хаброжители! PowerShell это одновременно язык сценариев и командная оболочка, которая позволяет управлять системой и автоматизировать практически любую задачу. В книге PowerShell для сисадминов обладатель Microsoft MVP Адам Бертрам aka the Automator покажет, как использовать PowerShell так, чтобы у читателя наконец-то появилось время на игрушки, йогу и котиков. Вы научитесь: -Комбинировать команды, управлять потоком выполнения, обрабатывать ошибки, писать сценарии, запускать их удаленно и тестировать их с помощью фреймворка тестирования Pester. -Анализировать структурированные данные, такие как XML и JSON, работать с популярными сервисами (например Active Directory, Azure и Amazon Web Services), создавать системы мониторинга серверов. -Создавать и проектировать модули PowerShell. -Использовать PowerShell для удобной, полностью автоматизированной установки Windows. -Создавать лес Active Directory, имея лишь узел Hyper-V и несколько ISO-файлов. -Создавать бесчисленные веб- и SQL-серверы с помощью всего нескольких строк кода! Реальные примеры помогают преодолеть разрыв между теорией и работой в настоящей системе, а легкий авторский юмор упрощает чтение. Перестаньте полагаться на дорогое ПО и невнятные советы из сети!

Для кого эта книга
Эта книга предназначена для ИТ-специалистов и системных администраторов, которым надоело постоянно использовать один и тот же интерфейс и выполнять одну и ту же задачу в пятисотый раз за этот год. Также она будет полезна для инженеров DevOps, которые испытывают затруднения с автоматизацией новых серверных сред, выполнением автоматических тестов или автоматизацией конвейера непрерывной интеграции / непрерывной доставки (CI/CD).

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

Поток управления


Немного повторим. В главе 3 мы узнали, как можно комбинировать команды с помощью конвейера и внешних сценариев. В главе 2 рассмотрели переменные и как их использовать для хранения значений. Одним из основных преимуществ работы с переменными является возможность писать с их помощью код, который работает не со значением, а со смыслом. Вместо того чтобы работать, например, с числом 3, вы будете работать с общим понятием $serverCount. За счет этого вы можете писать код, который работает одинаково, будь у вас один, два или тысяча серверов. Совместите эту способность с возможностью сохранять код в сценариях, которые можно запускать на разных компьютерах, и вы сможете начать решать задачи гораздо большего масштаба.

Однако в жизни порой имеет значение, работаете ли вы с одним сервером, с двумя или с тысячей. Пока что у вас нет подходящего способа учитывать это, и ваши сценарии работают просто сверху вниз, не имея возможности адаптироваться в зависимости от определенных значений. В этой главе мы будем использовать поток управления и условную логику для написания сценариев, которые будут выполнять различные команды в зависимости от значений, с которыми они работают. К концу главы вы узнаете, как использовать операторы if/then и switch, а также различные циклы, чтобы придать вашему коду столь необходимую гибкость.

Немного о потоке управления

Мы напишем сценарий, который считывает содержимое файла, хранящегося на нескольких удаленных компьютерах. Чтобы продолжить работу, загрузите файл под названием App_configuration.txt из прилагаемых к книге материалов по ссылке github.com/adbertram/PowerShellForSysadmins/ и поместите его в корень диска C:\ на нескольких удаленных компьютерах. Если у вас нет удаленных компьютеров, пока просто продолжайте читать. В этом примере я буду использовать серверы с именами SRV1, SRV2, SRV3, SRV4 и SRV5.

Чтобы получить доступ к содержимому файла, воспользуемся командой Get-Content и укажем путь к файлу в значении аргумента параметра Path, как показано ниже:

Get-Content -Path "\\servername\c$\App_configuration.txt"

Для начала сохраним все имена наших серверов в массиве и запустим эту команду для каждого сервера. Откройте новый файл .ps1 и введите в него код из листинга 4.1.

Листинг 4.1. Извлечение содержимого файла с нескольких серверов

$servers = @('SRV1','SRV2','SRV3','SRV4','SRV5')Get-Content -Path "\\$($servers[0])\c$\App_configuration.txt"Get-Content -Path "\\$($servers[1])\c$\App_configuration.txt"Get-Content -Path "\\$($servers[2])\c$\App_configuration.txt"Get-Content -Path "\\$($servers[3])\c$\App_configuration.txt"Get-Content -Path "\\$($servers[4])\c$\App_configuration.txt"


Теоретически, этот код должен работать без проблем. Но в этом примере предполагается, что у вас что-то идет не так. Что делать, если сервер SRV2 не работает? А если кто-то забыл положить App_configuration.txt на SRV4? А может, кто-то изменил путь к файлу? Вы можете написать отдельный сценарий для каждого сервера, но это решение не будет масштабироваться, особенно когда вы начнете добавлять все больше и больше серверов. Вам нужен код, который будет работать в зависимости от ситуации.

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

Мы начнем с рассмотрения самого простого типа потока управления условного оператора.

Использование условных операторов

В главе 2 мы узнали, что существуют логические значения: истина и ложь. Логические значения позволяют создавать условные операторы, которые ставят задачу PowerShell выполнить определенный блок кода в зависимости от того, имеет ли выражение (называемое условием) значение True или False. Условие это вопрос с вариантами ответов да/нет. У вас больше пяти серверов? Работает ли сервер 3? Существует ли путь к файлу? Чтобы начать использовать условные операторы, давайте посмотрим, как преобразовать такие вопросы в выражения.

Построение выражений с помощью операторов

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

PS> 1 eq 1
True

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

-eq сравнивает два значения и возвращает True, если они равны.

-ne сравнивает два значения и возвращает True, если они не равны.

-gt сравнивает два значения и возвращает True, если первое больше второго.

-ge сравнивает два значения и возвращает True, если первое больше или равно второму.

-lt сравнивает два значения и возвращает True, если первое меньше второго.

-le сравнивает два значения и возвращает True, если первое меньше или равно второму.

-contains возвращает True, если второе значение является частью первого. Например, этот оператор позволяет определить, находится ли значение внутри массива.

В PowerShell есть и более продвинутые операторы сравнения. Здесь мы не будем на них останавливаться, но я рекомендую вам почитать о них в документации Microsoft по ссылке docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_comparison_operators или в разделе справки PowerShell (см. главу 1).

Вы можете использовать приведенные выше операторы для сравнения переменных и значений. Но выражение не обязательно должно быть сравнением. Иногда команды PowerShell можно использовать как условия. В предыдущем примере мы хотели узнать доступность сервера. С помощью командлета Test-Connection можно проверить наличие связи с сервером. Обычно в выходных данных командлета Test-Connection содержится много разной информации, но с помощью параметра Quiet вы можете заставить команду вернуть True или False, а с помощью параметра Count можно ограничить тест одной попыткой.

PS> Test-Connection -ComputerName offlineserver -Quiet -Count 1
False

PS> Test-Connection -ComputerName onlineserver -Quiet -Count 1
True

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

PS> -not (Test-Connection -ComputerName offlineserver -Quiet -Count 1)
True

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

Оператор if

Оператор if работает просто: если выражение X истинно, то сделайте Y. Вот и все!

Чтобы использовать оператор в выражении, пишется ключевое слово if, за которым следуют круглые скобки, содержащие условие. После выражения следует блок кода, выделенный фигурными скобками. PowerShell выполнит этот блок кода только в том случае, если это выражение будет иметь значение True. Если выражение if имеет значение False либо вообще ничего не возвращает, блок кода не будет выполнен. Синтаксис оператора if/then показан в листинге 4.2.

Листинг 4.2. Синтаксис оператора if

if (условие) {   # выполняемый код, если условие истинно}


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

Теперь давайте еще раз посмотрим на код, показанный в листинге 4.1. Я расскажу вам о том, как использовать оператор if, чтобы не пытаться достучаться до неработающего сервера. В предыдущем разделе мы уже видели, что команду Test-Connection можно использовать в качестве выражения, которое возвращает True или False, поэтому сейчас давайте упакуем Test-Connection в оператор if, а затем воспользуемся командой Get-Content, чтобы не пытаться обращаться к неработающему серверу. Сейчас мы поменяем код только для первого сервера, как показано в листинге 4.3.

Листинг 4.3. Использование оператора if для выборочного обращения

$servers = @('SRV1','SRV2','SRV3','SRV4','SRV5')if (Test-Connection -ComputerName $servers[0] -Quiet -Count 1) {   Get-Content -Path "\\$($servers[0])\c$\App_configuration.txt"}Get-Content -Path "\\$($servers[1])\c$\App_configuration.txt"--пропуск--


Поскольку у вас есть Get-Content в операторе if, вы не столкнетесь с какими-либо ошибками, если попытаетесь получить доступ к неработающему серверу; если тест завершится неудачно, ваш сценарий будет знать, что не следует пытаться считать файл. Код попытается получить доступ к серверу, только если он уже знает, что тот включен. Но обратите внимание, что этот код срабатывает только в том случае, если условие истинно. Достаточно часто вам нужно будет задать одно поведение сценария для истинного условия и другое для ложного. В следующем разделе вы увидите, как определить поведение для ложного условия с помощью оператора else.

Оператор else

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

Листинг 4.4. Использование оператора else для запуска кода, если условие
не истинно

if (Test-Connection -ComputerName $servers[0] -Quiet -Count 1) {   Get-Content -Path "\\$($servers[0])\c$\App_configuration.txt"} else {   Write-Error -Message "The server $($servers[0]) is not responding!"}


Оператор if/else отлично работает, когда у вас есть две взаимоисключающие ситуации. В данном случае сервер либо подключен, либо нет, то есть нам нужно всего две ветви кода. Давайте посмотрим, как работать с более сложными ситуациями.

Оператор elseif

Оператор else охватывает противоположную ситуацию: если if не срабатывает, значит, выполните это в любом случае. Такой подход работает для двоичных условий, то есть когда сервер либо работает, либо нет. Но иногда приходится иметь дело с большим числом вариантов. Например, предположим, что у вас есть сервер, на котором нет нужного вам файла, и вы сохранили имя этого сервера в переменной $problemServer (добавьте в свой сценарий эту строку кода!). Это означает, что вам нужна дополнительная проверка, позволяющая узнать, является ли сервер, который вы опрашиваете в данный момент, проблемным. Это можно реализовать с помощью вложенных операторов if, как показано в коде ниже:

if (Test-Connection -ComputerName $servers[0] -Quiet -Count 1) {   if ($servers[0] eq $problemServer) {      Write-Error -Message "The server $servers[0] does not have the right         file!"   } else {      Get-Content -Path "\\$servers[0]\c$\App_configuration.txt"   }} else {   Write-Error -Message "The server $servers[0] is not responding!"}--пропуск--


Но есть и более аккуратный способ реализовать ту же логику с помощью оператора elseif, который позволяет вам проверить дополнительное условие, перед тем как вернуться к коду в блоке else. Синтаксис блока elseif идентичен синтаксису блока if. Итак, чтобы проверить проблемный сервер с помощью оператора elseif, запустите код из листинга 4.5.

Листинг 4.5. Использование блока elseif

if (-not (Test-Connection -ComputerName $servers[0] -Quiet -Count 1)) {    Write-Error -Message "The server $servers[0] is not responding!"} elseif ($servers[0] eq $problemServer)    Write-Error -Message "The server $servers[0] does not have the right file!"} else {   Get-Content -Path "\\$servers[0]\c$\App_configuration.txt" }--пропуск--


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

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

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

Оператор switch

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

Обратите внимание, что теперь мы будем работать с другим типом условия. Если раньше нам нужны были ответы на вопросы типа да/нет, то теперь мы хотим получить конкретное значение одной вещи. Это сервер SRV1? SRV2? И так далее. Если бы вы работали только с одним или двумя конкретными значениями, оператор if подошел бы, но в данном случае оператор switch сработает гораздо лучше.

Оператор switch позволяет выполнять различные фрагменты кода в зависимости от некоторого значения. Он состоит из ключевого слова switch, за которым следует выражение в скобках. Внутри блока switch находится серия операторов, построенных по следующему принципу: сначала указывается значение, за ним следует набор фигурных скобок, содержащих блок кода, и наконец ставится блок default, как указано в листинге 4.6.

Листинг 4.6. Шаблон для оператора switch

switch (выражение) {   значениевыражения {      # Код   }   значениевыражения {   }   default {     # Код, который выполняется при отсутствии совпадений   }}


Оператор switch может содержать практически неограниченное количество значений. Если выражение оценивается как значение, выполняется соответствующий код внутри блока. Важно то, что, в отличие от elseif, после выполнения одного блока кода PowerShell продолжит проверять и остальные условия, если не указано иное. Если ни одно из значений не подойдет, PowerShell выполнит код, указанный в блоке default. Чтобы прекратить перебор условий в операторе switch, используйте ключевое слово break в конце блока кода, как показано в листинге 4.7.

Листинг 4.7. Использование ключевого слова break в операторе switch

switch (выражение) {   значениевыражения {      # Код      break   }--пропуск--


Ключевое слово break позволяет сделать условия в операторе switch взаимоисключающими. Вернемся к нашему примеру с пятью серверами и одним и тем же файлом, имеющим разные пути. Вы знаете, что сервер, с которым вы работаете, может иметь только одно значение (то есть он не может одновременно называться и SRV1, и SRV2), поэтому вам нужно использовать операторы break. Ваш сценарий должен выглядеть примерно так, как показано в листинге 4.8.

Листинг 4.8. Проверка различных серверов с помощью оператора switch

$currentServer = $servers[0]switch ($currentServer) {   $servers[0] {      # Check if server is online and get content at SRV1 path.      break   }   $servers[1] {      ## Check if server is online and get content at SRV2 path.      break   }   $servers[2] {      ## Check if server is online and get content at SRV3 path.      break   }--пропуск--


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

Использование циклов

Существует хорошее практическое правило для работы за компьютером: не повторяйся (dont repeat yourself, DRY). Если вы обнаружите, что выполняете одну и ту же работу, то, скорее всего, существует способ ее автоматизировать. То же самое и с написанием кода: если вы используете одни и те же строки снова и снова, вероятно, существует решение получше.

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

PowerShell предлагает пять типов циклов: foreach, for, do/while, do/until и while. В этом разделе мы обсудим каждый тип цикла, отметим их уникальные черты и выделим лучшие ситуации для их использования.

Об авторе

Адам Бертрам (Adam Bertram) опытный ИТ-специалист и эксперт в области интернет-бизнеса с 20-летним стажем. Предприниматель, ИТ-инфлюенсер, специалист Microsoft MVP, блогер, тренинг-менеджер и автор материалов по контент-маркетингу, сотрудничающий со многими ИТ-компаниями. Также Адам основал популярную платформу TechSnips для развития навыков ИТ-специалистов (techsnips.io).

Более подробно с книгой можно ознакомиться на сайте издательства
Оглавление
Отрывок

Для Хаброжителей скидка 25% по купону PowerShell

По факту оплаты бумажной версии книги на e-mail высылается электронная книга.
Подробнее..

Представляем 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.

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

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

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

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

Подробнее..

Работаем с notebook в VS Code с помощью расширения dotnet interactive

20.02.2021 14:06:35 | Автор: admin
Скриншот notebook'a из VS CodeСкриншот notebook'a из VS Code

Сегодня я хочу рассказать вам о таком замечательном инструменте как "dotnet interactive". Я покажу на своём примере как и для чего я начал его использовать, и вкратце опишу с чего начать.

Проблема

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

По итогам протокола считаются различные статистические метрики, которые потом уходят в отчёт. Сейчас формулы, как считать нужную нам статистику, разбросаны по различным этапам ТЗ, поэтому, когда я вчера узнал о "dotnet-interactive" мне сразу пришла мысль о создании notebook'a в формате "Описание метрики"-"Формула"-"Код"-"График", в котором можно будет загрузить любой файл протокола и в интерактивном формате проходить и считать интересующие нас метрики.

Приступаем к созданию notebook'a

Прежде всего, у вас должен быть установлен .net5 sdk и последняя версия VS Code. Далее, нужно лишь установить расширение ".NET Interactive Notebooks". Данное расширение сейчас имеет статус "Preview", однако уже сейчас там можно делать много интересных вещей.

Когда мы установили расширение, можем создать рабочую директорию, в которой будут лежать нужные библиотеки, скрипты и файлы. Открываем её в VS Code и окне команд вбиваем ".NET Interactive: Create new blank notebook" и начинаем наполнять наш notebook.

В первом блоке кода я определил загрузку файла протокола:

#load "Load.fsx"open Loadlet Experiment = loadSep "2021.02.03_15.55.58_gen.sep"

Здесь я на F# подключил скрипт, который инкапсулирует в себе логику открытия файла и xml-сериализацию:

#r "nuget: System.Text.Encoding.CodePages"#r "AKIM.Protocol.dll"open System.IOopen AKIM.Protocolopen System.Xml.Serializationopen System.Textlet loadSep path=        let deserializeXml (xml : string) =        let toBytes (x : string) = Encoding.UTF8.GetBytes x        let xmlSerializer = XmlSerializer(typeof<Experiment>)        use stream = new MemoryStream(toBytes xml)        xmlSerializer.Deserialize stream :?> Experiment    Encoding.RegisterProvider(CodePagesEncodingProvider.Instance)        deserializeXml (File.ReadAllText(path, Encoding.GetEncoding(1251)))

В этом скрипте я подключаю nuget пакет для кодировки и свою библиотеку с dto-классами, написанную на C#.

Во втором блоке notebook'a уже в c#-intaractive я подключаю нужные для работы с экспериментом пространства имён и шарю объект в с# из f#, определенного в первом блоке

#r "AKIM.Protocol.dll"using AKIM.Protocol;using AKIM.Protocol.Events;using AKIM.Protocol.Events.OperatorSvn;using AKIM.Protocol.Events.OperSb;using AKIM.Protocol.Events.RespUnits;using AKIM.Protocol.Events.Intruders;using AKIM.Protocol.Events.Sens;using AKIM.Protocol.Events.System;#!share --from fsharp Experiment

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

var allTests = Experiment.Tests.Count;var penetrations = Experiment.Tests.Where(t => t.Events.Last() is PenetrationEvent).Count();var nonPenetrations = Experiment.Tests.Where(t => t.Events.Last() is NonPenetEvent).Count();var eve = Experiment.Tests.First().Events.FirstOrDefault(t => t is VisContactEvent);Console.WriteLine(eve?.GetDescription());Console.WriteLine($"Количество проникновений {penetrations} из {allTests}")

Нажав на запуск выполнения кода мы получаем следующий вывод:

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

#r "nuget: XPlot.Plotly"#!share --from csharp penetrations #!share --from csharp nonPenetrations#!share --from csharp allTestsopen XPlot.PlotlyChart.Pie(seq {("Кол-во проникновений",penetrations);               ("Нейтролизовали",allTests- penetrations-nonPenetrations);               ("Отказ от проникновения",nonPenetrations)}) |> Chart.Show

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

Итоговая диаграммаИтоговая диаграмма

Что дальше

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

Что не работает?

Мне не удалось загрузить файл скрипта на С# с расширениями "*.csx" в с#-interactive. Возможно еще не завезли, возможно не правильно готовлю. Плюс не удалось решить, почему графики открываются в браузере, а не снизу блока с кодом. Также в markdown блоке не хотят отображаться формулы в формате $$...$$.

Выводы

Я считаю, что этот инструмент должен попробовать каждый .net разработчик. Вариантов использования масса: обработка результатов, прототипирование каких-то идей, изучение F# или C#, скриптинг для работы с операционной системой, например, чтобы manage'ить какие-то файлы. Лично я, когда случайно узнал вчера об этом инструменте, был в диком восторге, что и побудило меня сделать этот пост, так как мало, кто об этой штуке слышал (но это не точно).

Хочу поблагодарить своего подписчика на ютубе, Arkadiy Kuznetsov, который подсказал мне о существовании такого инструмента.

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

Спасибо за внимание.

Полезные ссылки

Официальная репа, в которой есть также документация
.NET Interactive + ML.NET
Новые фичи f#(в начале видео использует dotnet-intaractive)

Подробнее..

Пошаговая инструкция по настройке и использованию Gitlab CI Visual Studio для сборки приложения .NET Framework

12.03.2021 14:20:14 | Автор: admin

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


Как только кто-либо из нашей команды вносит изменения в код (читай мерджит feature-ветку в develop), наш билд-сервер:


  • Собирает исходный код и установщик приложения
    • проставляет номер сборки, каждый раз увеличивая последнюю цифру. Например, текущая версия нашего ПО 3.3.0.202 часть 3.3.0 когда-то ввёл разработчик (привет, SemVer), а 202 проставляется в процессе сборки.
    • В процессе анализирует качество кода (с использованием SonarQube) и отправляет отчёт во внутренний SonarQube,
  • Сразу после сборки запускает автотесты (xUnit) и анализирует покрытие тестами (OpenCover),

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


  • отправка сборки (вместе с changelog-ом) в один или несколько телеграм-каналов (иногда удобнее брать сборки оттуда).
  • публикация файлов в систему автообновления ПО.

Под катом о том, как мы научили Gitlab CI делать за нас бОльшую часть этой муторной работы.


Оглавление


  1. Устанавливаем и регистрируем Gitlab Runner.
  2. Что нужно знать про .gitlab-ci.yml и переменные сборки.
  3. Активируем режим Developer PowerShell for VS.
  4. Используем CI для проставления версии во все сборки решения.
  5. Добавляем отправку данных в SonarQube.
  6. Причёсываем автотесты xUnit + добавляем вычисление покрытия тестами через OpenCover.
  7. Послесловие.

Перед началом


Чтобы быть уверенными, что написанное ниже работает, мы взяли на github небольшой проект, написанный на WPF и имеющий unit-тесты, и воспроизвели на нём описанные в статье шаги. Самые нетерпеливые могут сразу зайти в созданный на сайте gitlab.com репозиторий и посмотреть, как это выглядит.


Устанавливаем и регистрируем Gitlab Runner


Для того чтобы Gitlab CI мог что-либо собрать, сначала установите и настройте Gitlab Runner на машине, на которой будет осуществляться сборка. В случае проекта на .Net Framework это будет машина с ОС Windows.


Чтобы настроить Gitlab Runner, выполните следующие шаги:


  1. Установите Git для Windows с сайта git.
  2. Установите Visual Studio с сайта Microsoft. Мы поставили себе Build Tools для Visual Studio 2019. Чтобы скачать именно его, разверните список Инструменты для Visual Studio2019.
  3. Создайте папку C:\GitLab-Runner и сохраните в неё программу gitlab runner. Скачать её можно со страницы [документации Gitlab] (https://docs.gitlab.com/runner/install/windows.html) ссылки скрыты прямо в тексте: Download the binary for x86 or amd64.
  4. Запустите cmd или powershell в режиме администратора, перейдите в папку C:\GitLab-Runner и запустите скачанный файл с параметром install (Gitlab runner установится как системная служба).

.\gitlab-runner.exe install

  1. Посмотрите токен для регистрации Runner-а. В зависимости от того, где будет доступен ваш Runner:


    • только в одном проекте смотрите токен в меню проекта Settings > CI/CD в разделе Runners,
    • в группе проектов смотрите токен в меню группы Settings > CI/CD в разделе Runners,
    • для всех проектов Gitlab-а смотрите токен в секции администрирования, меню Overview > Runners.

  2. Выполните регистрацию Runner-а, с помощью команды



.\gitlab-runner.exe register

Далее надо ввести ответы на вопросы мастера регистрации Runner-а:


  • coordinator URL http или https адрес вашего сервера gitlab;
  • gitlab-ci token введите токен, полученный на предыдущем шаге;
  • gitlab-ci description описание Runner-а, которое будет показываться в интерфейсе Gitlab-а;
  • gitlab-ci tags через запятую введите тэги для Runner-а. Если вы не знакомы с этим механизмом, оставьте поле пустым отредактировать его можно позднее через интерфейс самого gitlab-а. Тэги можно использовать для того, чтобы определённые задачи выполнялись на определённых Runner-ах (например, чтобы настроить сборку ПО на Runner-е, развёрнутом на копьютере ОС Windows, а подготовку документации на Runner-е с ОС Linux);
  • enter the executor ответьте shell. На этом шаге указывается оболочка, в которой будут выполняться команды; при указании значения shell под windows выбирается оболочка powershell, а последующие скрипты написаны именно для неё.

Что нужно знать про .gitlab-ci.yml и переменные сборки


В процессе соей работы Gitlab CI берёт инструкции о том, что делать в процессе сборки того или иного репозитория из файла .gitlab-ci.yml, который следует создать в корне репозитория.


Вбив в поиске содержимое .gitlab-ci.yml для сборки приложения .NET Framework можно найти несколько шаблонов: 1, 2. Выглядят они примерно так:


variables:  # Максимальное количество параллельно собираемых проектов при сборке решения; зависит от количества ядер ПК, выбранного для сборки  MSBUILD_CONCURRENCY: 4  # Тут куча путей до утилит, которые просто оябзаны лежать там, где ожидается  NUGET_PATH: 'C:\Tools\Nuget\nuget.exe'  MSBUILD_PATH: 'C:\Program Files (x86)\Microsoft Visual Studio\2017\BuildTools\MSBuild\15.0\Bin\msbuild.exe'  XUNIT_PATH: 'C:\Tools\xunit.runner.console.2.3.1\xunit.console.exe'  TESTS_OUTPUT_FOLDER_PATH: '.\tests\CiCdExample.Tests\bin\Release\'# Тут указываются стадии сборки. Указывайте любые названия которые вам нравятся, но по умолчанию используют три стадии: build, test и deploy.# Стадии выполняются именно в такой последовательности.stages:  - build  - test# Далее описываются задачи (job-ы)build_job:  stage: build # указание, что задача принадлежит этапу build  # tags: windows # если тут указать тэг, задача будет выполняться только на Runner-е с указанным тэгом   only: # для каких сущностей требуется выполнять задачу    - branches  script: # код шага    - '& "$env:NUGET_PATH" restore'    - '& "$env:MSBUILD_PATH" /p:Configuration=Release /m:$env:MSBUILD_CONCURRENCY /nr:false /clp:ErrorsOnly' # сборка; ключ clp:ErrorsOnlyоставляет только вывод ошибок; ключ nr:false завершает инстансы msbuild   artifacts: # где по завершении задачи будут результаты, которые надо сохранить в gitlab (т.н. артефакты) и которые можно будет передать другим задачам по цепочке    expire_in: 2 days # сколько хранить артефакты    paths: # список путей, по которым находятся файлы для сохранения      - '$env:TESTS_OUTPUT_FOLDER_PATH'test_job:  stage: test  only:    - branches  script:    - '& "$env:XUNIT_PATH" "$env:TESTS_OUTPUT_FOLDER_PATH\CiCdExample.Tests.dll"'  dependencies: # указание, что для запуска этой задачи требуется успешно завершенная задача build_job    - build_job

И последнее: если нам требуется передавать в скрипт значение параметра, который мы не хотим хранить в самом скрипте (например, пароль для подключения куда-либо), мы можем использовать для этого объявление параметров в gitlab. Для этого зайдите в проекте (или в группе проекта) в Settings > CI/CD и найдите раздел Variables. Прописав в нём параметр с именем (key) SAMPLE_PARAMETER, вы сможете получить его значение в в скрипте .gitlab-ci.yml через обращение $env:SAMPLE_PARAMETER.
Также в этом разделе можно настроить передачу введенных параметров только при сборке защищённых веток (галочка Protected) и/или скрытие значения параметра из логов (галочка Masked).
Подробнее о параметрах окружения сборки смотрите в документации к Gitlab CI.


Активируем режим Developer PowerShell for VS


Скрипт, приведённый выше, уже можно использовать для сборки и вызова тестов. Правда, присутствует НО: крайне неудобно прописывать абсолютные пути к разным установкам Visual Studio. К примеру, если на одной билд-машине стоит Visual Studio 2017 BuildTools, а на другой Visual Studio Professional 2019, то такой скрипт будет работать только для одной из двух машин.


К счастью, с версии Visual Studio 2017 появился способ поиска всех инсталляций Visual Studio на компьютере. Для этого существует утилита vswhere, путь к которой не привязан ни к версии Visual Studio, ни к её редакции. А в Visual Studio 2019 (в версии 16.1 или более новой) есть библиотека, которая умеет трансформировать консоль Powershell в режим Developer Powershell, в котором уже прописаны пути к утилитам из поставки VS.


Как применить


Дописываем переменную к секции Variables:


variables:  VSWHERE_PATH: '%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe'

Затем создаём новую секцию before_msbuild якорем enter_vsdevshell и следующим текстом:


.before_msbuild: &enter_vsdevshell  before_script:    - '$vsWherePath = [System.Environment]::ExpandEnvironmentVariables($env:VSWHERE_PATH)'    - '& $vsWherePath -latest -format value -property installationPath -products Microsoft.VisualStudio.Product.BuildTools | Tee-Object -Variable visualStudioPath'    - 'Join-Path "$visualStudioPath" "\Common7\Tools\Microsoft.VisualStudio.DevShell.dll" | Import-Module'    - 'Enter-VsDevShell -VsInstallPath:"$visualStudioPath" -SkipAutomaticLocation'

И всюду, где нам надо использовать утилиты Visual Studio, добавляем этот якорь. После этого задача сборки начинает выглядеть намного более опрятно:


build_job:  <<: *enter_vsdevshell  stage: build  only:    - branches  script:    - 'msbuild /t:restore /m:$env:MSBUILD_CONCURRENCY /nr:false /clp:ErrorsOnly'    - 'msbuild /p:Configuration=Release /m:$env:MSBUILD_CONCURRENCY /nr:false /clp:ErrorsOnly'  artifacts:    expire_in: 2 days    paths:      - '$env:TESTS_OUTPUT_FOLDER_PATH'

Подробно о том, что написано в .before_msbuild
  1. Утилита vswhere.exe умеет находить и выдавать список найденных инсталляций Visual Studio. Расположен она всегда по одному и тому же пути (этот путь записан в переменной VSWHERE_PATH). Поскольку в переменной фигурирует подстановка %programfiles%, её требуется раскрыть до пути к этой папке. Такое раскрытие проще всего сделать через статический метод .NET System.Environment.ExpandEnvironmentVariables.

Результат: мы имеем путь к vswhere.


  1. Вызовом vswhere получим путь к папке с установленной Visual Studio.
    Все параметры утилиты можно посмотреть, если запустить vswhere.exe с параметром -help, в статье же только перечислю использованные:
    • -latest (искать самые свежие установки),
    • -property installationPath (вывести параметр пути установки),
    • -format value (при печати параметра вывести только значение параметра, без его имени),
    • -products <список искомых инсталляций Visual Studio, пробел считается разделителем элементов списка> (указание искомых редакций Visual Studio). Например, при запуске с параметром -products Microsoft.VisualStudio.Product.Community Microsoft.VisualStudio.Product.BuildTools утилита попробует найти Visual Studio редакций Community или BuildTools. Подробнее об идентификаторах продуктов смотрите по ссылке https://aka.ms/vs/workloads.

Результат: в переменную $visualStudioPath записан путь к Visual Studio или пустая строка, если инсталляций Visual Studio не найдено (обработку этой ситуации мы ещё не добавили).


  1. Команда Import-Module загружает библиотеку Microsoft.VisualStudio.DevShell.dll, в которой прописаны командлеты трансформации консоли Powershell в Developer-консоль. А командлет Join-Path формирует путь к этой библиотеке относительно пути установки Visual Studio.
    На этом шаге нам прилетит ошибка, если библиотека Microsoft.VisualStudio.DevShell.dll отсутствует или путь к установке Visual Studio нужной редакции не был найден Import-Module сообщит, что не может загрузить библиотеку.

Результат: загружен модуль Powershell с командлетом трансформации.


  1. Запускаем передёлку консоли в Developer Powershell. Чтобы корректно прописать пути к утилитам, командлету требуется путь к установленной Visual Studio (параметр -VsInstallPath). А указаниеSkipAutomaticLocation требует от командлета не менять текущее расположение (без этого параметра путь меняется на <домашнаяя папка пользователя>\source\repos.

Результат: мы получили полноценную консоль Developer Powershell с прописанными путями к msbuild и многим другим утилитам, которые можноиспользовать при сборке.


Используем CI для проставления версии во все сборки решения


Раньше мы использовали t4 шаблоны для проставления версий: номер версии собиралась из содержимого файла в формате <major>.<minor>.<revision>, далее к ней добавлялся номер сборки из Gitlab CI и он передавался в tt-шаблон, добавляющий в решение везде, где требуется, номер версии. Однако некоторое время назад был найден более оптимальный способ использование команд git tag и git describe.


Команда git tag устанавливает коммиту метку (тэг). Отметить таким образом можно любой коммит в любой момент времени. В отличие от веток, метка не меняется. То есть если после помеченного коммита вы добавите ещё один, метка останется на помеченном коммите. Если попробуете переписать отмеченный коммит командами git rebase или git commit --amend, метка также продолжит указывать на исходный коммит, а не на изменённый. Подробнее о метках смотрите в git book.


Команда git describe, к сожалению, в русскоязычном gitbook не описана. Но работает она примерно так: ищет ближайшего помеченного родителя текущего коммита. Если такого коммита нет команда возвращает ошибку fatal: No tags can describe '<тут хэш коммита>'. А вот если помеченный коммит нашёлся тогда команда возвращает строку, в которой участвует найденная метка, а также количество коммитов между помеченным и текущим.


На заметку: чтобы данная команда работала корректно во всех случаях, автор gitflow даже чуть-чуть поменял скрипты finish hotfix и finish release. Если кому интересно посмотреть обсуждение с автором gitflow, а также увидеть что изменилось (картинка с актуальной схемой в последнем сообщении в треде).


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


Не забывайте пересоздавать ветки от develop
(обратите внимание на историю git в левой части картинки: из-за отсутствия пути из текущего коммита до коммита с меткой 1.0.5, команда git describe выдаст неверный ответ)


Но вернёмся к автопроставлению версии. В нашем репозитории царит gitflow (точнее его rebase-версия), метки расставляются в ветке master и мы не занываем пересоздавать feature-ветки от develop, а также merge-ить master в develop после каждого релиза или хотфикса.


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


build_job:  <<: *enter_vsdevshell  stage: build  only:    - branches  script:    - 'msbuild /t:restore /m:$env:MSBUILD_CONCURRENCY /nr:false /clp:ErrorsOnly'    - '$versionGroup = git describe --long | Select-String -Pattern "(?<major>[0-9]+)\.(?<minor>[0-9]*)\.(?<patch>[0-9]*)\-(?<commit>[0-9]+)\-g[0-9a-f]+" | Select-Object -First 1'    - '[int]$major, [int]$minor, [int]$patch, [int]$commit = $versionGroup.Matches[0].Groups["major", "minor", "patch", "commit"].Value'    - '[string]$version = "$major.$minor.$patch.$commit"'    - 'msbuild /p:Configuration=Release /p:AssemblyVersionNumber=$version /m:$env:MSBUILD_CONCURRENCY /nr:false /clp:ErrorsOnly'  artifacts:    expire_in: 2 days    paths:      - '$env:TESTS_OUTPUT_FOLDER_PATH'

Как это работает:


  1. Мы проставляем метки в формате <major>.<minor>.<revision>.
  2. Тогда git describe --long возвращает нам строку, описывающую версию в формате <major>.<minor>.<revision>-<количество новых коммитов>-g<хэш текущего коммита>.
  3. Парсим полученную строку через регулярные выражения, выделяя нужные нам части и записывем части в $versionGroup.
  4. Преобразовываем четыре найденные подстроки в 4 числа и пишем их в переменные $major, $minor, $patch, $commit, после чего собираем из них строку уже в нужном нам формате.
  5. Передаём указанную строку в msbuild чтобы он сам проставил версию файлов при сборке.

Обратите внимание: если вы, согласно gitflow, будете отмечать (тэгировать) ветку master после вливания в неё release или hofix, будьте внимательны: до простановки метки автосборка будет вестись относительно последней существующей ветки. Например, сейчас опубликована версия 3.4, а вы создаёте release-ветку для выпуска версии 3.5. Так вот: всё время существования этой ветки, а также после её вливания в master, но до простановки тэга, автосборка будет проставлять версию 3.4.


Добавляем отправку данных в SonarQube


SonarQube это мощный инструмент контроля качества кода.


SonarQube имеет бесплатную Community-версию, которая способна проводить полный анализ. Правда, только одной ветки. Чтобы настроить её на контроль качества ветки разработки (develop), требуется выполнить следующие шаги (разумеется, помимо установки и развёртывания сервера SonarQube):


  1. Создайте в SonarQube новый проект, после чего запомнить его ключ.


  2. Скачайте SonarScanner for MSBuild (с сайта sonarqube.org)[https://docs.sonarqube.org/latest/analysis/scan/sonarscanner-for-msbuild/] мы используем версию .NET Framework 4.6+.


  3. Распакуйте содержимое архива в папку. Например, в C:\Tools\SonarScanner.



На заметку: эту утилиту также можно скачать на сборочную машину через NuGet, но тогда надо будет чуть по-иному указывать её путь.


  1. Зайдите в параметры CI/CD в свойствах проекта в Gitlab следующие параметры:
    • SONARQUBE_PROJECT_KEY ключ проекта,
    • SONARQUBE_AUTH_TOKEN токен авторизации.

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


  1. Допишите переменные к секции Variables:


    variables:SONARSCANNER_MSBUILD_PATH: 'C:\Tools\SonarScanner\SonarScanner.MSBuild.exe'SONARQUBE_HOST_URL: 'url вашего сервера SonarQube'
    

  2. Допишите в задачу тестирования ветки разработки (test_job) команды для запуска анализа кода и уберите зависимость от задачи build_job:


    test_job:stage: testonly:- /^develop$/<<: *enter_vsdevshellscript:- '$versionGroup = git describe --long | Select-String -Pattern "(?<major>[0-9]+)\.(?<minor>[0-9]*)\.(?<patch>[0-9]*)\-(?<commit>[0-9]+)\-g[0-9a-f]+" | Select-Object -First 1'- '[int]$major, [int]$minor, [int]$patch, [int]$commit = $versionGroup.Matches[0].Groups["major", "minor", "patch", "commit"].Value'- '[string]$version = "$major.$minor.$patch.$commit"'- '& "$env:SONARSCANNER_MSBUILD_PATH" begin /key:$env:SONARQUBE_PROJECT_KEY /d:sonar.host.url=$env:SONARQUBE_HOST_URL /d:sonar.login=$env:SONARQUBE_AUTH_TOKEN /d:sonar.gitlab.project_id=$CI_PROJECT_PATH /d:sonar.gitlab.ref_name=develop /v:$version /d:sonar.dotnet.excludeGeneratedCode=true'- 'msbuild /t:rebuild /m:$env:MSBUILD_CONCURRENCY /nr:false /clp:ErrorsOnly'- '& "$env:SONARSCANNER_MSBUILD_PATH" end /d:sonar.login=$env:SONARQUBE_AUTH_TOKEN'- '& "$env:XUNIT_PATH" "$env:TESTS_OUTPUT_FOLDER_PATH\CiCdExample.Tests.dll"'
    


Теперь при каждой сборке ветки develop в SonarQube будет отправляться подробный анализ нашего кода.


На заметку: вообще команда msbuild /t:rebuild полностью пересобирает решение. Вероятно, в большинстве проектов анализ можно было бы встроить прямо в стадию сборки. Но сейчас у нас анализ в отдельной задаче.


Пара слов об использованных параметрах:


  • key ключ проекта на сервере SonarQube,
  • v собираемая версия. ИМХО отлично комбинируется с предыдущим шагом автопроставления версии,
  • sonar.gitlab.project_id ID проекта на сервере Gitlab,
  • sonar.gitlab.ref_name название ветки, которое получает сервер SonarQube при передаче результатов анализа,
  • sonar.dotnet.excludeGeneratedCode не включать в анализ объекты, отмеченные атрибутом System.CodeDom.Compiler.GeneratedCode (чтобы не оценивать качество автосгенерированного кода).

Причёсываем автотесты xUnit + добавляем вычисление покрытия тестами через OpenCover


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


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

На заметку: обычно в паре с OpenCover используют ReportGenerator, но при наличии SonarQube мы с тем же успехом можем смотреть результаты через его интерфейс.


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


  1. Скачайте OpenCover в виде zip-файла с сайта github.


  2. Распакуйте содержимое архива в папку. Например, в C:\Tools\OpenCover.



На заметку: эту утилиту также можно скачать на сборочную машину через NuGet, но тогда надо будет чуть по-иному указывать её путь.


  1. Допишите переменные к секции Variables:


    variables:OBJECTS_TO_TEST_REGEX: '^Rt[^\n]*\.(dll|exe)$'OPENCOVER_PATH: 'C:\Tools\opencover-4.7.922\xunit.console.exe'OPENCOVER_FILTER: '+[Rt.*]* -[*UnitTests]* -[*AssemblyInfo]*'OPENCOVER_REPORT_FILE_PATH: '.\cover.xml'
    

  2. Модифицируйте задачу тестирования ветки разработки (test_job), чтобы она включала и команды вызова OpenCover:



test_job:  stage: test  only:    - /^develop$/  <<: *enter_vsdevshell  script:    - '$versionGroup = git describe --long | Select-String -Pattern "(?<major>[0-9]+)\.(?<minor>[0-9]*)\.(?<patch>[0-9]*)\-(?<commit>[0-9]+)\-g[0-9a-f]+" | Select-Object -First 1'    - '[int]$major, [int]$minor, [int]$patch, [int]$commit = $versionGroup.Matches[0].Groups["major", "minor", "patch", "commit"].Value'    - '[string]$version = "$major.$minor.$patch.$commit"'    - '& "$env:SONARSCANNER_MSBUILD_PATH" begin /key:$env:SONARQUBE_PROJECT_KEY /d:sonar.host.url=$env:SONARQUBE_HOST_URL /d:sonar.login=$env:SONARQUBE_AUTH_TOKEN /d:sonar.gitlab.project_id=$CI_PROJECT_PATH /d:sonar.gitlab.ref_name=develop /v:$version /d:sonar.cs.opencover.reportsPaths="$env:OPENCOVER_REPORT_FILE_PATH" /d:sonar.dotnet.excludeGeneratedCode=true'    - 'msbuild /t:rebuild /m:$env:MSBUILD_CONCURRENCY /nr:false /clp:ErrorsOnly'    - '$dllsToRunUnitTesting = @(Get-ChildItem "$env:TESTS_OUTPUT_FOLDER_PATH" -Recurse) | Where-Object {$_.Name -match $env:OBJECTS_TO_TEST_REGEX} | ForEach-Object { """""$_""""" } | Join-String -Separator " "'    - '& "$env:OPENCOVER_PATH" -register -target:"$env:XUNIT_PATH" -targetargs:"$dllsToRunUnitTesting -noshadow" -filter:"$env:OPENCOVER_FILTER" -output:"$env:OPENCOVER_REPORT_FILE_PATH" | Write-Host'    - 'if ($?) {'    - '[xml]$coverXml = Get-Content "$env:OPENCOVER_REPORT_FILE_PATH"'    - '$sequenceCoverage = $coverXml.CoverageSession.Summary.sequenceCoverage'    - '$branchCoverage = $coverXml.CoverageSession.Summary.branchCoverage'    - 'Write-Host "Total Sequence Coverage <!<$sequenceCoverage>!>"'    - 'Write-Host "Total Branch Coverage [![$branchCoverage]!]"'    - '} else {'    - 'Write-Host "One or more tests failed!"'    - 'Throw'    - '}'    - '& "$env:SONARSCANNER_MSBUILD_PATH" end /d:sonar.login=$env:SONARQUBE_AUTH_TOKEN'

Обратите внимание: в begin-команде запуска sonar scanner-а появился дополнительный параметр /d:sonar.cs.opencover.reportsPaths.


  1. (необязательный пункт) Ненадолго возврващаемся в Gitlab, заходим в меню проекта Settings > CI/CD и находим на странице настроек параметр Test coverage parsing. Указываем в нём регулярное выражение, которое позволит Gitlab-у также получать информацию о покрытии тестами приложения:
    • если хочется видеть значение покрытия тестов по строкам кода (его ешё называют Sequence Coverage или Statement Coverage), указываем выражение <!<([^>]+)>!>,
    • если хочется видеть значение покрытия тестов по веткам условных операторов (его называют Decision Coverage или Branch Coverage), указываем выражение \[!\[([^>]+)\]!\].

А теперь комментарии по изменениям в скрипте.


Длинная строка, начинающаяся с объявления переменной $dllsToRunUnitTesting, нужна для того, чтобы найти все библиотеки нашего приложения, которые потом будут участвовать в расчёте тестового покрытия. При этом выбираются они по регулярному выражению, заданному в параметре $env:OBJECTS_TO_TEST_REGEX (мы же не хотим, например, учитывать в покрытии библиотеки .net или сторонних nuget-пакетов). Пути ко всем найденным библиотекам склеиваются в строку с разделителем пробелом и двумя двойными кавычками для каждого параметра. Две двойные кавычки были добавлены потому, что OpenCover при вызове приложения xunit съедает одни из кавычек, а вторые кавычки нужны на случай наличия пробелов в абсолютных путях.


Следующая строка запуск утилиты OpenConver с передачей ей списка библиотек нашего приложения, фильтра, по которому OpenCover исключает из покрытия библиотеки с unit-тестами, а также часть классов, не требующих расчёта покрытия (например, AssemblyInfo). Конвейер и Write-Host были добавлены в порыве вдохновения, так как без него у нас не работал вывод OpenConver-а.


И следующий if проверяет, успешно ли завершился запуск OpenConver. Если не успешно кладём скрипт; если же успешно парсим получившийся xml-файлик с отчётом и печатаем значения покрытия тестами, чтобы затем его легко распарсил gitlab через указанное в настройках регулярное выражение.


Послесловие


Как известно, нет предела совершенству. Мы продолжим добавлять новые функции в используемые нами процессы сборки, тестирования и публикации. На момент написания тестируется переезд нашего ПО на .NET 5 (с ним OpenCover уже не работает, сборка выполняется через команду dotnet), а также мы переходим на другой способ публикации (к сожалению, пока не могу дать более подробный комментарий).


Если у вас появятся вопросы или предложения пишите в комментариях.


И спасибо за прочтение!

Подробнее..

Разбираемся с Powershell и делаем терминал в Windows юзабельным

03.02.2021 16:18:50 | Автор: admin


Не для кого не секрет что по умолчанию терминал в Windows тот ещё костыль. На смену стандартному терминалу пришёл терминал с Powershell в 2006 году. Тогда это хоть и был прорыв, однако терминал всё ещё не был таким удобным, как это было на Linux с его Bash. Майкрософты придумали довольно дурной синтаксис команд действие-объект, которое легло в основу синтаксиса всего Powershell и не давало быстро работать с терминалом. Не сразу, но спустя несколько релизов были добавлены алиасы, сначала они имитировали команды из cmd, а потом переняли команды bash по типу cp, mv, rm, ps и так далее (на самом деле всё это не команды, а просто сокращения, которые ссылаются на стандартные команды Powershell).


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


Сам я сижу уже 5-й год на линукс и перепробовал кучу шеллов (Zsh, Bash, Fish..) и хоть что-то в них, да понимаю. Пользователю терминала нужна прежде всего удобность консольного интерфейса. Стандартный терминал Powershell не даёт этого удобства да и глаза он выедает. Майкрософты недавно выпустили новый терминал полностью с открытым кодом. Новый терминал поддерживает темы, у него свой моноширинный шрифт, который довольно неплох на мой субъективный взгляд. Однако, смены одного лишь терминала хватать точно не будет, мы будем делать сам шелл более юзабельным.


А зачем это вообще нужно, когда можно просто юзать терминал в Linux?


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


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


Терминал в Windows просто ужасный. Мы будем это исправлять и делать его больше Linux-like.


Windows Terminal


Сначала для нормального экспириенса нам нужен Windows Terminal. Он настраивается с помощью файла формата .json. Первое что мне захотелось изменить стандартную тему. В самом терминале уже встроены темы, которые мне нравятся, поэтому мы просто открываем настройки терминала (Ctrl + Alt + ,), а там ищем профиль, который нам интересен



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


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


Темы Powershell


От восприятия шелла может многое поменяться. По умолчанию шелл приветствует нас довольно грубо, он просто показывает наше расположение, относительно текущего диска. Нас это не особо и устраивает. Есть 2 типа людей: те которые любят, чтобы шелл показывал время, расположение, ветку Git'а, их имя, имя хоста и другую информацию, а есть те, которые просто хотят знать когда в шелл что-то можно вводить, а когда нельзя (обычная стрелка как в fish) минималисты в общем.


Я скорее себя отношу ко вторым, однако изменять шелл по умолчанию достаточно сложно, благо люди придумали oh-my-posh. Его установка убийственно легкая:


Install-Module oh-my-posh -Scope CurrentUser # Устанавливаем сам oh-my-poshInstall-Module posh-git -Scope CurrentUser # Устанавливаем дополнение к нему, для мониторинга статуса git'а

После того, как мы всё установим мы можем запросить список тем с помощью команды Get-Theme, а затем установить определённую тему с помощью Set-Theme:


PS C:\Users\Daniil_Shilo> Get-ThemeName                  Type     Location----                  ----     --------Agnoster              Defaults C:\Users\Daniil_Shilo\Documents\WindowsPowerShell\Modules\oh-my-posh\2.0.496\Themes\A...AgnosterPlus          Defaults C:\Users\Daniil_Shilo\Documents\WindowsPowerShell\Modules\oh-my-posh\2.0.496\Themes\A...Avit                  Defaults C:\Users\Daniil_Shilo\Documents\WindowsPowerShell\Modules\oh-my-posh\2.0.496\Themes\A...cypher                Defaults C:\Users\Daniil_Shilo\Documents\WindowsPowerShell\Modules\oh-my-posh\2.0.496\Themes\c...Darkblood             Defaults C:\Users\Daniil_Shilo\Documents\WindowsPowerShell\Modules\oh-my-posh\2.0.496\Themes\D...Emodipt               Defaults C:\Users\Daniil_Shilo\Documents\WindowsPowerShell\Modules\oh-my-posh\2.0.496\Themes\E...Fish                  Defaults C:\Users\Daniil_Shilo\Documents\WindowsPowerShell\Modules\oh-my-posh\2.0.496\Themes\F...Honukai               Defaults C:\Users\Daniil_Shilo\Documents\WindowsPowerShell\Modules\oh-my-posh\2.0.496\Themes\H...Lambda                Defaults C:\Users\Daniil_Shilo\Documents\WindowsPowerShell\Modules\oh-my-posh\2.0.496\Themes\L...Material              Defaults C:\Users\Daniil_Shilo\Documents\WindowsPowerShell\Modules\oh-my-posh\2.0.496\Themes\M...Operator              Defaults C:\Users\Daniil_Shilo\Documents\WindowsPowerShell\Modules\oh-my-posh\2.0.496\Themes\O...Paradox               Defaults C:\Users\Daniil_Shilo\Documents\WindowsPowerShell\Modules\oh-my-posh\2.0.496\Themes\P...Pararussel            Defaults C:\Users\Daniil_Shilo\Documents\WindowsPowerShell\Modules\oh-my-posh\2.0.496\Themes\P...Powerlevel10k-Classic Defaults C:\Users\Daniil_Shilo\Documents\WindowsPowerShell\Modules\oh-my-posh\2.0.496\Themes\P...Powerlevel10k-Lean    Defaults C:\Users\Daniil_Shilo\Documents\WindowsPowerShell\Modules\oh-my-posh\2.0.496\Themes\P...Powerlevel9k          Defaults C:\Users\Daniil_Shilo\Documents\WindowsPowerShell\Modules\oh-my-posh\2.0.496\Themes\P...PowerLine             Defaults C:\Users\Daniil_Shilo\Documents\WindowsPowerShell\Modules\oh-my-posh\2.0.496\Themes\P...PowerlinePlus         Defaults C:\Users\Daniil_Shilo\Documents\WindowsPowerShell\Modules\oh-my-posh\2.0.496\Themes\P...Punk                  Defaults C:\Users\Daniil_Shilo\Documents\WindowsPowerShell\Modules\oh-my-posh\2.0.496\Themes\P...pure                  Defaults C:\Users\Daniil_Shilo\Documents\WindowsPowerShell\Modules\oh-my-posh\2.0.496\Themes\p...qwerty                Defaults C:\Users\Daniil_Shilo\Documents\WindowsPowerShell\Modules\oh-my-posh\2.0.496\Themes\q...robbyrussell          Defaults C:\Users\Daniil_Shilo\Documents\WindowsPowerShell\Modules\oh-my-posh\2.0.496\Themes\r...Sorin                 Defaults C:\Users\Daniil_Shilo\Documents\WindowsPowerShell\Modules\oh-my-posh\2.0.496\Themes\S...Star                  Defaults C:\Users\Daniil_Shilo\Documents\WindowsPowerShell\Modules\oh-my-posh\2.0.496\Themes\S...tehrob                Defaults C:\Users\Daniil_Shilo\Documents\WindowsPowerShell\Modules\oh-my-posh\2.0.496\Themes\t...ys                    Defaults C:\Users\Daniil_Shilo\Documents\WindowsPowerShell\Modules\oh-my-posh\2.0.496\Themes\y...Zash                  Defaults C:\Users\Daniil_Shilo\Documents\WindowsPowerShell\Modules\oh-my-posh\2.0.496\Themes\Z...Set-Theme ys


Стало уже намного лучше. Шелл стал цветным, а также у нас нет надписи на пол экрана нашего местоположения.


Пакетный менеджер


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


Установка:


Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))

Далее вы можете установить любую другую программу с помощью Chocolatey, будь-то Google Chrome, Firefox, Docker, VSCode, и так далее. Обычно я устанавливаю данную утилиту, как только поставлю чистую Windows, так как с помощью неё можно быстро установить нужные программы не кликая на галочки и не скачивая ничего лишнего


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

Поиск пакетов


Chocolatey не следует синтаксису Powershell, из которого мы можем его запустить, это означает что он может использоваться как другие утилиты на том же Linux. Поиск пакетов выполняется с помощью ключевого слова search.


% choco search Google ChromeChocolatey v0.10.15GoogleChrome 88.0.4324.146 [Approved]google-hangouts-chrome 2017.110.418.20 [Approved]google-translate-chrome 2.0.7 [Approved]...

Установка и удаление пакетов


Для того чтобы установить пакеты нам нужно лишь название этого пакета (что логично).


% choco install <имя_пакета>

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


% choco list --local-only

Для того чтобы удалить какой-нибудь пакет нам нужно ввести:


% choco uninstall <имя_пакета>

Заключение


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


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

Подробнее..

Microsoft Message Center в Telegram через PowerShell и Azure Automation

09.03.2021 20:23:18 | Автор: admin

И плюс, и минус любой SaaS системы в том, что она управляется не нами, и мы никак (в большинстве случаев) не можем повлиять на цикл обновлений основного функционала и добавление новых фич. Однако эти обновления могут носить собой как информативный характер и не нести никаких серьезных изменений в функционал, так и могут и быть критическими для инфраструктуры, что в свою очередь несет собой дополнительные риски для бизнеса, а стало быть, и для нашего спокойствия, как для IT инженеров все это дело поддерживающих. О том, как получать все необходимые сообщения об обновлениях в Microsoft 365 не устанавливая для этого никаких дополнительных приложений будет эта статья. Из всего что нам понадобится, это зарегестированное приложение для доступа в API в Azure Active Directory, Azure Automation, PowerShell и бот в Телеграм.

Задача:

Написать скрипт, который будет раз в час стучаться в API M365, проверять там наличие новых сообщений и присылать их нам в Teams канал или в Telegram.

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

  1. Microsoft 365 Roadmap

  2. Microsoft 365 Message Center

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

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

Для простоты разработки, был выбран PowerShell, т. к. он нативно поддерживается Azure Automation чуть ли не с самого начала. Чтобы получить доступ к информации внутри тенанта не вводя логин и пароль, нужно зарегистрировать приложение в Azure Active Directory и дать приложению соответствующие права.

1. Заходим на сайт Azure и в строке поиска вводим Active Directory

AAD в AzureAAD в Azure

2. В левом боковом меню в разделе Manage выбираем App registrations

3. Здесь нажимаем на кнопку New Registration и попадаем в меню создания нового приложений внутри Azure Active Directory

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

5. Нажимаем Register и попадаем в созданное приложение. Здесь присутствуют данные, которые понадобятся нам в дальнейшем, а именно:

  • Application ID

  • Directory ID

Их желательно заранее куда ни будь сохранить.

6. Для авторизации посредством приложения, нужно сгенерировать Client Secret, без которого токен не получить. Для этого в левом боковом меню выбираем Certificates & Secrets и нажимаем на New Client Secret. Как понятно из названия здесь так же присутствует возможность авторизации посредством сертификата, но в данном примере будет использоваться именно Client Secret.

7. В появившемся окне нужно добавить Description к создаваемому секрету и указать его срок жизни.

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

9. Далее необходимо предоставить приложению права читать Message Center. Для этого переходим в меню API Permissions

10. Здесь присутствуют стандартные права в Graph API User.Read, которые отвечают за доступ к информации об аккаунте авторизовавшегося юзера.

Давайте на этом пункте остановится чуточку подробнее.

В M365 есть два основных вида прав, это Delegated Permissions, которые могут быть использованы только при авторизации в приложении пользователем, и Application Permissions которые можно использовать без авторизации человеком. Это очень полезно, когда необходимо что-то автоматизировать, а у вас в тенанте настроен обязательный MFA для всех учетных записей, однако Delegated Permissions требуют подтверждение Global администратора. User.Read права доступные по умолчанию как раз первого типа, потому их можно сразу же удалить. Щелкаем на права и нажимаем Remove permission.

Теперь же нужно добавить права для чтения Message Center. Нажимаем Add Permission > Скролим вниз и находим Office 365 Management API

11. Выбираем Application Permissions > ServiceHealth.Read и нажимаем Add Permissions

12. Далее если есть роль Global Admin, нажимаем кнопку Grant admin consent, либо просим одобрить права того, у кого эта роль имеется

13. После получения подтверждения, напротив прав должна появится надпись Granted for <tenant name>

На этом история с регистрацией приложения закончена и можно перейти к скрипту.

Первое что необходимо написать, это функцию для авторизации в M365 API, назовем ее Get-APIToken. Функция должна принимать в себя три значения:

  • Application ID

  • Tenant ID (directory ID)

  • App Secret (Client Secret)

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

Функция представляет собой Rest запрос с определенными параметрами на URL вида:

https://login.microsoftonline.com/ + $TenantID + /oauth2/v2.0/token

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

Function Get-ApiToken {    [CmdletBinding()]    param (        [Parameter(Mandatory=$True)]        [String]        $AppId, $AppSecret, $TenantID    )    $AuthUrl = "https://login.microsoftonline.com/$TenantID/oauth2/v2.0/token"    $Scope = "https://manage.office.com/.default"    $Body = @{        client_id = $AppId        client_secret = $AppSecret        scope = $Scope        grant_type = 'client_credentials'    }    $PostSplat = @{        ContentType = 'application/x-www-form-urlencoded'        Method = 'POST'        Body = $Body        Uri = $AuthUrl    }    try {        Invoke-RestMethod @PostSplat -ErrorAction Stop    }    catch {        Write-Warning "$(Get-Date): Exception was caught: $($_.Exception.Message)"     }}

Теперь можно получить Token и проверить что все прошлые действия были выполнены правильно.

try {    $Token = Get-ApiToken -AppId $ClientId -AppSecret $ClientSecret -TenantID $TenantId -ErrorAction Stop    Write-Output "$(Get-Date): Token successfully issued"}catch {    Write-Error "$(Get-Date): Can't get the token!"    break}

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

Для того, чтобы получить сообщения из Message Center понадобится две функции, Get-MCMessages и Get-ApiRequestResult

Начнем с функции Get-ApiRequestResult.

Она будет принимать URL запроса, метод и токен.

Из токена формируется header запроса и все это оформляется в Splat.

Function Get-ApiRequestResult {    [CmdletBinding()]    param (        [Parameter(Mandatory=$True)]        [String]        $Url, $Method, $Token    )     $Header = @{        Authorization = "$($Token.token_type) $($Token.access_token)"    }    $PostSplat = @{        ContentType = 'application/json'        Method = $Method        Header = $Header        Uri = $Url    }    try {        Invoke-RestMethod @PostSplat -ErrorAction Stop    }    catch {        $Ex = $_.Exception        $ErrorResponse = $ex.Response.GetResponseStream()        $Reader = New-Object System.IO.StreamReader($errorResponse)        $Reader.BaseStream.Position = 0        $Reader.DiscardBufferedData()        $ResponseBody = $Reader.ReadToEnd();        Write-Output "$(Get-Date): Response content:`n$responseBody" -f Red        throw Write-Error "$(Get-Date): Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)"    }}

Далее необходимо написать функцию для получения сообщений из Message Center.

Для этого нужно сделать Get запрос на адрес https://manage.office.com/api/ServiceComms/Messages

Function Get-MCMessages {    [CmdletBinding()]    param (        [Parameter(Mandatory=$True)]        $APIUrl, $TenantId    )    $ApiVersion = "v1.0"    $MS_resource = "ServiceComms/Messages?&`$filter=MessageType%20eq%20'MessageCenter'"    $Uri = "$APIUrl/$ApiVersion/$($TenantId)/$MS_resource"        $Method = "GET"    try {        Get-ApiRequestResult -Url $Uri -Token $Token -Method $Method -ErrorAction Stop        Write-Output "$(Get-Date): New messages successfully collected"    }    catch {        $Ex = $_.Exception        $ErrorResponse = $ex.Response.GetResponseStream()        $Reader = New-Object System.IO.StreamReader($errorResponse)        $Reader.BaseStream.Position = 0        $Reader.DiscardBufferedData()        $ResponseBody = $Reader.ReadToEnd();        Write-Output "$(Get-Date): Response content:`n$responseBody" -f Red        throw Write-Error "$(Get-Date): Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)"    }}

Небольшое отступление

Зачем сам запрос делится на две функции? Все дело в том, что принцип запросов к различным API M365 выглядит по большей части одинаково, отличаются только ссылки куда следует запрос, а также ресурсы, которые идут после основной ссылки. Для того чтобы переиспользовать последние две функции, необходимо только заменить значения в переменной $MS_Resource и $URL в функции Get-MCMessages. Так например можно получить информацию о мобильниках заэнроленных в Intune, изменив имя функции на Get-IntuneManagedDevices, значение переменной MS_Resource на "deviceManagement/managedDevices", и URL на https://graph.microsoft.com, что существенно сэкономит время на будущих скриптах. Ну и соответственно у приложения должны быть права на чтение информации об устройствах

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

Предполагается, что скрипт будет крутиться в Azure Automation, что накладывает определенные ограничения. Например, как-то нужно проверить новые сообщения на предмет свежести без сохранения всего этого в базу данных, плюс сообщения имеют свойство обновляться. В сообщении имеется параметр LastUpdatedTime, его мы и будем проверять.

Максимальная частота отработки Runbook в Azure Automation 1 раз в час, за этот период мы и будем проверять новые сообщения каждый час. Таким образом, нужны две временные метки: время, когда скрипт будет запущен и час назад от этого времени. Получаем переменные:

$CurrentTime = Get-Date$СontrolTime = ($CurrentTime).AddMinutes(-60)

Получаем список всех сообщений воспользовавшись написанной функцией Get-MCMessages.

$Messages=Get-MCmessages-APIUrl$APIUrl-TenantId$TenantId

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

$NewMessages=$Messages.value|Where-Object{$(Get-date$($_.LastUpdatedTime))-ge$controlTime}

Необходимо проверить есть ли вообще за последний час новые сообщения

$NewMessagesCount = $NewMessages.id.countif ($NewMessagesCount -gt 0) {    Write-Output "$(Get-Date): There are $NewMessagesCount new messages"}else {    Write-Output "$(Get-Date): There is no new messages"    break}

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

if($NewMessagesCount-gt0){foreach($NewMessagein$NewMessages){}}

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

$MessagePreview=$NewMessage.Messages.MessageText$MessageID=$NewMessage.id$MessageTitle=$NewMessage.Title$MessageType=$NewMessage.actiontype$PublishedTime=Get-date$($NewMessage.Messages.publishedTime)$UpdatedTime=Get-Date$($NewMessage.LastUpdatedTime)

Данные в MessageText попадают в формате html, однако мы знаем, что Telegram умеет далеко не все тэги. По этой причине убираем все те тэги, которые были собраны из полученных сообщений, и оставляем только те, которые Telegram принимает. Для этого создаем функцию Remove-HtmlTags, которая принимала бы в себя html и удаляла все те тэги, которые не поддерживаются.

Для этого внутри функции создаем два одномерных массива и один двумерный. Таким образом у нас есть три категории тегов:

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

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

  • Тэги и просто символы - которые нужно заменить на что-то иное.

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

Function Remove-HtmlTags {    param (        $Text    )    $SimpleTags = @(        'p',        'i',        'span',        'div',        'ul',        'ol',        'h1',        'h2',        'h3',        'div'    )    $TagsToRemove = (        "\<\/?font[^>]*\>",        '\<br\s?\/?\>',        '\&rarr',        'style=""',        ' target\=\"_blank\"'    )    $TagsToReplace = @(        @('\[','<b>'),        @('\]','</b>'),        @('\<A','<a'),        @('\<\/A\>','</a>'),        @('\<img[^>]*\>','[There was an image]'),        @('&nbsp;',' '),        @('\<li\>',' -'),        @('\<\/li\>',"`n")    )    foreach($Tag in $SimpleTags){        $Pattern = "\<\/?$tag\>"        $Text = $Text -replace $Pattern    }    foreach($Tag in $TagsToRemove){        $Text = $Text -replace $Tag    }    foreach($Tag in $TagsToReplace){        $Text = $Text -replace $Tag    }        foreach($Tag in $SimpleTags){        $Pattern = "\<\/?$Tag\>"        $Text = $Text -replace $Pattern    }    $Text    }

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

Скрипт в том виде что вы его видите стал таким не сразу, и претерпел за время определенные изменения. Например, было замечено, что Microsoft особо не заморачивается со стандартизацией и форматированием контента своих сообщений, по этой причине доверять отступам и переносам строки в тексте было бы крайне опрометчиво. Это вынудило меня разбить полученный текст сначала по разрывам строк используя закрывающий тэг </p>, а далее обрабатывать его уже как массив, после же все это объединить в одну переменную, но уже с вынужденным переносом строк без html тэгов. Так же нужно удалить лишние пробелы в тексте.

$MessageTextWithHtmlString=$MessagePreview-split('\<\/p\>')$FormattedMesssageText=$(Remove-HtmlTags$MessageTextWithHtmlString)-creplace'(?m)^\s*\r?\n',''

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

$PublishingInfo="Published:$PublishedTime`nUpdated:$UpdatedTime"$TgmMessage="$BoldMessageTitle`n$MessageDescription`n$PublishingInfo`n$FormattedMesssageText"

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

$MessageActionRequiredByDate=$NewMessage.ActionRequiredByDate$MessageAdditionalInformation=$NewMessage.ExternalLink$MessageBlogLink=$NewMessage.BlogLinkif($MessageActionRequiredByDate){$TgmMessage+="`nActionrequiredbydate:$MessageActionRequiredByDate"}elseif($MessageAdditionalInformation){$TgmMessage+="`n$MessageAdditionalInformation'>Additionalinfo"}elseif($MessageBlogLink){$TgmMessage+="`n$MessageBlogLink'>Blog"}

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

Функция должна принять ChatID, Token, ParsingType, на случай если функцию нужно будет где-то использовать еще, ну и сам текст сообщения.

functionSend-TelegramMessage{[CmdletBinding()]param([Parameter(Mandatory=$true)][string]$MessageText,$TokenTelegram,$ChatID[Parameter(Mandatory=$true)][ValidateSet("html","markdown")][string]$ParsingType)$URL_set="http://personeltest.ru/aways/api.telegram.org/bot$TokenTelegram/sendMessage"$Body=@{text=$MessageTextparse_mode=$ParsingTypechat_id=$chatID}$MessageJson=$body|ConvertTo-Jsontry{Invoke-RestMethod$URL_set-MethodPost-ContentType'application/json;charset=utf-8'-Body$MessageJson-ErrorActionStopWrite-Output"$(Get-Date):Messagehasbeensent"}catch{Write-Error"$(Get-Date):Can'tsentmessage"Write-Output"$(Get-Date):StatusCode:"$_.Exception.Response.StatusCode.value__Write-Output"$(Get-Date):StatusDescription:"$_.Exception.Response.StatusDescriptionthrow}    }

И, собственно, сама отправка сообщения:

Send-TelegramMessage-MessageText$TgmMessage-TokenTelegram$TokenTelegram-ChatID$chatID-ParsingType'html'

В итоге мы получаем сообщения вида:

Что касается такой служебной информации как токены, чат ID, секреты и прочее, то все это можно и нужно хранить в месте специально для этого предназначенном. В Azure Automation это Secure Assets. Более подробную информацию можно получить по ссылке.

Ссылка на репозиторий с кодом

Ссылка на канал с запущенным ботом

P.S. буду рад контрибьюторам и предложениям по улучшению бота.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    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