PowerShell 技能连载 - Invoke-RestMethod 退出错误

无论是 Invoke-WebRequest 还是 Invoke-RestMethod,都是简单易用的命令,用于从网络上下载信息。例如,以下简单代码可以查询任何公共 IP 地址的注册信息并返回其注册所有者:

1
2
3
4
5
6
7
8
9
10
11
12
$IPAddress = '51.107.59.180'
Invoke-RestMethod -Uri "http://ipinfo.io/$IPAddress/json" -UseBasicParsing

ip : 51.107.59.180
city : Zürich
region : Zurich
country : CH
loc : 47.3667,8.5500
org : AS8075 Microsoft Corporation
postal : 8090
timezone : Europe/Zurich
readme : https://ipinfo.io/missingauth

如果在你的代码与互联网通信时出现问题,在 Windows PowerShell 中有一个硬编码的超时时间为 300 秒。因此,即使你的路由器出现故障,你的 PowerShell 脚本最终也会返回。

在 PowerShell 7 中,这两个命令的内部工作方式发生了重大改变。它们目前不再具有连接超时功能,因此如果你的连接中断,你的脚本将永远挂起。

幸运的是,这个错误目前正在解决中,并将在即将推出的 PowerShell 7 更新中修复。

PowerShell 技能连载 - 两种类型转换(和一个 bug)

为了明确将一个数据类型转换为另一个数据类型,PowerShell提供了两种方法:

1
2
3
4
5
PS> [int]5.6
6

PS> 5.6 -as [int]
6

虽然这两种方法在大多数情况下都会产生相同的结果,但存在细微差异:

  • 当你在数据之前添加目标类型时,PowerShell 使用美国文化,并在转换失败时引发异常。
  • 当你使用 “-as“ 运算符时,PowerShell 使用你的本地文化,在转换失败时不引发异常。

当你在非英语系统上进行字符串到日期时间的转换时,不同的文化变得重要:

1
2
3
4
5
6
7
PS> [datetime]'1.5.23'

Donnerstag, 5. Januar 2023 00:00:00

PS> '1.5.23' -as [datetime]

Montag, 1. Mai 2023 00:00:00

最后,这两种方法都存在一个奇怪的错误,即使输入的字符串明显损坏,类型转换仍然有效:

PS> [Type] ‘int]whatever’

IsPublic IsSerial Name BaseType


True True Int32 System.ValueType

PS> ‘int]whatever’ -as [Type]

IsPublic IsSerial Name BaseType


True True Int32 System.ValueType

不过,不要利用这个错误,因为在 PowerShell 7 中很快会修复它。

PowerShell 技能连载 - 挂载 ISO 文件

在我们之前的提示中,我们展示了如何轻松将本地文件夹转换为 ISO 文件镜像。今天,我们来看一下如何挂载(以及卸载)您自己和其他任何 ISO 文件,以便它们可以像本地文件系统驱动器一样使用。

挂载 ISO 文件很简单:

1
2
3
4
# 确保您调整此路径,使其指向现有的ISO文件:
$Path = "$env:temp\myImageFile.iso"
$result = Mount-DiskImage -ImagePath $Path -PassThru
$result

执行此代码后,Windows 资源管理器中会出现一个新的光驱,可以像其他驱动器一样使用。基于 ISO 镜像的驱动器当然是只读的,因为它们的行为就像常规的 CD-ROM。

虽然 Mount-DiskImage 可以成功挂载 ISO 镜像,但它不会将分配的驱动器字母返回给您。如果您想从脚本内部访问 ISO 镜像的内容,下面是如何找出它分配的驱动器字母:

1
2
3
4
5
6
7
8
9
# 确保您调整此路径,使其指向现有的ISO文件:
$Path = "$env:temp\myImageFile.iso"
$result = Mount-DiskImage -ImagePath $Path -PassThru
$result

$volume = $result | Get-Volume
$letter = $volume.Driveletter + ":\"

explorer $letter

在使用后卸载驱动器,请运行 Dismount-DiskImage 并指定您之前挂载的ISO文件的路径:

1
Dismount-DiskImage -ImagePath $Path

PowerShell 技能连载 - 创建 ISO 文件

PowerShell 可以将普通文件夹转换为 ISO 文件。ISO 文件是二进制文件,可以被挂载并表现得像只读 CD-ROM 驱动器。

过去,ISO 文件常用于挂载安装媒体。如今,您可以轻松地创建自己的 ISO 文件,这些文件是从您自己的文件夹和文件中创建的。这样,您可以创建一个简单的备份系统,或者轻松地在同事之间共享项目。由于 ISO 文件只是一个单一的文件,因此可以轻松地共享,而且由于 Windows 通过双击挂载它们,并在 Windows 资源管理器中显示它们作为 CD-ROM 驱动器,您可以立即使用数据而无需提取或解压任何内容。

