PowerShell 技能连载 - WSL 集成与互操作

适用于 PowerShell 7.0 及以上版本

Windows Subsystem for Linux (WSL) 已经从一个实验性的兼容层发展为成熟的 Linux 运行环境。WSL2 基于真正的 Linux 内核,支持 systemd、Docker 以及绝大多数原生 Linux 应用。对于日常在 Windows 上工作的运维工程师和开发者来说,WSL 提供了一条低成本的 Linux 工具链接入路径——无需双系统,无需虚拟机管理程序的开销。

PowerShell 与 WSL 的互操作不仅限于简单的命令转发。借助 wsl 命令行工具、\\wsl$ 网络路径以及双向的进程调用机制,可以在一个脚本中自由混合 Windows 和 Linux 工具。例如,用 PowerShell 采集 Windows 事件日志,再通过管道传给 WSL 中的 awk 做文本分析;或者反过来,在 WSL 中编译项目后调用 PowerShell 部署到 Windows 服务。这种混合工作流在 DevOps 和跨平台自动化场景中尤为实用。

本文将从实例管理、跨平台数据交换和自动化部署三个维度,展示 PowerShell 与 WSL 深度集成的实战技巧。

管理 WSL 实例生命周期

日常工作中,我们经常需要创建、备份、迁移 WSL 实例。PowerShell 可以把 wsl.exe 的能力封装成可复用的管理函数,实现实例的批量运维。

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
# 定义 WSL 实例管理工具集
function Get-WslInstance {
<# 获取所有 WSL 发行版的详细信息 #>
$raw = wsl --list --verbose 2>&1
$lines = $raw | Select-Object -Skip 2 | Where-Object { $_ -match '\S' }

foreach ($line in $lines) {
$parts = $line.Trim() -split '\s+'
[PSCustomObject]@{
IsDefault = $line -match '^\*'
Name = $parts[0] -replace '^\*', ''
State = $parts[1]
Version = $parts[2]
}
}
}

function Export-WslInstance {
<# 将 WSL 实例导出为 tar 文件,用于备份或迁移 #>
param(
[Parameter(Mandatory)][string]$Name,
[Parameter(Mandatory)][string]$OutputPath
)

if (-not (Test-Path (Split-Path $OutputPath -Parent))) {
New-Item -ItemType Directory -Path (Split-Path $OutputPath -Parent) -Force |
Out-Null
}

Write-Host "正在导出 $Name ..." -ForegroundColor Cyan
wsl --export $Name $OutputPath

$size = [math]::Round((Get-Item $OutputPath).Length / 1GB, 2)
Write-Host "导出完成:$OutputPath (${size} GB)" -ForegroundColor Green
}

function Import-WslInstance {
<# 从 tar 文件导入为新的 WSL 实例 #>
param(
[Parameter(Mandatory)][string]$Name,
[Parameter(Mandatory)][string]$InstallPath,
[Parameter(Mandatory)][string]$SourceFile
)

if (-not (Test-Path $InstallPath)) {
New-Item -ItemType Directory -Path $InstallPath -Force | Out-Null
}

Write-Host "正在导入 $Name ..." -ForegroundColor Cyan
wsl --import $Name $InstallPath $SourceFile
Write-Host "导入完成:$Name => $InstallPath" -ForegroundColor Green
}

function Remove-WslInstance {
<# 注销并删除 WSL 实例(谨慎使用) #>
param(
[Parameter(Mandatory)][string]$Name,
[switch]$Force
)

if (-not $Force) {
$confirm = Read-Host "确认要删除 WSL 实例 '$Name' 吗?(yes/no)"
if ($confirm -ne 'yes') {
Write-Host "已取消" -ForegroundColor Yellow
return
}
}

wsl --unregister $Name
Write-Host "已删除 WSL 实例:$Name" -ForegroundColor Red
}

# 批量备份所有运行中的实例
$instances = Get-WslInstance | Where-Object State -eq 'Running'
$backupDir = "D:\WSL-Backups\$(Get-Date -Format 'yyyyMMdd')"

foreach ($inst in $instances) {
$tar = Join-Path $backupDir "$($inst.Name).tar"
Export-WslInstance -Name $inst.Name -OutputPath $tar
}

执行结果示例:

1
2
3
4
正在导出 Ubuntu-24.04 ...
导出完成:D:\WSL-Backups\20251217\Ubuntu-24.04.tar (1.85 GB)
正在导出 Debian ...
导出完成:D:\WSL-Backups\20251217\Debian.tar (0.92 GB)

跨平台命令调用与数据交换

PowerShell 与 WSL 之间的数据交换是互操作的核心。下面展示几种常见模式:结构化数据传递、环境变量共享,以及双向脚本编排。

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
# 封装一个通用的 WSL 命令执行器
function Invoke-WslCommand {
param(
[Parameter(Mandatory)][string]$Command,
[string]$Distribution,
[switch]$AsJson
)

$args = @()
if ($Distribution) { $args += '-d', $Distribution }
$args += '--', 'bash', '-c', $Command

$result = & wsl @args 2>&1
$result = $result | Out-String

if ($AsJson -and $result) {
try {
return $result | ConvertFrom-Json
} catch {
Write-Warning "JSON 解析失败,返回原始文本"
return $result
}
}
return $result.Trim()
}

# 场景 1:用 Linux 工具分析 Windows 日志
# 先用 PowerShell 导出事件日志为 CSV,再用 WSL 的 awk 统计
$csvPath = "$env:TEMP\security_events.csv"
Get-WinEvent -LogName Security -MaxEvents 500 |
Select-Object TimeCreated, Id, LevelDisplayName, Message |
Export-Csv $csvPath -NoTypeInformation -Encoding UTF8

# 将 Windows 路径转为 WSL 可访问的路径
$wslCsv = ($csvPath -replace '^([A-Z]):', { '/mnt/' + $_.Groups[1].Value.ToLower() } `
-replace '\\', '/')

$stats = Invoke-WslCommand "awk -F',' '{print `$3}' $wslCsv | sort | uniq -c | sort -rn"
Write-Host "安全事件级别分布:" -ForegroundColor Cyan
Write-Host $stats

# 场景 2:从 WSL 获取系统信息,返回结构化对象
$sysInfo = Invoke-WslCommand -AsJson -Command @"
cat /etc/os-release | grep -E '^(NAME|VERSION)=' | while IFS='=' read -r k v; do echo "\"`$k\": \"`$v\","; done | sed '1s/^/{/;$s/,$/}/'
"@

if ($sysInfo -is [string]) {
# 备用方案:手动收集
$osName = (Invoke-WslCommand "cat /etc/os-release | grep '^NAME=' | cut -d'=' -f2").Trim('"')
$osVer = (Invoke-WslCommand "cat /etc/os-release | grep '^VERSION=' | cut -d'=' -f2").Trim('"')
$sysInfo = [PSCustomObject]@{ NAME = $osName; VERSION = $osVer }
}

Write-Host "`nWSL 系统信息:" -ForegroundColor Cyan
Write-Host " 发行版:$($sysInfo.NAME)"
Write-Host " 版本:$($sysInfo.VERSION)"

# 场景 3:利用 WSL 中独有的工具处理数据
# 使用 xq(jq 的 XML 版本)解析 Windows 无法原生处理的 XML
$xmlReport = Invoke-WslCommand @"
if command -v xq &>/dev/null; then
curl -s https://example.com/feed.xml | xq '.rss.channel.item[] | .title' 2>/dev/null
else
echo 'xq not installed, fallback to xmlstarlet'
curl -s https://example.com/feed.xml | xmlstarlet sel -t -m '//item/title' -v '.' -n 2>/dev/null
fi
"@

Write-Host "`nRSS 标题提取:" -ForegroundColor Cyan
Write-Host $xmlReport

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
安全事件级别分布:
420 信息
45 审核
28 警告
7 错误

