You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

473 line
24KB

  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. # An upgrade may introduce a component that did not exist in the currently
  136. # deployed tree. Accept only that additive case: every existing registration
  137. # was proven project-owned above, and every absent registration must also
  138. # have no corresponding component file in the old release. An absent
  139. # registration for an old file remains a suspicious partial state.
  140. foreach ($component in $Components) {
  141. $progIdKey = "HKLM:\SOFTWARE\Classes\$($component.ProgId)"
  142. $clsidKey = "HKLM:\SOFTWARE\Classes\CLSID\$($component.ClassId)"
  143. if (-not (Test-Path -LiteralPath $progIdKey) -and -not (Test-Path -LiteralPath $clsidKey)) {
  144. $oldComponentPath = Join-Path $ExistingProjectPath $component.RelativePath
  145. if (Test-Path -LiteralPath $oldComponentPath) {
  146. throw "Component registration for existing file '$($component.RelativePath)' is absent. Refusing to repair a partial registration state."
  147. }
  148. }
  149. }
  150. return 'Owned'
  151. }
  152. function Restore-SiteState {
  153. param($Snapshot)
  154. if (-not $Snapshot -or -not (Test-Path "IIS:\Sites\$($Snapshot.Name)")) { return }
  155. $parentPathsValue = if ($Snapshot.EnableParentPaths) { 'True' } else { 'False' }
  156. $appcmd = "$env:windir\system32\inetsrv\appcmd.exe"
  157. & $appcmd set config $Snapshot.Name -section:system.webServer/asp "/enableParentPaths:$parentPathsValue" /commit:apphost | Out-Null
  158. if ($LASTEXITCODE -ne 0) { throw "Unable to restore enableParentPaths for site '$($Snapshot.Name)'." }
  159. if ($Snapshot.State -eq 'Started') {
  160. Start-Website -Name $Snapshot.Name -ErrorAction SilentlyContinue
  161. } else {
  162. Stop-Website -Name $Snapshot.Name -ErrorAction SilentlyContinue
  163. }
  164. }
  165. function Wait-WebAppPoolState {
  166. param(
  167. [Parameter(Mandatory = $true)][string]$Name,
  168. [Parameter(Mandatory = $true)][string]$DesiredState,
  169. [int]$TimeoutSeconds = 30
  170. )
  171. $deadline = (Get-Date).AddSeconds($TimeoutSeconds)
  172. do {
  173. $current = [string](Get-WebAppPoolState -Name $Name).Value
  174. if ($current -eq $DesiredState) { return }
  175. Start-Sleep -Milliseconds 250
  176. } while ((Get-Date) -lt $deadline)
  177. throw "App pool '$Name' did not reach state '$DesiredState' within $TimeoutSeconds seconds (current: $current)."
  178. }
  179. function Sync-DirectoryTree {
  180. param(
  181. [Parameter(Mandatory = $true)][string]$Source,
  182. [Parameter(Mandatory = $true)][string]$Destination
  183. )
  184. if (-not (Test-Path -LiteralPath $Source -PathType Container)) {
  185. throw "Directory sync source does not exist: $Source"
  186. }
  187. & robocopy.exe $Source $Destination /MIR /COPY:DAT /DCOPY:DAT /R:2 /W:1 /NFL /NDL /NJH /NJS /NP
  188. $exitCode = $LASTEXITCODE
  189. if ($exitCode -ge 8) {
  190. throw "robocopy failed while mirroring '$Source' to '$Destination' (exit code $exitCode)."
  191. }
  192. }
  193. function Restore-PoolState {
  194. param([string]$Name, [string]$State)
  195. if (-not $State -or -not (Test-Path "IIS:\AppPools\$Name")) { return }
  196. $current = [string](Get-WebAppPoolState -Name $Name).Value
  197. if ($State -eq 'Started') {
  198. if ($current -eq 'Stopping') {
  199. Wait-WebAppPoolState -Name $Name -DesiredState 'Stopped'
  200. $current = 'Stopped'
  201. }
  202. if ($current -ne 'Started') { Start-WebAppPool -Name $Name }
  203. Wait-WebAppPoolState -Name $Name -DesiredState 'Started'
  204. } else {
  205. if ($current -eq 'Starting') {
  206. Wait-WebAppPoolState -Name $Name -DesiredState 'Started'
  207. $current = 'Started'
  208. }
  209. if ($current -ne 'Stopped') { Stop-WebAppPool -Name $Name }
  210. Wait-WebAppPoolState -Name $Name -DesiredState 'Stopped'
  211. }
  212. }
  213. Assert-Administrator
  214. Import-Module WebAdministration
  215. if (-not (Test-Path -LiteralPath $ArchivePath -PathType Leaf)) { throw "Deployment archive not found: $ArchivePath" }
  216. $actualArchiveSha256 = (Get-FileHash -LiteralPath $ArchivePath -Algorithm SHA256).Hash
  217. if ($actualArchiveSha256 -ne $ExpectedArchiveSha256) {
  218. throw "Deployment archive SHA-256 mismatch (expected $ExpectedArchiveSha256, got $actualArchiveSha256)."
  219. }
  220. Write-Output "Verified package SHA-256: $($actualArchiveSha256.ToLowerInvariant())"
  221. if (-not [IO.Path]::IsPathRooted($ProjectPath)) { throw 'ProjectPath must be an absolute Windows path.' }
  222. if ($ProductionSiteName -eq $TestSiteName -or $ProductionPoolName -eq $TestPoolName) { throw 'Production and test IIS names must differ.' }
  223. if ($ProductionPort -eq $TestPort) { throw 'Production and test ports must differ.' }
  224. $projectFullPath = [IO.Path]::GetFullPath($ProjectPath).TrimEnd('\')
  225. $projectParent = Split-Path -Parent $projectFullPath
  226. if (-not $projectParent -or $projectFullPath -eq [IO.Path]::GetPathRoot($projectFullPath).TrimEnd('\')) {
  227. throw "Unsafe ProjectPath: $ProjectPath"
  228. }
  229. $projectParentExisted = Test-Path -LiteralPath $projectParent -PathType Container
  230. $targetExisted = Test-Path -LiteralPath $projectFullPath
  231. if ($targetExisted) {
  232. $targetItem = Get-Item -LiteralPath $projectFullPath -Force
  233. if (-not $targetItem.PSIsContainer) { throw "ProjectPath exists but is not a directory: $projectFullPath" }
  234. if (($targetItem.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { throw "ProjectPath may not be a reparse point: $projectFullPath" }
  235. if (-not (Test-Path -LiteralPath (Join-Path $projectFullPath 'tools\Register-Components.ps1'))) {
  236. throw 'Existing ProjectPath is not a rollback-capable WSC-MVC deployment.'
  237. }
  238. }
  239. $stagingPath = "$projectFullPath.staging.$InvocationId"
  240. $backupPath = "$projectFullPath.rollback.$InvocationId"
  241. $failedPath = "$projectFullPath.failed.$InvocationId"
  242. foreach ($ownedPath in @($stagingPath, $backupPath, $failedPath)) {
  243. if (Test-Path -LiteralPath $ownedPath) { throw "Invocation-owned path already exists: $ownedPath" }
  244. }
  245. $productionPublicPath = Join-Path $projectFullPath 'public'
  246. $testPublicPath = Join-Path $projectFullPath 'test-app\public'
  247. $components = @(
  248. [PSCustomObject]@{ RelativePath = 'Framework\RequestContext.wsc'; ProgId = 'WscMvc.RequestContext'; ClassId = '{1C36FA55-34DF-4974-94B9-D657389362B2}' },
  249. [PSCustomObject]@{ RelativePath = 'Framework\Router.wsc'; ProgId = 'WscMvc.Router'; ClassId = '{C92F9338-B478-4EAD-B865-892FFB1E1C51}' },
  250. [PSCustomObject]@{ RelativePath = 'Framework\ViewRenderer.wsc'; ProgId = 'WscMvc.ViewRenderer'; ClassId = '{4948DF84-5DC6-448A-9F1B-EB596C28842B}' },
  251. [PSCustomObject]@{ RelativePath = 'Framework\Database.wsc'; ProgId = 'WscMvc.Database'; ClassId = '{E3F7C1A2-9B4D-4E6F-8C3A-2D5B7A9E1F04}' },
  252. [PSCustomObject]@{ RelativePath = 'Framework\Application.wsc'; ProgId = 'WscMvc.Application'; ClassId = '{851C7763-1638-42FE-A166-BF3DD3A96A88}' },
  253. [PSCustomObject]@{ RelativePath = 'Controllers\HomeController.wsc'; ProgId = 'WscMvc.HomeController'; ClassId = '{87488446-60BE-4068-8368-0B709BB68F3F}' },
  254. [PSCustomObject]@{ RelativePath = 'test-app\Controllers\SelfTestController.wsc'; ProgId = 'WscMvc.SelfTestController'; ClassId = '{D2634944-4646-4C55-956E-4C05E7E10904}' }
  255. )
  256. # Validate every archive entry before Expand-Archive can write anything.
  257. Add-Type -AssemblyName System.IO.Compression.FileSystem
  258. $zip = [IO.Compression.ZipFile]::OpenRead($ArchivePath)
  259. try {
  260. foreach ($entry in $zip.Entries) {
  261. $entryName = $entry.FullName.Replace('/', '\')
  262. if ([IO.Path]::IsPathRooted($entryName) -or $entryName -match '(^|\\)\.\.(\\|$)') {
  263. throw "Unsafe archive entry: $($entry.FullName)"
  264. }
  265. $entryDestination = [IO.Path]::GetFullPath((Join-Path $stagingPath $entryName))
  266. if (-not $entryDestination.StartsWith($stagingPath + '\', [StringComparison]::OrdinalIgnoreCase)) {
  267. throw "Archive entry escapes staging directory: $($entry.FullName)"
  268. }
  269. }
  270. } finally {
  271. $zip.Dispose()
  272. }
  273. # Read-only filesystem and IIS inspection completes before any target, registration, site, or pool mutation.
  274. $productionSnapshot = Get-SiteSnapshot -Name $ProductionSiteName
  275. $testSnapshot = Get-SiteSnapshot -Name $TestSiteName
  276. $productionPoolExisted = Test-Path "IIS:\AppPools\$ProductionPoolName"
  277. $testPoolExisted = Test-Path "IIS:\AppPools\$TestPoolName"
  278. $productionPoolState = if ($productionPoolExisted) { [string](Get-WebAppPoolState -Name $ProductionPoolName).Value } else { $null }
  279. $testPoolState = if ($testPoolExisted) { [string](Get-WebAppPoolState -Name $TestPoolName).Value } else { $null }
  280. Assert-SiteTarget -Name $ProductionSiteName -PoolName $ProductionPoolName -PhysicalPath $productionPublicPath -Port $ProductionPort
  281. Assert-SiteTarget -Name $TestSiteName -PoolName $TestPoolName -PhysicalPath $testPublicPath -Port $TestPort
  282. Assert-PoolTarget -PoolName $ProductionPoolName -AllowedSiteName $ProductionSiteName
  283. Assert-PoolTarget -PoolName $TestPoolName -AllowedSiteName $TestSiteName
  284. $componentRegistrationMode = Get-ComponentRegistrationMode -ExistingProjectPath $projectFullPath -Components $components
  285. $createdProductionSite = -not [bool]$productionSnapshot
  286. $createdTestSite = -not [bool]$testSnapshot
  287. $createdProductionPool = -not $productionPoolExisted
  288. $createdTestPool = -not $testPoolExisted
  289. $targetUpdated = $false
  290. $backupCreated = $false
  291. $installSucceeded = $false
  292. try {
  293. if (-not $projectParentExisted) {
  294. New-Item -ItemType Directory -Path $projectParent | Out-Null
  295. }
  296. Expand-Archive -LiteralPath $ArchivePath -DestinationPath $stagingPath
  297. $requiredRelativePaths = @(
  298. 'public\Default.asp',
  299. 'test-app\public\Default.asp',
  300. 'Framework\ViewRenderer.wsc',
  301. 'Framework\Database.wsc',
  302. 'tools\Register-Components.ps1',
  303. 'tools\Setup-Site.ps1',
  304. 'tests\Invoke-SelfTest.ps1',
  305. 'tests\Test-Http.ps1'
  306. )
  307. foreach ($relativePath in $requiredRelativePaths) {
  308. if (-not (Test-Path -LiteralPath (Join-Path $stagingPath $relativePath))) {
  309. throw "Package is missing required path: $relativePath"
  310. }
  311. }
  312. Get-ChildItem -LiteralPath $stagingPath -Recurse -Force | ForEach-Object {
  313. $resolved = [IO.Path]::GetFullPath($_.FullName)
  314. if (-not $resolved.StartsWith($stagingPath + '\', [StringComparison]::OrdinalIgnoreCase)) {
  315. throw "Archive entry escaped staging directory: $resolved"
  316. }
  317. if (($_.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) {
  318. throw "Archive contains a reparse point: $resolved"
  319. }
  320. }
  321. # Stop only this deployment's two sites and dedicated pools before copying
  322. # the release. The live root itself can remain open on IIS hosts even after
  323. # its dedicated pools stop, so keep the path stable and mirror a verified
  324. # backup/release tree instead of relying on a root-directory rename.
  325. foreach ($siteSnapshot in @($productionSnapshot, $testSnapshot)) {
  326. if ($siteSnapshot -and $siteSnapshot.State -eq 'Started') {
  327. Stop-Website -Name $siteSnapshot.Name
  328. }
  329. }
  330. foreach ($poolName in @($ProductionPoolName, $TestPoolName)) {
  331. if (Test-Path "IIS:\AppPools\$poolName") {
  332. $poolStateNow = [string](Get-WebAppPoolState -Name $poolName).Value
  333. if ($poolStateNow -eq 'Started') { Stop-WebAppPool -Name $poolName }
  334. Wait-WebAppPoolState -Name $poolName -DesiredState 'Stopped'
  335. }
  336. }
  337. if ($targetExisted) {
  338. Sync-DirectoryTree -Source $projectFullPath -Destination $backupPath
  339. $backupCreated = $true
  340. }
  341. Sync-DirectoryTree -Source $stagingPath -Destination $projectFullPath
  342. $targetUpdated = $true
  343. Remove-Item -LiteralPath $stagingPath -Recurse -Force
  344. & (Join-Path $projectFullPath 'tools\Register-Components.ps1') -ProjectRoot $projectFullPath
  345. & (Join-Path $projectFullPath 'tools\Setup-Site.ps1') -SiteName $ProductionSiteName -PoolName $ProductionPoolName -PhysicalPath $productionPublicPath -Port $ProductionPort
  346. & (Join-Path $projectFullPath 'tools\Setup-Site.ps1') -SiteName $TestSiteName -PoolName $TestPoolName -PhysicalPath $testPublicPath -Port $TestPort
  347. if ($RunTests) {
  348. & cscript.exe //nologo (Join-Path $projectFullPath 'tests\Test-Components.vbs')
  349. if ($LASTEXITCODE -ne 0) { throw "Test-Components.vbs failed with exit code $LASTEXITCODE" }
  350. $httpTestCommand = "& $(Quote-PowerShellLiteral (Join-Path $projectFullPath 'tests\Test-Http.ps1')) -BaseUrl $(Quote-PowerShellLiteral $ProductionBaseUrl) -TestBaseUrl $(Quote-PowerShellLiteral $TestBaseUrl)"
  351. $httpTestExitCode = Invoke-WindowsPowerShell -Command $httpTestCommand
  352. if ($httpTestExitCode -ne 0) { throw "Test-Http.ps1 failed with exit code $httpTestExitCode" }
  353. $apiTestCommand = "& $(Quote-PowerShellLiteral (Join-Path $projectFullPath 'tests\Invoke-SelfTest.ps1')) -BaseUrl $(Quote-PowerShellLiteral $TestBaseUrl)"
  354. $apiTestExitCode = Invoke-WindowsPowerShell -Command $apiTestCommand
  355. if ($apiTestExitCode -ne 0) { throw "Invoke-SelfTest.ps1 failed with exit code $apiTestExitCode" }
  356. }
  357. $installSucceeded = $true
  358. Write-Output "Deployment succeeded: $projectFullPath"
  359. if ($backupCreated) {
  360. Write-Output "Timestamped rollback retained at: $backupPath"
  361. } else {
  362. Write-Output 'No previous project tree existed; no rollback directory was created.'
  363. }
  364. } catch {
  365. $failure = $_
  366. $rollbackErrors = New-Object System.Collections.Generic.List[string]
  367. Write-Warning "Deployment failed; rolling back only changes owned by invocation $InvocationId."
  368. try {
  369. if ($targetUpdated -and (Test-Path -LiteralPath (Join-Path $projectFullPath 'tools\Unregister-Components.ps1'))) {
  370. & (Join-Path $projectFullPath 'tools\Unregister-Components.ps1') -ProjectRoot $projectFullPath
  371. }
  372. } catch { $rollbackErrors.Add("Unregister new components: $($_.Exception.Message)") }
  373. try {
  374. if ($createdProductionSite -and (Test-Path "IIS:\Sites\$ProductionSiteName")) { Remove-Website -Name $ProductionSiteName }
  375. } catch { $rollbackErrors.Add("Remove production site: $($_.Exception.Message)") }
  376. try {
  377. if ($createdTestSite -and (Test-Path "IIS:\Sites\$TestSiteName")) { Remove-Website -Name $TestSiteName }
  378. } catch { $rollbackErrors.Add("Remove test site: $($_.Exception.Message)") }
  379. try {
  380. if ($createdProductionPool -and (Test-Path "IIS:\AppPools\$ProductionPoolName")) { Remove-WebAppPool -Name $ProductionPoolName }
  381. } catch { $rollbackErrors.Add("Remove production pool: $($_.Exception.Message)") }
  382. try {
  383. if ($createdTestPool -and (Test-Path "IIS:\AppPools\$TestPoolName")) { Remove-WebAppPool -Name $TestPoolName }
  384. } catch { $rollbackErrors.Add("Remove test pool: $($_.Exception.Message)") }
  385. try {
  386. if ($targetUpdated -and (Test-Path -LiteralPath $projectFullPath)) {
  387. Sync-DirectoryTree -Source $projectFullPath -Destination $failedPath
  388. }
  389. } catch { $rollbackErrors.Add("Retain failed release: $($_.Exception.Message)") }
  390. try {
  391. if ($backupCreated -and (Test-Path -LiteralPath $backupPath)) {
  392. Sync-DirectoryTree -Source $backupPath -Destination $projectFullPath
  393. } elseif ($targetUpdated -and -not $targetExisted -and (Test-Path -LiteralPath $projectFullPath)) {
  394. Remove-Item -LiteralPath $projectFullPath -Recurse -Force
  395. }
  396. } catch { $rollbackErrors.Add("Restore previous project tree: $($_.Exception.Message)") }
  397. try {
  398. if ($backupCreated -and $componentRegistrationMode -eq 'Owned' -and
  399. (Test-Path -LiteralPath (Join-Path $projectFullPath 'tools\Register-Components.ps1'))) {
  400. & (Join-Path $projectFullPath 'tools\Register-Components.ps1') -ProjectRoot $projectFullPath
  401. }
  402. } catch { $rollbackErrors.Add("Restore previous component registrations: $($_.Exception.Message)") }
  403. try { Restore-PoolState -Name $ProductionPoolName -State $productionPoolState } catch { $rollbackErrors.Add("Restore production pool state: $($_.Exception.Message)") }
  404. try { Restore-PoolState -Name $TestPoolName -State $testPoolState } catch { $rollbackErrors.Add("Restore test pool state: $($_.Exception.Message)") }
  405. try { Restore-SiteState -Snapshot $productionSnapshot } catch { $rollbackErrors.Add("Restore production site state: $($_.Exception.Message)") }
  406. try { Restore-SiteState -Snapshot $testSnapshot } catch { $rollbackErrors.Add("Restore test site state: $($_.Exception.Message)") }
  407. if ($rollbackErrors.Count -gt 0) {
  408. throw "Deployment failed: $($failure.Exception.Message) Rollback also reported: $($rollbackErrors -join ' | ')"
  409. }
  410. throw $failure
  411. } finally {
  412. if (-not $installSucceeded -and (Test-Path -LiteralPath $stagingPath)) {
  413. # Staging was created by this invocation and never became the live target.
  414. Remove-Item -LiteralPath $stagingPath -Recurse -Force
  415. }
  416. if (-not $installSucceeded -and -not $projectParentExisted -and (Test-Path -LiteralPath $projectParent)) {
  417. $remaining = @(Get-ChildItem -LiteralPath $projectParent -Force)
  418. if ($remaining.Count -eq 0) { Remove-Item -LiteralPath $projectParent -Force }
  419. }
  420. if (Test-Path -LiteralPath $ArchivePath) {
  421. Remove-Item -LiteralPath $ArchivePath -Force
  422. }
  423. $selfPath = $MyInvocation.MyCommand.Path
  424. if ($selfPath -and $selfPath -like "$env:windir\Temp\*") {
  425. Remove-Item -LiteralPath $selfPath -Force -ErrorAction SilentlyContinue
  426. }
  427. }

Powered by TurnKey Linux.