自動更新介紹
我們做了程序,不免會有版本升級,這就需要程序有自動版本升級的功能。應用程序自動更新是由客戶端應用程序自身負責從一個已知服務器下載並安裝更新,用戶唯一需要進行干預的是決定是否願意現在或以后安裝新的更新。
客戶端程序要完成自動更新必須要做三件事情:檢查是否有更新;當發現有更新時,開始下載更新;當下載完成時,執行更新操作;分別分析一下這三個步驟:
1、檢查更新
客戶端要正確檢查是否有更新需要三個必要過程:
(1)到哪里去更新。即目標服務器的URI(URL或具體目錄)
(2)何時去檢查。即更新的頻率,每隔多長時間檢查一次。
(3)通過什么方式檢查。通訊協議,如HTTP、FTP、FILE等。
(4)如何檢查。如在后台運行,且開啟獨立線程。
2、下載更新
在普通用戶眼里,下載是一件再普通不過的事情了,但在開發者眼里我們需要考慮更多。我想需要考慮的如下:
(1)到哪里去下載。即目標Server的URI。
(2)通過什么方式下載。HTTP、FTP、FILE等,且斷點續傳。
(3)如何下載。在后台開啟獨立線程以不影響主線程工作。
(4)應對異常。如中斷連接后自動重新連接,多次失敗后拋棄等。
3、實現更新
實現更新不能簡單地認為是一個文件的覆蓋問題,原因很簡單:一個正在被使用的文件是無法被覆蓋的。也就是說程序自己是無法更新自己的。這樣看來,實現更新有兩種方案:
(1)在主程序之外,另開啟一個獨立進程來負責更新主程序。但前提是在更新前必須強制用戶退出主程序,很多現有產品就是這樣做的,如QQ。
(2)主程序根據下載的文件,生成一個比目前存在版本新的應用程序版本,用戶在下次重新打開應用程序時會自動使用新版本,同時原始應用程序的拷貝就可以被移除了。
二、可用框架
在.NET Framework2.0以后,微軟提供了采用內置的ClickOnce部署方式來實現系統更新,配置比較麻煩應用起來也不是很方便。.NET也有許多流行的開源自動更新組件如下:
序號 名稱 地址
1 AutoUpdater.NET https://autoupdaterdotnet.codeplex.com/
2 wyUpdate http://wyday.com/wyupdate/
3 Updater http://www.codeproject.com/Articles/9566/Updater
4 NetSparkle http://netsparkle.codeplex.com/
5 NAppUpdate https://github.com/synhershko/NAppUpdate
6 AutoUpdater https://autoupdater.codeplex.com/
這里我們想介紹的是開源自動更新框架NAppUpdate,NAppUpdate能很容易的和任何.Net桌面應用程序(包括WinForms應用應用和WPF應用程序)進行集成,針對版本訂閱、文件資源、更新任務提供了靈活方便可定制接口。而且可支持條件更新運行用戶實現無限制的更新擴展。
下面我們也會通過一個具體的實例來說明怎么在.NET桌面程序中使用NAppUpdate進行系統升級和更新。
三、NAppUpdate
NAppUpdate組件使用特定的xml文件來描述系統版本更新。Xml文件中包括下面內容,文件更新的描述信息、更新需要的特定邏輯、和更新需要執行的動作。
3.1 在項目中使用NAppUpdate
很簡單的就能集成到項目中:
(1)在項目添加NAppUpdate.Framework.dll引用。
(2)添加一個Class文件到項目進行更新檢查(具體參考后面實例)。
(3)根據版本更新的需要修改配置更新“源”。
(4)創建一個最小包含NAppUpdate.Framework.dll文件的運行包。
發布更新:
(1)Build項目
(2)手工創建或使用NAppUpdate內置提供的FeedBuilder工具創建更新xml配置文件。
(3)把生成的文件放入服務器版本更新URL所在位置。
(4)創建一個最小包含NAppUpdate.Framework.dll文件的運行包。
3.2 NAppUpdate工作流程
NAppUpdate如何更新系統:
(1)根據不同種類的更新需求,系統應該首先創建一個UpdateManager的實例,例如如果使用SimpleWebSource這種類型的更新,則需要提供URL指向遠程版本發布目錄。
(2)我們的系統通過調用NAppUpdate組件的CheckForUpdates方法,獲取更新信息。
(3)NAppUpdate下載版本描述的XML文件,通過比較這個文件來確定是否有版本需要更新。
(4)我們的系統調用NAppUpdate方法PrepareUpdates來進行更新初始化,下載更新文件到臨時目錄。
(5)NAppUpdate解壓一個updater可執行文件(.exe)到臨時目錄中。
(6)最后,我們系統調用NAppUpdate的方法ApplyUpdates(結束程序,執行更新,最后再啟動程序)。
四、實例代碼
4.1具體示例代碼
很簡單的就能集成到項目中:
(1)在項目添加NAppUpdate.Framework.dll引用。