WSL 系统信息:
发行版:Ubuntu
版本:24.04.1 LTS (Noble Numbat)

RSS 标题提取:
PowerShell 7.5 发布:新特性一览
.NET 9 正式可用
Azure 新增多云管理功能

自动化部署场景:混合 Windows/Linux 工具链

在实际的 CI/CD 和运维自动化中,经常需要将 Windows 原生工具与 Linux 工具链串联起来。下面的示例展示了一个完整的混合部署脚本:在 WSL 中编译 Node.js 项目,然后在 Windows 上完成 IIS 部署。

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
# 混合平台部署脚本
function Invoke-HybridDeploy {
param(
[string]$ProjectName = "webapp",
[string]$WslProjectPath = "/home/user/projects/$ProjectName",
[string]$WinDeployPath = "C:\inetpub\wwwroot\$ProjectName",
[string]$Distribution = "Ubuntu-24.04"
)

# 阶段 1:在 WSL 中拉取代码并构建
Write-Host "`n[1/4] 在 WSL 中拉取最新代码..." -ForegroundColor Cyan
$pullResult = Invoke-WslCommand -Distribution $Distribution -Command @"
cd $WslProjectPath && git pull origin main 2>&1 | tail -5
"@
Write-Host $pullResult

# 阶段 2:在 WSL 中执行 npm 构建
Write-Host "`n[2/4] 在 WSL 中执行构建..." -ForegroundColor Cyan
$buildResult = Invoke-WslCommand -Distribution $Distribution -Command @"
cd $WslProjectPath &&
npm ci --prefer-offline 2>&1 | tail -3 &&
npm run build 2>&1 | tail -5
"@
Write-Host $buildResult

# 阶段 3:将构建产物从 WSL 文件系统复制到 Windows
Write-Host "`n[3/4] 复制构建产物到 Windows..." -ForegroundColor Cyan
$wslDistPath = "\\wsl$\$Distribution$WslProjectPath\dist"

if (Test-Path $WinDeployPath) {
$backup = "$WinDeployPath.bak.$(Get-Date -Format 'yyyyMMdd_HHmmss')"
Move-Item $WinDeployPath $backup -Force
Write-Host "已备份旧版本到 $backup" -ForegroundColor Yellow
}

Copy-Item $wslDistPath $WinDeployPath -Recurse -Force
$fileCount = (Get-ChildItem $WinDeployPath -Recurse -File).Count
Write-Host "已复制 $fileCount 个文件到 $WinDeployPath" -ForegroundColor Green

# 阶段 4:用 PowerShell 进行 Windows 端配置
Write-Host "`n[4/4] 更新 IIS 配置..." -ForegroundColor Cyan

# 确保 IIS 应用池存在
$poolName = "${ProjectName}Pool"
if (-not (Get-IISAppPool -Name $poolName -ErrorAction SilentlyContinue)) {
New-IISAppPool -Name $poolName -Force
Write-Host "已创建应用池:$poolName"
}

# 更新站点物理路径
$site = Get-IISSite -Name $ProjectName -ErrorAction SilentlyContinue
if ($site) {
$site.Applications["/"].VirtualDirectories["/"].PhysicalPath = $WinDeployPath
Write-Host "已更新站点路径" -ForegroundColor Green
}

# 验证部署结果
$indexPath = Join-Path $WinDeployPath "index.html"
if (Test-Path $indexPath) {
$hash = (Get-FileHash $indexPath -Algorithm SHA256).Hash.Substring(0, 16)
Write-Host "`n部署成功!index.html 校验:$hash" -ForegroundColor Green
}

# 阶段 5:健康检查(用 WSL 中的 curl)
Write-Host "`n健康检查..." -ForegroundColor Cyan
$health = Invoke-WslCommand -Distribution $Distribution -Command @"
curl -sS -o /dev/null -w '%{http_code} %{time_total}s' http://localhost:8080/ 2>/dev/null || echo 'unreachable'
"@
Write-Host "HTTP 状态:$health"
}

# 执行部署
Invoke-HybridDeploy -ProjectName "webapp"

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[1/4] 在 WSL 中拉取最新代码...
Already up to date.
Updating 3f2a1b0..7c8d9e0
Fast-forward
src/App.js | 12 ++++++------
2 files changed, 6 insertions(+), 6 deletions(-)

[2/4] 在 WSL 中执行构建...
added 245 packages in 3.2s
> webapp@1.0.0 build
> vite build
✓ built in 4.21s

[3/4] 复制构建产物到 Windows...
已备份旧版本到 C:\inetpub\wwwroot\webapp.bak.20251217_083000
已复制 42 个文件到 C:\inetpub\wwwroot\webapp

[4/4] 更新 IIS 配置...
已更新站点路径

部署成功!index.html 校验:A3F8B2C1D4E5F607

健康检查...
HTTP 状态:200 0.032s

注意事项

  1. 路径风格转换:Windows 路径(C:\Users\)和 WSL 路径(/mnt/c/Users/)之间转换时,注意盘符大小写和斜杠方向,建议封装通用的转换函数避免手动拼接出错。
  2. \\wsl$ 网络路径性能:通过 \\wsl$\ 访问 WSL 文件系统比 /mnt/c 跨分区访问快得多,大文件操作优先在 WSL 原生文件系统中完成后再复制到 Windows。
  3. 环境变量隔离:Windows 和 WSL 的环境变量相互独立,$env:PATH 不会自动共享。如果需要传递变量,使用 WSLENV 配置共享映射规则。
  4. 并发安全:多个 PowerShell 脚本同时向同一个 WSL 实例发送命令可能导致输出交错,生产环境中建议对 WSL 调用加锁或使用独立实例。
  5. 退出码传递wsl -- command$LASTEXITCODE 返回的是 Linux 进程的退出码,但经过 PowerShell 管道处理后可能丢失,关键操作建议显式检查输出内容而非仅依赖退出码。
  6. 换行符陷阱:WSL 输出是 LF 换行,Windows 文件默认 CRLF。跨系统写入文本文件时注意使用统一的编码和换行符,否则可能导致配置文件解析失败。

PowerShell 技能连载 - .NET 互操作深入

适用于 PowerShell 7.0 及以上版本

PowerShell 本质上是 .NET 生态的一部分,每一个变量、每一个对象都运行在 CLR 之上。虽然日常工作中我们习惯使用 cmdlet 来完成任务,但在面对高性能计算、底层系统调用或需要精确控制内存的场景时,直接使用 .NET 类往往能获得数量级的性能提升。

理解 .NET 互操作不仅是突破 cmdlet 限制的手段,更是深入理解 PowerShell 运行时行为的钥匙。当你知道 [System.IO.File]::ReadAllText() 比同等的 Get-Content 快数十倍时,你就能够在脚本中做出更明智的选择——何时追求简洁,何时追求性能。

本文将从三个维度展开:直接调用 .NET 类实现高性能操作、利用反射动态调用方法和创建实例、以及通过 P/Invoke 调用原生 Win32 API,帮助你全面掌握 PowerShell 中的 .NET 互操作能力。

直接调用 .NET 类——高性能替代方案

PowerShell 的 cmdlet 设计强调易用性和管道友好性,但这通常会引入额外的开销。当你处理大量数据或需要极致性能时,直接使用 .NET 类是更优的选择。

以下示例展示了几个常见的场景:使用 System.IO 进行高性能文件操作,使用 System.Text.Json 进行快速 JSON 序列化,以及使用 System.Net.Http 发起 HTTP 请求。

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
# 场景一:高性能文件读写
# 使用 [System.IO.File] 替代 Get-Content,性能提升数十倍
$testFile = [System.IO.Path]::GetTempFileName()
$lines = 1..10000 | ForEach-Object { "Line $_ at $(Get-Date -Format 'HH:mm:ss.fff')" }

# 直接写入,比 Set-Content 快得多
[System.IO.File]::WriteAllLines($testFile, $lines)

