.Net桌面程序自动更新NAppUpdate
自动更新介绍
我们做了程序,不免会有版本升级,这就需要程序有自动版本升级的功能。应用程序自动更新是由客户端应用程序自身负责从一个已知服务器下载并安装更新,用户唯一需要进行干预的是决定是否愿意现在或以后安装新的更新。
客户端程序要完成自动更新必须要做三件事情:检查是否有更新;当发现有更新时,开始下载更新;当下载完成时,执行更新操作;分别分析一下这三个步骤:
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版本升级数据表