PowerShell 技能连载 - 列出已安装的更新(第 2 部分)

在前一个技能中我们演示了如何通过 Windows Update 客户端获取当前已安装的更新。

可以对此列表进行润色,例如您可以使用哈希表创建计算的属性,提取默认情况下属于其他属性的信息,如知识库文章编号作为标题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$severity = @{
Name = 'Severity'
Expression = { if ([string]::IsNullOrEmpty($_.MsrcSeverity)) { 'normal' } else { $_.MsrcSeverity }}
}

$time = @{
Name = 'Time'
Expression = { $_.LastDeploymentChangeTime }
}

$kb = @{
Name = 'KB'
Expression = { if ($_.Title -match 'KB\d{6,9}') { $matches[0] } else { 'N/A' }}
}

$UpdateSession = New-Object -ComObject Microsoft.Update.Session
$UpdateSession.CreateupdateSearcher().Search("IsInstalled=1").Updates |
Select-Object $time, Title, $kb, Description, $Severity |
Out-GridView -Title 'Installed Updates'

结果显示在一个网格视图窗口中。如果您移除了 Out-GridView 那么信息看起来类似这样:

Time        : 9/10/2019 12:00:00 AM
Title       : 2019-09 Security Update for Adobe Flash Player for Windows 10 Version 1903 for
              x64-based Systems (KB4516115)
KB          : KB4516115
Description : A security issue has been identified in a Microsoft software product that could
              affect your system. You can help protect your system by installing this update
              from Microsoft. For a complete listing of the issues that are included in this
              update, see the associated Microsoft Knowledge Base article. After you install
              this update, you may have to restart your system.
Severity    : Critical

Time        : 10/8/2019 12:00:00 AM
Title       : Windows Malicious Software Removal Tool x64 - October 2019 (KB890830)
KB          : KB890830
Description : After the download, this tool runs one time to check your computer for infection by specific, prevalent malicious software (including Blaster, Sasser,
and Mydoom) and helps remove any infection that is found. If an infection is found, the tool will display a status report the next time that you start your computer. A new version of the tool will be offered every month. If you want to manually run the tool on your computer, you can download a copy from the Microsoft Download Center, or you can run an online version from microsoft.com. This tool is not a replacement for an antivirus product. To help protect your computer, you should use an antivirus product.
Severity    : normal

Time        : 10/8/2019 12:00:00 AM
Title       : 2019-10 Cumulative Update for .NET Framework 3.5 and 4.8 for Windows 10 Version
              1903 for x64 (KB4524100)
KB          : KB4524100
Description : Install this update to resolve issues in Windows. For a complete listing of the issues that are included in this update, see the associated Microsoft Knowledge Base article for more information. After you install this item, you may have to restart your computer.
Severity    : normal

Time        : 10/28/2019 12:00:00 AM
Title       : Update for Windows Defender Antivirus antimalware platform - KB4052623 (Version 4.18.1910.4)
KB          : KB4052623
Description : This package will update Windows Defender Antivirus antimalware platform’s components on the user machine.
Severity    : normal

...

PowerShell 技能连载 - 列出已安装的更新(第 1 部分)

Get-Hotfix 只会列出操作系统相关的 hotfix:

1
Get-HotFix

实际上,它只是一个 WMI 查询的简单包装,结果是一样的:

1
Get-CimInstance -ClassName Win32_QuickFixEngineering

一个更简单更完整的方法是查询系统事件日志获取所有安装的更新:

1
2
3
4
5
6
7
8
9
10
Get-WinEvent @{
Logname='System'
ID=19
ProviderName='Microsoft-Windows-WindowsUpdateClient'
} | ForEach-Object {
[PSCustomObject]@{
Time = $_.TimeCreated
Update = $_.Properties.Value[0]
}
}

显然,当系统事件日志清除之后,查询结果就不完整了。此外,该日志只是记录任何更新安装,因此随着时间的推移,新的更新可能取代旧的更新。

要保证获取到完整的已安装更新列表,您需要请求 Windows Update 客户端,从实际安装的更新中重建列表,这要消耗更多的时间:

1
2
3
4
$result = (New-Object -ComObject Microsoft.Update.Session).CreateupdateSearcher().Search("IsInstalled=1").Updates |
Select-Object LastDeploymentChangeTime, Title, Description, MsrcSeverity

$result | Out-GridView -Title 'Installed Updates'