与 VHD 映像文件不同,挂载 ISO 文件不需要管理员特权。任何人都可以挂载和使用 ISO 文件。

由于没有内置的 cmdlet 将文件夹结构转换为 ISO 文件,您需要自己调用内部 API。下面的代码定义了新函数 New-IsoFile

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
function New-IsoFile
{
param
(
# path to local folder to store in
# new ISO file (must exist)
[Parameter(Mandatory)]
[String]
$SourceFilePath,

# name of new ISO image (arbitrary,
# turns later into drive label)
[String]
$ImageName = 'MyCDROM',

# path to ISO file to be created
[Parameter(Mandatory)]
[String]
$NewIsoFilePath,

# if specified, the source base folder is
# included into the image file
[switch]
$IncludeRoot
)

# use this COM object to create the ISO file:
$fsi = New-Object -ComObject IMAPI2FS.MsftFileSystemImage

# use this helper object to write a COM stream to a file:
# compile the helper code using these parameters:
$cp = [CodeDom.Compiler.CompilerParameters]::new()
$cp.CompilerOptions = '/unsafe'
$cp.WarningLevel = 4
$cp.TreatWarningsAsErrors = $true
$code = '
using System;
using System.IO;
using System.Runtime.InteropServices.ComTypes;

namespace CustomConverter
{
public static class Helper
{
// writes a stream that came from COM to a filesystem file
public static void WriteStreamToFile(object stream, string filePath)
{
// open output stream to new file
IStream inputStream = stream as IStream;
FileStream outputFileStream = File.OpenWrite(filePath);
int bytesRead = 0;
byte[] data;

// read stream in chunks of 2048 bytes and write to filesystem stream:
do
{
data = Read(inputStream, 2048, out bytesRead);
outputFileStream.Write(data, 0, bytesRead);
} while (bytesRead == 2048);

outputFileStream.Flush();
outputFileStream.Close();
}

// read bytes from stream:
unsafe static private byte[] Read(IStream stream, int byteCount, out int readCount)
{
// create a new buffer to hold the read bytes:
byte[] buffer = new byte[byteCount];
// provide a pointer to the location where the actually read bytes are reported:
int bytesRead = 0;
int* ptr = &bytesRead;
// do the read:
stream.Read(buffer, byteCount, (IntPtr)ptr);
// return the read bytes by reference to the caller:
readCount = bytesRead;
// return the read bytes to the caller:
return buffer;
}
}
}'

Add-Type -CompilerParameters $cp -TypeDefinition $code

# define the ISO file properties:

# create CDROM, Joliet and UDF file systems
$fsi.FileSystemsToCreate = 7
$fsi.VolumeName = $ImageName
# allow larger-than-CRRom-Sizes
$fsi.FreeMediaBlocks = -1

$msg = 'Creating ISO File - this can take a couple of minutes.'
Write-Host $msg -ForegroundColor Green

# define folder structure to be written to image:
$fsi.Root.AddTreeWithNamedStreams($SourceFilePath,$IncludeRoot.IsPresent)

# create image and provide a stream to read it:
$resultimage = $fsi.CreateResultImage()
$resultStream = $resultimage.ImageStream

# write stream to file
[CustomConverter.Helper]::WriteStreamToFile($resultStream, $NewIsoFilePath)

Write-Host 'DONE.' -ForegroundColor Green

}

运行此代码后,您现在将拥有一个名为“New-IsoFile”的新命令。从现有文件夹结构创建ISO文件现在变得轻而易举 - 只需确保源文件路径存在即可:

1
PS> New-IsoFile -NewIsoFilePath $env:temp\MyTest.iso -ImageName Holiday -SourceFilePath 'C:\HolidayPics'

您将在临时文件夹(或您指定的其他文件路径)中获得一个新的 ISO 文件。如果您按照示例操作,只需打开临时文件夹:

1
PS> explorer /select,$env:temp\MyTest.iso

当你在Windows资源管理器中双击ISO文件时,该映像将作为一个新的光驱挂载,你可以立即看到映像文件中存储的数据的副本。

在Windows资源管理器中右键单击新的光驱,并从上下文菜单中选择“弹出”将卸载该光驱。

PowerShell 技能连载 - Creating ISO Files

PowerShell 可以将普通文件夹转换为 ISO 文件。ISO 文件是二进制文件,可以被挂载并像只读 CD-ROM 驱动器一样运行。

过去,ISO 文件常用于挂载安装媒体。今天,您可以轻松地创建自己的 ISO 文件,并从自己的文件夹和文件中创建。这样,您可以创建一个简单的备份系统或者方便地与同事共享项目。由于 ISO 文件只是一个单独的文件,因此它们可以很容易地共享,并且由于 Windows 通过双击来挂载它们,并在 Windows Explorer 中显示它们作为 CD-ROM 驱动器,所以您可以立即使用数据而无需提取或解压任何内容。

