这里说的是C/S客户端更新,其他情况不做讨论
- 1.更新程序减少依赖
之前做过一次更相信需要更新Newtonsoft.Json.dll 的版本
但是我的更新程序引用了这个文件 也就是说执行更新程序的时候这个文件是占用状态,更新不了!
所以更新程序最好是零引用,只用系统自带的dll类库就可以,尽可能【小】
- 2.更新的脚本
软件更新有时候需要对系统设置进行一些更改,例如注册表,防火墙之类的,这个时候就需要执行一些脚本来写入注册表或开启防火墙端口
这里可以设定一个执行脚本,需要注意的是和更新程序一样,脚本的引用支持程序要简单,你不能为了执行脚本就要用户装一个脚本解析器。
额外说一句虽然vbs脚本系统默认都是认的,但是我也有遇到过【没有找到文件扩展VBS的脚本引擎】这样的错误,虽然可以修复这个错误但是也需要尽可能避免这个问题,用户不会在乎错误原因是什么,只会认为:“我用了你的系统报了一个错误”
- 3.容灾
更新不一定能够绝对成功的,就是你的代码再完美,用户操作可能不合法,可能被杀软拦截,可能操作系统本身就有问题,等等意外,这个时候首先尽可能保证更新失败也可以正常使用系统,如果旧版本确实放弃了,无法使用,可以给出友好提示,联系管理员之类的。
- 4.版本检查
如果不是最新版本不允许使用软件系统,软件有一个BUG不修复的话会导致系统异常,也就是不更新软件没法使用,这个时候需要检查版本,如果版本不对则直接禁止使用。
这个功能比容灾更复杂一些,首先为了保证设定有效控制,需要将限制放在服务端,然后客户端使用的时候将自己的版本号告诉服务端,服务端检查后决定是否放行。
版本号分为主程序版本号以及引用文件版本号,可能主程序版本号没有问题,但是某一个功能版本号没有更新,则这个功能无法使用,其他功能不受影响
- 5.方案
这里记录一下我正在使用的方案,有不少不足之处,先记下来,后面优化
首先服务器上放一个txt文件,更新时读取这个txt文件
txt里面包含软件最新版本,软件每个需要用到的文件的版本,用于比较最新版本然后下载有更新的文件
为了生成这样的txt文件我还写了一个生成的小工具
然后时vbs脚本放在服务器端,更新时下载到客户端执行(如上文描述,有部分客户端操作系统提示错误【没有找到文件扩展VBS的脚本引擎】,后面准备用bat或exe)
vbs里面是一个延时执行的代码,比如【更新程序】时a.exe 如果要更新【更新程序】本身则将新版本的【更新程序】重命名为a2.exe放在服务端,然后下载到客户端,因为延时程序设定在a.exe退出后删除a.exe然后将a2.exe重命名为a.exe,以实现更新自身
同时vbs里面也有删除函数,用来清除多余的文件
容灾方案正在设计中……
设想:将旧版本重命名,追加【.old】,然后将新版本文件下载追加【.new】,所有下载完成之后再统一删除重命名,如果出错则把【.old】恢复
还没有实践,因为加了这个功能本质上违反了【简单】的原则,功能越多越容易发生问题,先将方案实现看看效果
-----------------------正在使用的方案的代码
/******************************************************************************* * Copyright © 2010-2020 陈恩点版权所有 * Author: 陈恩点 * First Create: 2012/8/21 11:49:53 * Contact: 18115503914 * Description: MyRapid快速开发框架 *********************************************************************************/ using Newtonsoft.Json; using System; using System.Collections.Generic; using System.ComponentModel; using System.Configuration; using System.Data; using System.Diagnostics; using System.Drawing; using System.IO; using System.Linq; using System.Net; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; using System.Xml; using System.Xml.Serialization; namespace MyRapid.Launcher { public partial class MyWait : Form { Process process; string ProcessName = ""; string ScriptName = ""; string ImagePath = ""; string FormName = "主窗体"; string urlPath = ""; int WaitSecond = 30; Stopwatch sw = new Stopwatch(); public MyWait() { sw.Restart(); InitializeComponent(); //用参数把4个变量传入 //分别为 //1 ProcessName 程序路径 //2 ImagePath 动画路径 //3 ScriptName 预更新脚本 //4 FormName 启动程序的标识,用于判断程序是否加在完毕 ProcessName = GetVal("exePath"); ScriptName = GetVal("vbsPath"); ImagePath = GetVal("imgPath"); FormName = GetVal("exeTitle"); MoveHandle(label1); MoveHandle(this); MoveHandle(progressBar1); if (File.Exists("tips.txt")) { tips = File.ReadAllLines("tips.txt"); } } private void UpdateFile() { try { DateTime ver = DateTime.Parse(GetVal("version")); WebClient wc = new WebClient(); wc.Encoding = Encoding.UTF8; label1.Text = "正在检查更新..."; string updateFile = "update.txt"; urlPath = GetVal("urlPath"); wc.DownloadFile(urlPath, updateFile); if (!File.Exists(updateFile)) return; string[] verString = File.ReadAllLines(updateFile); if (verString.Length == 0) return; if (DateTime.Parse(verString[0]) <= ver) return; string fName = Path.GetFileNameWithoutExtension(ProcessName); fName = Path.GetFileName(ProcessName); var prcList = Process.GetProcesses().Where(pr => pr.ProcessName == Path.GetFileNameWithoutExtension(ProcessName) || pr.ProcessName == Path.GetFileName(ProcessName)); if (prcList.Count() > 0) { label1.Text = "主程序运行中,无法更新..."; return; } progressBar1.Style = ProgressBarStyle.Continuous; progressBar1.Maximum = verString.Length; progressBar1.Value = 0; foreach (string sf in verString) { //Tips if (tips != null && tips.Length > 0 && sw.Elapsed.TotalSeconds % 5 == 1) { int i = DateTime.Now.Millisecond % (tips.Length - 1); label3.Text = string.Format(tip, tips[i]); this.Refresh(); } if (progressBar1.Value < verString.Length) progressBar1.Value += 1; if (!sf.Contains("|")) continue; string[] ups = sf.Split('|'); if (DateTime.Parse(ups[0]) <= ver) continue; //如果目录不存在这创建 string fDir = Path.GetDirectoryName(ups[1]); if (!string.IsNullOrEmpty(fDir) && !Directory.Exists(fDir)) { Directory.CreateDirectory(fDir); } //下载文件 label1.Text = ups[1]; this.Refresh(); wc.DownloadFile(ups[2], ups[1]); } SetVal("version", DateTime.Now.ToString()); label1.Text = "更新结束:正在启动主程序..."; progressBar1.Style = ProgressBarStyle.Marquee; } catch (Exception ex) { label1.Text += "更新失败,请重试或联系管理员协助处理:" + ex.Message; } } private void StartMain() { //执行预更新脚本 if (File.Exists(ScriptName)) { if (ScriptName.EndsWith(".vbs")) { ProcessStartInfo startInfo = new ProcessStartInfo(); startInfo.FileName = "wscript.exe"; startInfo.Arguments = ScriptName; Process.Start(startInfo); } else { Process script = Process.Start(ScriptName); } } if (File.Exists(ProcessName)) { //启动程序 ProcessStartInfo processStartInfo = new ProcessStartInfo(); processStartInfo.FileName = ProcessName; processStartInfo.WorkingDirectory = Path.GetDirectoryName(ProcessName); process = Process.Start(processStartInfo); } else { Environment.Exit(0); } if (File.Exists(ImagePath)) { //加载动画 Image image = Image.FromFile(ImagePath); this.BackgroundImage = image; } } private void timer1_Tick(object sender, EventArgs e) { //这里是用于判断结束 分两种 超时 或 已完成 if (sw.Elapsed.TotalSeconds > WaitSecond) Environment.Exit(0); if (process == null) Environment.Exit(0); if (process.HasExited) Environment.Exit(0); process.Refresh(); if (process.MainWindowTitle.Equals(FormName)) Environment.Exit(0); //Console.WriteLine(process.MainWindowTitle); //Console.WriteLine(sw.Elapsed.TotalMilliseconds); } private void MyWait_Shown(object sender, EventArgs e) { this.Refresh(); UpdateFile(); if (File.Exists("update.txt")) File.Delete("update.txt"); sw.Restart(); timer1.Enabled = true; StartMain(); } #region Tips string[] tips; string tip = "小贴士:{0}"; #endregion #region Function public string GetVal(string key) { try { return ConfigurationManager.AppSettings.Get(key); } catch { throw; } } public void SetVal(string key, string value) { try { Configuration configuration = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None); configuration.AppSettings.Settings[key].Value = value; configuration.Save(ConfigurationSaveMode.Modified); } catch { throw; } } /// <summary> /// 为控件添加移动功能 /// </summary> /// <param name="ctrl">鼠标按下的控件</param> /// <param name="who">移动的控件(若不存在则为控件自己) /// 1:控件所在窗体; /// 2:控件的父级; /// 3:控件自己. </param> public void MoveHandle(Control ctrl, int who = 1) { try { Control mCtrl;//= ctrl.FindForm(); switch (who) { case 1: mCtrl = ctrl.FindForm(); break; case 2: mCtrl = ctrl.Parent; break; case 3: mCtrl = ctrl; break; default: mCtrl = ctrl.FindForm(); break; } if (mCtrl == null) { mCtrl = ctrl; } Point sourcePoint = new Point(0, 0); bool isMove = false; ctrl.MouseDown += delegate (object sender, MouseEventArgs e) { sourcePoint = e.Location; isMove = true; }; ctrl.MouseMove += delegate (object sender, MouseEventArgs e) { if (isMove) mCtrl.Location = new Point(mCtrl.Location.X + e.X - sourcePoint.X, mCtrl.Location.Y + e.Y - sourcePoint.Y); }; ctrl.MouseUp += delegate (object sender, MouseEventArgs e) { isMove = false; }; } catch (Exception ex) { throw ex; } } #endregion } }
----------配置文件
<?xml version="1.0" encoding="utf-8"?> <configuration> <appSettings> <add key="exePath" value="MyRapid.Client.exe"/> <add key="vbsPath" value="update.vbs"/> <add key="imgPath" value=""/> <add key="exeTitle" value="用户登录"/> <add key="urlPath" value="http://127.0.0.1:4824/update.txt"/> <add key="version" value="2019/9/17 9:12:23" /> </appSettings> <startup> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/> </startup> </configuration>
----------服务端update.txt的格式
2019/8/23 16:48:18 2019/8/23 16:48:17|MyRapid.Client.exe|http://myerp.oicp.io:4824/cBest/MyRapid.Client.exe 2019/8/23 13:17:09|MyRapid.Launcher2.exe|http://myerp.oicp.io:4824/cBest/MyRapid.Launcher2.exe 2019/8/19 11:37:20|tips.txt|http://myerp.oicp.io:4824/cBest/tips.txt 2019/8/19 11:28:07|update.rar|http://myerp.oicp.io:4824/cBest/update.rar 2049/8/19 11:27:43|update.vbs|http://myerp.oicp.io:4824/cBest/update.vbs