Private/Installation/Transaction.ps1
|
function Enter-VSOLock { param([string]$Target, $Context) $bytes=[Text.Encoding]::UTF8.GetBytes($Target.ToUpperInvariant()); $sha=[Security.Cryptography.SHA256]::Create() try { $key=([BitConverter]::ToString($sha.ComputeHash($bytes))).Replace('-','').ToLowerInvariant() } finally { $sha.Dispose() } $path=Join-Path ([IO.Path]::GetDirectoryName($Target)) ('.vso-lock-'+$key) Assert-VSONoReparse $path $Context try { return [IO.FileStream]::new($path,[IO.FileMode]::OpenOrCreate,[IO.FileAccess]::ReadWrite,[IO.FileShare]::None,1,[IO.FileOptions]::DeleteOnClose) } catch { Throw-VSOError $Context Busy @() $_.Exception } } function Assert-VSOIdle { param([string]$Target, $Context, $Receipt, [string]$ApplicationPath) if (-not $ApplicationPath) { $ApplicationPath=Join-VSOChild $Target 'VSO7' $Context } $activity=Get-VSOProcesses $ApplicationPath $Context $Receipt if (-not $activity.Reliable -or $activity.Processes.Count -gt 0) { Throw-VSOError $Context Busy } } function Repair-VSOTransactions { param([string]$Target, $Context) $parent=[IO.Path]::GetDirectoryName($Target) foreach ($dir in @(Get-ChildItem -LiteralPath $parent -Directory -Filter '.vso-txn-*' -Force -ErrorAction Stop)) { if ($dir.Name -notmatch '^\.vso-txn-[a-f0-9]{32}$') { continue } Assert-VSONoReparse $dir.FullName $Context $journalPath=Join-VSOChild $dir.FullName 'transaction.json' $Context if (-not [IO.File]::Exists($journalPath)) { continue } $j=Read-VSOJson $journalPath $Context if ((Get-VSOProperty $j Target '') -ine $Target) { continue } if ((Get-VSOProperty $j Schema '') -ne 1 -or (Get-VSOProperty $j Product '') -cne 'VicoSafeOptimizer.Transaction' -or (Get-VSOProperty $j State '') -cnotin @('Prepared','Promoted','Committed','RolledBack')) { Throw-VSOError $Context Interrupted @($dir.FullName) } if ($j.State -in @('Committed','RolledBack')) { continue } Assert-VSOProtectedDirectory $dir.FullName $Context -UserAllowed $previous=Join-VSOChild $dir.FullName 'previous' $Context if (-not [IO.Directory]::Exists($Target) -and [IO.Directory]::Exists($previous)) { # Recovery only moves the saved directory back; no downloaded code is executed. $null=@(Get-VSOTreeFiles $previous $Context) $oldMarker=Read-VSOJson (Join-VSOChild $previous '.vso-manager.json' $Context) $Context if ($oldMarker.Product -cne 'VicoSafeOptimizer' -or $oldMarker.Id -cne $j.InstallationId -or $oldMarker.Path -ine $Target) { Throw-VSOError $Context Interrupted @($dir.FullName) } $null=ConvertTo-VSOFileMap $oldMarker.Files $previous $Context (Get-VSOProperty $oldMarker DistributionContract $null) [IO.Directory]::Move($previous,$Target); $j.State='RolledBack'; Write-VSOJson $journalPath $j $Context } else { # An existing target could contain new user data: never replace it automatically. Throw-VSOError $Context Interrupted @($dir.FullName) } } } function Invoke-VSOInstallTransaction { param($Options, $Release, $Context, [switch]$Force, [switch]$Update) $target=$Options.Path; $scope=$Options.Scope; $parent=[IO.Path]::GetDirectoryName($target) if ($scope -eq 'AllUsers') { if (-not (Test-VSOAdministrator)) { Throw-VSOError $Context Admin } if (-not [IO.Directory]::Exists($parent)) { Throw-VSOError $Context ProtectedParent } Assert-VSOProtectedDirectory $parent $Context } elseif (-not [IO.Directory]::Exists($parent)) { [void][IO.Directory]::CreateDirectory($parent) } Assert-VSONoReparse $parent $Context $lock=Enter-VSOLock $target $Context $work=$null; $txn=$null; $journal=$null; $oldMoved=$false; $promoted=$false; $committed=$false; $registered=$false try { Repair-VSOTransactions $target $Context $old=$null if ([IO.File]::Exists((Join-Path $target '.vso-manager.json'))) { $old=Read-VSOReceipt $target $Context if ($old.Scope -cne $scope) { Throw-VSOError $Context ScopeConflict } Test-VSOInstalledFiles $old $Context if (-not $Force -and -not $Update) { Throw-VSOError $Context Existing } if (-not $Force -and (Compare-VSOVersion $Release.Version $old.Version $Context) -le 0) { return [pscustomobject]@{ Changed=$false; Message=$Context.Text.Current; Path=$target; Version=$old.Version } } } elseif ($Update) { Throw-VSOError $Context Manual } Assert-VSOIdle $target $Context $old $work=Join-Path $parent ('.vso-work-'+[guid]::NewGuid().ToString('N')) New-VSOSecureDirectory $work $scope $Context $archive=Join-VSOChild $work 'distribution.zip' $Context Invoke-VSOHttp ([uri]$Release.AssetUri) $Context -OutFile $archive -ExpectedSize $Release.Size -MaximumBytes 134217728 if ((Get-VSOHash $archive) -cne $Release.Sha256) { Throw-VSOError $Context Integrity @('archive SHA-256') } $extracted=Join-VSOChild $work 'extracted' $Context [void][IO.Directory]::CreateDirectory($extracted) Expand-VSOArchive $archive $extracted $Context $packageRoot=Find-VSOPackageRoot $extracted $Context $distribution=Get-VSODistribution $packageRoot $Context -ExpectedVersion $Release.Version -Package if ($old -and $distribution.Generation -ne $old.Generation) { Throw-VSOError $Context Generation } $txn=Join-Path $parent ('.vso-txn-'+[guid]::NewGuid().ToString('N')) New-VSOSecureDirectory $txn $scope $Context $stage=Join-VSOChild $txn 'next' $Context # Preserve the official VSO7 leaf name: upstream otherwise offers relocation # on first launch, invalidating manager paths and process attribution. [void][IO.Directory]::CreateDirectory($stage) [IO.Directory]::Move($packageRoot,(Join-VSOChild $stage 'VSO7' $Context)) $distributionFiles=[ordered]@{} foreach ($relative in $distribution.Files.Keys) { $distributionFiles['VSO7/'+$relative]=$distribution.Files[$relative] } $oldFiles=if ($old) { ConvertTo-VSOFileMap $old.Files $target $Context (Get-VSOProperty $old DistributionContract $null) } else { [ordered]@{} } if ([IO.Directory]::Exists($target)) { $contents=@(Get-VSOTreeFiles $target $Context) if (-not $old -and $contents.Count -gt 0) { if (-not $Force) { Throw-VSOError $Context Existing } # Adoption requires byte-identical files from the independently verified release. foreach ($relative in $distributionFiles.Keys) { $existing=Join-VSOChild $target $relative $Context if (-not [IO.File]::Exists($existing) -or (Get-VSOHash $existing) -cne $distributionFiles[$relative]) { Throw-VSOError $Context Destination @($relative) } $oldFiles[$relative]=$distributionFiles[$relative] } } foreach ($file in $contents) { $relative=$file.FullName.Substring($target.Length+1).Replace('\','/') if ($relative -eq '.vso-manager.json' -or $oldFiles.Contains($relative)) { continue } if ($distributionFiles.Contains($relative)) { Throw-VSOError $Context Destination @($relative) } $destination=Join-VSOChild $stage $relative $Context [void][IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName($destination)) [IO.File]::Copy($file.FullName,$destination,$false) if ((Get-VSOHash $file.FullName) -cne (Get-VSOHash $destination)) { Throw-VSOError $Context Destination @($relative) } } } $id=if ($old) { $old.Id } else { [guid]::NewGuid().ToString('N') } $fileRecords=@(foreach ($relative in $distributionFiles.Keys) { [ordered]@{ Path=$relative; Sha256=$distributionFiles[$relative] } }) $backups=@(); if ($old) { $backups=@(Get-VSOProperty $old Backups @()) } if ([IO.Directory]::Exists($target)) { $backups+=[IO.Path]::GetFileName($txn) } $journal=[ordered]@{ Schema=1; Product='VicoSafeOptimizer.Transaction'; Target=$target; InstallationId=$id; State='Committed'; CreatedUtc=[DateTime]::UtcNow.ToString('o') } $sha=[Security.Cryptography.SHA256]::Create() try { $journalHash=([BitConverter]::ToString($sha.ComputeHash([Text.Encoding]::UTF8.GetBytes(($journal|ConvertTo-Json -Depth 15))))).Replace('-','').ToLowerInvariant() } finally {$sha.Dispose()} $journal.State='Prepared' $receipt=[ordered]@{ Schema=1; Product='VicoSafeOptimizer'; Id=$id; Version=$Release.Version; Generation=$distribution.Generation; Path=$target; Scope=$scope; ReleaseTag=$Release.Tag; ReleaseUri=$Release.ReleaseUri; AssetUri=$Release.AssetUri; ArchiveSha256=$Release.Sha256; IntegrityMethod=$Release.IntegrityMethod; InstalledUtc=[DateTime]::UtcNow.ToString('o'); Layout=$distribution.Layout; ApplicationDirectory='VSO7'; Launcher='VSO7/START VSO7.bat'; State='Installed'; Files=$fileRecords; Backups=$backups } $receipt.DistributionContract=$distribution.Contract $receipt.Transactions=@(@(Get-VSOProperty $old Transactions @())+@([IO.Path]::GetFileName($txn))) $receipt.TransactionReceipts=@(@(Get-VSOProperty $old TransactionReceipts @())+@([ordered]@{Transaction=[IO.Path]::GetFileName($txn);JournalSha256=$journalHash})) $receipt.BackupReceipts=@(Get-VSOProperty $old BackupReceipts @()) if ($old) { $receipt.BackupReceipts+=@([ordered]@{Transaction=[IO.Path]::GetFileName($txn);ReceiptSha256=(Get-VSOHash (Join-Path $target '.vso-manager.json'))}) } $receipt.MemorySourceSha256=Get-VSOSourceTextHash (Join-VSOChild $stage 'VSO7/VSO7.ps1' $Context) $Context Write-VSOJson (Join-VSOChild $stage '.vso-manager.json' $Context) $receipt $Context $journalPath=Join-VSOChild $txn 'transaction.json' $Context Write-VSOJson $journalPath $journal $Context Assert-VSOIdle $target $Context $old Assert-VSONoReparse $target $Context if ($old) { Test-VSOInstalledFiles $old $Context } if ([IO.Directory]::Exists($target)) { [IO.Directory]::Move($target,(Join-VSOChild $txn 'previous' $Context)); $oldMoved=$true } [IO.Directory]::Move($stage,$target); $promoted=$true $journal.State='Promoted'; Write-VSOJson $journalPath $journal $Context $validated=Read-VSOReceipt $target $Context; Test-VSOInstalledFiles $validated $Context Register-VSOReceipt $validated $Context $registered=$true $journal.State='Committed'; Write-VSOJson $journalPath $journal $Context; $committed=$true return [pscustomobject]@{ PSTypeName='VSO.Result'; Changed=$true; Operation=$(if ($Update) { 'Update' } else { 'Install' }); Path=$target; Version=$Release.Version; Scope=$scope; InstallationId=$id; BackupPath=$(if ($oldMoved) { Join-Path $txn 'previous' } else { $null }); IntegrityMethod=$Release.IntegrityMethod } } catch { $originalError=$_ if ($txn -and -not $committed) { try { if ($promoted -and [IO.Directory]::Exists($target)) { [IO.Directory]::Move($target,(Join-VSOChild $txn 'failed' $Context)) } if ($oldMoved) { [IO.Directory]::Move((Join-VSOChild $txn 'previous' $Context),$target) } if ($old) { Register-VSOReceipt $old $Context } elseif ($registered) { $index=Join-VSOChild (Get-VSORegistryRoot $scope $Context) ($id+'.json') $Context $entry=Read-VSOJson $index $Context if ($entry.Id -cne $id -or $entry.Path -ine $target) { Throw-VSOError $Context Metadata @('failed-install registration') } [IO.File]::Delete($index) } if ($journal) { $journal.State='RolledBack'; Write-VSOJson (Join-Path $txn 'transaction.json') $journal $Context } } catch { Throw-VSOError $Context Rollback @($txn) $_.Exception } } throw $originalError } finally { try { if ($work) { Remove-VSOPrivateTree $work $parent $Context } } finally { $lock.Dispose(); Write-VSOProgress $Context Verify -Completed } } } function Get-VSORemovalPlan { param($Installation, $Context, [switch]$Full, [switch]$RemoveBackups) if ($RemoveBackups -and -not $Full) { Throw-VSOError $Context BackupIntent } $receipt=Read-VSOReceipt $Installation.Path $Context $contract=Get-VSOProperty $receipt DistributionContract $null $map=ConvertTo-VSOFileMap $receipt.Files $receipt.Path $Context $contract $roles=if ($contract) { Get-VSOContractFiles $contract $Context } else { [ordered]@{} } $remove=[Collections.Generic.List[object]]::new(); $preserve=[Collections.Generic.List[string]]::new(); $protected=[Collections.Generic.List[object]]::new() foreach ($file in @(Get-VSOTreeFiles $receipt.Path $Context)) { $relative=$file.FullName.Substring($receipt.Path.Length+1).Replace('\','/') if ($relative -eq '.vso-manager.json') { continue } $role='Application'; $leaf=$relative.Substring([Math]::Min(5,$relative.Length)) if ($roles.Contains($leaf)) { $role=$roles[$leaf].Role } $reason=$null if (-not $map.Contains($relative)) { $reason='UntrackedOwnership' } elseif ((Get-VSOHash $file.FullName) -cne $map[$relative]) { $reason='ModifiedSinceInstall' } elseif ($role -eq 'Recovery') { $reason='RecoveryRequiresSeparateOwnership' } elseif (-not $Full -and $role -ne 'Application') { $reason='PersistentDataNormalReset' } if ($reason) { $preserve.Add($file.FullName); $protected.Add([pscustomobject]@{Path=$file.FullName;Reason=$reason;Ownership=$role}) } else { $remove.Add([pscustomobject]@{Path=$file.FullName;Sha256=$map[$relative];Ownership='ReceiptHash';Role=$role}) } } $marker=Join-Path $receipt.Path '.vso-manager.json' $pending=[Collections.Generic.Stack[string]]::new();$pending.Push($receipt.Path) while ($pending.Count) { $folder=$pending.Pop();Assert-VSONoReparse $folder $Context $children=@(Get-ChildItem -LiteralPath $folder -Force -ErrorAction Stop) foreach ($child in $children) {if ($child.PSIsContainer) {$pending.Push($child.FullName)}} if ($children.Count -eq 0 -and $folder -ine $receipt.Path) { $prefix=$folder.Substring($receipt.Path.Length+1).Replace('\','/')+'/' if (@($map.Keys|Where-Object {$_.StartsWith($prefix,[StringComparison]::OrdinalIgnoreCase)}).Count -eq 0) { $preserve.Add($folder);$protected.Add([pscustomobject]@{Path=$folder;Reason='UntrackedEmptyDirectory';Ownership='Unknown'}) } } } $index=Join-Path (Get-VSORegistryRoot $receipt.Scope $Context) ($receipt.Id+'.json') if ([IO.File]::Exists($index)) { $entry=Read-VSOJson $index $Context if ($entry.Id -cne $receipt.Id -or $entry.Path -ine $receipt.Path -or $entry.Scope -cne $receipt.Scope -or $entry.ReceiptSha256 -cne (Get-VSOHash $marker)) { Throw-VSOError $Context Metadata @('removal index') } $remove.Add([pscustomobject]@{Path=$index;Sha256=(Get-VSOHash $index);Ownership='ReceiptIndex';Role='Metadata'}) } $names=@(@($receipt.Backups)+@(Get-VSOProperty $receipt Transactions @()) | Select-Object -Unique) foreach ($name in $names) { $backupRoot=Join-VSOChild ([IO.Path]::GetDirectoryName($receipt.Path)) $name $Context if (-not [IO.Directory]::Exists($backupRoot)) { continue } Assert-VSOProtectedDirectory $backupRoot $Context -UserAllowed:($receipt.Scope -eq 'CurrentUser') $journalPath=Join-VSOChild $backupRoot 'transaction.json' $Context $journalPins=@(Get-VSOProperty $receipt TransactionReceipts @()|Where-Object Transaction -CEQ $name) if ($journalPins.Count -eq 0) { $preserve.Add($backupRoot);$protected.Add([pscustomobject]@{Path=$backupRoot;Reason='TransactionJournalNotPinned';Ownership='Unproven'});continue } if ($journalPins.Count -ne 1 -or (Get-VSOHash $journalPath) -cne $journalPins[0].JournalSha256) { Throw-VSOError $Context Metadata @('transaction journal changed') } $journal=Read-VSOJson $journalPath $Context if ($journal.Schema -ne 1 -or $journal.Product -cne 'VicoSafeOptimizer.Transaction' -or $journal.Target -ine $receipt.Path -or $journal.InstallationId -cne $receipt.Id -or $journal.State -cne 'Committed') { Throw-VSOError $Context Metadata @('backup ownership') } $previous=Join-VSOChild $backupRoot 'previous' $Context $keepJournal=$false if ([IO.Directory]::Exists($previous)) { if (-not $RemoveBackups) { $preserve.Add($previous); $protected.Add([pscustomobject]@{Path=$previous;Reason='BackupLossNotAcknowledged';Ownership='Transaction'}); $keepJournal=$true } else { $oldMarker=Join-VSOChild $previous '.vso-manager.json' $Context if (-not [IO.File]::Exists($oldMarker)) { $preserve.Add($previous);$protected.Add([pscustomobject]@{Path=$previous;Reason='MissingBackupReceipt';Ownership='Unknown'});$keepJournal=$true } else { $pins=@(Get-VSOProperty $receipt BackupReceipts @()|Where-Object Transaction -CEQ $name) if ($pins.Count -eq 0) { $preserve.Add($previous);$protected.Add([pscustomobject]@{Path=$previous;Reason='BackupReceiptNotPinned';Ownership='Unproven'});$preserve.Add($journalPath);$protected.Add([pscustomobject]@{Path=$journalPath;Reason='PreservedTransactionProvenance';Ownership='Metadata'});continue } if ($pins.Count -ne 1 -or (Get-VSOHash $oldMarker) -cne $pins[0].ReceiptSha256) { Throw-VSOError $Context Metadata @('backup receipt changed') } $old=Read-VSOJson $oldMarker $Context if ($old.Id -cne $receipt.Id -or $old.Path -ine $receipt.Path -or $old.Product -cne 'VicoSafeOptimizer') { Throw-VSOError $Context Metadata @('backup receipt') } $oldMap=ConvertTo-VSOFileMap $old.Files $previous $Context (Get-VSOProperty $old DistributionContract $null) $oldContract=Get-VSOProperty $old DistributionContract $null $oldRoles=if ($oldContract) { Get-VSOContractFiles $oldContract $Context } else { [ordered]@{} } foreach ($file in @(Get-VSOTreeFiles $previous $Context)) { $relative=$file.FullName.Substring($previous.Length+1).Replace('\','/') if ($relative -eq '.vso-manager.json') { continue } $oldLeaf=$relative.Substring([Math]::Min(5,$relative.Length));$recoveryRole=$oldRoles.Contains($oldLeaf) -and $oldRoles[$oldLeaf].Role -eq 'Recovery' if ($oldMap.Contains($relative) -and (Get-VSOHash $file.FullName) -ceq $oldMap[$relative] -and $relative -cnotlike 'VSO7/Recuperacion/*' -and -not $recoveryRole) { $remove.Add([pscustomobject]@{Path=$file.FullName;Sha256=$oldMap[$relative];Ownership='BackupReceiptHash';Role='Backup'}) } else { $preserve.Add($file.FullName);$protected.Add([pscustomobject]@{Path=$file.FullName;Reason='UntrackedModifiedOrRecoveryBackup';Ownership='Unknown'});$keepJournal=$true } } # Keep proof for any backup data that survives, permitting later inspection. if ($keepJournal) { $preserve.Add($oldMarker);$protected.Add([pscustomobject]@{Path=$oldMarker;Reason='PreservedBackupProvenance';Ownership='Metadata'}) } else { $remove.Add([pscustomobject]@{Path=$oldMarker;Sha256=(Get-VSOHash $oldMarker);Ownership='BackupReceipt';Role='Metadata'}) } } } } foreach ($file in @(Get-VSOTreeFiles $backupRoot $Context)) { if ($file.FullName -ieq $journalPath -or $file.FullName.StartsWith($previous+'\',[StringComparison]::OrdinalIgnoreCase)) { continue } $preserve.Add($file.FullName);$protected.Add([pscustomobject]@{Path=$file.FullName;Reason='UntrackedTransactionData';Ownership='Unknown'});$keepJournal=$true } if ($Full -and -not $keepJournal) { $remove.Add([pscustomobject]@{Path=$journalPath;Sha256=(Get-VSOHash $journalPath);Ownership='ReceiptTransaction';Role='Metadata'}) } else { $preserve.Add($journalPath);$protected.Add([pscustomobject]@{Path=$journalPath;Reason='PreservedTransactionProvenance';Ownership='Metadata'}) } } # Receipt is last: partial removal retains the map for a new plan/retry by Path. $remove.Add([pscustomobject]@{Path=$marker;Sha256=(Get-VSOHash $marker);Ownership='Receipt';Role='Metadata'}) $shared=Join-Path $env:ProgramData 'VicoreDevelopment\VSO7' $preserve.Add($shared);$protected.Add([pscustomobject]@{Path=$shared;Reason='SharedRecoveryAndRuntimeData';Ownership='SharedOrUntracked'}) # Orphans have no receipt binding. Diagnose them instead of guessing ownership. foreach ($dir in @(Get-ChildItem -LiteralPath ([IO.Path]::GetDirectoryName($receipt.Path)) -Directory -Filter '.vso-txn-*' -Force -ErrorAction Stop)) { if ($dir.Name -in $names -or $dir.Name -notmatch '^\.vso-txn-[a-f0-9]{32}$') { continue } if ($dir.Attributes -band [IO.FileAttributes]::ReparsePoint) { continue } $path=Join-Path $dir.FullName 'transaction.json' if ([IO.File]::Exists($path)) { $orphan=Read-VSOJson $path $Context if ((Get-VSOProperty $orphan Target '') -ieq $receipt.Path) { $preserve.Add($dir.FullName);$protected.Add([pscustomobject]@{Path=$dir.FullName;Reason='TransactionNotBoundToReceipt';Ownership='Unproven'}) } } } $directorySet=[Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase) $roots=@($receipt.Path)+@($names|ForEach-Object {Join-VSOChild ([IO.Path]::GetDirectoryName($receipt.Path)) $_ $Context}) $directorySources=@($remove)+@($map.Keys|ForEach-Object {[pscustomobject]@{Path=(Join-VSOChild $receipt.Path $_ $Context)}}) foreach ($file in $directorySources) { foreach ($root in $roots) { if (-not $file.Path.StartsWith($root+'\',[StringComparison]::OrdinalIgnoreCase)) {continue} $cursor=[IO.Path]::GetDirectoryName($file.Path) while ($cursor -and ($cursor -ieq $root -or $cursor.StartsWith($root+'\',[StringComparison]::OrdinalIgnoreCase))) { $null=$directorySet.Add($cursor);$cursor=[IO.Path]::GetDirectoryName($cursor) } } } $directories=@($directorySet|Sort-Object Length -Descending) foreach ($item in $protected) { $item|Add-Member -NotePropertyName Exists -NotePropertyValue ([IO.File]::Exists($item.Path) -or [IO.Directory]::Exists($item.Path)) } return [pscustomobject]@{PSTypeName='VSO.RemovalPlan';Path=$receipt.Path;Full=[bool]$Full;RemoveBackups=[bool]$RemoveBackups;Remove=@($remove.ToArray());Directories=$directories;Preserve=@($preserve.ToArray());Protected=@($protected.ToArray());Receipt=$receipt} } function Open-VSORemovalFile { param([string]$Path, $Context, [switch]$Directory) if (-not ('VsoRemovalFile' -as [type])) { Add-Type -TypeDefinition @' using System; using System.Runtime.InteropServices; using Microsoft.Win32.SafeHandles; public static class VsoRemovalFile { [DllImport("kernel32.dll", CharSet=CharSet.Unicode, SetLastError=true)] public static extern SafeFileHandle CreateFile(string path, uint access, uint share, IntPtr security, uint creation, uint flags, IntPtr template); } '@ } Assert-VSONoReparse $Path $Context if (-not $Directory -and ((Get-Item -LiteralPath $Path -Force -ErrorAction Stop).Attributes -band [IO.FileAttributes]::ReadOnly)) { Throw-VSOError $Context Destination @('read-only file: '+$Path) } # Request DELETE during preflight without deleting. Keep the object pinned against # writes/replacement until its turn; all permission failures precede the first delete. $access=if ($Directory) { [uint32]65536 } else { [uint32]2147549184 } $flags=if ($Directory) { [uint32]35651584 } else { [uint32]2097152 } $handle=[VsoRemovalFile]::CreateFile($Path,$access,1,[IntPtr]::Zero,3,$flags,[IntPtr]::Zero) if ($handle.IsInvalid) { $code=[Runtime.InteropServices.Marshal]::GetLastWin32Error();$handle.Dispose();throw [ComponentModel.Win32Exception]::new($code) } if ($Directory) { return $handle } try { return [IO.FileStream]::new($handle,[IO.FileAccess]::Read) } catch { $handle.Dispose();throw } } function Invoke-VSORemoval { param($Plan, $Context) $lock=Enter-VSOLock $Plan.Path $Context $handles=[Collections.Generic.List[IO.FileStream]]::new() $directoryHandles=[Collections.Generic.Dictionary[string,object]]::new([StringComparer]::OrdinalIgnoreCase) try { Assert-VSOProtectedDirectory $Plan.Path $Context -UserAllowed:($Plan.Receipt.Scope -eq 'CurrentUser') Assert-VSOIdle $Plan.Path $Context $Plan.Receipt foreach ($file in $Plan.Remove) { $stream=Open-VSORemovalFile $file.Path $Context; $handles.Add($stream) $sha=[Security.Cryptography.SHA256]::Create() try { $hash=([BitConverter]::ToString($sha.ComputeHash($stream))).Replace('-','').ToLowerInvariant() } finally { $sha.Dispose() } if ($hash -cne $file.Sha256) { Throw-VSOError $Context Destination @($file.Path) } } # Preflight DELETE access for directories expected to become empty, too. # Unknown children keep a directory out of this set; no recursive deletion. $scheduled=[Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase) foreach ($file in $Plan.Remove) { $null=$scheduled.Add($file.Path) } foreach ($folder in $Plan.Directories) { Assert-VSONoReparse $folder $Context if (-not [IO.Directory]::Exists($folder)) { continue } $children=@(Get-ChildItem -LiteralPath $folder -Force -ErrorAction Stop) if (@($children|Where-Object {-not $scheduled.Contains($_.FullName)}).Count -eq 0) { $directoryHandles.Add($folder,(Open-VSORemovalFile $folder $Context -Directory)) $null=$scheduled.Add($folder) } } $done=0 foreach ($file in $Plan.Remove) { Assert-VSONoReparse $file.Path $Context $handles[$done].Dispose() if ((Get-VSOHash $file.Path) -cne $file.Sha256) { Throw-VSOError $Context Destination @($file.Path) } [IO.File]::Delete($file.Path) if ([IO.File]::Exists($file.Path)) { Throw-VSOError $Context Destination @($file.Path) } $done++; Write-VSOProgress $Context Remove $done $Plan.Remove.Count } foreach ($folder in @($Plan.Directories)+@((Join-Path $Plan.Path 'VSO7'),$Plan.Path)) { Assert-VSONoReparse $folder $Context if ($directoryHandles.ContainsKey($folder)) { $directoryHandles[$folder].Dispose() } if ([IO.Directory]::Exists($folder) -and @(Get-ChildItem -LiteralPath $folder -Force -ErrorAction Stop).Count -eq 0) { [IO.Directory]::Delete($folder,$false) } } return [pscustomobject]@{PSTypeName='VSO.Result';Changed=$true;Operation='Reset';Path=$Plan.Path;Full=$Plan.Full;Removed=@($Plan.Remove.Path);Preserved=$Plan.Preserve;Protected=$Plan.Protected;RestoredWindowsTweaks=$false;CompleteCleanup=($Plan.Protected.Count -eq 0)} } finally { foreach ($stream in $handles) { $stream.Dispose() };foreach ($handle in $directoryHandles.Values) { $handle.Dispose() };$lock.Dispose();Write-VSOProgress $Context Remove -Completed } } |