一、問題背景
本地程序在實際項目使用過程中,因為可以操作電腦本地的一些信息,並且對於串口、OPC、並口等數據可以方便的進行收發,雖然現在軟件行業看着動不動都是互聯網啊啥的,大有Web服務就是高大上的感覺,但是作為本地的應用還是有着非常重要的位置,特別是在制造業工廠里,車間里相關的程序。
拋開一切業務上的功能不談,本地程序一直比較詬病的地方就是在於軟件的更新上,由於程序都在客戶端電腦上運行,當需要更新的時候,就不得不由專門的實施人員過去,部署更新,無形中增加項目成本,SO,對於c/s程序的自動更新也是比較苦惱的問題,下面我就來稍微解析下,一個自動更新程序應該要怎么實現(PS:思路可能比較傳統,歡迎大家拍磚提供更好的思路)
二、自動更新的關注點
如圖所示,對於一個自動更新程序,關注點應該都是以上幾個點
- 管理員權限,在win7以后,如果應用位置在C盤的話,每次操作目錄都會申請管理員權限,emmmm,所以這個必須要考慮
- 對於要實現一個較為通用的自動更新,應該要安裝了.NetFrameWork的都要可以使用,並且方便使用
- 更新程序同時要只能啟動一個,不然肯定出事兒,雖然很少有會有人去點2次,但是還是要考慮
- 界面要求上,更新說明以及進度條要顯示
- 很多時候可能我們也是需要一個靜默更新的操作
- 運行更新的時候,記得要關閉運行的程序,不然肯定更新失敗
- 對於更新失敗,得有完善的回滾以及備份機制
- 更新成功后,得可以啟動對應的主程序
- 有些 時候程序部分信息是記錄在注冊表里,如果注冊表要修改咋辦呢,so,對於注冊表也得要支持
- 有些時候程序更新到后面,會出現一些多余的DLL,這些DLL那也是要干掉滴(雖然覺得有點雞肋)
大概就是以上的一些點,這些是我自己思考的時候羅列出來的,可能比較亂,大家明白就好
三、設計說明
更新程序主要流程如圖所示,大的流程方向上是比較簡單的,但是如果深入后,還是有部分會比較復雜
程序類的一些簡單說明
config.update:注冊表的增刪規則以及文件的刪除規則,如下規則所示
[regedit_del] //刪除注冊表 SOFTWARE\\XXX\\XXX,name [regedit_add]//新增注冊表 SOFTWARE\\XXX\\XXXX,name=John Doe [file_del] //刪除文件 hello.dll
Server.xml&RemoteInfo.cs:服務端的版本配置文件信息
Local.xml&LocalInfo.cs:客戶端的版本配置文件信息
UpdateWork.cs:核心的更新方法,為了方便后續有界面定制化的需求,將更新相關的全部放在UpdateWork中,使用UpdateWork.Do方法就可以進行更新
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
一些細節說明
1、如何讓程序盡量的方便集成?
由於自動更新程序必須是要與主程序分開的,所以我們要讓主程序啟動更新程序的時候,將主程序自己的信息帶進去,這樣才可以盡可能的做到通用
/// <summary> /// 應用程序的主入口點。
/// <param name="args">[0]程序名稱,[1]靜默更新 0:否 1:是</param> /// </summary> [STAThread] static void Main(string[] args) { if (f) { try { if (String.IsNullOrEmpty(args[0]) == false) { UpdateWork updateWork = new UpdateWork(args[0]); if (updateWork.UpdateVerList.Count > 0) { /* 當前用戶是管理員的時候,直接啟動應用程序 * 如果不是管理員,則使用啟動對象啟動程序,以確保使用管理員身份運行 */ //獲得當前登錄的Windows用戶標示 System.Security.Principal.WindowsIdentity identity = System.Security.Principal.WindowsIdentity.GetCurrent(); //創建Windows用戶主題 Application.EnableVisualStyles(); System.Security.Principal.WindowsPrincipal principal = new System.Security.Principal.WindowsPrincipal(identity); //判斷當前登錄用戶是否為管理員 if (principal.IsInRole(System.Security.Principal.WindowsBuiltInRole.Administrator)) { if (args[1] == "1") { updateWork.Do(); } else { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new MainForm(updateWork)); } } else { String result = Environment.GetEnvironmentVariable("systemdrive"); if (AppDomain.CurrentDomain.BaseDirectory.Contains(result)) { //創建啟動對象 ProcessStartInfo startInfo = new ProcessStartInfo { //設置運行文件 FileName = System.Windows.Forms.Application.ExecutablePath, //設置啟動動作,確保以管理員身份運行 Verb = "runas", Arguments = " " + args[0] + " " + args[1] }; //如果不是管理員,則啟動UAC System.Diagnostics.Process.Start(startInfo); } else { if (args[1] == "1") { updateWork.Do(); } else { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new MainForm(updateWork)); } } } } } } catch (Exception ex) { MessageBox.Show(ex.Message); } } }
winform在啟動的main方法里,有一個參數是args,這就是我們可以接收來自外界的參數,從代碼中可以看到,總共傳遞進來的參數有2個,args[0]程序名稱 args[1] 靜默安裝的配置信息,通過這2個參數,我們就可以將自動更新與主程序分開
2、更新前備份
/// <summary> /// 備份當前的程序目錄信息 /// </summary> private UpdateWork Bak() { try { LogTool.AddLog("更新程序:准備執行備份操作"); DirectoryInfo di = new DirectoryInfo(AppDomain.CurrentDomain.BaseDirectory); foreach (var item in di.GetFiles()) { if (item.Name != mainName)//當前文件不需要備份 { if (item.Name == "DotNetZip.dll") { } else { File.Copy(item.FullName, bakPath + item.Name, true); } } } //文件夾復制 foreach (var item in di.GetDirectories()) { if (item.Name != "bak" && item.Name != "temp") { CopyDirectory(item.FullName, bakPath); } } LogTool.AddLog("更新程序:備份操作執行完成,開始關閉應用程序"); OnUpdateProgess?.Invoke(20); return this; } catch (Exception EX) { throw EX; } }
對於自動更新來說,如果更新失敗的話,我們需要保證,程序是可回滾的,那就需要在更新前要對程序進行一個備份,如代碼所示,由於我這邊用到了DotNetZip.dll,所以這個dll是不能備份的,不然恢復的時候由於自動更新程序還在跑,覆蓋的時候會報錯,現在已經將DotNetZip的代碼直接放到項目中,所以不需要去管DotNetZip.dll的問題,上述代碼邏輯還是很簡單的,拷貝當前運行程序下的所有文件以及文件夾到備份目錄(ps:由於windows的安全限制,c盤目錄普通用戶只有對temp文件夾有操作權限,so需要將bakPath設置到temp目錄下,如下代碼所示)
String bakPath = Path.Combine(Environment.GetEnvironmentVariable("TEMP"), @"MAutoUpdate\bak\");//獲取temp文件夾下的bak目錄,如果不存在記得通過程序去創建
3、更新文件的下載
/// <summary> /// 下載方法 /// </summary> private UpdateWork DownLoad() { using (WebClient web = new WebClient()) { foreach (var item in UpdateVerList) { try { LogTool.AddLog("更新程序:下載更新包文件" + item.ReleaseVersion); web.DownloadFile(item.ReleaseUrl, tempPath + item.ReleaseVersion + ".zip"); OnUpdateProgess?.Invoke(60 / UpdateVerList.Count); } catch (Exception ex) { LogTool.AddLog("更新程序:更新包文件" + item.ReleaseVersion + "下載失敗,本次停止更新,異常信息:" + ex.Message); throw ex; } } return this; } }
要更新嘛,那當然得有下載,索性.Net給我們提供了一個非常簡單的下載玩意兒,WebClient,給url就下載,絕對不二話,WebClient本身也有不少的事件可以使用,這個大家自己摸索,由於更新可能會存在好幾個包一起更新的情況,所以這邊使用循環先將所有要更新的下載下來,這部分下載代碼還是比較簡單的
4、更新方法
流程如上圖所示,文字表達捉急,只能靠圖了,阿門
private UpdateWork Update() { foreach (var item in UpdateVerList) { try { //如果是覆蓋安裝的話,先刪除原先的所有程序 if (item.UpdateMode == "Cover") { DelLocal(); } string path = tempPath + item.ReleaseVersion + ".zip"; using (ZipFile zip = new ZipFile(path)) { LogTool.AddLog("更新程序:解壓" + item.ReleaseVersion + ".zip"); zip.ExtractAll(AppDomain.CurrentDomain.BaseDirectory, ExtractExistingFileAction.OverwriteSilently); LogTool.AddLog("更新程序:" + item.ReleaseVersion + ".zip" + "解壓完成"); ExecuteINI();//執行注冊表等更新以及刪除文件 } localInfo.LastUdpate = item.ReleaseDate; localInfo.LocalVersion = item.ReleaseVersion; localInfo.SaveXml(); } catch (Exception ex) { LogTool.AddLog("更新程序出現異常:異常信息:" + ex.Message); LogTool.AddLog("更新程序:更新失敗,進行回滾操作"); Restore(); break; } finally { //刪除下載的臨時文件 LogTool.AddLog("更新程序:刪除臨時文件" + item.ReleaseVersion); DelTempFile(item.ReleaseVersion + ".zip");//刪除更新包 LogTool.AddLog("更新程序:臨時文件刪除完成" + item.ReleaseVersion); } } OnUpdateProgess?.Invoke(98); return this; }
四、如何在程序中使用
新建winform項目后,在Main里加入以下代碼
/// <summary> /// 應用程序的主入口點。 /// </summary> [STAThread] static void Main() { String path = AppDomain.CurrentDomain.BaseDirectory + "MAutoUpdate.exe"; //同時啟動自動更新程序 if (File.Exists(path)) { ProcessStartInfo processStartInfo = new ProcessStartInfo() { FileName = "MAutoUpdate.exe", Arguments = " MAutoUpdate.Test 1"//1表示靜默更新 0表示彈窗提示更新 }; Process proc = Process.Start(processStartInfo); if (proc != null) { proc.WaitForExit(); } } Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new Form1()); }
更新界面圖(可以自己改,這個反正很簡單),細心的小伙伴們可能發現了,我這是有點魔方微信的圖(ps:也是看了微信的升級后,突然想到我們項目里目前為止比較糾結的自動更新,所以趁着這幾個晚上搗鼓出來)
五、總結
花了大概三個晚上的時間搗鼓這個,里面其實考慮的因素還是很多,以前思考的時候不會深入思考,想着自動更新么 就是判斷、更新啟動結束,但是做下來,細節點真的是很多,后續可能會在公司內部項目里進行一個小的推廣,看看符不符合真實的需求,由於個人原因,在測試上可能會有所不夠,這個也是一個拋磚引玉,如果有小伙伴有更好更方便的升級方式,也請告知(ps:文字表達弱雞,大家多多包涵)
項目地址:https://github.com/Hello-Mango/MAutoUpdate,代碼比較亂,見諒
作者: Mango
出處: http://www.cnblogs.com/OMango/
關於自己:專注.Net桌面開發以及Web后台開發,開始接觸微服務、docker等互聯網相關(最近被互聯網架構搞的死去活來- -)
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接,如有問題, 可站內信告知.