PowerShell 技能连载 - PowerShell 中的并行处理

如果想提升一个脚本的执行速度,您也许会发现后台任务十分有用。它们适用于做大量并发处理的脚本。

PowerShell 是单线程的,一个时间只能处理一件事。使用后台任务时,后台会创建额外的 PowerShell 进程并且共享负荷。这只在任务彼此独立,并且后台任务不产生很多数据的情况下能很好地工作。从后台任务中发回数据是一个开销很大的过程,并且很有可能把节省出来的时间给消耗了,导致脚本执行起来反而更慢。

以下是三个可以并发执行的任务:

$start = Get-Date

# get all hotfixes
$task1 = { Get-Hotfix }

# get all scripts in your profile
$task2 = { Get-Service | Where-Object Status -eq Running }

# parse log file
$task3 = { Get-Content -Path $env:windir\windowsupdate.log | Where-Object { $_ -like '*successfully installed*' } }

# run 2 tasks in the background, and 1 in the foreground task
$job1 =  Start-Job -ScriptBlock $task1
$job2 =  Start-Job -ScriptBlock $task2
$result3 = Invoke-Command -ScriptBlock $task3

# wait for the remaining tasks to complete (if not done yet)
$null = Wait-Job -Job $job1, $job2

# now they are done, get the results
$result1 = Receive-Job -Job $job1
$result2 = Receive-Job -Job $job2

# discard the jobs
Remove-Job -Job $job1, $job2

$end = Get-Date
Write-Host -ForegroundColor Red ($end - $start).TotalSeconds

在一个测试环境中,执行所有三个任务消耗 5.9 秒。三个任务的结果分别保存到 $result1,$result2,$result3。

我们测试一下三个任务在前台顺序执行所消耗的时间:

$start = Get-Date

# get all hotfixes
$task1 = { Get-Hotfix }

# get all scripts in your profile
$task2 = { Get-Service | Where-Object Status -eq Running }

# parse log file
$task3 = { Get-Content -Path $env:windir\windowsupdate.log | Where-Object { $_ -like '*successfully installed*' } }

# run them all in the foreground:
$result1 = Invoke-Command -ScriptBlock $task1
$result2 = Invoke-Command -ScriptBlock $task2
$result3 = Invoke-Command -ScriptBlock $task3

$end = Get-Date
Write-Host -ForegroundColor Red ($end - $start).TotalSeconds

这段代码仅仅执行了 5.05 秒。所以后台任务只对于长期运行并且各自占用差不多时间的任务比较有效。由于这三个测试任务返回了大量的数据,所以并发执行带来的好处差不多被将执行结果序列化并传回前台进程的过程给抵消掉了。

PowerShell 技能连载 - 将 Tick 转换为真实的日期

Active Directory 内部使用 tick (从 1601 年起的百纳秒数)来表示日期和时间。在以前,要将这个大数字转换为人类可读的日期和时间是很困难的。以下是一个很简单的办法:

[DateTime]::FromFileTime(635312826377934727)

类似地,要将一个日期转换为 tick 数,使用以下方法:

PowerShell 技能连载 - 记录脚本的运行时间

如果您想记录脚本的运行时间,您可以使用 Measure-Command,但是这个 cmdlet 仅适合诊断目的,并且没有计算输出时间。

另一种方法是创建两个快照,并且在结束时计算时间差。

这段代码将告诉您 Get-Hotfix cmdlet 的执行时间,包括输出数据的时间:

$start = Get-Date

Get-HotFix

$end = Get-Date
Write-Host -ForegroundColor Red ('Total Runtime: ' + ($end - $start).TotalSeconds)

PowerShell 技能连载 - 修正 Excel CSV 的编码

当您将 Microsoft Excel 的数据保存为 CSV 格式时,很不幸的是保存的编码和 Import-Csv 的缺省编码并不匹配。所以当您将 CSV 文件导入 PowerShell 时,无论您指定哪种编码,特殊字符都会变成乱码。

以下是一个我从 Excel 导出的 list.csv 文件,它包含一些特殊字符。如果您使用缺省编码,特殊字符会变成乱码,并且如果您指定了 -Encoding 参数,无论您传什么值,特殊字符都不会显示回原来正常的状态:

当您模拟在这些场景中 Import-Csv 的行为时,它很意外地可以完美处理:

这说明要正确地读取 Excel CSV 文件,您必须显式地指定“缺省”编码(这引出了一个问题:当您未指定编码的时候,缺省使用的是什么编码):

PowerShell 技能连载 - 读取整个文本文件

您可以用 Get-Content 来读入整个文本文件。但是,Get-Content 是逐行返回文件的内容,您得到的是一个 string 数组,并且换行符被去掉了。

要一次性读取整个文本文件,从 PowerShell 3.0 开始,您可以使用 -Raw 参数(它还有个好处,能够大大加快读取文件的速度)。

所以通过以下代码您可以获得一个字符串数组,每个元素是一行文本:

Length 属性表示文件的行数。

以下代码一次性读取整个文本文件,返回单个字符串:

这回,Length 属性表示整个文件的字符数,并且读取文件的速度大大提高(虽然也更占内存了)。

那种方法更好?这取决于您要如何使用这些数据。

PowerShell 技能连载 - 存储秘密数据

如果您想以只有您能获取的方式保存敏感数据,您可以使用这个有趣的方法:将明文转换成密文,需要时将密文转换回明文,并将它保存到磁盘中:

$storage = "$env:temp\secretdata.txt"
$mysecret = 'Hello, I am safe.'

$mysecret |
  ConvertTo-SecureString -AsPlainText -Force |
  ConvertFrom-SecureString |
  Out-File -FilePath $storage

