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 版本的一部分。

PowerShell 技能连载 - Get-ComputerInfo 和 systeminfo.exe 的对比(第 2 部分)

在 PowerShell 5 中,引入了一个名为 Get-ComputerInfo 的新的 cmdlet,它完成曾经 systeminfo.exe 的功能,而 Get-ComputerInfo 是直接面向对象的。没有本地化的问题:

1
$infos = Get-ComputerInfo

您现在可以查询您电脑独立的详情:

1
2
3
$infos.OsInstallDate
$infos.OsFreePhysicalMemory
$infos.BiosBIOSVersion

或者使用 Select-Object 来选择所有兴趣的属性:

1
$infos | Select-Object -Property OSInstallDate, OSFreePhysicalMemory, BiosBIOSVersion

在缺点方面,请考虑这一点:Get-ComputerInfo 是在 PowerShell 5 中引入的,您可以很容易地更新到该版本,或者将 PowerShell Core 与旧版本的 Windows PowerShell 并行使用。然而,Get-ComputerInfo 检索到的许多信息仅来自于最近的 Windows 操作系统中添加的 WMI 类。

如果您在 Windows 7 中更新到了 Windows PowerShell 5.1,Get-ComputerInfo 有可能不能正常工作。在旧的系统中,systeminfo.exe 是您的最佳依赖,而在新的操作系统中,Get-ComputerInfo 用起来方便得多。

PowerShell 技能连载 - Get-ComputerInfo 和 systeminfo.exe 的对比(第 1 部分)

在很长一段时间内,命令行工具 systeminfo.exe 提供了大量计算机的信息,并且可以通过一个小技巧返回面向对象的结果:

1
2
3
4
$objects = systeminfo.exe /FO CSV |
ConvertFrom-Csv

$objects.'Available Physical Memory'

从好的一方面来说,systeminfo.exe 基本上在所有 Windows 系统中都可用。从坏的一方面来说,结果是语言本地化的,并且属性名可能会成为一个问题:在英文的系统中,一个属性可能名为 ‘Available Physical Memory’ 而在一个德文系统中可能会不同。要使表头一致,您可以将它们去除并替换成自己的:

1
2
3
4
$headers = 1..30 | ForEach-Object { "Property$_" }
$objects = systeminfo.exe /FO CSV |
Select-Object -Skip 1 |
ConvertFrom-Csv -Header $headers

以下是执行结果:

PS> $objects


Property1  : DESKTOP-8DVNI43
Property2  : Microsoft Windows 10 Pro
Property3  : 10.0.18362 N/A Build 18362
Property4  : Microsoft Corporation
Property5  : Standalone Workstation
Property6  : Multiprocessor Free
Property7  : hello@test.com
Property8  : N/A
Property9  : 00330-50000-00000-AAOEM
Property10 : 9/3/2019, 11:42:41 AM
Property11 : 11/1/2019, 10:42:53 AM
Property12 : Dell Inc.
Property13 : XPS 13 7390 2-in-1
Property14 : x64-based PC
Property15 : 1 Processor(s) Installed.,[01]: Intel64 Family 6 Model 126 Stepping 5
             GenuineIntel ~1298 Mhz