与 VHD 映像文件相比,挂载 ISO 文件不需要管理员权限。任何人都可以挂载和使用 ISO 文件。

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
function New-IsoFile
{
param
(
# path to local folder to store in
# new ISO file (must exist)
[Parameter(Mandatory)]
[String]
$SourceFilePath,

# name of new ISO image (arbitrary,
# turns later into drive label)
[String]
$ImageName = 'MyCDROM',

# path to ISO file to be created
[Parameter(Mandatory)]
[String]
$NewIsoFilePath,

# if specified, the source base folder is
# included into the image file
[switch]
$IncludeRoot
)

# use this COM object to create the ISO file:
$fsi = New-Object -ComObject IMAPI2FS.MsftFileSystemImage

# use this helper object to write a COM stream to a file:
# compile the helper code using these parameters:
$cp = [CodeDom.Compiler.CompilerParameters]::new()
$cp.CompilerOptions = '/unsafe'
$cp.WarningLevel = 4
$cp.TreatWarningsAsErrors = $true
$code = '
using System;
using System.IO;
using System.Runtime.InteropServices.ComTypes;

namespace CustomConverter
{
public static class Helper
{
// writes a stream that came from COM to a filesystem file
public static void WriteStreamToFile(object stream, string filePath)
{
// open output stream to new file
IStream inputStream = stream as IStream;
FileStream outputFileStream = File.OpenWrite(filePath);
int bytesRead = 0;
byte[] data;

// read stream in chunks of 2048 bytes and write to filesystem stream:
do
{
data = Read(inputStream, 2048, out bytesRead);
outputFileStream.Write(data, 0, bytesRead);
} while (bytesRead == 2048);

outputFileStream.Flush();
outputFileStream.Close();
}

// read bytes from stream:
unsafe static private byte[] Read(IStream stream, int byteCount, out int readCount)
{
// create a new buffer to hold the read bytes:
byte[] buffer = new byte[byteCount];
// provide a pointer to the location where the actually read bytes are reported:
int bytesRead = 0;
int* ptr = &bytesRead;
// do the read:
stream.Read(buffer, byteCount, (IntPtr)ptr);
// return the read bytes by reference to the caller:
readCount = bytesRead;
// return the read bytes to the caller:
return buffer;
}
}
}'

Add-Type -CompilerParameters $cp -TypeDefinition $code

# define the ISO file properties:

# create CDROM, Joliet and UDF file systems
$fsi.FileSystemsToCreate = 7
$fsi.VolumeName = $ImageName
# allow larger-than-CRRom-Sizes
$fsi.FreeMediaBlocks = -1

$msg = 'Creating ISO File - this can take a couple of minutes.'
Write-Host $msg -ForegroundColor Green

# define folder structure to be written to image:
$fsi.Root.AddTreeWithNamedStreams($SourceFilePath,$IncludeRoot.IsPresent)

# create image and provide a stream to read it:
$resultimage = $fsi.CreateResultImage()
$resultStream = $resultimage.ImageStream

# write stream to file
[CustomConverter.Helper]::WriteStreamToFile($resultStream, $NewIsoFilePath)

Write-Host 'DONE.' -ForegroundColor Green

}

一旦你运行了这段代码,你现在就有了一个名为 “New-IsoFile” 的新命令。从现有文件夹结构创建 ISO 文件现在变得非常简单 - 只需确保源文件路径存在即可:

1
PS> New-IsoFile -NewIsoFilePath $env:temp\MyTest.iso -ImageName Holiday -SourceFilePath 'C:\HolidayPics'  

您将在临时文件夹中获得一个新的 ISO 文件(或者您指定的任何其他文件路径)。如果您按照示例操作,只需打开临时文件夹即可:

1
PS> explorer /select,$env:temp\MyTest.iso

当您在 Windows 资源管理器中双击 ISO 文件时,该映像将作为新的 CD-ROM 驱动器挂载,并且您可以立即看到存储在图像文件中的数据副本。

右键单击 Windows 资源管理器中的新 CD-ROM 驱动器并从上下文菜单中选择“弹出”将卸载该驱动器。

PowerShell 技能连载 - 进度条技巧(第 4 部分)

由于广大用户的要求,这里提供了一段代码,演示如何使用嵌套进度条并显示每个任务的“真实”进度指示器:

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
$servers = 'dc-01', 'dc-02', 'msv3', 'msv4'
$ports = 80, 445, 5985