当您打开该文件的时候,它读起来像这个样子:

您的秘密被 Windows 自带的数据保护 API(DPAPI) 用您的身份和机器作为密钥加密。所以只有您(或任何以您的身份运行的进程)可以将该密文解密,而且只能在加密时所用的计算机上解密。

要得到明文,请使用这段代码:

$storage = "$env:temp\secretdata.txt"
$secureString = Get-Content -Path $storage |
  ConvertTo-SecureString

$ptr = [System.Runtime.InteropServices.Marshal]::SecureStringToGlobalAllocUnicode($secureString)
$mysecret = [System.Runtime.InteropServices.Marshal]::PtrToStringUni($ptr)

$mysecret

它可以正常使用——您可以获得和加密前一模一样的文本。

现在,以其他人的身份试一下。您会发现其他人无法解密该加密文件。而且您在别的机器上也无法解密。

PowerShell 技能连载 - 使用加密文件系统(EFS)来保护密码

如果您必须在脚本中以硬编码的方式包含密码和其它隐私信息(正常情况下应避免使用),那么您还可以通过 EFS(加密文件系统)的方式来保障安全性。加密的脚本只能被加密者读取(和执行),所以只有您在自己的机器上能运行该脚本。

一下是加密一个 PowerShell 脚本的简单方法:

# create some sample script
# replace path with some real-world existing script if you want
# and remove the line that creates the script
$path = "$env:temp\test.ps1"
"Write-Host 'I run only for my master.'" > $path

$file = Get-Item -Path $path
$file.Encrypt()

当您运行这段脚本时,它将在您的临时文件夹中创建一个用 EFS 加密的新的 PowerShell 脚本(如果您见到一条错误提示信息,那么很有可能您机器上的 EFS 不可用或者被禁用了)。

加密之后,该文件在 Windows 资源管理器中呈现绿色,并且只有您能够运行它。别人无法看见源代码。

请注意在许多企业环境中,EFS 系统是通过恢复密钥部署的。指定的维护人员可以通过主密钥解密文件。如果没有主密钥,一旦您丢失了您的 EFS 证书,就连您也无法查看或运行加密的脚本。

PowerShell 技能连载 - 验证 UNC 路径

Test-Path 命令可以检测指定的文件或文件夹是否存在。它对于使用盘符的路径工作正常,但是对于纯 UNC 路径则不可用。

最简单的情况下,这应该返回 $true,并且它的确返回了 $true(假设您没有禁用管理员共享):

$path = '\\127.0.0.1\c$'

Test-Path -Path $path

现在,同样的代码却返回 $false:

Set-Location -Path HKCU:\
$path = '\\127.0.0.1\c$'

Test-Path -Path $path

如果路径不是使用一个盘符,PowerShell 将使用当前路径,如果该路径指向一个非文件系统位置,Test-Path 将在该 provider 的上下文中解析 UNC 路径。由于注册表中没有这个路径,Test-Path 返回 $false。

要让 Test-Path 在 UNC 路径下可靠地工作,请确保您在 UNC 路径之前添加了 FileSystem provider。现在,无论当前位于哪个驱动器路径,结果都是正确的:

Set-Location -Path HKCU:\
$path = 'filesystem::\\127.0.0.1\c$'

Test-Path -Path $path

PowerShell 技能连载 - 启用 PowerShell 远程管理

如果您希望用 PowerShell 远程管理来执行另一台机器上的命令或脚本,那么您需要以完整管理员权限启用目标机器上的远程管理功能:

在客户端,当您在同一个域中并且使用同一个域用户登录时,您不需要做任何额外的事情。

如果您希望通过非 Kerberos 验证方式连接目标计算机时(目标计算机在另一个域中,或您希望使用 IP 地址或非完整限定 DNS 名来连接),那么您需要以管理员权限运行一次以下代码:

将信任的主机设置为“*”之后,PowerShell 将允许您连接任何 IP 或机器名,如果无法用 Kerberos 验证身份,将使用 NTLM 验证。所以该设置不影响哪些人可以和该主机通信(通过防火墙规则设置)。它只是告诉 PowerShell 您将在 Kerberos 不可用的时候使用(更不安全一些的)NTLM 验证方式。NTLM 更不安全一些,因为它无法知道目标计算机是否真的是您想要访问的计算机。Kerberos 认证有相互认证过程,而 NTLM 没有。您的凭据直接被发送到指定的计算机中。假如当一个攻击者有机会用他的机器替换掉目标机器,并且占据了它的 IP 地址,而您使用 NTLM 的话,不会得到任何通知。

注意:如果你打开了远程并设置了信任列表后想关闭远程请运行 Disable-PSRemoting,不禁用远程将可能被人利用。

当远程管理打开以后,您可以通过 Enter-PSSession 访问远程系统,并且您可以用 Invoke-Command 在这些机器上运行命令或脚本。

PowerShell 技能连载 - 启用传统远程控制

许多 cmdlet 有内置的远程功能,例如 Get-ServiceGet-Process 都具有 -ComputerName 参数,同样的还有 Get-WmiObject

然而,要真正地远程使用这些 cmdlet,还需要一些先决条件。多数使用传统远程技术的 cmdlet 需要在目标机器上启用“远程管理”防火墙规则。它允许 DCOM 通信。还有一些需要目标计算机运行远程注册表服务。

所以在多数场景中,当您拥有目标机器的管理员,并且运行以下命令,则管理员可以通过传统远程 cmdlet 访问目标机器:

注意新版的 Windows 中 netsh firewall 命令可能会被废弃,不过目前仍然可以用。该命令比新版的 netsh advfirewall 命令用起来更简单。