asp.net core 不停机更新(powershell 脚本)

asp.net core 不停机更新
c# 不停机更新

参考资料

Windows 上的应用程序在运行期间可以给自己改名(可以做 OTA 自我更新)

不止是 exe 文件,dll 文件也是可以改名的

实际上,不止是 exe 文件,在 exe 程序运行期间,即使用到了某些 dll 文件,这些 dll 文件也是可以改名的。

当然,一个 exe 的运行不一定在启动期间就加载好了所有的 dll,所以如果你在 exe 启动之后,某个 dll 加载之前改了那个 dll 的名称,那么会出现找不到 dll 的情况,可能导致程序崩溃。

为什么 Windows 上的可执行程序可以在运行期间改名?

Windows 的文件系统由两个主要的表示结构:一个是目录信息,它保存有关文件的元数据(如文件名、大小、属性和时间戳);第二个是文件的数据链。

当运行程序加载一个程序集的时候,会为此程序集创建一个内存映射文件。为了优化性能,往往只有实际用到的部分才会被加入到内存映射文件中;当需要用到程序集文件中的某块数据时,Windows 操作系统就会将需要的部分加载到内存中。但是,内存映射文件只会锁定文件的数据部分,以保证文件文件的数据不会被其他的进程修改。

这里就是关键,内存映射文件只会锁定文件的数据部分,而不会锁住文件元数据信息。这意味着你可以随意修改这些元数据信息而不会影响程序的正常运行。这就包括你可以修改文件名,或者把程序从一个文件夹下移动到另一个文件夹去。

但是跨驱动器移动文件,就意味着需要在原来的驱动器下删除文件,而这个操作会影响到文件的数据部分,所以此操作不被允许。

思路

写入 lock 文件
检查更新服务器, 如果服务器有更新, 下载更新文件到本地。
重命名本地待更新 exe dll 为 .history.dll
更新本地 exe dll
更新本地端口 + 1 (最后1位 0 - 9 循环)
启动新进程
更新nginx配置, hot reload
sleep 30秒
删除 .hsitory.dll
删除 lock 文件

update.ps1

核心更新脚本

