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