PowerShell 技能连载 - 退出 PowerShell 管道(第 2 部分:手动退出)

在前一个技能中我们学到了如何当达到一定次数的时候退出 PowerShell 管道,这样可以节约很多时间:

1
2
3
$fileToSearch = 'ngen.log'
Get-ChildItem -Path c:\Windows -Recurse -ErrorAction SilentlyContinue -Filter $fileToSearch |
Select-Object -First 1

显然,当一定数量的结果传递给 Select-Object 之后,Select-Object 会发送秘密的信息到上一级管道的 cmdlet,告知它们停止。实际上,Select-Object 会抛出一个特殊的异常,PowerShell 会处理这个异常,才产生这个魔术的效果。

但是如果事先不知道确切的结果数量呢?如果您希望在其它情况下中断管道呢?如果您希望实现某种超时呢?要手动退出一个管道,只需要让 Select-Object 发出这个特殊的异常。以下是一个专门做这件事的 Stop-Pipeline 函数:

1
2
3
4
5
6
7
function Stop-Pipeline
{
$pipeline = { Select-Object -First 1 }.GetSteppablePipeline()
$pipeline.Begin($true)
$pipeline.Process(1)
$pipeline.End()
}

它调用了一个 Select-Object 并且模仿在管道中执行。它是通过 GetSteppablePipeline() 实现的。您现在可以通过 Process() 人工传递数据给 Select-Object。由于通过 -First 1 参数执行 Select-Object 命令,所以当传递任何数据给 Select-Object,都会产生该特殊的异常。

您现在获得了控制权,并且可以通过任何条件来调用 Stop-Pipeline。以下示例程序将搜索文件并且在最长 2 秒之内退出管道:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function Stop-Pipeline
{
$pipeline = { Select-Object -First 1 }.GetSteppablePipeline()
$pipeline.Begin($true)
$pipeline.Process(1)
$pipeline.End()
}

# abort pipeline after 2000 milliseconds
$timeout = 2000
# create a stopwatch
$stopwatch = [system.diagnostics.stopwatch]::StartNew()

Get-ChildItem -Path c:\Windows -Recurse -ErrorAction SilentlyContinue |
ForEach-Object {
if ($stopwatch.ElapsedMilliseconds -gt $timeout)
{
$stopwatch.Stop()
Write-Warning "Timeout, Pipeline Aborted."
# abort pipeline
Stop-Pipeline
}

# return the original object
$_
}

PowerShell 技能连载 - 退出 PowerShell 管道(第 1 部分:Select-Object)

有些时候人工退出 PowerShell 管道可以节约很多时间。例如,在开始递归搜索之前不能确切地知道一个文件所在的位置,当搜到文件的时候立刻停止。当找到文件以后没理由继续搜索其它目录。

下面是一个演示该问题的场景:假设您在 Windows 文件夹中的某个地方搜索一个名为 “ngen.log” 的文件:

1
2
3
$fileToSearch = 'ngen.log'

Get-ChildItem -Path c:\Windows -Recurse -ErrorAction SilentlyContinue -Filter $fileToSearch

PowerShell 将会查找这个文件但是找到之后还会继续搜索目录树的其它部分,这会消耗很多时间。

如果您知道需要查找结果的个数,那么可以当搜到指定数量的结果后使用 Select-Object 来立刻退出管道。

1
2
3
$fileToSearch = 'ngen.log'
Get-ChildItem -Path c:\Windows -Recurse -ErrorAction SilentlyContinue -Filter $fileToSearch |
Select-Object -First 1

在这个例子中,当找到文件时 PowerShell 立即退出。作为最佳实践,如果您事先知道需要从命令中查找多少个结果,那么在管道尾部添加 Select-Object,并且用 -First 参数来告诉 PowerShell 需要的结果个数。

PowerShell 技能连载 - 使用一个计时器来测量执行时间

有一些情况下您希望知道一段代码需要执行多长时间,例如返回统计或者对比代码,有许多方法可以测量命令,包括 Measure-Command cmdlet:

1
2
3
4
5
6
7
$duration = Measure-Command -Expression {
$result = Get-Hotfix
}

$time = $duration.TotalMilliseconds

'{0} results in {1:n1} milliseconds' -f $result.Count, $time

不过 Measure-Command 有一些不受人喜欢的副作用:

  • 所有输出都被丢弃,这样输出数据不会影响测量时间,而且您无法控制这个行为
  • 出于好几个原因,它会减慢您的代码执行,其中一个是 Measure-Command 在 dot-sourced 表达式中以一个独立的脚本快执行