$counterServers = 0
$servers | ForEach-Object {
# increment server counter and calculate progress
$counterServers++
$percentServers = $counterServers * 100 / $servers.Count

$server = $_
Write-Progress -Activity 'Checking Servers' -Status $server -Id 1 -PercentComplete $percentServers

$counterPorts = 0
$ports | ForEach-Object {
# increment port counter and calculate progress
$counterPorts++
$percentPorts = $counterPorts * 100 / $ports.Count


$port = $_
Write-Progress -Activity 'Checking Port' -Status $port -Id 2 -PercentComplete $percentPorts

# here would be your code that performs some task, i.e. a port test:
Start-Sleep -Seconds 1
}
}

PowerShell 技能连载 - 进度条技巧(第 3 部分)

PowerShell 内置的进度条可以嵌套,每个任务显示一个进度条。为了使其正常工作,请为您的进度条分配不同的 ID 号码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$servers = 'dc-01', 'dc-02', 'msv3', 'msv4'
$ports = 80, 445, 5985

$servers | ForEach-Object {
$server = $_
Write-Progress -Activity 'Checking Servers' -Status $server -Id 1

$ports | ForEach-Object {
$port = $_
Write-Progress -Activity 'Checking Port' -Status $port -Id 2

# here would be your code that performs some task, i.e. a port test:
Start-Sleep -Seconds 1
}
}

PowerShell 技能连载 - 进度条技巧(第 2 部分)

内置的 PowerShell 进度条支持“真实”的进度指示器,只要您提交一个在 0 到 100 范围内的“percentCompleted”值:

1
2
3
4
5
6
0..100 | ForEach-Object {
$message = '{0:p0} done' -f ($_/100)
Write-Progress -Activity 'I am busy' -Status $message -PercentComplete $_

Start-Sleep -Milliseconds 100
}

为了显示一个“真实”的进度指示器,因此您的脚本需要“知道”已经处理了多少给定任务。

以下是一个修改后的示例,它定义了需要处理多少个任务,然后从中计算出完成百分比:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$data = Get-Service  # for illustration, let's assume you want to process all services

$counter = 0
$maximum = $data.Count # number of items to be processed

$data | ForEach-Object {
# increment counter
$counter++
$percentCompleted = $counter * 100 / $maximum
$message = '{0:p1} done, processing {1}' -f ($percentCompleted/100), $_.DisplayName
Write-Progress -Activity 'I am busy' -Status $message -PercentComplete $percentCompleted

Write-Host $message
Start-Sleep -Milliseconds 100
}

PowerShell 技能连载 - 进度条技巧(第 1 部分)

PowerShell自带内置进度条。通常情况下,当脚本完成时,它会自动消失:

1
2
3
4
Write-Progress -Activity 'I am busy' -Status 'Step A'
Start-Sleep -Seconds 2
Write-Progress -Activity 'I am busy' -Status 'Step B'
Start-Sleep -Seconds 2

如果您想在脚本仍在运行时关闭进度条,则需要使用“-Completed”开关参数:

1
2
3
4
5
6
7
Write-Progress -Activity 'I am busy' -Status 'Step A'
Start-Sleep -Seconds 2
Write-Progress -Activity 'I am busy' -Status 'Step B'
Start-Sleep -Seconds 2
Write-Progress -Completed -Activity 'I am busy'
Write-Host 'Progress bar closed, script still running.'
Start-Sleep -Seconds 2

如您所见,关闭进度条需要同时指定“-Activity”参数,因为它是一个强制性的参数。但是,如果您只想关闭所有可见的进度条,则“-Activity”参数的值并不重要。你可以提交一个空格或数字等任何值(除了null值或空字符串),因为这些都不会被强制性参数接受。

写入以下代码以定义“- Activity” 参数的默认值:

1
2
3
4
5
6
7
Write-Progress -Activity 'I am busy' -Status 'Step A'
Start-Sleep -Seconds 2
Write-Progress -Activity 'I am busy' -Status 'Step B'
Start-Sleep -Seconds 2
Write-Progress -Completed -Activity ' '
Write-Host 'Progress bar closed, script still running.'
Start-Sleep -Seconds 2

写入以下代码以定义 “-Activity“ 参数的默认值:

1
$PSDefaultParameterValues['Write-Progress:Activity']='xyz'  

现在,”Write-progress“ 将接受 “Completed“ 参数而无需提交 “Activity“ 参数:

1
2
3
4
5
6
7
Write-Progress -Activity 'I am busy' -Status 'Step A'
Start-Sleep -Seconds 2
Write-Progress -Activity 'I am busy' -Status 'Step B'
Start-Sleep -Seconds 2
Write-Progress -Completed # due to the previously defined new default value, -Activity can now be omitted
Write-Host 'Progress bar closed, script still running.'
Start-Sleep -Seconds 2