using CookComputing.XmlRpc; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using System; using System.IO; using System.IO.Compression; using System.Net; using System.Net.Http; using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Xml; namespace IoTDameon { public class UpdateIoTNodeService : BackgroundService { private readonly ILogger _logger; private readonly IWebHostEnvironment _env; private readonly IHttpClientFactory _httpClientFactory; private readonly IHubContext _hub; public bool IsUpdating { get; set; } public UpdateIoTNodeService(ILogger logger, IWebHostEnvironment env, IHttpClientFactory httpClientFactory, IHubContext hub) { this._logger = logger; this._env = env; this._httpClientFactory = httpClientFactory; this._hub = hub; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { this.Log($"update service start"); stoppingToken.Register(() => this.Log("update service stop")); while (!stoppingToken.IsCancellationRequested) { this.Update(); await Task.Delay(10 * 60 * 1000, stoppingToken); } await Task.CompletedTask; } public void Update() { if (this.IsUpdating) { return; } else { this.IsUpdating = true; } try { UpdateInternal(); } catch (Exception ex) { this.LogError(ex.ToString()); } this.IsUpdating = false; } private void UpdateInternal() { var processName = "iotnode"; var appFolder = "IoTNode"; var port = 8002; var proxyUrl = "http://localhost:9001/RPC2"; var root = Directory.GetParent(_env.ContentRootPath).FullName; var appPath = Path.Combine(root, appFolder); var name = $"{appFolder}.zip"; var file = Path.Combine(root, name); string currentVersion; try { currentVersion = this._httpClientFactory.CreateClient().GetAsync($"http://localhost:{port}/Home/GetVersion").Result.Content.ReadAsStringAsync().Result; this.Log($"节点的当前版本:{currentVersion}"); } catch (Exception ex) { this.LogError("获取当前版本失败"); this.LogError(ex.ToString()); throw ex; } var server = this._httpClientFactory.CreateClient().GetAsync($"http://localhost:{port}/Home/GetServer").Result.Content.ReadAsStringAsync().Result; var serverUrl = $"{server}/{processName}.xml"; this.Log($"request url:{serverUrl}"); string info; try { info = this._httpClientFactory.CreateClient().GetAsync(serverUrl).Result.Content.ReadAsStringAsync().Result; this.Log("最新版本信息:"); this.Log(info); } catch (Exception ex) { this.LogError("获取最新版本失败"); this.LogError(ex.ToString()); throw ex; } var doc = new XmlDocument(); doc.LoadXml(info); var lastVersion = doc.GetElementsByTagName("version")[0].InnerText.Trim(); var lastCheckSum = doc.GetElementsByTagName("checksum")[0].InnerText.Trim(); if (currentVersion != lastVersion) { this.Log($"当前版本 {currentVersion} 不同于最新版本 {lastVersion}"); //查看是否已下载更新并删除旧的更新文件 if (File.Exists(file)) { var currentCheckSum = GetCheckSum(file); if (currentCheckSum != lastCheckSum) { this.Log($"已下载更新hash {currentCheckSum} 不匹配最新更新的hash {lastCheckSum}"); File.Delete(file); this.Log($"删除过时的文件 {file}"); } } //下载更新并校验 if (!File.Exists(file)) { var bytes = this._httpClientFactory.CreateClient().GetAsync($"{server}/{name}").Result.Content.ReadAsByteArrayAsync().Result; using (var fs = File.Create(file)) { fs.Write(bytes); } var currentCheckSum = GetCheckSum(file); if (currentCheckSum != lastCheckSum) { this.Log($"已下载更新hash {currentCheckSum} 不匹配最新更新的hash {lastCheckSum}"); File.Delete(file); this.Log($"删除过时的文件 {file}"); } } if (File.Exists(file)) { this.Log($"开始更新"); //关闭要更新的程序 var proxy = XmlRpcProxyGen.Create(); proxy.Url = proxyUrl; proxy.Credentials = new NetworkCredential("usr", "pwd"); try { proxy.stopProcess(processName); this.Log($"关闭进程 {processName}"); } catch (XmlRpcFaultException ex) { if (ex.FaultCode != 60) { throw ex; } else { this.Log($"关闭失败,进程 {processName} 已经关闭"); } } //备份要更新的程序 var backupPath = Path.Combine(root, $"{appFolder}_{currentVersion}_bk"); try { if (Directory.Exists(backupPath)) { Directory.Delete(backupPath, true); } Directory.Move(appPath, backupPath); this.Log($"备份 {appPath}"); } catch (Exception ex) { this.Log($"备份失败:"); this.LogError(ex.ToString()); throw ex; } Directory.CreateDirectory(appPath); this.Log($"创建目录 {appPath}"); foreach (var item in Directory.GetFiles(backupPath)) { if (item.EndsWith("license") || item.EndsWith(".db") || item == "appsettings.json") { File.Copy(item, Path.Combine(appPath, Path.GetFileName(item))); } } this.Log($"还原数据库db文件 {appPath}"); //更新程序 try { ZipFile.ExtractToDirectory(file, root, Encoding.UTF8, true); this.Log($"解压更新文件 {file} 到 {appPath}"); } catch (Exception ex) { this.LogError("解压失败,开始还原"); this.LogError(ex.ToString()); Directory.Delete(appPath, true); this.LogError("删除失败的更新目录"); Directory.Move(backupPath, appPath); this.LogError("还原备份的程序"); throw ex; } //设置权限 if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { var updateScript = Path.Combine(appPath, "update.sh"); var command = $"chmod 755 {updateScript}"; this.Log(command.Bash()); this.Log($"修改权限:{updateScript}"); this.Log(updateScript.Bash()); this.Log($"执行更新脚本:{updateScript}"); } //启动更新程序 proxy.startProcess(processName); this.Log($"启动进程:{processName}"); } } } private string GetCheckSum(string file) { using (var sha = SHA1.Create()) { using (var fs = File.OpenRead(file)) { var checksum = BitConverter.ToString(sha.ComputeHash(fs)).Replace("-", ""); return checksum; } } } private void Log(string message) { try { this._logger.LogInformation(message); this._hub.Clients.All.SendAsync("ServerToClient", message); } catch (Exception ex) { this.LogError(ex.ToString()); } } private void LogError(string message) { try { this._logger.LogError(message); this._hub.Clients.All.SendAsync("ServerToClient", message); } catch (Exception ex) { this._logger.LogError(ex, ex.Message); } } } }