Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.

455 wiersze
23KB

  1. [CmdletBinding()]
  2. param(
  3. [Parameter(Mandatory = $true)][string]$ArchivePath,
  4. [Parameter(Mandatory = $true)][string]$ProjectPath,
  5. [Parameter(Mandatory = $true)][string]$InvocationId,
  6. [Parameter(Mandatory = $true)]
  7. [ValidatePattern('^[0-9A-Fa-f]{64}$')]
  8. [string]$ExpectedArchiveSha256,
  9. [Parameter(Mandatory = $true)][string]$ProductionSiteName,
  10. [Parameter(Mandatory = $true)][string]$ProductionPoolName,
  11. [Parameter(Mandatory = $true)][int]$ProductionPort,
  12. [Parameter(Mandatory = $true)][string]$ProductionBaseUrl,
  13. [Parameter(Mandatory = $true)][string]$TestSiteName,
  14. [Parameter(Mandatory = $true)][string]$TestPoolName,
  15. [Parameter(Mandatory = $true)][int]$TestPort,
  16. [Parameter(Mandatory = $true)][string]$TestBaseUrl,
  17. [switch]$RunTests
  18. )
  19. $ErrorActionPreference = 'Stop'
  20. Set-StrictMode -Version 2.0
  21. function Quote-PowerShellLiteral {
  22. param([Parameter(Mandatory = $true)][string]$Value)
  23. return "'" + $Value.Replace("'", "''") + "'"
  24. }
  25. function Invoke-WindowsPowerShell {
  26. param([Parameter(Mandatory = $true)][string]$Command)
  27. $encoded = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($Command))
  28. $process = Start-Process -FilePath 'powershell.exe' -ArgumentList @(
  29. '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-EncodedCommand', $encoded
  30. ) -Wait -PassThru -NoNewWindow
  31. return $process.ExitCode
  32. }
  33. function Assert-Administrator {
  34. $identity = [Security.Principal.WindowsIdentity]::GetCurrent()
  35. $principal = New-Object Security.Principal.WindowsPrincipal($identity)
  36. if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
  37. throw 'Remote installation requires an elevated Windows account.'
  38. }
  39. }
  40. function Get-SiteSnapshot {
  41. param([string]$Name)
  42. if (-not (Test-Path "IIS:\Sites\$Name")) { return $null }
  43. $site = Get-Website -Name $Name
  44. $parentPaths = (Get-WebConfigurationProperty -PSPath 'MACHINE/WEBROOT/APPHOST' -Location $Name -Filter 'system.webServer/asp' -Name 'enableParentPaths').Value
  45. return [PSCustomObject]@{
  46. Name = $Name
  47. State = [string]$site.State
  48. PhysicalPath = [string]$site.PhysicalPath
  49. ApplicationPool = [string]$site.ApplicationPool
  50. EnableParentPaths = [bool]$parentPaths
  51. }
  52. }
  53. function Assert-SiteTarget {
  54. param(
  55. [string]$Name,
  56. [string]$PoolName,
  57. [string]$PhysicalPath,
  58. [int]$Port
  59. )
  60. $snapshot = Get-SiteSnapshot -Name $Name
  61. if ($snapshot) {
  62. if ([IO.Path]::GetFullPath($snapshot.PhysicalPath).TrimEnd('\') -ne [IO.Path]::GetFullPath($PhysicalPath).TrimEnd('\')) {
  63. throw "Existing site '$Name' has physical path '$($snapshot.PhysicalPath)', expected '$PhysicalPath'. Refusing to adopt or rewrite it."
  64. }
  65. if ($snapshot.ApplicationPool -ne $PoolName) {
  66. throw "Existing site '$Name' uses app pool '$($snapshot.ApplicationPool)', expected '$PoolName'. Refusing to adopt it."
  67. }
  68. $httpBinding = @(Get-WebBinding -Name $Name -Protocol http | Where-Object { $_.bindingInformation -match ":$Port`:" })
  69. if ($httpBinding.Count -ne 1) {
  70. throw "Existing site '$Name' must have exactly one HTTP binding on port $Port."
  71. }
  72. }
  73. $conflictingSiteNames = @()
  74. foreach ($otherSite in @(Get-Website | Where-Object { $_.Name -ne $Name })) {
  75. $matchingBindings = @(Get-WebBinding -Name $otherSite.Name -Protocol http | Where-Object { $_.bindingInformation -match ":$Port`:" })
  76. if ($matchingBindings.Count -gt 0) { $conflictingSiteNames += $otherSite.Name }
  77. }
  78. if ($conflictingSiteNames.Count -gt 0) {
  79. throw "HTTP port $Port is already used by unrelated site(s): $($conflictingSiteNames -join ', ')."
  80. }
  81. }
  82. function Assert-PoolTarget {
  83. param([string]$PoolName, [string]$AllowedSiteName)
  84. if (-not (Test-Path "IIS:\AppPools\$PoolName")) { return }
  85. $otherUsers = @(Get-Website | Where-Object { $_.ApplicationPool -eq $PoolName -and $_.Name -ne $AllowedSiteName })
  86. if ($otherUsers.Count -gt 0) {
  87. throw "App pool '$PoolName' is used by unrelated site(s): $($otherUsers.Name -join ', ')."
  88. }
  89. }
  90. function Get-ComponentRegistrationMode {
  91. param(
  92. [Parameter(Mandatory = $true)][string]$ExistingProjectPath,
  93. [Parameter(Mandatory = $true)][object[]]$Components
  94. )
  95. $absentCount = 0
  96. $ownedCount = 0
  97. foreach ($component in $Components) {
  98. $progIdKey = "HKLM:\SOFTWARE\Classes\$($component.ProgId)"
  99. $clsidKey = "HKLM:\SOFTWARE\Classes\CLSID\$($component.ClassId)"
  100. $progIdExists = Test-Path -LiteralPath $progIdKey
  101. $clsidExists = Test-Path -LiteralPath $clsidKey
  102. if (-not $progIdExists -and -not $clsidExists) {
  103. $absentCount++
  104. continue
  105. }
  106. if (-not $progIdExists -or -not $clsidExists) {
  107. throw "Component registration is partial for '$($component.ProgId)'. Refusing to overwrite or repair registry state that this invocation does not own."
  108. }
  109. $registeredClassId = (Get-ItemProperty -LiteralPath (Join-Path $progIdKey 'CLSID')).'(default)'
  110. if ($registeredClassId -ne $component.ClassId) {
  111. throw "ProgID '$($component.ProgId)' maps to '$registeredClassId', not this project's CLSID '$($component.ClassId)'."
  112. }
  113. $scriptletKey = Join-Path $clsidKey 'ScriptletURL'
  114. if (-not (Test-Path -LiteralPath $scriptletKey)) {
  115. throw "CLSID '$($component.ClassId)' has no ScriptletURL. Refusing to overwrite it."
  116. }
  117. $registeredPath = [string](Get-ItemProperty -LiteralPath $scriptletKey).'(default)'
  118. $expectedPath = [IO.Path]::GetFullPath((Join-Path $ExistingProjectPath $component.RelativePath))
  119. $registeredFilePath = $null
  120. if (-not [string]::IsNullOrWhiteSpace($registeredPath)) {
  121. $registeredUri = $null
  122. if ([Uri]::TryCreate($registeredPath, [UriKind]::Absolute, [ref]$registeredUri) -and $registeredUri.IsFile) {
  123. $registeredFilePath = [IO.Path]::GetFullPath($registeredUri.LocalPath)
  124. } elseif ([IO.Path]::IsPathRooted($registeredPath)) {
  125. $registeredFilePath = [IO.Path]::GetFullPath($registeredPath)
  126. }
  127. }
  128. if (-not $registeredFilePath -or $registeredFilePath.TrimEnd('\') -ne $expectedPath.TrimEnd('\')) {
  129. throw "CLSID '$($component.ClassId)' is registered from '$registeredPath', not the existing deployment path '$expectedPath'."
  130. }
  131. $ownedCount++
  132. }
  133. if ($absentCount -eq $Components.Count) { return 'Absent' }
  134. if ($ownedCount -eq $Components.Count) { return 'Owned' }
  135. throw 'Component registrations are a mixture of absent and existing entries. Refusing a deployment that could overwrite or delete unrelated registry state.'
  136. }
  137. function Restore-SiteState {
  138. param($Snapshot)
  139. if (-not $Snapshot -or -not (Test-Path "IIS:\Sites\$($Snapshot.Name)")) { return }
  140. $parentPathsValue = if ($Snapshot.EnableParentPaths) { 'True' } else { 'False' }
  141. $appcmd = "$env:windir\system32\inetsrv\appcmd.exe"
  142. & $appcmd set config $Snapshot.Name -section:system.webServer/asp "/enableParentPaths:$parentPathsValue" /commit:apphost | Out-Null
  143. if ($LASTEXITCODE -ne 0) { throw "Unable to restore enableParentPaths for site '$($Snapshot.Name)'." }
  144. if ($Snapshot.State -eq 'Started') {
  145. Start-Website -Name $Snapshot.Name -ErrorAction SilentlyContinue
  146. } else {
  147. Stop-Website -Name $Snapshot.Name -ErrorAction SilentlyContinue
  148. }
  149. }
  150. function Wait-WebAppPoolState {
  151. param(
  152. [Parameter(Mandatory = $true)][string]$Name,
  153. [Parameter(Mandatory = $true)][string]$DesiredState,
  154. [int]$TimeoutSeconds = 30
  155. )
  156. $deadline = (Get-Date).AddSeconds($TimeoutSeconds)
  157. do {
  158. $current = [string](Get-WebAppPoolState -Name $Name).Value
  159. if ($current -eq $DesiredState) { return }
  160. Start-Sleep -Milliseconds 250
  161. } while ((Get-Date) -lt $deadline)
  162. throw "App pool '$Name' did not reach state '$DesiredState' within $TimeoutSeconds seconds (current: $current)."
  163. }
  164. function Sync-DirectoryTree {
  165. param(
  166. [Parameter(Mandatory = $true)][string]$Source,
  167. [Parameter(Mandatory = $true)][string]$Destination
  168. )
  169. if (-not (Test-Path -LiteralPath $Source -PathType Container)) {
  170. throw "Directory sync source does not exist: $Source"
  171. }
  172. & robocopy.exe $Source $Destination /MIR /COPY:DAT /DCOPY:DAT /R:2 /W:1 /NFL /NDL /NJH /NJS /NP
  173. $exitCode = $LASTEXITCODE
  174. if ($exitCode -ge 8) {
  175. throw "robocopy failed while mirroring '$Source' to '$Destination' (exit code $exitCode)."
  176. }
  177. }
  178. function Restore-PoolState {
  179. param([string]$Name, [string]$State)
  180. if (-not $State -or -not (Test-Path "IIS:\AppPools\$Name")) { return }
  181. $current = [string](Get-WebAppPoolState -Name $Name).Value
  182. if ($State -eq 'Started') {
  183. if ($current -eq 'Stopping') {
  184. Wait-WebAppPoolState -Name $Name -DesiredState 'Stopped'
  185. $current = 'Stopped'
  186. }
  187. if ($current -ne 'Started') { Start-WebAppPool -Name $Name }
  188. Wait-WebAppPoolState -Name $Name -DesiredState 'Started'
  189. } else {
  190. if ($current -eq 'Starting') {
  191. Wait-WebAppPoolState -Name $Name -DesiredState 'Started'
  192. $current = 'Started'
  193. }
  194. if ($current -ne 'Stopped') { Stop-WebAppPool -Name $Name }
  195. Wait-WebAppPoolState -Name $Name -DesiredState 'Stopped'
  196. }
  197. }
  198. Assert-Administrator
  199. Import-Module WebAdministration
  200. if (-not (Test-Path -LiteralPath $ArchivePath -PathType Leaf)) { throw "Deployment archive not found: $ArchivePath" }
  201. $actualArchiveSha256 = (Get-FileHash -LiteralPath $ArchivePath -Algorithm SHA256).Hash
  202. if ($actualArchiveSha256 -ne $ExpectedArchiveSha256) {
  203. throw "Deployment archive SHA-256 mismatch (expected $ExpectedArchiveSha256, got $actualArchiveSha256)."
  204. }
  205. Write-Output "Verified package SHA-256: $($actualArchiveSha256.ToLowerInvariant())"
  206. if (-not [IO.Path]::IsPathRooted($ProjectPath)) { throw 'ProjectPath must be an absolute Windows path.' }
  207. if ($ProductionSiteName -eq $TestSiteName -or $ProductionPoolName -eq $TestPoolName) { throw 'Production and test IIS names must differ.' }
  208. if ($ProductionPort -eq $TestPort) { throw 'Production and test ports must differ.' }
  209. $projectFullPath = [IO.Path]::GetFullPath($ProjectPath).TrimEnd('\')
  210. $projectParent = Split-Path -Parent $projectFullPath
  211. if (-not $projectParent -or $projectFullPath -eq [IO.Path]::GetPathRoot($projectFullPath).TrimEnd('\')) {
  212. throw "Unsafe ProjectPath: $ProjectPath"
  213. }
  214. $projectParentExisted = Test-Path -LiteralPath $projectParent -PathType Container
  215. $targetExisted = Test-Path -LiteralPath $projectFullPath
  216. if ($targetExisted) {
  217. $targetItem = Get-Item -LiteralPath $projectFullPath -Force
  218. if (-not $targetItem.PSIsContainer) { throw "ProjectPath exists but is not a directory: $projectFullPath" }
  219. if (($targetItem.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { throw "ProjectPath may not be a reparse point: $projectFullPath" }
  220. if (-not (Test-Path -LiteralPath (Join-Path $projectFullPath 'tools\Register-Components.ps1'))) {
  221. throw 'Existing ProjectPath is not a rollback-capable WSC-MVC deployment.'
  222. }
  223. }
  224. $stagingPath = "$projectFullPath.staging.$InvocationId"
  225. $backupPath = "$projectFullPath.rollback.$InvocationId"
  226. $failedPath = "$projectFullPath.failed.$InvocationId"
  227. foreach ($ownedPath in @($stagingPath, $backupPath, $failedPath)) {
  228. if (Test-Path -LiteralPath $ownedPath) { throw "Invocation-owned path already exists: $ownedPath" }
  229. }
  230. $productionPublicPath = Join-Path $projectFullPath 'public'
  231. $testPublicPath = Join-Path $projectFullPath 'test-app\public'
  232. $components = @(
  233. [PSCustomObject]@{ RelativePath = 'Framework\RequestContext.wsc'; ProgId = 'WscMvc.RequestContext'; ClassId = '{1C36FA55-34DF-4974-94B9-D657389362B2}' },
  234. [PSCustomObject]@{ RelativePath = 'Framework\Router.wsc'; ProgId = 'WscMvc.Router'; ClassId = '{C92F9338-B478-4EAD-B865-892FFB1E1C51}' },
  235. [PSCustomObject]@{ RelativePath = 'Framework\ViewRenderer.wsc'; ProgId = 'WscMvc.ViewRenderer'; ClassId = '{4948DF84-5DC6-448A-9F1B-EB596C28842B}' },
  236. [PSCustomObject]@{ RelativePath = 'Framework\Application.wsc'; ProgId = 'WscMvc.Application'; ClassId = '{851C7763-1638-42FE-A166-BF3DD3A96A88}' },
  237. [PSCustomObject]@{ RelativePath = 'Controllers\HomeController.wsc'; ProgId = 'WscMvc.HomeController'; ClassId = '{87488446-60BE-4068-8368-0B709BB68F3F}' },
  238. [PSCustomObject]@{ RelativePath = 'test-app\Controllers\SelfTestController.wsc'; ProgId = 'WscMvc.SelfTestController'; ClassId = '{D2634944-4646-4C55-956E-4C05E7E10904}' }
  239. )
  240. # Validate every archive entry before Expand-Archive can write anything.
  241. Add-Type -AssemblyName System.IO.Compression.FileSystem
  242. $zip = [IO.Compression.ZipFile]::OpenRead($ArchivePath)
  243. try {
  244. foreach ($entry in $zip.Entries) {
  245. $entryName = $entry.FullName.Replace('/', '\')
  246. if ([IO.Path]::IsPathRooted($entryName) -or $entryName -match '(^|\\)\.\.(\\|$)') {
  247. throw "Unsafe archive entry: $($entry.FullName)"
  248. }
  249. $entryDestination = [IO.Path]::GetFullPath((Join-Path $stagingPath $entryName))
  250. if (-not $entryDestination.StartsWith($stagingPath + '\', [StringComparison]::OrdinalIgnoreCase)) {
  251. throw "Archive entry escapes staging directory: $($entry.FullName)"
  252. }
  253. }
  254. } finally {
  255. $zip.Dispose()
  256. }
  257. # Read-only filesystem and IIS inspection completes before any target, registration, site, or pool mutation.
  258. $productionSnapshot = Get-SiteSnapshot -Name $ProductionSiteName
  259. $testSnapshot = Get-SiteSnapshot -Name $TestSiteName
  260. $productionPoolExisted = Test-Path "IIS:\AppPools\$ProductionPoolName"
  261. $testPoolExisted = Test-Path "IIS:\AppPools\$TestPoolName"
  262. $productionPoolState = if ($productionPoolExisted) { [string](Get-WebAppPoolState -Name $ProductionPoolName).Value } else { $null }
  263. $testPoolState = if ($testPoolExisted) { [string](Get-WebAppPoolState -Name $TestPoolName).Value } else { $null }
  264. Assert-SiteTarget -Name $ProductionSiteName -PoolName $ProductionPoolName -PhysicalPath $productionPublicPath -Port $ProductionPort
  265. Assert-SiteTarget -Name $TestSiteName -PoolName $TestPoolName -PhysicalPath $testPublicPath -Port $TestPort
  266. Assert-PoolTarget -PoolName $ProductionPoolName -AllowedSiteName $ProductionSiteName
  267. Assert-PoolTarget -PoolName $TestPoolName -AllowedSiteName $TestSiteName
  268. $componentRegistrationMode = Get-ComponentRegistrationMode -ExistingProjectPath $projectFullPath -Components $components
  269. $createdProductionSite = -not [bool]$productionSnapshot
  270. $createdTestSite = -not [bool]$testSnapshot
  271. $createdProductionPool = -not $productionPoolExisted
  272. $createdTestPool = -not $testPoolExisted
  273. $targetUpdated = $false
  274. $backupCreated = $false
  275. $installSucceeded = $false
  276. try {
  277. if (-not $projectParentExisted) {
  278. New-Item -ItemType Directory -Path $projectParent | Out-Null
  279. }
  280. Expand-Archive -LiteralPath $ArchivePath -DestinationPath $stagingPath
  281. $requiredRelativePaths = @(
  282. 'public\Default.asp',
  283. 'test-app\public\Default.asp',
  284. 'Framework\ViewRenderer.wsc',
  285. 'tools\Register-Components.ps1',
  286. 'tools\Setup-Site.ps1',
  287. 'tests\Invoke-SelfTest.ps1',
  288. 'tests\Test-Http.ps1'
  289. )
  290. foreach ($relativePath in $requiredRelativePaths) {
  291. if (-not (Test-Path -LiteralPath (Join-Path $stagingPath $relativePath))) {
  292. throw "Package is missing required path: $relativePath"
  293. }
  294. }
  295. Get-ChildItem -LiteralPath $stagingPath -Recurse -Force | ForEach-Object {
  296. $resolved = [IO.Path]::GetFullPath($_.FullName)
  297. if (-not $resolved.StartsWith($stagingPath + '\', [StringComparison]::OrdinalIgnoreCase)) {
  298. throw "Archive entry escaped staging directory: $resolved"
  299. }
  300. if (($_.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) {
  301. throw "Archive contains a reparse point: $resolved"
  302. }
  303. }
  304. # Stop only this deployment's two sites and dedicated pools before copying
  305. # the release. The live root itself can remain open on IIS hosts even after
  306. # its dedicated pools stop, so keep the path stable and mirror a verified
  307. # backup/release tree instead of relying on a root-directory rename.
  308. foreach ($siteSnapshot in @($productionSnapshot, $testSnapshot)) {
  309. if ($siteSnapshot -and $siteSnapshot.State -eq 'Started') {
  310. Stop-Website -Name $siteSnapshot.Name
  311. }
  312. }
  313. foreach ($poolName in @($ProductionPoolName, $TestPoolName)) {
  314. if (Test-Path "IIS:\AppPools\$poolName") {
  315. $poolStateNow = [string](Get-WebAppPoolState -Name $poolName).Value
  316. if ($poolStateNow -eq 'Started') { Stop-WebAppPool -Name $poolName }
  317. Wait-WebAppPoolState -Name $poolName -DesiredState 'Stopped'
  318. }
  319. }
  320. if ($targetExisted) {
  321. Sync-DirectoryTree -Source $projectFullPath -Destination $backupPath
  322. $backupCreated = $true
  323. }
  324. Sync-DirectoryTree -Source $stagingPath -Destination $projectFullPath
  325. $targetUpdated = $true
  326. Remove-Item -LiteralPath $stagingPath -Recurse -Force
  327. & (Join-Path $projectFullPath 'tools\Register-Components.ps1') -ProjectRoot $projectFullPath
  328. & (Join-Path $projectFullPath 'tools\Setup-Site.ps1') -SiteName $ProductionSiteName -PoolName $ProductionPoolName -PhysicalPath $productionPublicPath -Port $ProductionPort
  329. & (Join-Path $projectFullPath 'tools\Setup-Site.ps1') -SiteName $TestSiteName -PoolName $TestPoolName -PhysicalPath $testPublicPath -Port $TestPort
  330. if ($RunTests) {
  331. & cscript.exe //nologo (Join-Path $projectFullPath 'tests\Test-Components.vbs')
  332. if ($LASTEXITCODE -ne 0) { throw "Test-Components.vbs failed with exit code $LASTEXITCODE" }
  333. $httpTestCommand = "& $(Quote-PowerShellLiteral (Join-Path $projectFullPath 'tests\Test-Http.ps1')) -BaseUrl $(Quote-PowerShellLiteral $ProductionBaseUrl) -TestBaseUrl $(Quote-PowerShellLiteral $TestBaseUrl)"
  334. $httpTestExitCode = Invoke-WindowsPowerShell -Command $httpTestCommand
  335. if ($httpTestExitCode -ne 0) { throw "Test-Http.ps1 failed with exit code $httpTestExitCode" }
  336. $apiTestCommand = "& $(Quote-PowerShellLiteral (Join-Path $projectFullPath 'tests\Invoke-SelfTest.ps1')) -BaseUrl $(Quote-PowerShellLiteral $TestBaseUrl)"
  337. $apiTestExitCode = Invoke-WindowsPowerShell -Command $apiTestCommand
  338. if ($apiTestExitCode -ne 0) { throw "Invoke-SelfTest.ps1 failed with exit code $apiTestExitCode" }
  339. }
  340. $installSucceeded = $true
  341. Write-Output "Deployment succeeded: $projectFullPath"
  342. if ($backupCreated) {
  343. Write-Output "Timestamped rollback retained at: $backupPath"
  344. } else {
  345. Write-Output 'No previous project tree existed; no rollback directory was created.'
  346. }
  347. } catch {
  348. $failure = $_
  349. $rollbackErrors = New-Object System.Collections.Generic.List[string]
  350. Write-Warning "Deployment failed; rolling back only changes owned by invocation $InvocationId."
  351. try {
  352. if ($targetUpdated -and (Test-Path -LiteralPath (Join-Path $projectFullPath 'tools\Unregister-Components.ps1'))) {
  353. & (Join-Path $projectFullPath 'tools\Unregister-Components.ps1') -ProjectRoot $projectFullPath
  354. }
  355. } catch { $rollbackErrors.Add("Unregister new components: $($_.Exception.Message)") }
  356. try {
  357. if ($createdProductionSite -and (Test-Path "IIS:\Sites\$ProductionSiteName")) { Remove-Website -Name $ProductionSiteName }
  358. } catch { $rollbackErrors.Add("Remove production site: $($_.Exception.Message)") }
  359. try {
  360. if ($createdTestSite -and (Test-Path "IIS:\Sites\$TestSiteName")) { Remove-Website -Name $TestSiteName }
  361. } catch { $rollbackErrors.Add("Remove test site: $($_.Exception.Message)") }
  362. try {
  363. if ($createdProductionPool -and (Test-Path "IIS:\AppPools\$ProductionPoolName")) { Remove-WebAppPool -Name $ProductionPoolName }
  364. } catch { $rollbackErrors.Add("Remove production pool: $($_.Exception.Message)") }
  365. try {
  366. if ($createdTestPool -and (Test-Path "IIS:\AppPools\$TestPoolName")) { Remove-WebAppPool -Name $TestPoolName }
  367. } catch { $rollbackErrors.Add("Remove test pool: $($_.Exception.Message)") }
  368. try {
  369. if ($targetUpdated -and (Test-Path -LiteralPath $projectFullPath)) {
  370. Sync-DirectoryTree -Source $projectFullPath -Destination $failedPath
  371. }
  372. } catch { $rollbackErrors.Add("Retain failed release: $($_.Exception.Message)") }
  373. try {
  374. if ($backupCreated -and (Test-Path -LiteralPath $backupPath)) {
  375. Sync-DirectoryTree -Source $backupPath -Destination $projectFullPath
  376. } elseif ($targetUpdated -and -not $targetExisted -and (Test-Path -LiteralPath $projectFullPath)) {
  377. Remove-Item -LiteralPath $projectFullPath -Recurse -Force
  378. }
  379. } catch { $rollbackErrors.Add("Restore previous project tree: $($_.Exception.Message)") }
  380. try {
  381. if ($backupCreated -and $componentRegistrationMode -eq 'Owned' -and
  382. (Test-Path -LiteralPath (Join-Path $projectFullPath 'tools\Register-Components.ps1'))) {
  383. & (Join-Path $projectFullPath 'tools\Register-Components.ps1') -ProjectRoot $projectFullPath
  384. }
  385. } catch { $rollbackErrors.Add("Restore previous component registrations: $($_.Exception.Message)") }
  386. try { Restore-PoolState -Name $ProductionPoolName -State $productionPoolState } catch { $rollbackErrors.Add("Restore production pool state: $($_.Exception.Message)") }
  387. try { Restore-PoolState -Name $TestPoolName -State $testPoolState } catch { $rollbackErrors.Add("Restore test pool state: $($_.Exception.Message)") }
  388. try { Restore-SiteState -Snapshot $productionSnapshot } catch { $rollbackErrors.Add("Restore production site state: $($_.Exception.Message)") }
  389. try { Restore-SiteState -Snapshot $testSnapshot } catch { $rollbackErrors.Add("Restore test site state: $($_.Exception.Message)") }
  390. if ($rollbackErrors.Count -gt 0) {
  391. throw "Deployment failed: $($failure.Exception.Message) Rollback also reported: $($rollbackErrors -join ' | ')"
  392. }
  393. throw $failure
  394. } finally {
  395. if (-not $installSucceeded -and (Test-Path -LiteralPath $stagingPath)) {
  396. # Staging was created by this invocation and never became the live target.
  397. Remove-Item -LiteralPath $stagingPath -Recurse -Force
  398. }
  399. if (-not $installSucceeded -and -not $projectParentExisted -and (Test-Path -LiteralPath $projectParent)) {
  400. $remaining = @(Get-ChildItem -LiteralPath $projectParent -Force)
  401. if ($remaining.Count -eq 0) { Remove-Item -LiteralPath $projectParent -Force }
  402. }
  403. if (Test-Path -LiteralPath $ArchivePath) {
  404. Remove-Item -LiteralPath $ArchivePath -Force
  405. }
  406. $selfPath = $MyInvocation.MyCommand.Path
  407. if ($selfPath -and $selfPath -like "$env:windir\Temp\*") {
  408. Remove-Item -LiteralPath $selfPath -Force -ErrorAction SilentlyContinue
  409. }
  410. }

Powered by TurnKey Linux.