所以,常常使用另一个技术,用的是 Get-Date,例如这样:

1
2
3
4
5
6
$start = Get-Date
$result = Get-Hotfix
$end = Get-Date
$time = ($end - $start).TotalMilliseconds

'{0} results in {1:n1} milliseconds' -f $result.Count, $time

它可以有效工作,不过也有一些不受欢迎的副作用:

  • 它产生更多的代码
  • 当计算机处于睡眠或休眠状态,将会影响结果,因为计算机关闭的时间不应该计入统计时间

一个更简洁的解决方案是使用 .NET 的 stopwatch 对象,它产生的代码更少,并且不会减缓代码的执行,而且不受睡眠或休眠的影响:

1
2
3
4
5
$stopwatch =  [system.diagnostics.stopwatch]::StartNew()
$result = Get-Hotfix
$time = $stopwatch.ElapsedMilliseconds

'{0} results in {1:n1} milliseconds' -f $result.Count, $time

此外,您可以对 stopwatch 对象调用 Stop()Restart()Reset() 方法。通过这些方法,您可以暂停测量代码中的某些部分(例如数据输出)并且继续测量。

PowerShell 技能连载 - Foreach -parallel (第 3 部分:批量 Ping)

在 PowerShell 7 中,带来了一个新的并行的 Foreach-Object,它可以并行执行代码并显著加快操作的速度。同样的技术可通过以下模块在 Windows PowerShell 中使用:

1
Install-Module -Name PSParallel -Scope CurrentUser -Force

我们来看看有趣的案例。例如,如果您需要获得一个能够响应 ping (ICMP) 的计算机列表,这将需要很长时间。一个加速操作的方法是使用带超时设置的 ping,这样无需对不响应的机器等待太久。