(2)添加一個版本更新菜單。

(3)點擊菜單會彈出模式對話框,提示版本更新。

(3)點擊版本升級按鈕會進行版本升級,結束程序並重新啟動。
版本檢查:
private UpdateManager updManager; //聲明updateManager
在窗體的Onload事件中,創建一個后台線程進行版本檢查,如果發現新版本則把版本描述信息顯示在窗體界面上。
this.IsPushVersion = AppContext.Context.AppSetting.IsPushVersionUpgrade;
var appUpgradeURL = ConfigurationManager.AppSettings["VersionUpdateURL"];
if (string.IsNullOrEmpty(appUpgradeURL))
{
this.MessageContent = "更新服務器URL配置為空,請檢查修改更新配置。";
UpgradeEnabled = false;
return;
}
updManager = UpdateManager.Instance;
// Only check for updates if we haven't done so already
if (updManager.State != UpdateManager.UpdateProcessState.NotChecked)
{
updManager.CleanUp();
//return;
}
updManager.UpdateSource = PrepareUpdateSource(appUpgradeURL);
updManager.ReinstateIfRestarted();
try
{
updManager.CheckForUpdates();
}
catch(Exception ex)
{
UpgradeEnabled = false;
//LogManager.Write(ex);
this.MessageContent =
string.Format("版本更新服務器{0}連接失敗!n請檢查修改更新配置信息。", appUpgradeURL);
return;
}
if (updManager.UpdatesAvailable == 0)
{
var currentVersion = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString();
this.MessageContent = string.Format("已經是最新的版本{0}n沒有可用的更新。", currentVersion);
}
else
{
this.UpgradeEnabled = true;
updateTaskHelper = new UpdateTaskHelper();//自定義一個TaskHelper類負責合並處理版本描述信息。
var desc = updateTaskHelper.UpdateDescription;
var currentVersion = updateTaskHelper.CurrentVersion;
this.MessageContent = string.Format("有可更新的版本,更新文件數量: ({0})n版本描述:n{1} 。",
updManager.UpdatesAvailable, desc);
var taskInfo = this.updateTaskHelper.TaskListInfo;
}
VersionUpdateSource 是對SimpleWebSource實現的的一個簡單擴展。
private NAppUpdate.Framework.Sources.IUpdateSource PrepareUpdateSource(string url)
{
// Normally this would be a web based source.
// But for the demo app, we prepare an in-memory source.
var source = new VersionUpdateSource(url);
/
return source;
}
版本升級代碼:
使用UpdManager調用異步方法進行版本升級,然后結束程序並重新啟動。
updManager.BeginPrepareUpdates(
asyncResult =>
{
try
{
if (asyncResult.IsCompleted)
{
isBeginPrepareUpdates = false;
this.IsBusy = false;
//UpdateManager updManager = UpdateManager.Instance;
this.SettingsPageView.Dispatcher.Invoke(new Action(() =>
{
IsBusy = false;
var dr1 = DialogHelper.GetDialogWindow(
"安裝更新需要退出系統,請您務必保存好您的數據,n系統更新完成后再從新登錄,您確定要現在更新系統嗎?",
CMessageBoxButton.OKCancel);
if (dr1 == CMessageBoxResult.OK)
{
// This is a synchronous method by design, make sure to save all user work before calling
// it as it might restart your application
//updManager.ApplyUpdates(true, true, true);
updManager.ApplyUpdates(true);
}
else
{
this.Cancel();
}
})
);
}
}
catch (Exception ex)
{
DialogHelper.GetDialogWindow("An error occurred while trying to install software updates",CMessageBoxButton.OK);
}
finally
{
updManager.CleanUp();
}
}, null);
(4)如果選中“開啟版本更新提示”,則程序啟動時會自動檢查新版本情況,並提示給用戶,見下圖。