function Get-Relative
{
    Param(
        [string] $from,
        [string] $to
    )
	
	$toUri = New-Object -TypeName System.Uri -ArgumentList $to
	$fromUri = New-Object -TypeName System.Uri -ArgumentList $from
	return $toUri.MakeRelative($fromUri).Replace("/","\")
}

function Get-FileVersion
{
    Param(
        [string] $DdpstorePath
    )
    if(Test-Path $DdpstorePath)
    {
        $DdpStoreFileVersionObj = [System.Diagnostics.FileVersionInfo]::GetVersionInfo($DdpstorePath)
        if($DdpStoreFileVersionObj)
        {
            $DdpStoreFileVersion = "{0}.{1}.{2}.{3}" -f $DdpStoreFileVersionObj.FileMajorPart,$DdpStoreFileVersionObj.FileMinorPart,$DdpStoreFileVersionObj.FileBuildPart,$DdpStoreFileVersionObj.FilePrivatePart
            return [version]$DdpStoreFileVersion
        }else{
            Write-Error "invalid file version: $DdpstorePath"
        }
    }
    else{
        Write-Error "invalid file path: $DdpstorePath"
    }
}

function Get-AppUpdateInfo
{
    Param(
        [Parameter(Mandatory,Position=0)]
        [string] 
        $url,

        [Parameter(Mandatory,Position=1)]
        [string] 
        $appLocalPath
    )

    $file = Invoke-RestMethod -Uri $url
    if($null -ne $file.item.version)
    {
        $lastVersion = [Version]$file.item.version
        $curVersion = Get-FileVersion $appLocalPath
        return $file.item | Select-Object -Property version, url, @{name='hasnew'; expression={$lastVersion -gt $curVersion}},@{name='curVersion'; expression={$curVersion}}   # version   url
    }
    else{
        Write-Error "invalid app info url: $url"
    }
}

# 写入 lock 文件
# 检查更新服务器, 如果服务器有更新, 下载更新文件到本地。
# 重命名本地待更新 exe dll 为  .history.dll 
# 更新本地 exe dll
# 更新本地端口 + 1 (最后1位 0 - 9 循环)
# 启动新进程
# 更新nginx配置, hot reload
# sleep 30秒
# 删除 .hsitory.dll
# 删除 lock 文件

# [environment]::SetEnvironmentvariable("ERP_APP_HOME", "D:\ccproxy.erp", "Machine")
# [environment]::GetEnvironmentvariable("ERP_APP_HOME", "Machine")
# [environment]::GetEnvironmentvariable("Path", "User")

$updateXmlUrl = 'http://172.17.1.244:9008/IAutoUpdaterNETService/GetNewApp?configfile=ccproxy.Erp.xml'
$basePath = [environment]::GetEnvironmentvariable("ERP_APP_HOME", "Machine")
$appPath = "$basePath\netcoreapp3.1"
$appExe = "$appPath\ccproxy.Erp.Web.Host.exe"
$appConfPath = "$appPath\appsettings.json"
$nginxPath = [System.Environment]::GetEnvironmentVariable('NGINX_HOME', 'Machine')
$nginxExe = "$nginxPath\nginx.exe"
$nginxConfPath = "$nginxPath\conf\nginx.conf"

# 写入并发锁
if(Test-Path "$basePath\.lock" -NewerThan (Get-Date).AddMinutes(-5))
{
	Write-Warning -Message '检测到 .lock 文件, 已经有更新程序运行中.'
	exit 1
}
if(Test-Path "$basePath\.lock")
{
	Remove-Item -Path "$basePath\.lock" | Out-Null	
}
New-Item -Path "$basePath\.lock" -ItemType File | Out-Null


# 下载更新文件
$updateInfo = Get-AppUpdateInfo $updateXmlUrl $appExe 
if($updateInfo.hasnew)
{
	Write-Output  "$(Split-Path -Path $appExe -Leaf) upgrade success cur: $($updateInfo.version) local: $($updateInfo.curVersion)"
}else{
	Write-Output  "$(Split-Path -Path $appExe -Leaf) already new cur: $($updateInfo.version) local: $($updateInfo.curVersion)"
	Remove-Item -Path "$basePath\.lock" | Out-Null
	exit 0
}

if($updateInfo.url -match 'filename=([\w\-\.]+.zip)$')
{
	$outfilePath = "$basePath\$($Matches[1])"
	if((Test-Path $outfilePath) -eq $false)
	{
		Invoke-WebRequest -Uri $updateInfo.url -OutFile $outfilePath
	}
	if(Test-Path -Path "$basePath\temp")
	{
		Remove-Item -Path "$basePath\temp" -Force -Recurse
	}
	$outfileExpandPath = "$basePath\temp\$((Get-Item $outfilePath).BaseName)"
	Expand-Archive -LiteralPath $outfilePath -DestinationPath $outfileExpandPath -Force
# 更新程序
	Write-Host "start update $appPath"
	Get-ChildItem $outfileExpandPath -File -Recurse | foreach {
		$relativePath = (Get-Relative -from $_.FullName -to "$outfileExpandPath\")
		$destPath = Join-Path $appPath $relativePath
		if(Test-Path -Path $destPath)
		{
			if((Get-FileHash $_.FullName).hash -ne (Get-FileHash $destPath).hash)
			{
				if($_.FullName -match '(.exe|.dll)$')
				{
					Move-Item -Path $destPath -Destination "$destPath._deleted" -Force
					Copy-Item -Path $_.FullName -Destination $destPath
					Write-Host "src update, copy to: $relativePath"
				}else{
					Copy-Item -Path $_.FullName -Destination $destPath -Force
					Write-Host "src update, copy to: $relativePath"
				}
			}
		}else{
			Copy-Item -Path $_.FullName -Destination $destPath 
			Write-Host "src add, copy to: $relativePath"
		}

	}
	# Compare-Object -ReferenceObject $upgrade_items -DifferenceObject $app_items
	
}

# 更新APP端口
$newPort = ''
(Get-Content -Path $appConfPath) |
	ForEach-Object {if($_ -match '"Url": "http://localhost:(\d+)/"'){
		$lastNu = [int]"$($Matches[1][-1])" + 1
		if($lastNu -ge 10)
		{
			$lastNu = 0
		}
		$newPort = $Matches[1].SubString(0,$Matches[1].Length - 1) + $lastNu
		$_ -Replace '"Url": "http://localhost:(\d+)/"', "`"Url`": `"http://localhost:$newPort/`""
	}else{$_}} |
            Set-Content -Path $appConfPath

# 启动APP
$oldProcess = Get-Process | Where-Object {$_.Name -like "*$((Get-Item $appExe).BaseName)*"}			
Start-Process $appExe -WindowStyle Hidden
Write-Host "current local port: $newPort"

# 更新Nginx端口
(Get-Content -Path $nginxConfPath) |
	ForEach-Object {if($_ -match 'server 127.0.0.1:\d+;'){
		$_ -Replace 'server 127.0.0.1:\d+;', "server 127.0.0.1:$newPort;"
	}else{$_}}  |
            Set-Content -Path $nginxConfPath
# Nginx reload
Invoke-Expression "& `"$nginxExe`" -s reload -p $nginxPath"

# 关闭旧App
Write-Host "wait second 10"
Sleep 10	
$oldProcess | Stop-Process -Force 
$oldProcess | Wait-Process

Sleep 3
Get-ChildItem -Path "$appPath\*._deleted" -Recurse | Remove-Item -Force

Remove-Item -Path "$basePath\.lock" | Out-Null
Write-Host "success"
exit 0

deploy.ps1

winrm 更新远程服务器


# [environment]::SetEnvironmentvariable("ERP_DB_USERNAME", 'u', "User")
# [environment]::SetEnvironmentvariable("ERP_DB_PASSWORD", 'p', "User")
# [environment]::SetEnvironmentvariable("ERP_DB_HOST", '127.0.0.1', "User")
# [environment]::GetEnvironmentvariable("Path", "User")
# [environment]::GetEnvironmentvariable("Path", "Machine")

$baseDir = (Split-Path -Parent $MyInvocation.MyCommand.Definition)
$remotehost = [environment]::GetEnvironmentvariable("ERP_DB_HOST", "User")
$scriptPath = "$baseDir\update.ps1"

# Enable-PSRemoting
Test-WSMan $remotehost
# set-item WSMan:\localhost\Client\TrustedHosts -Value $remotehost -Force  # 需要管理员权限

$Username = [environment]::GetEnvironmentvariable("ERP_DB_USERNAME", "User")
$Password = [environment]::GetEnvironmentvariable("ERP_DB_PASSWORD", "User")
$pass = ConvertTo-SecureString -AsPlainText $Password -Force
$Cred = New-Object System.Management.Automation.PSCredential -ArgumentList $Username,$pass
# Invoke-Command -ComputerName $remotehost -ScriptBlock { Get-Service WinRM } -credential $Cred
Invoke-Command -ComputerName $remotehost -FilePath $scriptPath  -credential $Cred
posted @ 2022-08-07 17:35  colin_xia  阅读(547)  评论(0)    收藏  举报