Property16 : Dell Inc. 1.0.9, 8/2/2019
Property17 : C:\Windows
Property18 : C:\Windows\system32
Property19 : \Device\HarddiskVolume1
Property20 : de;German (Germany)
Property21 : de;German (Germany)
Property22 : (UTC+01:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna
Property23 : 32,536 MB
Property24 : 19,169 MB
Property25 : 37,400 MB
Property26 : 22,369 MB
Property27 : 15,031 MB
Property28 : C:\pagefile.sys
Property29 : WORKGROUP
Property30 : \\DESKTOP-8DVNI43




PS> $objects.Property23
32,536 MB

您可以通过在 $headers 中构造一个自定义的属性名列表,任意对属性命名:

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
30
31
32
33
34
35
36
37
38
39
$headers = 'HostName',
'OSName',
'OSVersion',
'OSManufacturer',
'OSConfiguration',
'OSBuildType',
'RegisteredOwner',
'RegisteredOrganization',
'ProductID',
'OriginalInstallDate',
'SystemBootTime',
'SystemManufacturer',
'SystemModel',
'SystemType',
'Processors',
'BIOSVersion',
'WindowsDirectory',
'SystemDirectory',
'BootDevice',
'SystemLocale',
'InputLocale',
'TimeZone',
'TotalPhysicalMemory',
'AvailablePhysicalMemory',
'VirtualMemoryMaxSize',
'VirtualMemoryAvailable',
'VirtualMemoryInUse',
'PageFileLocations',
'Domain',
'LogonServer',
'Hotfix',
'NetworkCard',
'HyperVRequirements'

$objects = systeminfo.exe /FO CSV |
Select-Object -Skip 1 |
ConvertFrom-Csv -Header $headers

$objects.ProductID

PowerShell 技能连载 - 在 PowerShell 中安全地使用 WMI(第 4 部分)

在这个迷你系列中,我们在探索 Get-WmiObjectGet-CimInstance 之间的差异。未来的 PowerShell 版本不再支持 Get-WMIObject,因此,如果您尚未加入,则需要切换到 Get-CimInstance

在前一个部分中您学到了通过网络查询信息时有明显的区别,并且 Get-CimInstance 可以使用可充分定制且可复用的会话对象,来帮助网络访问速度更快以及消耗更少的资源。

由于 Get-CimInstance 工作机制类似 Web Service,而不像 Get-WmiObject 是基于 DCOM 的,这在返回的数据上有许多重要的含义。Get-CimInstance 总是通过序列化,所以您总是不可避免地收到一个副本,而不是原始对象。这是为什么 Get-CimInstance 永远不返回方法的原因。您永远只会获取到属性。

以下是一个可实践的示例:Win32_Process WMI 类有一个名为 GetOwner() 的方法返回进程的所有者。如果您希望找出谁登录到您的计算机,您需要查询 explorer.exe 进程并列出他们的所有者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# find all explorer.exe instances
Get-WmiObject -Class Win32_Process -Filter 'Name="explorer.exe"' |
ForEach-Object {
# call the WMI method GetOwner()
$owner = $_.GetOwner()
if ($owner.ReturnValue -eq 0)
{
# return either the process owner...
'{0}\{1}' -f $owner.Domain, $owner.User
}
else
{
# ...or the error code
'N/A (Error Code {0})' -f $owner.ReturnValue
}
} |
# remove duplicates
Sort-Object -Unique

如果您希望将这段代码转换为使用 Get-CimInstance,那么将无法访问 GetOwner() 方法,因为 Get-CimInstance 只返回一个属性集合。相应地需要执行 Invoke-CimMethod 方法来调用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# find all explorer.exe instances
Get-CimInstance -ClassName Win32_Process -Filter 'Name="explorer.exe"' |
ForEach-Object {
# call the WMI method GetOwner()
$owner = $_ | Invoke-CimMethod -MethodName GetOwner
if ($owner.ReturnValue -eq 0)
{
# return either the process owner...
'{0}\{1}' -f $owner.Domain, $owner.User
}
else
{
# ...or the error code
'N/A (Error Code {0})' -f $owner.ReturnValue
}
} |
# remove duplicates
Sort-Object -Unique

Invoke-CimMethod 或者需要和 CimInstance 配合使用,或者需要和原始的查询合并,这可以进一步简化代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# find all explorer.exe instances
Invoke-CimMethod -Query 'Select * From Win32_Process Where Name="explorer.exe"' -MethodName GetOwner |
ForEach-Object {
if ($_.ReturnValue -eq 0)
{
# return either the process owner...
'{0}\{1}' -f $_.Domain, $_.User
}
else
{
# ...or the error code
'N/A (Error Code {0})' -f $_.ReturnValue
}
} |
# remove duplicates
Sort-Object -Unique

Invoke-CimMethod 也可以调用静态的 WMI 方法,这些方法是属于 WMI 类的方法而不是属于一个独立的实例。如果您希望在本地或者远程创建一个新的进程(启动一个新的程序),那么可以使用这样的一行代码:

1
2
3
4
5
PS> Invoke-CimMethod -ClassName Win32_Process -MethodName "Create" -Arguments @{ CommandLine = 'notepad.exe'; CurrentDirectory = "C:\windows\system32" }

ProcessId ReturnValue PSComputerName
--------- ----------- --------------
3308 0

注意:如果您在一台远程计算机上启动了一个程序,它将会在您的隐藏登录会话中运行并且在屏幕上不可见。

PowerShell 技能连载 - 在 PowerShell 中安全地使用 WMI(第 3 部分)

在这个迷你系列中,我们在探索 Get-WmiObjectGet-CimInstance 之间的差异。未来的 PowerShell 版本不再支持 Get-WMIObject,因此,如果您尚未加入,则需要切换到 Get-CimInstance

在前一个部分中您学习了对于 WMI 类,两个 cmdlet 返回相同的基本信息,但是两个 cmdlet 添加的元数据属性差异显著,并且偶然情况下,属性的数据类型也不相同。例如,日期和时间类型在 Get-CimInstance 命令中返回的是 DateTime 类型,而在 Get-WmiObject 命令中返回的是字符串类型。不过总的来说,差异不大,容易调整。

当您在网络上查询,Get-WmiObjectGet-CimInstance 的差异就比较明显了:

  • Get-WmiObject 使用 DCOM 进行所有远程处理。它是硬连线的。DCOM 是一个旧的远程处理协议。它使用大量的资源,需要在防火墙中打开许多端口。
  • Get-CimInstance 缺省情况下使用的是 WinRM 并且工作方式类似 Web Service。不过,您可以通过多种方式改变这种行为,所以远程处理层可以和实际的 WMI 查询层分离。

当连到旧的服务器时,您可能会注意到这个区别,服务器能正确响应 Get-WmiObject 但是对于 Get-CimInstance 命令抛出异常。旧的服务器只支持旧的 DCOM 协议。

以下是您如何告诉 Get-CimInstance 回落到 Get-WmiObject 使用的旧式 DCOM 协议:

1
2
3
4
5
6
7
8
9
10
11
# replace server name to some server that you control
$server = 'SOMESERVER1'

# Get-CimInstance uses WinRM remoting by default
Get-CimInstance -ClassName Win32_BIOS -ComputerName $server

# telling Get-CimInstance to use the old DCOM to contact an old system
$options = New-CimSessionOption -Protocol Dcom
$session = New-CimSession -SessionOption $options -ComputerName $server
Get-CimInstance -ClassName Win32_BIOS -CimSession $session
Remove-CimSession -CimSession $session

如您所见,您可以配置一个 CimSession 对象,它是可以充分配置的。使用 CIMSession 对象而不是计算机名还有一个额外的好处:您可以在多个查询中复用该会话来节约许多时间。只需要确保结束时关闭会话即可。

PowerShell 技能连载 - 在 PowerShell 中安全地使用 WMI(第 2 部分)

在这个迷你系列中,我们在探索 Get-WmiObjectGet-CimInstance 之间的差异。未来的 PowerShell 版本不再支持 Get-WMIObject,因此,如果您尚未加入,则需要切换到 Get-CimInstance

在前面的部分中,您了解到两个 cmdlet 都返回相同的 WMI 类的基本信息,但是两个 cmdlet 添加的元数据属性有很大不同。现在让我们仔细看看它们返回的基本信息。

我们细化了测试脚本,也可以考虑数据类型,因此我们不仅查找属性名称,而且还考虑了这些属性返回的数据的类型。为此,我们检查属性“TypeNameOfValue”。

由于这是一个字符串,类型名并不一定是一致的。它们可能显示为 “Bool” 与 “Boolean”,或者 “String” 与 “System.String”。要使结果可以用来对比,代码使用了一个位于 $typeName 的计算属性来忽略类型命名空间,并且在一个 switch 语句中人工做调整。如果您恰好发现另一个名字显示不正确,只需要扩展 switch 语句。

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
30
31
32
33
34
35
36
37
38
39
40
41
# we are comparing this WMI class (feel free to adjust)
$wmiClass = 'Win32_OperatingSystem'

# get information about the WMI class Win32_OperatingSystem with both cmdlets
$a = Get-WmiObject -Class $wmiClass | Select-Object -First 1
$b = Get-CimInstance -ClassName $wmiClass | Select-Object -First 1

# create a calculated property that returns only the basic type name
# and omits the namespace
$typeName = @{
Name = 'Type'
Expression = {
$type = $_.TypeNameOfValue.Split('.')[-1].ToLower()
switch ($type)
{
'boolean' { 'bool' }
default { $type }
}
}
}

# ignore the metadata properties which we already know are different
$meta = '__CLASS','__DERIVATION','__DYNASTY','__GENUS','__NAMESPACE','__PATH','__PROPERTY_COUNT','__RELPATH','__SERVER','__SUPERCLASS','CimClass','CimInstanceProperties','CimSystemProperties','ClassPath','Container','Options','Properties','Qualifiers','Scope','Site','SystemProperties'

# return the properties and their data type. Add the origin so we later know
# which cmdlet emitted them
$aDetail = $a.PSObject.Properties |
# exclude the metadata we already know is different
Where-Object { $_.Name -notin $meta } |
# add the origin command as new property "Origin"
Select-Object -Property Name, $typeName, @{N='Origin';E={'Get-WmiObject'}}
$bDetail = $b.PSObject.Properties |
# exclude the metadata we already know is different
Where-Object { $_.Name -notin $meta } |
# add the origin command as new property "Origin"
Select-Object -Property Name, $typeName, @{N='Origin';E={'Get-CimInstance'}}

# compare differences
Compare-Object -ReferenceObject $aDetail -DifferenceObject $bDetail -Property Name, Type -PassThru |
Select-Object -Property Name, Origin, Type |
Sort-Object -Property Name

以下是执行结果:

Name           Origin          Type
----           ------          ----
InstallDate    Get-CimInstance ciminstance#datetime
InstallDate    Get-WmiObject   string
LastBootUpTime Get-CimInstance ciminstance#datetime
LastBootUpTime Get-WmiObject   string
LocalDateTime  Get-CimInstance ciminstance#datetime
LocalDateTime  Get-WmiObject   string
Path           Get-WmiObject   managementpath
  • Get-CimInstanceDateTime 对象的形式返回日期和时间,而 Get-WmiObject 以字符串的形式返回它们,字符串格式是内部的 WMI 格式
  • Get-WmiObject 增加了另一个名为 “Path” 的元数据属性

Get-CimInstance 处理日期和时间比 Get-WmiObject 更简单得多:

1
2
3
4
5
6
PS> $a.InstallDate
20190903124241.000000+120

PS> $b.InstallDate

Tuesday, September 3, 2019 12:42:41

通过以上代码,您现在有了一个方便的工具来测试您脚本使用的 WMI 类,并在从 Get-WmiObject 迁移到 Get-CimInstance 时标识可能返回不同数据类型的属性。