在系統啟動的時候,開啟一個后台線程進行版本檢查,如果發現新版本則提示給用戶。
void bgWorkerVersionUpgrade_DoWork(object sender, DoWorkEventArgs e)
{
VersionUpgradeContent = string.Empty;
var updManager = UpdateManager.Instance;
// Only check for updates if we haven't done so already
if (updManager.State != UpdateManager.UpdateProcessState.NotChecked)
{
//DialogHelper.GetDialogWindow("Update process has already initialized; current state: " + updManager.State.ToString()
// , CMessageBoxButton.OK);
updManager.CleanUp();
//Foundation.Wpf.Toolkit.MessageBox.Show("Update process has already initialized; current state: " + updManager.State.ToString());
//return;
}
var appUpgradeURL = ConfigurationManager.AppSettings["VersionUpdateURL"];
updManager.UpdateSource = PrepareUpdateSource(appUpgradeURL);
updManager.ReinstateIfRestarted();
updManager.CheckForUpdates();
if (updManager.UpdatesAvailable != 0)
{
updateTaskHelper = new UpdateTaskHelper();
var desc = updateTaskHelper.UpdateDescription;
var currentVersion = updateTaskHelper.CurrentVersion;
this.VersionUpgradeContent = string.Format("更新文件數量: ({0})n更新內容:n{1} 。",
updManager.UpdatesAvailable, desc);
}
}
void bgWorkerVersionUpgrade_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if(e.Error == null && this.VersionUpgradeContent != string.Empty)
{
this.View.ShowNotifyWindow("請升級新版本", this.VersionUpgradeContent, Hardcodet.Wpf.TaskbarNotification.BalloonIcon.Warning);
}
//throw new NotImplementedException();
}
4.2版本發布xml配置文件示例

版本更新可對不同類型的文件更新,設置不同類型的條件,這些條件(文件大小檢查、文件版本檢查、文件更新日期、文件是否存在,操作系統版本,文件完整性檢查等)可靈活組合使用。
<Feed>
<Tasks>
<FileUpdateTask hotswap="true" updateTo="http://SomeSite.com/Files/NewVersion.txt" localPath="CurrentVersion.txt">
<Description>Fixes a bug where versions should be odd numbers.</Description>
<Conditions>
<FileChecksumCondition checksumType="sha256" checksum="6B00EF281C30E6F2004B9C062345DF9ADB3C513710515EDD96F15483CA33D2E0" />
</Conditions>
</FileUpdateTask>
</Tasks>
</Feed>

