最近在做一个.Net C/S的系统,需要实现自动更新。MS已经提供了ClickOnce,很方便,但是用起来不太习惯,还是决定自己写一个简单的。
自动更新无非文件比较、下载、启动程序几个步骤,其中文件比较可以通过手动在配置文件中维护版本号,也可以比较文件的MD5值,或者在.Net里还可以用Assembly或文件的版本号。因为怕麻烦,手动维护不考虑,剩下两者各有所长,都提供了以供选择。下载就比较简单了,http、ftp、WebService都可以选择。启动程序一般用System.Diagnostics.Process.Start就可以,我用的是AppDomain.ExecuteAssembly。
要自动生成文件版本信息,需要有个服务端,可以是WebService等等,我采用的是自己实现IHttpHandler,提供文件的版本信息和下载。
下面是UpdateHelper的类图和定义:
GetUpdateInfos、CheckIfNeedUpdate、DownloadFile分别是获取文件版本信息、比较文件是否是最新的、下载文件的作用,很简单。
- public static class UpdateHelper
- {
- public static UpdateInfo[] GetUpdateInfos()
- {
- HttpWebRequest request = (HttpWebRequest)WebRequest.Create(Properties.Settings.Default.Server);
- request.Method = "GET";
- HttpWebResponse response = (HttpWebResponse)request.GetResponse();
- if (response.StatusCode == HttpStatusCode.OK)
- {
- using (Stream output = request.GetResponse().GetResponseStream())
- {
- XmlSerializer xml = new XmlSerializer(typeof(UpdateInfo[]));
- return (UpdateInfo[])xml.Deserialize(output);
- }
- }
- else
- {
- throw new Exception("Exception occurs on the server.");
- }
- }
- public static bool CheckIfNeedUpdate(UpdateInfo update)
- {
- if (!File.Exists(update.FileName))
- {
- return true;
- }
- else
- {
- switch (update.VersionType)
- {
- case VersionType.FileVersion:
- return FileVersionInfo.GetVersionInfo(update.FileName).FileVersion != update.Version;
- case VersionType.MD5:
- using (FileStream file = File.OpenRead(update.FileName))
- {
- return Convert.ToBase64String(MD5.Create().ComputeHash(file)) != update.Version;
- }
- default: return false;
- }
- }
- }
- public static void Update()
- {
- UpdateInfo[] updates = UpdateHelper.GetUpdateInfos();
- List<Thread> downloadsThreads = new List<Thread>();
- foreach (UpdateInfo update in updates)
- {
- if (UpdateHelper.CheckIfNeedUpdate(update))
- {
- Thread thread = new Thread(delegate(object param) { UpdateHelper.DownloadFile((string)param); });
- thread.Start(update.FileName);
- downloadsThreads.Add(thread);
- }
- }
- foreach (Thread thread in downloadsThreads) thread.Join();
- }
- public static void DownloadFile(string fileName)
- {
- HttpWebRequest request = (HttpWebRequest)WebRequest.Create(Properties.Settings.Default.Server + "?download=" + fileName);
- request.Method = "GET";
- HttpWebResponse response = (HttpWebResponse)request.GetResponse();
- if (response.StatusCode == HttpStatusCode.OK)
- {
- byte[] buffer = new byte[8096];
- using (Stream output = request.GetResponse().GetResponseStream())
- {
- using (FileStream file = File.Open(fileName, FileMode.OpenOrCreate))
- {
- int length = output.Read(buffer, 0, buffer.Length);
- while (length != 0)
- {
- file.Write(buffer, 0, length);
- length = output.Read(buffer, 0, buffer.Length);
- }
- }
- }
- }
- else
- {
- throw new Exception("Exception occurs on the server.");
- }
- }
- }
- public class UpdateInfo
- {
- public UpdateInfo() { }
- public UpdateInfo(string filename, VersionType versionType, string version)
- {
- FileName = filename;
- VersionType = versionType;
- Version = version;
- }
- public string FileName;
- public VersionType VersionType;
- public string Version;
- }
- public enum VersionType
- {
- FileVersion,
- MD5
- }
UpdateServiceHandler定义:
- public class UpdateServiceHandler : IHttpHandler
- {
- #region IHttpHandler Members
- public bool IsReusable
- {
- get { return true; }
- }
- public void ProcessRequest(HttpContext context)
- {
- if (String.Equals(context.Request.RequestType, "GET", StringComparison.OrdinalIgnoreCase))
- {
- string filename = context.Request.QueryString["download"];
- UpdateInfoSection updateInfoSection = GetUpdateConfig();
- if (String.IsNullOrEmpty(filename))
- {
- context.Response.ContentEncoding = Encoding.UTF8;
- context.Response.ContentType = "text/xml";
- foreach (UpdateInfo update in updateInfoSection.UpdateFiles.Values)
- {
- switch (update.VersionType)
- {
- case VersionType.FileVersion:
- update.Version = FileVersionInfo.GetVersionInfo(update.Path).FileVersion;
- break;
- case VersionType.MD5:
- using (FileStream file = File.OpenRead(update.Path)) update.Version = Convert.ToBase64String(MD5.Create().ComputeHash(file));
- break;
- }
- }
- XmlSerializer xml = new XmlSerializer(typeof(List<UpdateInfo>));
- xml.Serialize(context.Response.Output, new List<UpdateInfo>(updateInfoSection.UpdateFiles.Values));
- }
- else
- {
- UpdateInfo update;
- if (updateInfoSection.UpdateFiles.TryGetValue(filename, out update))
- {
- context.Response.ContentEncoding = Encoding.UTF8;
- context.Response.ContentType = "application/x-msdownload";
- context.Response.AppendHeader("Content-Disposition", "attachment;filename=" + filename);
- context.Response.WriteFile(update.Path);
- }
- else
- {
- context.Response.ContentEncoding = Encoding.UTF8;
- context.Response.ContentType = "text/html";
- context.Response.Output.WriteLine("File {0} not fount.", filename);
- }
- }
- }
- }
- private UpdateInfoSection GetUpdateConfig()
- {
- UpdateInfoSection updateInfoSection = (UpdateInfoSection)ConfigurationManager.GetSection("ClientUpdateInfo");
- if (!updateInfoSection.Initialize)
- {
- foreach (UpdateInfo update in updateInfoSection.UpdateFiles.Values)
- {
- if (String.IsNullOrEmpty(update.Path)) update.Path = Path.Combine(updateInfoSection.DefaultForlder, update.FileName);
- update.Path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, update.Path);
- }
- updateInfoSection.Initialize = true;
- }
- return updateInfoSection;
- }
- #endregion
- }
- public class UpdateInfoSection : ConfigurationSection
- {
- public string DefaultForlder;
- public Dictionary<string, UpdateInfo> UpdateFiles = new Dictionary<string, UpdateInfo>();
- public bool Initialize = false;
- protected override void DeserializeSection(System.Xml.XmlReader reader)
- {
- while (reader.Read())
- {
- if (reader.NodeType == System.Xml.XmlNodeType.Element)
- {
- switch (reader.Name)
- {
- case "File":
- string versionType = reader.GetAttribute("VersionType");
- UpdateFiles.Add(reader.GetAttribute("FileName"),
- new UpdateInfo(
- reader.GetAttribute("FileName"),
- String.IsNullOrEmpty(versionType) ? VersionType.FileVersion : (VersionType)Enum.Parse(typeof(VersionType), versionType, true),
- reader.GetAttribute("Version"),
- reader.GetAttribute("Path")));
- break;
- case "DefaultFolder":
- DefaultForlder = reader.ReadString();
- break;
- }
- }
- }
- }
- }
- public class UpdateInfo
- {
- public UpdateInfo() { }
- public UpdateInfo(string filename, VersionType versionType, string version, string path)
- {
- FileName = filename;
- VersionType = versionType;
- Version = version;
- Path = path;
- }
- public string FileName;
- [XmlIgnore]
- public string Path;
- public VersionType VersionType;
- public string Version;
- }
- public enum VersionType
- {
- FileVersion,
- MD5
- }
注意下其中UpdateInfo多了一个Path属性,表示文件的实际路径,不需要显示给客户端,因此加了XmlIgnore标记。
在服务端需要配置UpdateInfoSection项和UpdateServiceHandler的地址映射。
在<system.web>节<httpHandlers>下添加一项:
- <httpHandlers>
- <add verb="*" path="update.aspx" type="MyService.UpdateServiceHandler"/>
- </httpHandlers>
其中MyService.UpdateServiceHandler需要替换为UpdateServiceHandler的完整名称。
然后添加UpdateInfoSection项:
- <configSections>
- <section name="ClientUpdateInfo" type="MyService.UpdateInfoSection"/>
- </configSections>
- <ClientUpdateInfo>
- <DefaultFolder>bin</DefaultFolder>
- <File FileName ="log4net.dll"/>
- <File FileName ="Castle.Core.dll"/>
- <File FileName ="Castle.DynamicProxy2.dll"/>
- <File FileName ="Client.dll" Path ="bin/Client.exe" />
- <File FileName ="client.config" VersionType="MD5"/>
- </ClientUpdateInfo>
DefaultFolder是在没有为文件提供Path时默认的包含文件的目录,配置比较简单。
好了,现在可以打开update.aspx页面看到文件的版本信息
在update.aspx后加上参数的话,例如?download=log4net.dll,就可以下载文件了。
最后,客户端只需要更新并运行程序就可以了,当然还需要设置一下服务端的地址(Properties.Settings.Default.Server):
- static class Program
- {
- /// <summary>
- /// The main entry point for the application.
- /// </summary>
- [STAThread]
- static void Main()
- {
- UpdateHelper.Update();
- AppDomain domain = AppDomain.CreateDomain("client");
- domain.SetData("APP_CONFIG_FILE", "client.config");
- domain.ExecuteAssembly("Client.dll", AppDomain.CurrentDomain.Evidence, Environment.GetCommandLineArgs());
- }
- }