# 读取全部内容为单个字符串
$content = [System.IO.File]::ReadAllText($testFile)
Write-Host "文件大小: $([System.IO.FileInfo]::new($testFile).Length) 字节"

# 按行读取(延迟枚举,内存友好)
$lineCount = 0
$reader = [System.IO.StreamReader]::new($testFile)
while ($null -ne $reader.ReadLine()) { $lineCount++ }
$reader.Close()
Write-Host "总行数: $lineCount"

# 场景二:System.Text.Json 高性能序列化
$product = [ordered]@{
Name = "PowerShell 7.4"
Version = "7.4.0"
Features = @("Pipeline Chain", "Null Coalescing", "Ternary Operator")
ReleaseDate = "2023-11-16"
IsLTS = $true
}

# 使用 System.Text.Json(比 ConvertTo-Json 更快)
$jsonOptions = [System.Text.Json.JsonSerializerOptions]::new()
$jsonOptions.WriteIndented = $true
$jsonOptions.PropertyNamingPolicy = [System.Text.Json.JsonNamingPolicy]::CamelCase

$json = [System.Text.Json.JsonSerializer]::Serialize(
[System.Collections.Generic.Dictionary[string, object]]$product,
$jsonOptions
)
Write-Host $json

# 反序列化
$deserialized = [System.Text.Json.JsonSerializer]::Deserialize(
$json,
[System.Collections.Generic.Dictionary[string, object]].MakeGenericType(@([string], [object])),
$jsonOptions
)
Write-Host "反序列化结果 - Name: $($deserialized['name'])"

# 场景三:使用 System.Net.Http 发起请求(比 Invoke-WebRequest 更灵活)
$httpClient = [System.Net.Http.HttpClient]::new()
$httpClient.Timeout = [System.TimeSpan]::FromSeconds(10)
$httpClient.DefaultRequestHeaders.Add("User-Agent", "PowerShell-DotNetInterop/1.0")

try {
$response = $httpClient.GetAsync("https://httpbin.org/get").GetAwaiter().GetResult()
$body = $response.Content.ReadAsStringAsync().GetAwaiter().GetResult()
Write-Host "状态码: $($response.StatusCode)"
Write-Host "响应长度: $($body.Length) 字符"
}
finally {
$httpClient.Dispose()
}

# 清理临时文件
[System.IO.File]::Delete($testFile)

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
文件大小: 358896 字节
总行数: 10000
{
"name": "PowerShell 7.4",
"version": "7.4.0",
"features": [
"Pipeline Chain",
"Null Coalescing",
"Ternary Operator"
],
"releaseDate": "2023-11-16",
"isLTS": true
}
反序列化结果 - Name: PowerShell 7.4
状态码: OK
响应长度: 358 字符

使用反射动态调用方法和创建实例

反射是 .NET 的核心能力之一,它允许你在运行时检查类型信息、动态调用方法、创建实例,甚至修改私有字段。在 PowerShell 中,反射特别适合处理那些无法在编译时确定类型的场景,比如插件系统、动态加载程序集,或者绕过某些访问限制。

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
# 演示:使用反射探索和调用 .NET 类型

# 获取 System.String 的所有公共方法
$stringType = [string]
$methods = $stringType.GetMethods([System.Reflection.BindingFlags]::Public -bor [System.Reflection.BindingFlags]::Instance)
Write-Host "String 类型共有 $($methods.Count) 个公共方法"
Write-Host "部分方法列表:"
$methods | Select-Object -First 8 Name, ReturnType | Format-Table -AutoSize

# 使用反射动态调用方法
$text = "Hello, PowerShell .NET Interop!"
$methodInfo = $stringType.GetMethod("Contains", [type[]]@([string]))
$result = $methodInfo.Invoke($text, [object[]]@("PowerShell"))
Write-Host "Contains('PowerShell'): $result"

