128 lines
6.1 KiB
PowerShell
128 lines
6.1 KiB
PowerShell
param([string]$WorkspaceRoot = (Join-Path $PSScriptRoot '..\..'))
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
$workspace = [IO.Path]::GetFullPath($WorkspaceRoot)
|
|
$scriptPath = Join-Path $workspace 'deploy\install-windows.ps1'
|
|
$tokens = $null
|
|
$parseErrors = $null
|
|
$scriptText = [IO.File]::ReadAllText($scriptPath, [Text.Encoding]::UTF8)
|
|
$ast = [Management.Automation.Language.Parser]::ParseInput($scriptText, [ref]$tokens, [ref]$parseErrors)
|
|
if ($parseErrors.Count -ne 0) {
|
|
throw '安装脚本存在语法错误,停止隔离验证'
|
|
}
|
|
$tries = @($ast.EndBlock.Statements | Where-Object { $_ -is [Management.Automation.Language.TryStatementAst] })
|
|
if ($tries.Count -ne 1) {
|
|
throw '安装脚本事务结构已改变,需要重新审核测试入口'
|
|
}
|
|
$allowedCommands = @('Stop-SmailService', 'Copy-Item', 'Test-Path', 'Move-Item', 'Get-Service', 'Invoke-Native', 'Set-RestrictedAcl', 'Write-Output', 'Start-Service', 'Remove-Item', 'Join-Path')
|
|
$commands = $tries[0].FindAll({ param($node) $node -is [Management.Automation.Language.CommandAst] }, $true)
|
|
foreach ($command in $commands) {
|
|
if ($command.GetCommandName() -notin $allowedCommands) {
|
|
throw "发现未隔离的命令,拒绝执行:$($command.GetCommandName())"
|
|
}
|
|
}
|
|
|
|
# 只执行真实事务语句,所有文件和服务操作均为内存函数桩,不执行安装脚本主体。
|
|
$transaction = [scriptblock]::Create($tries[0].Extent.Text)
|
|
$results = @()
|
|
foreach ($stage in @('stop', 'copy-main', 'copy-admin', 'backup-main', 'backup-admin', 'install-main', 'install-admin', 'service')) {
|
|
$results += & {
|
|
param($failureStage, $body)
|
|
$virtualRoot = [IO.Path]::GetFullPath((Join-Path $workspace 'test-log\virtual-install-audit'))
|
|
$resolvedInstallRoot = Join-Path $virtualRoot 'install'
|
|
$transactionRoot = Join-Path $resolvedInstallRoot '.upgrade-audit'
|
|
$backupRoot = Join-Path $resolvedInstallRoot '.backup-audit'
|
|
$mainSource = Join-Path $virtualRoot 'package\smail.exe'
|
|
$adminSource = Join-Path $virtualRoot 'package\smail-admin.exe'
|
|
$mainTarget = Join-Path $resolvedInstallRoot 'smail.exe'
|
|
$adminTarget = Join-Path $resolvedInstallRoot 'smail-admin.exe'
|
|
$mainBackup = Join-Path $backupRoot 'smail.exe'
|
|
$adminBackup = Join-Path $backupRoot 'smail-admin.exe'
|
|
$serviceCreated = $false
|
|
$StartService = $false
|
|
$state = @{
|
|
Files = @{}
|
|
Operations = [Collections.Generic.List[string]]::new()
|
|
Injected = $false
|
|
}
|
|
$state.Files[$mainSource] = 'new-main'
|
|
$state.Files[$adminSource] = 'new-admin'
|
|
$state.Files[$mainTarget] = 'old-main'
|
|
$state.Files[$adminTarget] = 'old-admin'
|
|
|
|
function Assert-AuditPath {
|
|
param([string]$Value)
|
|
$absolute = [IO.Path]::GetFullPath($Value)
|
|
if (-not $absolute.StartsWith($virtualRoot + '\', [StringComparison]::OrdinalIgnoreCase)) {
|
|
throw '模拟路径超出隔离范围'
|
|
}
|
|
}
|
|
function Invoke-Failure {
|
|
param([string]$Stage)
|
|
if ($failureStage -eq $Stage -and -not $state.Injected) {
|
|
$state.Injected = $true
|
|
throw "audit-injected:$Stage"
|
|
}
|
|
}
|
|
function Stop-SmailService {
|
|
Invoke-Failure 'stop'
|
|
}
|
|
function Test-Path {
|
|
param([string]$LiteralPath)
|
|
Assert-AuditPath $LiteralPath
|
|
return $state.Files.ContainsKey($LiteralPath)
|
|
}
|
|
function Copy-Item {
|
|
param([string]$LiteralPath, [string]$Destination)
|
|
Assert-AuditPath $LiteralPath
|
|
Assert-AuditPath $Destination
|
|
if ($LiteralPath -eq $mainSource) { Invoke-Failure 'copy-main' }
|
|
if ($LiteralPath -eq $adminSource) { Invoke-Failure 'copy-admin' }
|
|
$state.Files[$Destination] = $state.Files[$LiteralPath]
|
|
$state.Operations.Add("copy:$LiteralPath")
|
|
}
|
|
function Move-Item {
|
|
param([string]$LiteralPath, [string]$Destination, [switch]$Force)
|
|
Assert-AuditPath $LiteralPath
|
|
Assert-AuditPath $Destination
|
|
if ($LiteralPath -eq $mainTarget) { Invoke-Failure 'backup-main' }
|
|
if ($LiteralPath -eq $adminTarget) { Invoke-Failure 'backup-admin' }
|
|
if ($Destination -eq $mainTarget -and $LiteralPath -ne $mainBackup) { Invoke-Failure 'install-main' }
|
|
if ($Destination -eq $adminTarget -and $LiteralPath -ne $adminBackup) { Invoke-Failure 'install-admin' }
|
|
$state.Files[$Destination] = $state.Files[$LiteralPath]
|
|
$state.Files.Remove($LiteralPath)
|
|
$state.Operations.Add("move:$LiteralPath")
|
|
}
|
|
function Remove-Item {
|
|
param([string]$LiteralPath, [switch]$Recurse, [switch]$Force)
|
|
Assert-AuditPath $LiteralPath
|
|
$state.Files.Remove($LiteralPath)
|
|
$state.Operations.Add("remove:$LiteralPath")
|
|
}
|
|
function Get-Service { Invoke-Failure 'service'; throw '不得调用真实系统服务' }
|
|
function Invoke-Native { throw '不得调用原生系统工具' }
|
|
function Set-RestrictedAcl { throw '不得修改真实权限' }
|
|
function Start-Service { throw '不得启动真实服务' }
|
|
|
|
$caught = ''
|
|
try {
|
|
& $body
|
|
} catch {
|
|
$caught = $_.Exception.Message
|
|
}
|
|
if ($caught -ne "audit-injected:$failureStage") {
|
|
throw "隔离失败注入未按预期命中:$caught"
|
|
}
|
|
[pscustomobject]@{
|
|
Stage = $failureStage
|
|
OriginalMainPreserved = $state.Files[$mainTarget] -eq 'old-main'
|
|
OriginalAdminPreserved = $state.Files[$adminTarget] -eq 'old-admin'
|
|
Operations = @($state.Operations.ToArray())
|
|
}
|
|
} $stage $transaction
|
|
}
|
|
$results | ConvertTo-Json -Depth 5
|
|
if (@($results | Where-Object { -not $_.OriginalMainPreserved -or -not $_.OriginalAdminPreserved }).Count -gt 0) {
|
|
throw '安装事务在备份完成前失败时,回滚会删除未被替换的旧程序;以上操作全部为内存模拟'
|
|
}
|