这段代码 ping powershell.one 并且等待最长 1000 毫秒(代码来自 https://powershell.one/tricks/network/ping):

1
2
3
4
5
6
$ComputerName = 'powershell.one'
$timeout = 1000
$ping = New-Object System.Net.NetworkInformation.Ping

$ping.Send($ComputerName, $timeout) |
Select-Object -Property Status, @{N='IP';E={$ComputerName}}, Address

现在让我们来加速整件事,整批 ping 整个 IP 段!

我们先来看看在 PowerShell 7 中如何实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#requires -Version 7.0

# IP range to ping
$IPAddresses = 1..255 | ForEach-Object {"192.168.0.$_"}

# timeout in milliseconds
$timeout = 1000

# number of simultaneous pings
$throttleLimit = 80

# measure execution time
$start = Get-Date

$result = $IPAddresses | ForEach-Object -ThrottleLimit $throttleLimit -parallel {
$ComputerName = $_
$ping = [System.Net.NetworkInformation.Ping]::new()

$ping.Send($ComputerName, $using:timeout) |
Select-Object -Property Status, @{N='IP';E={$ComputerName}}, Address
} | Where-Object Status -eq Success

$end = Get-Date
$time = ($end - $start).TotalMilliseconds

Write-Warning "Execution Time $time ms"

$result

在 5 秒钟左右,整个 IP 段都 ping 完毕,并且返回会 ICMP 请求的 IP 地址。

在 Windows PowerShell 中,这段代码基本差不多(假设您已从 PowerShell Gallery 中安装了 PSParallel 模块):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#requires -Modules PSParallel
#requires -Version 3.0

# IP range to ping
$IPAddresses = 1..255 | ForEach-Object {"192.168.0.$_"}

# number of simultaneous pings
$throttleLimit = 80

# measure execution time
$start = Get-Date

$result = $IPAddresses | Invoke-Parallel -ThrottleLimit $throttleLimit -ScriptBlock {
$ComputerName = $_
# timeout in milliseconds
$timeout = 1000

$ping = New-Object -TypeName System.Net.NetworkInformation.Ping

$ping.Send($ComputerName, $timeout) |
Select-Object -Property Status, @{N='IP';E={$ComputerName}}, Address
} | Where-Object Status -eq Success

$end = Get-Date
$time = ($end - $start).TotalMilliseconds

Write-Warning "Execution Time $time ms"

$result

ForEach-Object 不同,代码使用的是 Invoke-Parallel,而且由于 Invoke-Parallel 不支持 “use:“ 前缀,所以所有局部变量都必须包含在脚本块中(在我们的示例中,是变量$timeout)。

Invoke-Parallel 支持一个友好的进度条,这样您可以知道有多少个任务正在并行执行。

PowerShell 技能连载 - Foreach -parallel (第 2 部分:Windows PowerShell)

PowerShell 7 发布了一个内置参数,可以并行运行不同的任务:

1
1..100 | ForEach-Object -ThrottleLimit 20 -Parallel { Start-Sleep -Seconds 1; $_ }

如果您使用的是 Windows PowerShell,那么您也可以使用类似的并行技术。例如,下载并安装这个免费的模块:

1
Install-Module -Name PSParallel -Scope CurrentUser -Force

它带来一个新的命令:Invoke-Parallel:您可以这样使用它:

1
1..100 | Invoke-Parallel -ThrottleLimit 20 -Scrip-tBlock { Start-Sleep -Seconds 1; $_ }

由于 Invoke-Parallel 使用的是和 PowerShell 7 相同的技术,所以有相同的限制:每个线程都在它自己的线程中执行,并且不能存取本地变量。在下一个技能中,我们将学习一些有趣的示例。

PowerShell 技能连载 - Foreach -parallel(第 1 部分:PowerShell 7)

PowerShell 7 发布了一个内置参数,可以并行运行不同的任务。以下是一个简单示例:

1
1..100 | ForEach-Object -ThrottleLimit 20 -Parallel { Start-Sleep -Seconds 1; $_ }

在普通的 ForEach-Object 循环中,这将花费 100 秒的时间执行。如果使用了 parallel,代码可以并行地执行。-ThrottleLimit 定义了“块”,因此在本例中,有20个线程并行运行,使总执行时间减少到5秒。

在过于激动之前,请记住每个线程都在其自己的 PowerShell 环境中运行。幸运的是,您可以访问前缀为“using:”的局部变量:

1
2
3
$text = "Output: "

1..100 | ForEach-Object -ThrottleLimit 20 -Parallel { Start-Sleep -Seconds 1; "$using:text $_" }

不过,当您开始使用多线程时,您需要了解线程安全知识。复杂对象,例如 ADUser 对象可能无法在多个线程之间共享,所以需要每个案例独立判断是否适合使用并行。

由于并行的 ForEach-Object 循环内置在 PowerShell 7 中,这并不意味着可以在 Windows PowerShell 中使用并行。在 Windows PowerShell 中有许多模块实现了该功能。我们将会在接下来的技能中介绍它们。

PowerShell 技能连载 - -RepeatHeader 参数

有一个不太为人所知的参数:RepeatHeader,它是做什么用的?

假设您希望分页显示结果(在命令行中有效,而在 PowerShell ISE 中无效):

1
PS> Get-Process | Out-Host -Paging

结果输出时每页会暂停,直到按下空格键。然而,列标题只显示在第一页中。

以下是更好的输出形式:

1
PS> Get-Process | Format-Table -RepeatHeader | Out-Host -Paging

现在,每一页都会重新显示列标题。-RepeatHeader 在所有的 Format-* cmdlet 中都有效。再次提醒,这个技巧只在基于控制台的 PowerShell 宿主中有效,并且在 PowerShell ISE 中无效。原因是:PowerShell ISE 没有固定的页大小,所以它无法知道一页什么时候结束。

PowerShell 技能连载 - PowerShell 7 中的三元操作符

在 PowerShell 7 中,语言增加了一个新的操作符,引发了大量的辩论。基本上,您不必要使用它,但是有编程背景的用户会喜欢它。

直到现在,要创建一个条件判断,总是需要写许多代码。例如,要查询脚本运行环境是 32 位还是 64 位,您可以像这样查询指针的长度:

1
2
3
4
5
6
7
8
if ([IntPtr]::Size -eq 8)
{
'64-bit'
}
else
{
'32-bit'
}

三元操作符可以大大缩短代码:

1
[IntPtr]::Size -eq 8 ? '64-bit' : '32-bit'

本质上,三元操作符 (“?“) 是标准 “if“ 条件判断的缩写。它对所有取值为 $true$false 的表达式有效。如果该表达式的执行结果是 $true,那么执行 “?“ 之后的表达式。如果该表达式的执行结果是 $false,那么执行 “:“ 之后的表达式。

如果您安装了 PowerShell 7 preview 版,请确保您更新到了最新版本,才能确保使用三元操作符。它并不是 PowerShell 7 preview 版本的一部分。