4.3實現更新數據庫
我們項目中使用的是MySql數據庫,用戶想功過sql文件的方式來更新數據。
實現流程:
(1)把數據庫更新以sql文本文件的方式發布到版本服務器,設置更新時間(根據文件本身創建時間)
<Conditions>
<FileDateCondition what="older" timestamp="2017/12/5" />
</Conditions>
(2)客戶端會根據實際判斷是否需要下載這個更新。
(3)為了安全和防止文件被篡改,可以考慮對文件進行加密解密和完整性校驗。
<FileChecksumCondition checksumType="sha256" checksum="6B00EF281C30E6F2004B9C062345DF9ADB3C513710515EDD96F15483CA33D2E0" />
(4)使用NAppUpdate下載sql文件到客戶端特定的目錄中。
(5)程序啟動時獲取mysql連接信息,打開一個后台進程,調用mysql工具,使用隱藏的命令行執行sql文件。
(6)存儲以及執行過的sql文件信息到數據庫(作為記錄和避免重復執行)。
示例代碼:
//開進程執行sql文件
private static void UpdateMySqlScript(IList<string> sqlFiles,IVersionUpgradLogService versionService)
{
string connStr = LoadMySqlConnectionString();
DbConnectionSetting db = GetDBConnectionSetting(connStr);
foreach(var sqlFile in sqlFiles)
{
if (string.IsNullOrWhiteSpace(sqlFile) || string.IsNullOrEmpty(sqlFile))
{
continue;
}
UpdateDBByMySql(db, sqlFile);
versionService.ExecuteSqlFile(GetFileName(sqlFile));
}
}
private static void UpdateDBByMySql(DbConnectionSetting db, string sqlFile)
{
Process proc = new Process();
proc.StartInfo.FileName = "cmd.exe"; // 啟動命令行程序
proc.StartInfo.UseShellExecute = false; // 不使用Shell來執行,用程序來執行
proc.StartInfo.RedirectStandardError = true; // 重定向標准輸入輸出
proc.StartInfo.RedirectStandardInput = true;
proc.StartInfo.RedirectStandardOutput = true;
proc.StartInfo.CreateNoWindow = true; // 執行時不創建新窗口
proc.StartInfo.WorkingDirectory = GetWorkingDirectory(@"offlineappmysqlbin");
proc.Start();
proc.StandardInput.WriteLine(""mysql.exe" -h " + db.Server + " -u" + db.User + " -p" + db.PWD + " --default-character-set=" + "utf8" + " " + db.DBName + " <"" + sqlFile + """);
proc.StandardOutput.ReadLine();
proc.Close();
}
//獲取需要執行的sql文件
private IList<string> GetNeedToRunSqlFiles(IVersionUpgradLogService versionService)
{
IList<string> result = new List<string>();
string updateSqlScriptFolder = "updatesqlscripts";
string folder = Path.Combine( LoadApplicationPath(),updateSqlScriptFolder);
var files = Directory.GetFiles(folder, "*.*", SearchOption.AllDirectories)
.Where(s => s.EndsWith(".sql") );
var lockedFiles = Directory.GetFiles(folder, "*.lock", SearchOption.AllDirectories);
foreach(var file in files)
{
if(versionService.IsExecute(GetFileName(file)))
{
continue;
}
result.Add(file);
}
return result;
}
//記錄sql執行情況的服務
public class VersionUpgradLogService :IVersionUpgradLogService
{
private readonly IVersionUpgradLogRepository versionUpgradLogRepository;
private IList<string> cachedVersionFileNames;
public VersionUpgradLogService(IVersionUpgradLogRepository versionUpgradLogRepository)
{
this.versionUpgradLogRepository = versionUpgradLogRepository;
this.cachedVersionFileNames = LoadAllExecutedFileNames();
}
private IList<string> LoadAllExecutedFileNames()
{
IList<string> result = new List<string>();
var dtoList = versionUpgradLogRepository.GetAll();
foreach(var item in dtoList)
{
if(HasExecuted(item))
{
if (!result.Contains(item.Name))
{
result.Add(item.Name);
}
}
}
return result;
}
private bool HasExecuted(VersionUpgradLogDTO item)
{
return item.Status == 1 & item.UpgradType == 0;
}
public IList<VersionUpgradLog> GetAllByTaskId(string taskId)
{
return DomainAdapter.Adapter.Adapt<IList<VersionUpgradLogDTO>, List<VersionUpgradLog>>(
versionUpgradLogRepository.GetAllByTaskId(taskId));
}
public bool IsExecute(string fileName)
{
if(cachedVersionFileNames.Contains(fileName))
{
return true;
}
return false;
}
public void ExecuteSqlFile(string fileName)
{
VersionUpgradLog versionLog = new VersionUpgradLog();
versionLog.UpgradType = 0;
versionLog.UpdateDate = System.DateTime.Now;
versionLog.Status = 1;
versionLog.Version = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString();
versionLog.Name = fileName;
var item = DomainAdapter.Adapter.Adapt<VersionUpgradLog, VersionUpgradLogDTO>(versionLog);
versionUpgradLogRepository.Add(item);
}
}
}
Sql版本升級數據表

