解析大型系统自动更新,及开源框架
C/S架构的应用程序需要支持自动更新功能,当新版本程序发布后,正在运行的客户端能检测到新版本的程序,通知用户是否下载更新。工作以来参与过几个自动更新模块的设计与维护,撰文总结自动更新模块设计与实现。
自动更新组件主要内容
1 版本比较。旧版本如何检测到新版本,版本信息是程序集自描述的,还是用单独的文件保存。.NET程序集是自描述的,程序集包含以下几种版本信息,每个Assebmly包含三个Version
AssemblyFileVersion : 存储在Win32资源中, CLR不关心这个版本号。
AssemblyInformationnalVersion :存储在Win32资源中, CLR不关心这个版本号。
AssemblyVersion: 存储在AssemblyDef manifest metadata table中,CLR会使用这个版本号。
标准版本号物理形式表示为用句点隔开的四段数字,如下面的代码示例所示。
<major version>.<minor version>.<build number>.<revision>
实际使用中,我们一般只用到前面三段。即
<major version>.<minor version>.<build number>
版本比较举例:
旧版本 2.4.1.2
新版本 2.4.1.3 或 2.4.2.2 或2.5.1.2。
2 程序下载。采用何种方式,从哪里下载最新版本的程序。
1) Http 协议。从IIS或Tomcat 等Web服务器中下载新版本的程序文件。
2) Ftp协议。从Ftp服务器下载新版本的程序文件。
3) 局域网共享。从内部局域网下载新版本的程序文件。
下载过程中应该支持断点续传,支持以压缩文件的方式传输,下载完成后自动解压缩,执行更新。还可以参考Windows的BITS更新服务,BITS (后台智能传送服务) 是一个 Windows 组件,它可以在前台或后台异步传输文件,为保证其他网络应用程序获得响应而调整传输速度,并在重新启动计算机或重新建立网络连接之后自动恢复文件传输。
为了达到传输过程中速度最优化,可以考虑调用第三方下载API,实现穿越局域网时,下载速度最快。
4) ClickOnce方式。将程序部署在IIS网站中,配置ClickOnce,参考文章 ClickOnce部署。
3 执行更新。如果更新程序是压缩格式的zip/rar格式文件,可解压缩后复制到应用程序目录即可。如果更新程序是安装格式的Installer文件,则需要先退出当前程序,启动安装程序包。
当前程序正在执行时,将更新程序覆盖过来,会遇到文件被进程占用的情况,有以下三种解决方案:
1) 根据更新程序生成一个批处理命令,主要内容是将更新程序中的文件复制到当前程序所在文件夹。命令主要包含以下三个部分,退出当前程序,执行文件复制,启动应用程序。
2) 应用程序启动一个独立的更新程序Update.EXE,由更新程序完成文件的复制和程序的启动。
3) 调用Volume Shadow Copy Service,这个服务支持当文件正在被进程使用,仍可以复制。
详细参考以下网站信息 Volume Shadow Copy Service。
文件的存储方式
1) 文件包含版本信息,举例EnterpriseSolution-5.3.0.0-20150401.zip,表示是5.3版本的,构建日期是2015年4月1日。在检测更新文件时,需要遍历同版本或是高版本的文件,取最新的那个文件。
2) 文件不包含版本信息,需要用独立的描述文件表达版本信息。比如
<?xml version="1.0" encoding="utf-8" ?> <Content> <Project id="Enterprise.Sales.dll" Edition="1.2"> </Project> <Project id="Enterprise.Purchasing.dll" Edition="1.2"> </Project> <Project id="Enterprise.Inventory.dll" Edition="1.3"> </Project> <Project id="Enterprise.GeneralLedger.dll" Edition="1.5"> </Project> </Content>
通过这个描述文件,找到最新版本的文件,分批下载回来。对于服务器中存在多个版本的文件情况,可用文件夹(1.2,1.5)的方式将各版本的程序文件放在各自的目录中,通过上面的版本描述文件分别在各自的版本目录中寻找文件。
版本检测
检测版本,发现新版本后启动更新程序Update.EXE,退出当前程序。
string clientVersion = FileVersionInfo.GetVersionInfo(Assembly.GetEntryAssembly().Location).ProductVersion; string serverVersion = FileVersionInfo.GetVersionInfo("Main.exe", server)).ProductVersion; if (new Version(serverVersion).CompareTo(new Version(clientVersion)) > 0) { var process = new Process { StartInfo = new ProcessStartInfo { FileName = "Update.exe", Arguments = "LOCAL" } }; process.Start(); Environment.Exit(0); }
外壳程序
启动外壳程序,在局域网(工作组)中常用来解决权限问题。
private static void Shell(string arguments) { Process process = new Process(); ProcessStartInfo processStartInfo = new ProcessStartInfo("cmd.exe", arguments); processStartInfo.WindowStyle = ProcessWindowStyle.Hidden; process.StartInfo = processStartInfo; process.Start(); }
常用在net use命令,举例如下:
net use ipipc$ " " /user:" " 建立IPC空链接
net use ipipc$ "密码" /user:"用户名" 建立IPC非空链接
net use h: ipc$ "密码" /user:"用户名" 直接登陆后映射对方C:到本地为H:
net use h: ipc$ 登陆后映射对方C:到本地为H:
网络连接检查
当准备检测更新时,我需要检测网络是否通畅,需要用下面的方法:
1) 局域网模式下,用SystemInformation.Network可达到目的。
2) 要从外网下载程序,需要检查外网是否连通,可以采用PING一个IP地址的方式,要求这个IP地址或是域名非常稳定。
可以调用C#的Ping类型完成目的,参考代码:
Ping ping = new Ping(); PingOptions poptions = new PingOptions(); poptions.DontFragment = true;
也可以调用COM接口InternetGetConnectedState,用于检测网络连接状态。
[DllImport("wininet")] private extern static bool InternetGetConnectedState(out int connectionDescription, int reservedValue);
参考说明 InternetGetConnectedState function。
stackoverflow中有一篇讲解判断网络连接的问答,地址是http://stackoverflow.com/questions/13457407/why-is-getisnetworkavailable-always-returning-true。
要检测广域网是否连接,需要使用InternetGetConnectedState()或WinINet API。
出处:https://www.cnblogs.com/JamesLi2015/p/4749476.html
=======================================================================================
自动更新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版本升级数据表
出处:https://www.cnblogs.com/umlchina/p/9055999.html
=======================================================================================
自动更新组件分享
一. 摘要
前两天在博客上发布了一篇英文的自动更新组件文章Release a AutoUpdater tool,那么在这篇文章中,我们也对其功能进行一些简单说明,这个组件非常简单,所以大家可以下载进行一些改进。众所周知,对于一般的软件开发,在开始的时候都会有一个技术选型的阶段,最大的选型就是首先要确定是选择Client/Server模式还是Browser/Server模式。综合而论:两者各有优劣,在很多方面都不能被对方互相取代,如在适用Internet、维护工作量等方面,B/S比C/S要强很多;但在运行速度、数据安全、人机交互等方面,B/S就远不如C/S那么强大。所以综上所述,凡是C/S的强项,便是B/S的弱项,反之亦然。由于今天讨论的是自动更新组件,所以接下来我们就往这方面细讲,既然C/S模式在运行速度、数据安全、人机交互有这么多的优点,尤其是客户端技术日益发展的今天,如何解决客户端的部署与自动升级问题便是一个非常重要的问题。
二. 本文提纲
· 1.摘要
· 2.本文提纲
· 3.为什么不使用ClickOnce
· 4.简要介绍
· 5.项目中如何使用
· 6.具体效果
· 7.维护与下载
· 8.总结
三. 为什么不使用ClickOnce
在前面的摘要中我们简单介绍了自动更新功能的重要性,在这一小节里我们来谈一下为什么不使用微软给我们提供的自动更新组件ClickOnce,大家都知道ClickOnce给我们提供了很多功能:简单说来,ClickOnce 应用程序就是任何使用 ClickOnce 技术发布的 Windows 窗体或控制台应用程序。可以采用三种不同的方法发布 ClickOnce 应用程序:从网页发布、从网络文件共享发布或是从媒体(如 CD-ROM)发布。ClickOnce 应用程序既可以安装在最终用户的计算机上并在本地运行(即使当计算机脱机时也可以运行),也可以仅以联机模式运行,而不在最终用户的计算机上永久安装任何内容。ClickOnce 应用程序可以自行更新;这些应用程序可以在较新版本变为可用时检查较新版本,并自动替换所有更新的文件。开发人员可以指定更新行为;网络管理员也可以控制更新策略,如将更新标记为强制性的。最终用户或管理员还可以对更新进行回滚,使应用程序恢复到早期的版本。
从上面大家可以看出ClickOnce 无疑是微软对Client/Server模式部署的最佳解决方案,但正是因为它的功能特别强大而且又要使用相当简单,所以在产品的封装上就特别严实,基本上就暴露了一些简单的操作接口,这样就无形把一些定制化的操作拒之于门外,比如:
1,用户不能自己指定安装路径。
2,对自动更新流程不能做定制化的操作。
3,对自动更新的UI不能定制化的设计。
正因为这几个原因,所以很多企业都会做一些定制化的组件来实现自动更新的功能,基于此,我们这里也实现了一个非常简单的自动更新组件.
四. 简要介绍
其实自动更新的原理很简单,分析起来无非就是简单的几步操作,当然实现方式也是大同小异,这里我们就选一种较简单的方式:
1.启动主程序,主程序里面调用升级程序,升级程序连接到IIS或者FTP。
2.升级程序获取服务器端XML配置文件中新版本程序的更新日期或版本号或文件大小。
3.升级程序获取原有客户端应用程序的最近一次更新日期或版本号或文件大小,然后两者进行比较;如果新版本日期>原有程序的最新日期,则提示用户是否升级;或如果新版本版本号>原有程序的版本号,则提示用户是否升级;再或如果新版本文件大小>原有程序的文件大小,则提示用户是否升级。本文主要采用一般的做法,就是通过版本号来进行对比。
4.如果用户选择升级,则获取下载文件列表;
5.在本地建立与远程IIS或者FTP相应的临时目录,然后下载到这个临时目录文件下;
6.删除旧的主程序,拷贝临时文件夹中的文件到相应的位置;
8.结束升级流程并重新启动主程序。
根据前面的流程,我们可以简单设计如下的项目:
图1
具体类介绍:
IAutoUpdater.cs 提供外部调用的接口
AutoUpdater.cs 该组件的主操作类
Autoupdater.config 本地配置文件
DownloadConfirm.cs 提示是否有更新页面
DownloadProgress.cs 下载进度页面
CommonUnitity.cs 一些常用功能
Config.cs 当更新完毕之后需要更新Config,所以这里需要一个提供序列化的Config类
ConstFile.cs 一些常量文件
DownloadFileInfo.cs 需要下载的文件实体类
LocalFile.cs 本地文件实体类
RemoteFile.cs 远程文件实体类
UpdateFileList.cs 本地的实体类集合
代码非常简单,具体可以下载进行查看,所以这里就不做过多阐述。
五. 项目中如何使用
第一步:Host更新的版本到服务器
如果需要让客户端获取最新的版本,首先我们需要开发人员编译源代码并生成文件,然后拷贝到FTP或者IIS目录下,运行一个自动生成XML文件的程序,把所有的文件都自动生成到一个XML文件,详细见下图:
第二步:配置本地的Config
经过第一步的流程,这一步要做的就是配置本地的Config用于监测并下载远程IIS或者FTP下需要更新的文件,具体如下图所示:
图3
第三步:修改主程序
首先把AutoUpdater这个DLL引入我们的主项目,然后在主项目中添加如下代码,当然你可以根据自己的需要进行书写,这个DLL提供了两个外部接口,一个接口用于判断是否有更新及下载,另一个接口则是用于更新出错时进行回滚操作,具体代码如下:
#region check and download new version program bool bHasError = false; IAutoUpdater autoUpdater = new AutoUpdater(); try { autoUpdater.Update(); } catch (WebException exp) { MessageBox.Show("Can not find the specified resource"); bHasError = true; } catch (XmlException exp) { bHasError = true; MessageBox.Show("Download the upgrade file error"); } catch (NotSupportedException exp) { bHasError = true; MessageBox.Show("Upgrade address configuration error"); } catch (ArgumentException exp) { bHasError = true; MessageBox.Show("Download the upgrade file error"); } catch (Exception exp) { bHasError = true; MessageBox.Show("An error occurred during the upgrade process"); } finally { if (bHasError == true) { try { autoUpdater.RollBack(); } catch (Exception) { //Log the message to your file or database } } } #endregion
使用就是这么简单,更详细的操作,大家可以下载源码,也正因为它的简单,所以大家可以对其修改以满足具体项目的需求。
六. 具体效果
当我们运行主程序(WinForm或者WPF),如果服务器上有最新的版本,就会弹出如下页面进行提示并让用户选择是否更新。
图4
当用户不需要更新时,可以选择Skip按钮跳过并继续主程序流程,反之则进入如下页面。
图5
在下载的过程中,用户可以选择Cancel停止下载并重新回到主流程。
七. 维护与下载
该组件已经托管到CodePlex,所以大家可以到上面下载其源代码,具体地址:http://autoupdater.codeplex.com/
图6
八. 总结
我们这篇文章简单的讲解了一个简单的自动更新组件,由于它比较简单而且功能单一,所以没有花费大量的篇幅进行论述,感兴趣的读者可以下载其源码。后面我会把自己开发的一套MVVM框架也托管上去,由于目前对Silverlight的支持还不太好,所以正在持续做相关的改进,其中借鉴了一些其他优秀框架的思想并且融入了一些实际应用场景,所以到时也希望大家能够多多指教!
http://www.cnblogs.com/KnightsWarrior/archive/2010/10/20/1856255.html
=======================================================================================
c# 自动更新程序
首先看获取和更新的接口
更新程序Program.cs
1 using System; 2 using System.Collections.Generic; 3 using System.Diagnostics; 4 using System.IO; 5 using System.Linq; 6 using System.Threading.Tasks; 7 using System.Windows.Forms; 8 9 namespace Update 10 { 11 static class Program 12 { 13 /// <summary> 14 /// 更新程序启动后复制自身,使用副本进行更新 15 /// -h 不显示界面 16 /// -c 不使用copy更新程序 17 /// -d 更新完成删除自身,通常用在copy的更新程序 18 /// -b 更新下载到备份文件,不替换原文件 19 /// -r 更新完成运行的文件,下一个参数为文件路径 20 /// -k 如果系统正在运行则干掉 21 /// </summary> 22 [STAThread] 23 static void Main(string[] args) 24 { 25 Application.EnableVisualStyles(); 26 Application.SetCompatibleTextRenderingDefault(false); 27 Application.ThreadException += Application_ThreadException; 28 29 List<string> lst = args.ToList(); 30 if (!lst.Contains("-b") && !lst.Contains("-k")) 31 { 32 //这里判断成程序是否退出 33 if (Process.GetProcessesByName("serviceclient").Length > 0) 34 { 35 MessageBox.Show("服务正在运行,请退出后重试。", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information); 36 return; 37 } 38 } 39 40 if (lst.Contains("-k")) 41 { 42 var ps = Process.GetProcessesByName("serviceclient"); 43 if (ps.Length > 0) 44 { 45 ps[0].Kill(); 46 } 47 } 48 49 //副本更新程序运行 50 if (!lst.Contains("-c"))//不存在-c 则进行复制运行 51 { 52 string strFile = Path.Combine(Path.GetDirectoryName(Application.ExecutablePath), Guid.NewGuid().ToString() + ".exe"); 53 File.Copy(Application.ExecutablePath, strFile); 54 lst.Add("-c"); 55 lst.Add("-d"); 56 Process.Start(strFile, string.Join(" ", lst)); 57 } 58 else 59 { 60 Action actionAfter = null; 61 //将更新文件替换到当前目录 62 if (!lst.Contains("-b")) 63 { 64 actionAfter = () => 65 { 66 string strUpdatePath = Path.Combine(System.AppDomain.CurrentDomain.BaseDirectory, "UpdateCache\\"); 67 if (Directory.Exists(strUpdatePath) && Directory.GetFiles(strUpdatePath).Length > 0) 68 { 69 CopyFile(strUpdatePath, System.AppDomain.CurrentDomain.BaseDirectory, strUpdatePath); 70 if (File.Exists(Path.Combine(strUpdatePath, "ver.xml"))) 71 File.Copy(Path.Combine(strUpdatePath, "ver.xml"), Path.Combine(System.AppDomain.CurrentDomain.BaseDirectory, "ver.xml"), true); 72 Directory.Delete(strUpdatePath, true); 73 } 74 }; 75 } 76 try 77 { 78 //隐藏运行 79 if (!lst.Contains("-h")) 80 { 81 Application.Run(new FrmUpdate(actionAfter, true)); 82 } 83 else 84 { 85 FrmUpdate frm = new FrmUpdate(actionAfter); 86 frm.Down(); 87 } 88 } 89 catch (Exception ex) 90 { } 91 //运行更新后的文件 92 if (lst.Contains("-r")) 93 { 94 int index = lst.IndexOf("-r"); 95 if (index + 1 < lst.Count) 96 { 97 string strFile = Path.Combine(System.AppDomain.CurrentDomain.BaseDirectory, lst[index + 1]); 98 if (File.Exists(strFile)) 99 { 100 Process.Start(strFile, "-u"); 101 } 102 } 103 } 104 //删除自身 105 if (lst.Contains("-d")) 106 { 107 DeleteItself(); 108 } 109 } 110 Application.Exit(); 111 Process.GetCurrentProcess().Kill(); 112 } 113 114 private static void Application_ThreadException(object sender, System.Threading.ThreadExceptionEventArgs e) 115 { 116 throw new NotImplementedException(); 117 } 118 private static void CopyFile(string strSource, string strTo, string strBasePath) 119 { 120 string[] files = Directory.GetFiles(strSource); 121 foreach (var item in files) 122 { 123 string strFileName = Path.GetFileName(item).ToLower(); 124 125 if (strFileName == "ver.xml ") 126 { 127 continue; 128 } 129 //如果是版本文件和文件配置xml则跳过,复制完成后再替换这2个文件 130 string strToPath = Path.Combine(strTo, item.Replace(strBasePath, "")); 131 var strdir = Path.GetDirectoryName(strToPath); 132 if (!Directory.Exists(strdir)) 133 { 134 Directory.CreateDirectory(strdir); 135 } 136 File.Copy(item, strToPath, true); 137 } 138 string[] dires = Directory.GetDirectories(strSource); 139 foreach (var item in dires) 140 { 141 CopyFile(item, strTo, strBasePath); 142 } 143 } 144 145 146 private static void DeleteItself() 147 { 148 ProcessStartInfo psi = new ProcessStartInfo("cmd.exe", "/C ping 1.1.1.1 -n 1 -w 1000 > Nul & Del " + Application.ExecutablePath); 149 psi.WindowStyle = ProcessWindowStyle.Hidden; 150 psi.CreateNoWindow = true; 151 Process.Start(psi); 152 } 153 } 154 }
更新程序界面
using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.IO; using System.Linq; using System.Net; using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; using System.Xml; namespace HW.Print.ServiceClient.Update { public partial class FrmUpdate : Form { private static string m_strkey = "sdfadsfdsfasdf";//定义一个密钥用以验证权限,不适用ticket Random r = new Random(); Action m_actionAfter = null; bool m_blnShow = false; public FrmUpdate(Action actionAfter, bool blnShow = false) { m_blnShow = blnShow; m_actionAfter = actionAfter; InitializeComponent(); } private void Form1_VisibleChanged(object sender, EventArgs e) { if (Visible) { var rect = Screen.PrimaryScreen.WorkingArea; this.Location = new Point(rect.Right - this.Width, rect.Bottom - this.Height); } } private void FrmUpdate_Load(object sender, EventArgs e) { Thread th = new Thread(() => { Down(); this.BeginInvoke(new MethodInvoker(delegate () { this.Close(); })); }); th.IsBackground = true; th.Start(); } private string CheckIsXP(string strUrl) { bool blnXp = false; if (Environment.OSVersion.Version.Major == 5 && Environment.OSVersion.Version.Minor == 1) { blnXp = true; } if (blnXp && strUrl.StartsWith("https")) { strUrl = "http" + strUrl.Substring(5); } return strUrl; } private void SetProcess(string strTitle, int? value, int? maxValue = null) { this.lblMsg.BeginInvoke(new MethodInvoker(delegate () { if (maxValue.HasValue) { this.progressBar1.Maximum = maxValue.Value; } if (value.HasValue) { this.progressBar1.Value = value.Value; } if (!string.IsNullOrEmpty(strTitle)) { this.lblMsg.Text = strTitle; } lblValue.Text = this.progressBar1.Value + "/" + this.progressBar1.Maximum; })); } public void Down() { if (m_blnShow) SetProcess("正在检查版本", null); try { //先清理掉旧文件 try { if (Directory.Exists(System.AppDomain.CurrentDomain.BaseDirectory + "UpdateCache")) { Directory.Delete(System.AppDomain.CurrentDomain.BaseDirectory + "UpdateCache", true); } } catch { } if (!File.Exists(System.AppDomain.CurrentDomain.BaseDirectory + "setting.dat")) { Log.WriteLog("配置文件setting.dat不存在!"); return; } string strFileUrl = File.ReadAllText(System.AppDomain.CurrentDomain.BaseDirectory + "setting.dat"); strFileUrl = CheckIsXP(strFileUrl); //获取列表文件 string json = HttpGet(strFileUrl.Trim('/') + "/getUpdaterList?key=" + Encrypt(m_strkey), Encoding.UTF8); ResponseMessage rm = fastJSON.JSON.ToObject<ResponseMessage>(json); if (rm == null) { Log.WriteLog("获取更新文件错误"); return; } if (!rm.Result) { Log.WriteLog("获取更新文件错误:" + rm.ErrorMessage); return; } //云列表 Dictionary<string, DateTime> lstNewFiles = new Dictionary<string, DateTime>(); XmlDocument doc = new XmlDocument(); doc.LoadXml(rm.KeyValue); var documentElement = doc.DocumentElement; var nodes = documentElement.SelectNodes("//files/file"); foreach (XmlNode item in nodes) { lstNewFiles[item.InnerText] = DateTime.Parse(item.Attributes["time"].Value); } List<string> lstUpdateFile = new List<string>(); string locationXml = System.AppDomain.CurrentDomain.BaseDirectory + "ver.xml"; if (!File.Exists(locationXml)) { lstUpdateFile = lstNewFiles.Keys.ToList(); } else { XmlDocument docLocation = new XmlDocument(); docLocation.Load(locationXml); var documentElementLocation = docLocation.DocumentElement; var nodesLocation = documentElementLocation.SelectNodes("//files/file"); foreach (XmlNode item in nodesLocation) { if (!lstNewFiles.ContainsKey(item.InnerText)) { lstUpdateFile.Add(item.InnerText); } else if (lstNewFiles[item.InnerText] < DateTime.Parse(item.Attributes["time"].Value)) { lstUpdateFile.Add(item.InnerText); } } } if (lstUpdateFile.Count > 0) { string strRootPath = System.AppDomain.CurrentDomain.BaseDirectory + "UpdateCache"; if (!System.IO.Directory.Exists(strRootPath)) { System.IO.Directory.CreateDirectory(strRootPath); } SetProcess("", null, lstUpdateFile.Count); for (int i = 0; i < lstUpdateFile.Count; i++) { if (m_blnShow) SetProcess("正在下载:" + lstUpdateFile[i], i + 1); string filejson = HttpGet(strFileUrl.Trim('/') + "/downloadUpdaterFile?key=" + Encrypt(m_strkey) + "&file=" + System.Web.HttpUtility.UrlEncode(lstUpdateFile[i]), Encoding.UTF8); ResponseMessage filerm = fastJSON.JSON.ToObject<ResponseMessage>(filejson); if (rm == null) { Log.WriteLog("下载更新文件错误"); return; } if (!rm.Result) { Log.WriteLog("下载更新文件错误:" + rm.ErrorMessage); return; } string saveFile = Path.Combine(strRootPath, lstUpdateFile[i]); if (!Directory.Exists(Path.GetDirectoryName(saveFile))) { System.IO.Directory.CreateDirectory(Path.GetDirectoryName(saveFile)); } string strbase64 = filerm.KeyValue; MemoryStream stream = new MemoryStream(Convert.FromBase64String(strbase64)); FileStream fs = new FileStream(strRootPath + "\\" + lstUpdateFile[i], FileMode.OpenOrCreate, FileAccess.Write); byte[] b = stream.ToArray(); fs.Write(b, 0, b.Length); fs.Close(); } doc.Save(System.AppDomain.CurrentDomain.BaseDirectory + "UpdateCache//ver.xml"); if (m_actionAfter != null) { if (m_blnShow) SetProcess("替换文件", null); m_actionAfter(); } if (m_blnShow) SetProcess("更新完成。", null); } else { if (m_blnShow) SetProcess("没有需要更新的文件。", null); } } catch (Exception ex) { if (m_blnShow) SetProcess("获取更新列表失败:" + ex.Message, null); Log.WriteLog(ex.ToString()); } finally { if (m_blnShow) Thread.Sleep(3000); } } private static string encryptKey = "111222333444555666"; //默认密钥向量 private static byte[] Keys = { 0x41, 0x72, 0x65, 0x79, 0x6F, 0x75, 0x6D, 0x79, 0x53, 0x6E, 0x6F, 0x77, 0x6D, 0x61, 0x6E, 0x3F }; /// <summary> /// 加密 /// </summary> /// <param name="encryptString"></param> /// <returns></returns> public static string Encrypt(string encryptString) { if (string.IsNullOrEmpty(encryptString)) return string.Empty; RijndaelManaged rijndaelProvider = new RijndaelManaged(); rijndaelProvider.Key = Encoding.UTF8.GetBytes(encryptKey.Substring(0, 32)); rijndaelProvider.IV = Keys; ICryptoTransform rijndaelEncrypt = rijndaelProvider.CreateEncryptor(); byte[] inputData = Encoding.UTF8.GetBytes(encryptString); byte[] encryptedData = rijndaelEncrypt.TransformFinalBlock(inputData, 0, inputData.Length); return System.Web.HttpUtility.UrlEncode(Convert.ToBase64String(encryptedData)); } public static string HttpGet(string url, Encoding encodeing, Hashtable headht = null) { HttpWebRequest request; //如果是发送HTTPS请求 //if (url.StartsWith("https", StringComparison.OrdinalIgnoreCase)) //{ //ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(CheckValidationResult); request = WebRequest.Create(url) as HttpWebRequest; request.ServicePoint.Expect100Continue = false; request.ProtocolVersion = HttpVersion.Version11; request.KeepAlive = true; //} //else //{ // request = WebRequest.Create(url) as HttpWebRequest; //} request.Method = "GET"; //request.ContentType = "application/x-www-form-urlencoded"; request.Accept = "*/*"; request.Timeout = 30000; request.AllowAutoRedirect = false; WebResponse response = null; string responseStr = null; if (headht != null) { foreach (DictionaryEntry item in headht) { request.Headers.Add(item.Key.ToString(), item.Value.ToString()); } } try { response = request.GetResponse(); if (response != null) { StreamReader reader = new StreamReader(response.GetResponseStream(), encodeing); responseStr = reader.ReadToEnd(); reader.Close(); } } catch (Exception) { throw; } return responseStr; } } }
定义服务端接口,你可以用任意接口都行,我这里用webapi
获取文件列表
1 [HttpGet] 2 public HttpResponseMessage GetUpdaterList(string key) 3 { 4 HttpResult httpResult = new HttpResult(); 5 if (!CheckKey(key)) 6 { 7 httpResult.KeyValue = ""; 8 httpResult.Result = false; 9 httpResult.ErrorMessage = "无权限访问"; 10 } 11 else 12 { 13 //获取printupdate目录下update.exe的修改日期返回 14 string path = Path.Combine(HttpRuntime.AppDomainAppPath, "printupdate"); 15 StringBuilder strXml = new StringBuilder(); 16 strXml.AppendLine("<?xml version=\"1.0\" encoding=\"utf-8\" ?>"); 17 strXml.AppendLine("<files>"); 18 if (Directory.Exists(path)) 19 { 20 string[] fs = Directory.GetFiles(path, "*.*", SearchOption.AllDirectories); 21 var _p = path.ToLower().Trim().Length + 1; 22 foreach (var item in fs) 23 { 24 var dt = File.GetLastAccessTime(item); 25 strXml.AppendLine("<file time=\"" + dt.ToString("yyyy-MM-dd HH:mm:ss") + "\">" + item.Substring(_p) + "</file>"); 26 } 27 } 28 strXml.AppendLine("</files>"); 29 30 httpResult.KeyValue = strXml.ToString(); 31 httpResult.Result = true; 32 httpResult.ErrorMessage = ""; 33 } 34 return new HttpResponseMessage { Content = new StringContent(httpResult.ToJson(), Encoding.GetEncoding("UTF-8"), "application/json") }; 35 }
下载文件,我这里将文件序列号为base64字符串了,你可以直接返回文件流也行
1 [HttpGet] 2 public HttpResponseMessage DownloadUpdaterFile(string key, string file) 3 { 4 HttpResult httpResult = new HttpResult(); 5 if (!CheckKey(key)) 6 { 7 httpResult.KeyValue = ""; 8 httpResult.Result = false; 9 httpResult.ErrorMessage = "无权限访问"; 10 } 11 else 12 { 13 string path = Path.Combine(HttpRuntime.AppDomainAppPath + "printupdate", file); 14 if (!File.Exists(path)) 15 { 16 httpResult.KeyValue = ""; 17 httpResult.Result = false; 18 httpResult.ErrorMessage = "文件不存在"; 19 } 20 else 21 { 22 httpResult = ConvertToBase64Type(path); 23 } 24 } 25 return new HttpResponseMessage { Content = new StringContent(httpResult.ToJson(), Encoding.GetEncoding("UTF-8"), "application/json") }; 26 27 }
1 HttpResult ConvertToBase64Type(string fileName) 2 { 3 HttpResult httpResult = new HttpResult(); 4 var byts = File.ReadAllBytes(fileName); 5 httpResult.KeyValue = Convert.ToBase64String(byts); 6 return httpResult; 7 }
1 bool CheckKey(string key) 2 { 3 return key == Encryption.Encrypt(m_strkey); 4 }
1 private static string encryptKey = "111222333444"; 2 3 //默认密钥向量 4 private static byte[] Keys = { 0x41, 0x72, 0x65, 0x79, 0x6F, 0x75, 0x6D, 0x79, 0x53, 0x6E, 0x6F, 0x77, 0x6D, 0x61, 0x6E, 0x3F }; 5 /// <summary> 6 /// 加密 7 /// </summary> 8 /// <param name="encryptString"></param> 9 /// <returns></returns> 10 public static string Encrypt(string encryptString) 11 { 12 if (string.IsNullOrEmpty(encryptString)) 13 return string.Empty; 14 RijndaelManaged rijndaelProvider = new RijndaelManaged(); 15 rijndaelProvider.Key = Encoding.UTF8.GetBytes(encryptKey.Substring(0, 32)); 16 rijndaelProvider.IV = Keys; 17 ICryptoTransform rijndaelEncrypt = rijndaelProvider.CreateEncryptor(); 18 19 byte[] inputData = Encoding.UTF8.GetBytes(encryptString); 20 byte[] encryptedData = rijndaelEncrypt.TransformFinalBlock(inputData, 0, inputData.Length); 21 22 return Convert.ToBase64String(encryptedData); 23 }
需要注意的地方:
1、我这里用到了json,那么不能直接饮用json的dll文件,会出现更新时候占用的问题,可以使用fastjson的开源代码,放进来解决,你可以直接使用xml格式的返回内容,这样就不需要json了,这样更方便
2、如果你的下载接口是返回的文件流,那么你更新程序里面直接接收流保存文件就行了
3、Program.cs里面,停止服务的功能,其实是可以通过传递参数的形式来停止,我这里写死了,你们根据自己需求修改
效果
你可以根据自己的需求,修改下界面效果,这是最简单的示例界面而已。
出处:https://www.cnblogs.com/bfyx/p/13985825.html
关注我】。(●'◡'●)
如果,您希望更容易地发现我的新博客,不妨点击一下绿色通道的【因为,我的写作热情也离不开您的肯定与支持,感谢您的阅读,我是【Jack_孟】!
本文来自博客园,作者:jack_Meng,转载请注明原文链接:https://www.cnblogs.com/mq0036/p/12889184.html
【免责声明】本文来自源于网络,如涉及版权或侵权问题,请及时联系我们,我们将第一时间删除或更改!
posted on 2020-05-14 15:48 jack_Meng 阅读(1355) 评论(0) 编辑 收藏 举报