# 动态创建泛型集合
$listType = [System.Collections.Generic.List`1].MakeGenericType([int])
$list = [Activator]::CreateInstance($listType)
$addMethod = $listType.GetMethod("Add")
1..5 | ForEach-Object { $addMethod.Invoke($list, [object[]]$_) }
Write-Host "动态创建的 List<int> 内容: $($list -join ', ')"

# 反射加载外部程序集并调用其中的类型
# 模拟:加载 System.Text.RegularExpressions 并使用反射调用
$asm = [System.Reflection.Assembly]::LoadWithPartialName("System.Text.RegularExpressions")
$regexType = $asm.GetType("System.Text.RegularExpressions.Regex")
$pattern = "\b\w+@\w+\.\w+\b"

# 通过反射创建 Regex 实例
$regexCtor = $regexType.GetConstructor([type[]]@([string]))
$regex = $regexCtor.Invoke([object[]]@($pattern))

# 调用 Matches 方法
$matchesMethod = $regexType.GetMethod("Matches", [type[]]@([string]))
$sampleText = "联系我们: admin@vichamp.com 或 support@example.org"
$matches = $matchesMethod.Invoke($regex, [object[]]@($sampleText))

Write-Host "在文本中找到 $($matches.Count) 个邮箱地址:"
foreach ($m in $matches) {
Write-Host " - $($m.Value) (位置: $($m.Index))"
}

# 使用反射访问静态方法和属性
$env = [System.Environment]
$props = $env.GetProperties([System.Reflection.BindingFlags]::Static -bor [System.Reflection.BindingFlags]::Public)
Write-Host "`nSystem.Environment 静态属性:"
$props | Select-Object -First 5 Name, PropertyType | ForEach-Object {
$val = $env.GetProperty($_.Name).GetValue($null)
Write-Host " $($_.Name) = $val"
}

执行结果示例:

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
String 类型共有 108 个公共方法
部分方法列表:
Name ReturnType
---- ----------
CompareTo System.Int32
Contains System.Boolean
CopyTo System.Void
EndsWith System.Boolean
Equals System.Boolean
Equals System.Boolean
Equals System.Boolean
GetEnumerator System.CharEnumerator

Contains('PowerShell'): True
动态创建的 List<int> 内容: 1, 2, 3, 4, 5
在文本中找到 2 个邮箱地址:
- admin@vichamp.com (位置: 5)
- support@example.org (位置: 28)

System.Environment 静态属性:
CommandLine = /opt/powershell/pwsh
CurrentDirectory = /home/user/scripts
ExitCode = 0
HasShutdownStarted = False
Is64BitProcess = True

P/Invoke 调用 Win32 API

P/Invoke(Platform Invocation Services)是 .NET 提供的调用非托管 DLL 函数的机制。在 PowerShell 中,你可以通过 Add-Type 定义签名,然后直接调用 Windows 系统 DLL 中的原生函数。这在需要与操作系统底层交互时非常有用,例如获取系统信息、操作窗口句柄或访问注册表的底层 API。

以下示例展示了几种常见的 Win32 API 调用方式。

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
# P/Invoke 调用 Win32 API 示例
# 注意:以下代码仅在 Windows 平台上运行

if ($IsWindows -or ($env:OS -eq "Windows_NT")) {

# 示例一:获取系统内存信息
$memorySignature = @"
using System;
using System.Runtime.InteropServices;

public class MemoryHelper
{
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public class MEMORYSTATUSEX
{
public uint dwLength;
public uint dwMemoryLoad;
public ulong ullTotalPhys;
public ulong ullAvailPhys;
public ulong ullTotalPageFile;
public ulong ullAvailPageFile;
public ulong ullTotalVirtual;
public ulong ullAvailVirtual;
public ulong ullAvailExtendedVirtual;

public MEMORYSTATUSEX()
{
this.dwLength = (uint)Marshal.SizeOf(typeof(MEMORYSTATUSEX));
}
}

[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool GlobalMemoryStatusEx(
[In, Out] MEMORYSTATUSEX lpBuffer);
}
"@

Add-Type -TypeDefinition $memorySignature -Language CSharp

$memStatus = [MemoryHelper+MEMORYSTATUSEX]::new()
[MemoryHelper]::GlobalMemoryStatusEx($memStatus) | Out-Null

$totalGB = [math]::Round($memStatus.ullTotalPhys / 1GB, 2)
$availGB = [math]::Round($memStatus.ullAvailPhys / 1GB, 2)
$usedPercent = $memStatus.dwMemoryLoad

Write-Host "=== 系统内存信息 ==="
Write-Host "总物理内存: ${totalGB} GB"
Write-Host "可用物理内存: ${availGB} GB"
Write-Host "内存使用率: ${usedPercent}%"

# 示例二:获取精确的系统启动时间(比 Get-Date 更准确)
$uptimeSignature = @"
using System;
using System.Runtime.InteropServices;

public class UptimeHelper
{
[DllImport("kernel32.dll")]
public static extern UInt64 GetTickCount64();
}
"@

Add-Type -TypeDefinition $uptimeSignature -Language CSharp

$tickCount = [UptimeHelper]::GetTickCount64()
$uptime = [TimeSpan]::FromMilliseconds($tickCount)
Write-Host "`n=== 系统运行时间 ==="
Write-Host "已运行: $($uptime.Days) 天 $($uptime.Hours) 小时 $($uptime.Minutes) 分钟"

# 示例三:获取当前控制台窗口标题
$consoleSignature = @"
using System;
using System.Text;
using System.Runtime.InteropServices;

public class ConsoleHelper
{
[DllImport("kernel32.dll", CharSet = CharSet.Auto)]
public static extern int GetConsoleTitle(
StringBuilder lpConsoleTitle,
int nSize);
}
"@

Add-Type -TypeDefinition $consoleSignature -Language CSharp

$titleBuilder = [System.Text.StringBuilder]::new(256)
[ConsoleHelper]::GetConsoleTitle($titleBuilder, 256) | Out-Null
Write-Host "`n=== 控制台信息 ==="
Write-Host "当前窗口标题: $($titleBuilder.ToString())"

} else {
Write-Host "P/Invoke 示例需要在 Windows 平台上运行"
Write-Host "当前平台: $($PSVersionTable.OS)"

# 跨平台替代方案:使用 .NET API 获取系统信息
Write-Host "`n=== 跨平台系统信息 ==="
Write-Host "机器名: $([System.Environment]::MachineName)"
Write-Host "用户名: $([System.Environment]::UserName)"
Write-Host "处理器核心数: $([System.Environment]::ProcessorCount)"
Write-Host "系统目录: $([System.Environment]::SystemDirectory)"
}

执行结果示例:

1
2
3
4
5
6
7
8
9
10
=== 系统内存信息 ===
总物理内存: 32.00 GB
可用物理内存: 14.37 GB
内存使用率: 55%

=== 系统运行时间 ===
已运行: 3 天 7 小时 42 分钟

=== 控制台信息 ===
当前窗口标题: Windows PowerShell

注意事项

  1. 性能权衡:直接使用 .NET 类虽然性能更高,但代码可读性通常不如 cmdlet。对于一次性脚本或管理任务,优先使用 cmdlet;对于需要处理大量数据或高频调用的场景,再考虑直接使用 .NET 类。可以用 Measure-Command 对比实际性能差异后再做决策。

  2. 类型转换陷阱:PowerShell 会自动包装 .NET 对象,某些情况下类型转换可能不如预期。例如 [System.Collections.Generic.List[int]] 和 PowerShell 数组之间存在隐式转换,但频繁转换会抵消性能优势。建议尽量在 .NET API 之间传递数据,减少跨越 PowerShell 管道的次数。

  3. 反射的性能开销:反射调用比直接调用慢 10-100 倍。如果需要大量调用同一方法,应该缓存 MethodInfo 对象并重复使用,而不是每次都重新查找。在高性能循环中,应避免使用反射。

  4. P/Invoke 平台限制:P/Invoke 调用的 Win32 API 仅在 Windows 上可用。如果你的脚本需要跨平台运行,务必添加平台检测逻辑(如 $IsWindows),并提供基于 .NET API 的跨平台替代方案。同时要注意区分 x86 和 x64 的函数签名差异。

  5. 程序集加载策略Add-Type 编译的 C# 代码会生成临时程序集,无法卸载。大量使用 Add-Type 会导致内存增长。对于复杂场景,建议将 C# 代码编译为独立的 DLL,使用 [System.Reflection.Assembly]::LoadFrom() 按需加载。

  6. Dispose 模式:许多 .NET 类(如 HttpClientStreamReaderFileStream)实现了 IDisposable 接口。在 PowerShell 中应使用 try/finally 块或 .Dispose() 方法显式释放资源,避免因垃圾回收延迟导致的文件锁或连接泄漏。

PowerShell 技能连载 - Win32 API 调用

适用于 PowerShell 5.1 及以上版本(Windows)

PowerShell 虽然已经提供了丰富的 cmdlet 和 .NET 类库,但在某些场景下仍然需要直接调用 Win32 API 才能完成任务。比如获取系统硬件信息、操作窗口句柄、管理进程内存、控制屏幕分辨率等底层操作,往往没有对应的 .NET 封装。这时候,Platform Invoke(P/Invoke)机制就成为了连接 PowerShell 与 Win32 原生 API 的桥梁。

P/Invoke 是 .NET 提供的一种互操作机制,允许托管代码调用非托管的 DLL 导出函数。在 PowerShell 中,我们可以通过 Add-Type 动态编译 C# 代码来声明 Win32 API 的签名,然后在脚本中像调用普通方法一样调用这些原生函数。这种方式的灵活性极高,几乎可以访问 Windows 系统的全部底层能力。

本文将通过三个实用案例,演示如何在 PowerShell 中声明和调用 Win32 API:获取系统内存状态、操作剪贴板,以及控制窗口的显示状态。每个案例都包含完整的签名声明、参数说明和错误处理。

获取系统全局内存状态

GlobalMemoryStatusEx 是 Win32 API 中用于获取系统内存使用情况的核心函数,它返回的 MEMORYSTATUSEX 结构体包含了物理内存、虚拟内存的总量和可用量等关键信息。虽然 Get-CimInstance 也能获取部分内存数据,但 Win32 API 返回的信息更加实时和精确,且调用开销更小。

下面的代码通过 Add-Type 定义了必要的结构体和函数签名,然后封装为一个 PowerShell 函数,返回格式化的内存信息对象。

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
# 定义 MEMORYSTATUSEX 结构体和 GlobalMemoryStatusEx 函数签名
$memApiDefinition = @'
using System;
using System.Runtime.InteropServices;

public class MemoryApi
{
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public class MEMORYSTATUSEX
{
public uint dwLength;
public uint dwMemoryLoad;
public ulong ullTotalPhys;
public ulong ullAvailPhys;
public ulong ullTotalPageFile;
public ulong ullAvailPageFile;
public ulong ullTotalVirtual;
public ulong ullAvailVirtual;
public ulong ullAvailExtendedVirtual;

public MEMORYSTATUSEX()
{
this.dwLength = (uint)Marshal.SizeOf(typeof(MEMORYSTATUSEX));
}
}

[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool GlobalMemoryStatusEx(
[In, Out] MEMORYSTATUSEX lpBuffer);
}
'@

Add-Type -TypeDefinition $memApiDefinition -Language CSharp

# 封装为易用的 PowerShell 函数
function Get-SystemMemoryStatus {
$status = New-Object MemoryApi+MEMORYSTATUSEX
$result = [MemoryApi]::GlobalMemoryStatusEx($status)

if (-not $result) {
$errorCode = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error()
Write-Error "GlobalMemoryStatusEx 调用失败,错误码:$errorCode"
return
}

$gb = [math]::Round(1GB, 2)

[PSCustomObject]@{
MemoryLoadPercent = $status.dwMemoryLoad
TotalPhysicalGB = [math]::Round($status.ullTotalPhys / $gb, 2)
AvailablePhysicalGB = [math]::Round($status.ullAvailPhys / $gb, 2)
UsedPhysicalGB = [math]::Round(($status.ullTotalPhys - $status.ullAvailPhys) / $gb, 2)
TotalPageFileGB = [math]::Round($status.ullTotalPageFile / $gb, 2)
AvailablePageFileGB = [math]::Round($status.ullAvailPageFile / $gb, 2)
TotalVirtualGB = [math]::Round($status.ullTotalVirtual / $gb, 2)
AvailableVirtualGB = [math]::Round($status.ullAvailVirtual / $gb, 2)
}
}

# 调用并展示结果
$memStatus = Get-SystemMemoryStatus
Write-Host "=== 系统内存状态 ===" -ForegroundColor Cyan
Write-Host ("内存使用率:{0}%" -f $memStatus.MemoryLoadPercent)
Write-Host ("物理内存总量:{0} GB" -f $memStatus.TotalPhysicalGB)
Write-Host ("物理内存可用:{0} GB" -f $memStatus.AvailablePhysicalGB)
Write-Host ("物理内存已用:{0} GB" -f $memStatus.UsedPhysicalGB)
Write-Host ("页面文件总量:{0} GB" -f $memStatus.TotalPageFileGB)
Write-Host ("虚拟内存总量:{0} GB" -f $memStatus.TotalVirtualGB)
Write-Host ("虚拟内存可用:{0} GB" -f $memStatus.AvailableVirtualGB)
1
2
3
4
5
6
7
8
=== 系统内存状态 ===
内存使用率:68%
物理内存总量:31.88 GB
物理内存可用:10.21 GB
物理内存已用:21.67 GB
页面文件总量:35.88 GB
虚拟内存总量:131,072.00 GB
虚拟内存可用:130,882.16 GB

从结果可以看到,GlobalMemoryStatusEx 返回的内存使用率百分比为 68%,物理内存总量约 32 GB,与系统实际配置一致。虚拟内存总量显示为 128 TB,这是 64 位 Windows 的虚拟地址空间上限,并非实际的物理存储。

通过 Win32 API 操作剪贴板

PowerShell 5.1 中的 Get-ClipboardSet-Clipboard 虽然方便,但功能有限,无法处理非文本格式的剪贴板数据,也无法检测剪贴板是否包含特定格式的内容。Win32 剪贴板 API 提供了更精细的控制能力,包括打开/关闭剪贴板、清空剪贴板、检测数据格式等。下面的代码展示了如何用底层 API 实现剪贴板文本的读写,并提供比内置 cmdlet 更丰富的错误处理。

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
109
110
# 定义剪贴板相关的 Win32 API
$clipboardApiDefinition = @'
using System;
using System.Runtime.InteropServices;
using System.Text;

public class ClipboardApi
{
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool OpenClipboard(IntPtr hWndNewOwner);

[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool CloseClipboard();

[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool EmptyClipboard();

[DllImport("user32.dll", SetLastError = true)]
public static extern bool IsClipboardFormatAvailable(uint format);

[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr GetClipboardData(uint uFormat);

[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr SetClipboardData(uint uFormat, IntPtr hMem);

[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr GlobalLock(IntPtr hMem);

[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool GlobalUnlock(IntPtr hMem);

[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr GlobalAlloc(uint uFlags, UIntPtr dwBytes);

[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr GlobalFree(IntPtr hMem);

// CF_UNICODETEXT = 13
public const uint CF_UNICODETEXT = 13;
// GMEM_MOVEABLE = 0x0002
public const uint GMEM_MOVEABLE = 0x0002;
}
'@

Add-Type -TypeDefinition $clipboardApiDefinition -Language CSharp

# 封装:检测剪贴板是否包含文本
function Test-ClipboardTextAvailable {
[ClipboardApi]::IsClipboardFormatAvailable([ClipboardApi]::CF_UNICODETEXT)
}

# 封装:通过 Win32 API 读取剪贴板文本
function Get-ClipboardTextNative {
if (-not (Test-ClipboardTextAvailable)) {
Write-Warning "剪贴板中不包含文本数据"
return
}

if (-not [ClipboardApi]::OpenClipboard([IntPtr]::Zero)) {
$err = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error()
Write-Error "无法打开剪贴板,错误码:$err"
return
}

try {
$handle = [ClipboardApi]::GetClipboardData([ClipboardApi]::CF_UNICODETEXT)
if ($handle -eq [IntPtr]::Zero) {
Write-Error "获取剪贴板数据失败"
return
}

$pointer = [ClipboardApi]::GlobalLock($handle)
if ($pointer -eq [IntPtr]::Zero) {
Write-Error "锁定内存失败"
return
}

try {
$text = [System.Runtime.InteropServices.Marshal]::PtrToStringUni($pointer)
Write-Output $text
}
finally {
[ClipboardApi]::GlobalUnlock($handle) | Out-Null
}
}
finally {
[ClipboardApi]::CloseClipboard() | Out-Null
}
}

# 测试剪贴板操作
$hasText = Test-ClipboardTextAvailable
Write-Host "剪贴板包含文本数据:$hasText" -ForegroundColor Cyan

if ($hasText) {
$clipContent = Get-ClipboardTextNative
if ($clipContent) {
$preview = $clipContent
if ($preview.Length -gt 80) {
$preview = $preview.Substring(0, 80) + "..."
}
Write-Host ("剪贴板内容预览:{0}" -f $preview) -ForegroundColor Yellow
Write-Host ("内容长度:{0} 字符" -f $clipContent.Length)
}
}
1
2
3
剪贴板包含文本数据:True
剪贴板内容预览:Hello from PowerShell Win32 API!
内容长度:32 字符

上面的代码通过 OpenClipboardGetClipboardDataGlobalLock 等一系列 API 调用完成了剪贴板文本的读取。注意每个 API 调用都有对应的错误检查,且使用 try/finally 确保资源被正确释放。这种编程模式在调用 Win32 API 时非常重要,因为原生 API 不会像 .NET 那样自动管理资源。

控制窗口的显示状态

在自动化测试和运维场景中,我们经常需要控制窗口的行为,比如隐藏某个窗口、最小化所有窗口、或者判断窗口是否处于响应状态。ShowWindowIsWindow 等 Win32 API 为此提供了底层支持。下面的代码展示了如何查找窗口句柄并控制窗口的显示状态。

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
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
# 定义窗口管理相关的 Win32 API
$windowApiDefinition = @'
using System;
using System.Runtime.InteropServices;
using System.Text;

public class WindowApi
{
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);

[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);

[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool IsWindow(IntPtr hWnd);

[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool IsWindowVisible(IntPtr hWnd);

[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
public static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);

[DllImport("user32.dll", SetLastError = true)]
public static extern bool SetForegroundWindow(IntPtr hWnd);

// ShowWindow 命令常量
public const int SW_HIDE = 0;
public const int SW_SHOWNORMAL = 1;
public const int SW_SHOWMINIMIZED = 2;
public const int SW_SHOWMAXIMIZED = 3;
public const int SW_RESTORE = 9;
}
'@

Add-Type -TypeDefinition $windowApiDefinition -Language CSharp

# 封装:按窗口标题查找窗口句柄
function Find-WindowByTitle {
param(
[Parameter(Mandatory)]
[string]$Title
)

$handle = [WindowApi]::FindWindow($null, $Title)
if ($handle -eq [IntPtr]::Zero) {
Write-Warning "未找到标题为「$Title」的窗口"
return $null
}
$handle
}

# 封装:获取窗口标题
function Get-WindowTitle {
param(
[Parameter(Mandatory)]
[IntPtr]$Handle
)

if (-not [WindowApi]::IsWindow($handle)) {
Write-Warning "无效的窗口句柄:$handle"
return
}

$sb = New-Object System.Text.StringBuilder(256)
[WindowApi]::GetWindowText($handle, $sb, $sb.Capacity) | Out-Null
$sb.ToString()
}

# 封装:控制窗口显示状态
function Set-WindowState {
param(
[Parameter(Mandatory)]
[IntPtr]$Handle,

[Parameter(Mandatory)]
[ValidateSet('Hide', 'ShowNormal', 'Minimize', 'Maximize', 'Restore')]
[string]$State
)

$stateMap = @{
Hide = [WindowApi]::SW_HIDE
ShowNormal = [WindowApi]::SW_SHOWNORMAL
Minimize = [WindowApi]::SW_SHOWMINIMIZED
Maximize = [WindowApi]::SW_SHOWMAXIMIZED
Restore = [WindowApi]::SW_RESTORE
}

$cmdValue = $stateMap[$State]
$result = [WindowApi]::ShowWindow($handle, $cmdValue)

if (-not $result) {
$err = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error()
Write-Error "ShowWindow 调用失败,错误码:$err"
return
}

$isVisible = [WindowApi]::IsWindowVisible($handle)
Write-Host ("窗口状态已设置为:{0},当前可见性:{1}" -f $State, $isVisible)
}

# 演示:查找记事本窗口并控制其状态
$notepadTitle = "Untitled - Notepad"
$handle = Find-WindowByTitle -Title $notepadTitle

if ($handle) {
$currentTitle = Get-WindowTitle -Handle $handle
Write-Host "找到窗口:$currentTitle" -ForegroundColor Cyan
Write-Host "窗口句柄:$handle"

# 最小化窗口
Write-Host "`n--- 最小化窗口 ---"
Set-WindowState -Handle $handle -State Minimize

Start-Sleep -Milliseconds 500

# 恢复窗口
Write-Host "`n--- 恢复窗口 ---"
Set-WindowState -Handle $handle -State Restore

Start-Sleep -Milliseconds 500

# 最大化窗口
Write-Host "`n--- 最大化窗口 ---"
Set-WindowState -Handle $handle -State Maximize

Start-Sleep -Milliseconds 500

# 恢复到正常状态
Write-Host "`n--- 恢复到正常大小 ---"
Set-WindowState -Handle $handle -State Restore
}
else {
Write-Host "请先打开记事本(Notepad),然后重新运行此脚本" -ForegroundColor Yellow
Write-Host "提示:也可以尝试查找其他窗口,如修改 -Title 参数" -ForegroundColor Yellow

# 列出一些常见窗口标题供参考
Write-Host "`n你可以尝试以下窗口标题:"
$commonWindows = @("Calculator", "Task Manager", "Windows PowerShell")
foreach ($win in $commonWindows) {
$testHandle = [WindowApi]::FindWindow($null, $win)
$status = if ($testHandle -ne [IntPtr]::Zero) { "存在" } else { "未找到" }
Write-Host (" {0} - {1}" -f $win, $status)
}
}
1
2
3
4
5
6
7
请先打开记事本(Notepad),然后重新运行此脚本
提示:也可以尝试查找其他窗口,如修改 -Title 参数

你可以尝试以下窗口标题:
Calculator - 未找到
Task Manager - 存在
Windows PowerShell - 存在

上述代码通过 FindWindow 按窗口标题查找句柄,然后使用 ShowWindow 配合不同的命令常量(SW_MINIMIZESW_MAXIMIZESW_RESTORE 等)来控制窗口的显示状态。在实际自动化场景中,这种能力可以用来在执行 UI 测试前确保窗口处于正确状态,或在无人值守任务中隐藏不必要的窗口。

注意事项

  1. Add-Type 无法重复定义:同一个类型名称在一个 PowerShell 会话中只能通过 Add-Type 定义一次。如果修改了 API 签名,需要重启 PowerShell 会话才能生效。建议在开发阶段使用不同的类名或重启控制台进行测试。

  2. 数据类型映射要准确:Win32 API 中的 BOOL 对应 C# 的 boolDWORD 对应 uintHANDLE 对应 IntPtrLPCWSTR 对应 string(带 CharSet.Auto)。类型映射错误会导致调用失败甚至进程崩溃,务必查阅 PInvoke.net 获取准确的签名。

  3. 始终检查返回值和错误码:大多数 Win32 API 通过返回值(如 BOOLHANDLE)指示成功或失败,并通过 SetLastError = true 配合 Marshal.GetLastWin32Error() 获取详细错误信息。不要忽略返回值检查。

  4. 注意 32 位和 64 位兼容性:在 64 位系统上运行 32 位 PowerShell 时,部分 API 的行为可能不同(如窗口句柄大小为 4 字节而非 8 字节)。建议始终使用 64 位 PowerShell 运行涉及 Win32 API 的脚本,以确保指针类型的一致性。

  5. 使用 try/finally 确保资源释放:Win32 API 中的资源(如剪贴板的打开状态、内存锁定的指针)需要手动释放。一定要用 try/finally 块包裹相关调用,确保即使发生异常也能正确清理资源,避免内存泄漏或系统状态异常。

  6. 参考官方文档和 PInvoke.net:Win32 API 数量庞大,参数和常量的含义需要查阅 Windows SDK 文档。PInvoke.net 网站提供了大量现成的 C# 签名定义,可以直接复制使用,省去手动映射的麻烦。在调用不熟悉的 API 之前,务必先在测试环境中验证其行为。

PowerShell 技能连载 - 类型加速器与 .NET 互操作

适用于 PowerShell 5.1 及以上版本

PowerShell 构建在 .NET 之上,这意味着你可以直接使用 .NET 的全部类库——从文件操作到加密算法,从网络编程到并行处理。但很多用户不知道的是,PowerShell 提供了大量”类型加速器”(Type Accelerator),让你可以用简短的名称代替冗长的命名空间路径。比如 [xml] 实际上是 [System.Xml.XmlDocument][mailaddress][System.Net.Mail.MailAddress]

本文将讲解常用的类型加速器、.NET 类的直接调用,以及一些通过 .NET 互操作实现的高级功能。

内置类型加速器

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
# 查看所有内置类型加速器
[System.Management.Automation.PSObject].Assembly.GetType(
'System.Management.Automation.TypeAccelerators'
)::Get | Sort-Object Key | ForEach-Object {
[PSCustomObject]@{
Shortcut = $_.Key
FullType = $_.Value.FullName
}
} | Format-Table -AutoSize | Out-String -Width 120 | Write-Host

# 常用加速器示例
# [xml] => System.Xml.XmlDocument
[xml]$doc = '<config><server>prod-db01</server><port>5432</port></config>'
Write-Host "XML 服务器:$($doc.config.server)"

# [regex] => System.Text.RegularExpressions.Regex
$matches = [regex]::Matches("版本 1.2.3 和 2.0.1", '\d+\.\d+\.\d+')
$matches | ForEach-Object { Write-Host "匹配:$($_.Value)" }

# [guid] => System.Guid
$guid = [guid]::NewGuid()
Write-Host "新 GUID:$guid"

# [datetime] => System.DateTime
$dt = [datetime]::ParseExact("2025-07-03 08:30", "yyyy-MM-dd HH:mm", $null)
Write-Host "解析日期:$($dt.ToString('yyyy年M月d日 HH:mm'))"

# [timespan] => System.TimeSpan
$ts = [timespan]::FromHours(2.5)
Write-Host "时间跨度:$($ts.ToString('hh\:mm\:ss'))"

# [math] => System.Math
Write-Host "PI:$([math]::PI)"
Write-Host "四舍五入:$([math]::Round(3.14159, 2))"
Write-Host "最大值:$([math]::Max(10, 20))"

执行结果示例:

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
Shortcut   FullType
-------- --------
adsi System.DirectoryServices.DirectoryEntry
adsisearcher System.DirectoryServices.DirectorySearcher
bool System.Boolean
byte System.Byte
char System.Char
datetime System.DateTime
decimal System.Decimal
double System.Double
float System.Single
guid System.Guid
hashtable System.Collections.Hashtable
int System.Int32
long System.Int64
mailaddress System.Net.Mail.MailAddress
regex System.Text.RegularExpressions.Regex
scriptblock System.Management.Automation.ScriptBlock
string System.String
timespan System.TimeSpan
uri System.Uri
version System.Version
xml System.Xml.XmlDocument

XML 服务器:prod-db01
匹配:1.2.3
匹配:2.0.1
新 GUID:a1b2c3d4-e5f6-7890-abcd-ef1234567890
解析日期:20257308:30
时间跨度:02:30:00
PI:3.14159265358979
四舍五入:3.14
最大值:20

常用 .NET 类直接调用

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
# System.IO —— 文件操作(比 cmdlet 更快的批量操作)
$sw = [System.Diagnostics.Stopwatch]::StartNew()

# 快速读取所有行
$lines = [System.IO.File]::ReadAllLines("C:\Logs\app.log")
Write-Host "读取 $($lines.Count) 行,耗时:$($sw.ElapsedMilliseconds)ms"

# 快速写入
[System.IO.File]::WriteAllLines("C:\Temp\output.txt", $lines)

$sw.Stop()

# System.Text.StringBuilder —— 高效字符串拼接
$sb = [System.Text.StringBuilder]::new()
$sb.AppendLine("服务器巡检报告") | Out-Null
$sb.AppendLine("生成时间:$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')") | Out-Null
$sb.AppendLine("") | Out-Null

@("SRV01", "SRV02", "SRV03") | ForEach-Object {
$sb.AppendLine(" $_ : 在线") | Out-Null
}

Write-Host $sb.ToString()

# System.Net —— DNS 查询
$dnsResult = [System.Net.Dns]::GetHostAddresses("www.microsoft.com")
$dnsResult | ForEach-Object { Write-Host "IP:$($_.IPAddressFamily) $($_.ToString())" }

# System.Security.Cryptography —— 计算文件哈希
function Get-FileHashFast {
param([string]$Path, [string]$Algorithm = "SHA256")

$hashAlg = [System.Security.Cryptography.HashAlgorithm]::Create($Algorithm)
$stream = [System.IO.File]::OpenRead($Path)
try {
$hashBytes = $hashAlg.ComputeHash($stream)
$hashHex = [BitConverter]::ToString($hashBytes) -replace '-', ''
return $hashHex.ToLower()
} finally {
$stream.Close()
}
}

$hash = Get-FileHashFast -Path "C:\Windows\notepad.exe"
Write-Host "SHA256:$($hash.Substring(0, 16))..."

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
读取 54230 行,耗时:45ms

服务器巡检报告
生成时间:2025-07-03 08:30:15

SRV01 : 在线
SRV02 : 在线
SRV03 : 在线

IP:InterNetwork 20.190.159.2
IP:InterNetworkV6 2603:1030:20...
SHA256:a1b2c3d4e5f67890...

自定义类型加速器

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
# 注册自定义类型加速器
$accel = [System.Management.Automation.PSObject].Assembly.GetType(
'System.Management.Automation.TypeAccelerators'
)

# 添加自定义加速器
$accel::Add("ws", [System.Net.WebSockets.ClientWebSocket])
$accel::Add("http", [System.Net.Http.HttpClient])

# 验证
Write-Host "ws => $($accel::Get['ws'].FullName)"
Write-Host "http => $($accel::Get['http'].FullName)"

# 实际使用:用 [http] 代替完整类型名
$client = [http]::new()
$client.Timeout = [timespan]::FromSeconds(30)
Write-Host "HTTP 客户端超时:$($client.Timeout)"

# 添加常用 .NET 缩写
$mappings = @{
"sb" = [System.Text.StringBuilder]
"stopw" = [System.Diagnostics.Stopwatch]
"zip" = [System.IO.Compression.ZipArchive]
"json" = [System.Text.Json.JsonSerializer]
"crypto" = [System.Security.Cryptography.Aes]
}

foreach ($key in $mappings.Keys) {
$accel::Add($key, $mappings[$key])
Write-Host "已注册:$key => $($mappings[$key].FullName)" -ForegroundColor Green
}

执行结果示例:

1
2
3
4
5
6
7
8
ws => System.Net.WebSockets.ClientWebSocket
http => System.Net.Http.HttpClient
HTTP 客户端超时:00:00:30
已注册:sb => System.Text.StringBuilder
已注册:stopw => System.Diagnostics.Stopwatch
已注册:zip => System.IO.Compression.ZipArchive
已注册:json => System.Text.Json.JsonSerializer
已注册:crypto => System.Security.Cryptography.Aes

使用 .NET 泛型集合

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
# List<T> —— 动态数组
$list = [System.Collections.Generic.List[string]]::new()
$list.AddRange(@("apple", "banana", "cherry"))
$list.Add("date")
Write-Host "列表内容:$($list -join ', ')"
Write-Host "数量:$($list.Count)"

$list.Remove("banana")
$list.Sort()
Write-Host "排序后:$($list -join ', ')"

# Dictionary<TKey, TValue> —— 强类型字典
$dict = [System.Collections.Generic.Dictionary[string, int]]::new()
$dict["apple"] = 5
$dict["banana"] = 3
$dict["cherry"] = 7

Write-Host "苹果数量:$($dict['apple'])"

# 遍历强类型字典
foreach ($entry in $dict) {
Write-Host " $($entry.Key):$($entry.Value) 个"
}

# HashSet<T> —— 去重集合
$set = [System.Collections.Generic.HashSet[string]]::new()
$items = @("server01", "server02", "server01", "server03", "server02")
foreach ($item in $items) {
$added = $set.Add($item)
if (-not $added) {
Write-Host "重复项:$item" -ForegroundColor Yellow
}
}
Write-Host "去重后:$($set -join ', ')"

# Queue<T> —— 先进先出队列
$queue = [System.Collections.Generic.Queue[string]]::new()
"task1", "task2", "task3" | ForEach-Object { $queue.Enqueue($_) }

while ($queue.Count -gt 0) {
$task = $queue.Dequeue()
Write-Host "处理:$task"
}

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
列表内容:apple, banana, cherry, date
数量:4
排序后:apple, cherry, date
苹果数量:5
apple:5
banana:3
cherry:7
重复项:server01
重复项:server02
去重后:server01, server02, server03
处理:task1
处理:task2
处理:task3

注意事项

  1. 类型加速器作用域:自定义类型加速器只在当前会话中生效,放入 $PROFILE 中可持久化
  2. 性能差异:批量操作(大量文件读写、字符串拼接)使用 .NET 类比 cmdlet 快 10-100 倍
  3. 内存管理IDisposable 对象(如 StreamHttpClient)应使用 using 语句或 try/finally 释放
  4. 泛型语法:PowerShell 中泛型用方括号表示 [List[string]],不能使用 C# 的 List<string> 语法
  5. 平台兼容:部分 .NET 类仅在 Windows 上可用(如 System.Drawing),跨平台脚本注意检查
  6. 加载程序集:使用 Add-Type -AssemblyName 加载非默认引用的程序集

PowerShell 技能连载 - 剪贴板与 GUI 互操作

适用于 PowerShell 5.1 及以上版本(Windows),剪贴板功能需要 Windows 环境

PowerShell 不仅仅是命令行工具——它可以与 Windows 图形界面深度交互。从读写剪贴板、弹出消息框、打开文件对话框,到操控 GUI 窗口和发送按键,PowerShell 可以成为连接命令行和桌面操作的桥梁。在日常工作中,这些功能可以大幅简化重复性的 GUI 操作。

本文将讲解剪贴板操作、消息框、文件对话框,以及窗口自动化的技巧。

剪贴板操作

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
# 设置剪贴板内容(文本)
Set-Clipboard -Value "Hello from PowerShell!"
Write-Host "已复制到剪贴板" -ForegroundColor Green

# 获取剪贴板内容
$clipContent = Get-Clipboard
Write-Host "剪贴板内容:$clipContent"

# 复制命令输出到剪贴板
Get-Process | Select-Object Name, Id,
@{N='内存MB'; E={[math]::Round($_.WorkingSet64/1MB, 1)}} |
Sort-Object 内存MB -Descending |
Select-Object -First 10 |
Format-Table -AutoSize |
Out-String | Set-Clipboard
Write-Host "进程列表已复制到剪贴板" -ForegroundColor Green

# 复制文件路径到剪贴板
Get-ChildItem "C:\Projects" -Filter *.ps1 -Recurse |
Select-Object -ExpandProperty FullName |
Set-Clipboard
Write-Host "已复制所有 .ps1 文件路径" -ForegroundColor Green

# 从剪贴板读取并处理
$clipboardText = Get-Clipboard
if ($clipboardText -match '^\d{1,3}(\.\d{1,3}){3}$') {
Write-Host "检测到 IP 地址:$clipboardText" -ForegroundColor Cyan
Test-NetConnection -ComputerName $clipboardText -Port 443 |
Select-Object ComputerName, TcpTestSucceeded
}

执行结果示例:

1
2
3
已复制到剪贴板
剪贴板内容:Hello from PowerShell!
进程列表已复制到剪贴板

消息框与交互

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
# 简单消息框
Add-Type -AssemblyName PresentationFramework

[System.Windows.MessageBox]::Show(
"部署已完成",
"提示",
[System.Windows.MessageBoxButton]::OK,
[System.Windows.MessageBoxImage]::Information
)

# 确认对话框
$result = [System.Windows.MessageBox]::Show(
"确定要重启服务 MyApp 吗?",
"确认操作",
[System.Windows.MessageBoxButton]::YesNo,
[System.Windows.MessageBoxImage]::Warning
)

if ($result -eq 'Yes') {
Restart-Service "MyApp"
Write-Host "服务已重启" -ForegroundColor Green
} else {
Write-Host "操作已取消" -ForegroundColor Yellow
}

# 带超时的通知(使用 Balloon Tip)
function Show-BalloonTip {
param(
[string]$Title = "PowerShell",
[string]$Message,
[int]$DurationMs = 5000
)

Add-Type -AssemblyName System.Windows.Forms

$notify = New-Object System.Windows.Forms.NotifyIcon
$notify.Icon = [System.Drawing.SystemIcons]::Information
$notify.BalloonTipTitle = $Title
$notify.BalloonTipText = $Message
$notify.Visible = $true
$notify.ShowBalloonTip($DurationMs)

Start-Sleep -Milliseconds ($DurationMs + 500)
$notify.Dispose()
}

Show-BalloonTip -Title "部署完成" -Message "MyApp v2.5.0 已成功部署到生产环境"

执行结果示例:

1
2
# 弹出 Windows 消息框
# 系统托盘弹出通知气泡

文件和文件夹对话框

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
# 打开文件选择对话框
Add-Type -AssemblyName System.Windows.Forms

$openFileDialog = New-Object System.Windows.Forms.OpenFileDialog
$openFileDialog.Title = "选择 CSV 文件"
$openFileDialog.Filter = "CSV 文件 (*.csv)|*.csv|所有文件 (*.*)|*.*"
$openFileDialog.Multiselect = $true

if ($openFileDialog.ShowDialog() -eq 'OK') {
foreach ($file in $openFileDialog.FileNames) {
Write-Host "已选择:$file" -ForegroundColor Green
Import-Csv $file | Select-Object -First 5
}
}

# 保存文件对话框
$saveDialog = New-Object System.Windows.Forms.SaveFileDialog
$saveDialog.Title = "保存报告"
$saveDialog.Filter = "HTML 报告 (*.html)|*.html|CSV 文件 (*.csv)|*.csv"
$saveDialog.FileName = "report-$(Get-Date -Format 'yyyyMMdd').html"

if ($saveDialog.ShowDialog() -eq 'OK') {
Write-Host "保存到:$($saveDialog.FileName)" -ForegroundColor Green
# 生成报告...
}

# 文件夹浏览对话框
$folderDialog = New-Object System.Windows.Forms.FolderBrowserDialog
$folderDialog.Description = "选择项目目录"
$folderDialog.ShowNewFolderButton = $true

if ($folderDialog.ShowDialog() -eq 'OK') {
$selectedPath = $folderDialog.SelectedPath
Write-Host "已选择目录:$selectedPath" -ForegroundColor Green

$files = Get-ChildItem $selectedPath -Recurse -File
Write-Host "目录中有 $($files.Count) 个文件"
}

执行结果示例:

1
2
3
4
5
# 弹出标准的 Windows 文件选择对话框
已选择:C:\Data\customers.csv
保存到:C:\Reports\report-20250625.html
已选择目录:C:\Projects\MyApp
目录中有 245 个文件

窗口自动化

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
# 使用 SendKeys 自动化 GUI 操作
Add-Type -AssemblyName System.Windows.Forms

# 模拟键盘快捷键
function Send-Keys {
param([string]$Keys, [int]$DelayMs = 500)

Start-Sleep -Milliseconds $DelayMs
[System.Windows.Forms.SendKeys]::SendWait($Keys)
}

# Ctrl+C 复制
Send-Keys "^c"

# Alt+Tab 切换窗口
Send-Keys "%{TAB}"

# 打开运行对话框 (Win+R)
Send-Keys "^{ESC}r"

# 输入文本
Start-Sleep -Seconds 2
Send-Keys "notepad.exe{ENTER}"

# 自动化记事本操作示例
Start-Sleep -Seconds 2
Send-Keys "这段文字由 PowerShell 自动输入"

# 保存 (Ctrl+S)
Start-Sleep -Milliseconds 500
Send-Keys "^s"

# 输入文件名
Start-Sleep -Seconds 1
Send-Keys "auto-generated.txt{ENTER}"

执行结果示例:

1
# 自动化操作 GUI 窗口

实用工具函数

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
# 快速复制 IP 配置
function Copy-IPConfig {
$ipConfig = Get-NetIPAddress -AddressFamily IPv4 |
Where-Object { $_.InterfaceAlias -notmatch 'Loopback' } |
Select-Object InterfaceAlias, IPAddress, PrefixLength |
Format-Table -AutoSize |
Out-String

Set-Clipboard -Value $ipConfig
Write-Host "IP 配置已复制到剪贴板" -ForegroundColor Green
}

# 快速复制文件内容
function Copy-FileContent {
param([Parameter(Mandatory)][string]$Path)
Get-Content $Path -Raw | Set-Clipboard
Write-Host "文件内容已复制:$Path ($((Get-Item $Path).Length) bytes)" -ForegroundColor Green
}

# 快速打开选中文件所在目录
function Open-FileLocation {
param([Parameter(Mandatory)][string]$Path)
$dir = Split-Path $Path -Parent
Start-Process explorer.exe $dir
}

Copy-IPConfig
Copy-FileContent -Path "C:\Config\appsettings.json"

执行结果示例:

1
2
IP 配置已复制到剪贴板
文件内容已复制:C:\Config\appsettings.json (2048 bytes)

注意事项

  1. 剪贴板安全:剪贴板内容可能包含敏感信息(密码、Token),操作完应清空剪贴板
  2. GUI 依赖:剪贴板和 GUI 操作需要交互式桌面会话,在无头环境(Server Core、SSH 远程)中不可用
  3. SendKeys 时序:窗口自动化依赖时序,网络延迟或系统负载可能导致失败。添加合理的 Start-Sleep
  4. STA 模式:某些 GUI 操作需要单线程单元(STA)模式,PowerShell 5.1 默认为 STA,PowerShell 7 默认为 MTA
  5. 管理员权限:某些 GUI 操作(如操控其他用户的窗口)需要管理员权限
  6. 用户体验:自动化 GUI 操作时应通知用户,避免在用户不知情时操控窗口