共享WinCE6.0 下的一个软件升级程序
需求场境
厂里搞了几把PDA扫描枪,用来对下料工序进行扫描确认,本来以为就几把,直接连到电脑上用ActiveSync复制程序上去就好了,但是刚开发的程序一天要改个好几次,把枪收回来,装好再拿回去,一天做个4,5次简直叫人抓狂了。于是就决定自己整个简单的更新程序.
基本设计
1.要发布到WinCE上的程序比较简单,每个文件也比较小(都在500KB以下)所以决定直接让WebService 返回byte[]类型的文件内容然后在WinCE里写下文件,
另外也提供GetFile.ashx页面,里面使用Response.TransmitFile(path)以供客户端下载比较大的文件,当然在局域网环境下网速不是问题.
2.版本方面,不打算搞成每个文件都有一个版本,而是一个总版本,只要本地版本号小于服务器端版本号那么就下载服务器上的文件覆盖本地全部文件,
服务端与本地升级程序都有个配置文件来记录当前版本。
3.服务端使用IIS,不使用数据库,在站点下建立一个目录,把要发布的软件copy到目录下,客户端升级程序会在其工作目录创建一个跟服务器目录结构相同的文件夹结构,并下载各文件夹下的文件。
扩展与加强
当然一个完整的更新程序还有很多地方需要做,比方多版本管理、客户端有嵌入数据库时不应该覆盖,自动生成桌面快捷方式,注册启动项目等,但是对我来说,只要能下载就减少我80%工作量了,剩下的直接人肉搞定,作为上了年纪的程序员,尽量避免做折磨前列腺、颈椎、腰椎这些折寿的事情。
代码部分
升级站点跟扫描的WebService放在一起,分离打包上来太麻烦了,所以就把主要文件copy上来,相信你化个10几分钟就能把项目建立会来。
1.服务端部分
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Services; namespace PDAJob.PDAService.Service { /// <summary> /// ServiceAppSync 的摘要说明 /// </summary> [WebService(Namespace = "http://tempuri.org/")] [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)] [System.ComponentModel.ToolboxItem(false)] // 若要允许使用 ASP.NET AJAX 从脚本中调用此 Web 服务,请取消对下行的注释。 // [System.Web.Script.Services.ScriptService] public class ServiceAppSync : System.Web.Services.WebService { [WebMethod] public FileReadResponse GetFile(string path) { return new AppSyncMgr().GetFile(path); } [WebMethod] public FileReadData GetFileData(string path) { var r = new AppSyncMgr().GetFile(path); var response = new FileReadData() { Code = r.Code, Msg = r.Msg }; if (r.Code == 0) { response.DataB64= Convert.ToBase64String(r.Data); } return response; } [WebMethod] public List<AppSyncItem> GetList() { return new AppSyncMgr().GetList(); } [WebMethod] public string GetVersion() { return new AppSyncMgr().GetVersion(); } } }
注意:上面说直接返回byte[]给客户端,但是在wince + vs2005下发现不行,于是直接将byte[] basic64成string后在WinCE上还原。
2.主要业务逻辑文件
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Hosting; using System.Configuration; using System.Collections.Specialized; using System.Collections; namespace PDAJob.PDAService.Service { using System.Xml; using System.Xml.Serialization; using System.IO; public class AppSyncMgr { private List<FileSystemInfo> ExcludeItems = new List<FileSystemInfo>(); private AppSyncConfig _Config; public AppSyncConfig Config { get { if (_Config == null) { _Config = new AppSyncConfig( new FileConfigStore()) ; } return _Config; } } /// <summary> /// 获取版本号 /// 客户端版本只有被服务端版本号小时才同步 /// </summary> /// <returns></returns> public string GetVersion() { if (!Config.Enabled) return "0.0.0"; return Config.Version; } public FileReadResponse GetFile(string path) { var root=Config.Path; var filename = Path.Combine(root, path); var response = new FileReadResponse(); try { var bytes = File.ReadAllBytes(filename); response.Data = bytes; } catch (Exception ex) { response.Code = 1; response.Msg = ex.Message; } return response; } /// <summary> /// 获取同步列表 /// </summary> /// <returns></returns> public List<AppSyncItem> GetList() { var root = new DirectoryInfo(Config.Path); #region 构建排除目录列表 Config.Bill.Where(ent => ent.IsExclude).ToList().ForEach(ent => { string path = Path.Combine(root.FullName, ent.FileName.Trim(@"/\".ToArray())); if (ent.IsDir) { ExcludeItems.Add(new DirectoryInfo(path)); } else { ExcludeItems.Add(new FileInfo(path)); } }); #endregion #region 加载目录下文件与目录 var list = new List<AppSyncItem>(); ReadList(list, root); //去掉前缀目录 list.ForEach(ent => { ent.FileName= ent.FileName.Remove(0, root.FullName.Length); }); #endregion return list; } /// <summary> /// 项目是否在排除列表中 /// </summary> /// <param name="fs"></param> /// <returns></returns> private bool IsExcludeItem(FileSystemInfo fs) { foreach (var item in ExcludeItems) { if (string.Compare(item.FullName, fs.FullName, true) == 0 && fs.GetType() == item.GetType() ) { return true; } } return false; } private void ReadList(List<AppSyncItem> list, DirectoryInfo dir) { foreach (var fs in dir.GetFileSystemInfos()) { if (IsExcludeItem(fs)) continue; var item = new AppSyncItem(); item.FileName = fs.FullName.Replace(@"/",@"\"); if(fs is FileInfo) { item.FileSize = (fs as FileInfo).Length; item.IsDir=false; }else{ item.IsDir=true; ReadList(list, (fs as DirectoryInfo)); } list.Add(item); } } } /// <summary> /// 读取一个文件 /// Code=0表示成功 /// </summary> [Serializable] public class FileReadResponse { public int Code { get; set; } public string Msg { get; set; } public byte[] Data { get; set; } } [Serializable] public class FileReadData { public int Code { get; set; } public string Msg { get; set; } public string DataB64 { get; set; } } /// <summary> /// 同步目录下的全部文件, /// 排除清单文件中的排除项目 /// </summary> public class AppSyncConfig { public const string AppSync_Path = "AppSync_Path"; public const string AppSync_Version = "AppSync_Version"; public const string AppSync_Enabled = "AppSync_Enabled"; public const string AppSync_BillPath = "AppSync_BillPath"; public const string AppSync_Mode = "AppSync_Model"; public ICfgStore Store { get; set; } /// <summary> /// 需要发布文件存放目录 /// </summary> public string Path { get; set; } public string Version { get; set; } /// <summary> /// 是否开启同步系统 /// </summary> public bool Enabled { get; set; } /// <summary> /// 同步清单文件路径 /// </summary> public string BillPath { get; set; } public string Mode { get; set; } private List<AppSyncItem> _Bill = null; public List<AppSyncItem> Bill { get { if (_Bill == null) { _Bill = ReadBill(); } return _Bill; } } /// <summary> /// 从配置文件读取相关设置 /// </summary> public AppSyncConfig(ICfgStore store) { Store = store; Store.LoadConfig(this); } /// <summary> /// 可能返回null /// </summary> /// <param name="path"></param> /// <returns></returns> public List<AppSyncItem> ReadBill() { string path = BillPath; List<AppSyncItem> list = null; if (File.Exists(path)) { XmlSerializer serializer = new XmlSerializer(typeof(List<AppSyncItem>)); using (var sr = new StreamReader(path)) { list = serializer.Deserialize(sr) as List<AppSyncItem>; sr.Close(); } } if (list == null) list = new List<AppSyncItem>(); return list; } public void SaveBill() { string path = BillPath; List<AppSyncItem> list = this.Bill; XmlSerializer serializer = new XmlSerializer(typeof(List<AppSyncItem>)); using (var sr = new StreamWriter(path)) { serializer.Serialize(sr, list); sr.Close(); } } } #region 主配置文件存储 public interface ICfgStore { void LoadConfig(AppSyncConfig cfg); } /// <summary> /// 配置文件加在实现 /// </summary> public class WebConfigStore : ICfgStore { public void LoadConfig(AppSyncConfig cfg) { cfg.Path = ConfigurationManager.AppSettings[AppSyncConfig.AppSync_Path]; cfg.Path = HostingEnvironment.MapPath(cfg.Path); cfg.Version = ConfigurationManager.AppSettings[AppSyncConfig.AppSync_Version]; cfg.Enabled = bool.Parse(ConfigurationManager.AppSettings[AppSyncConfig.AppSync_Enabled]); cfg.BillPath = ConfigurationManager.AppSettings[AppSyncConfig.AppSync_BillPath]; cfg.BillPath = HostingEnvironment.MapPath(cfg.BillPath); } } /// <summary> /// 文本文件实现 /// 一行保存一个配置 /// 如:AppSync_Path=/SyncDir/Files/ /// /// </summary> public class FileConfigStore : ICfgStore { public string _FileName = "/AppSyncCfg.txt"; public string FileName { get { return HostingEnvironment.MapPath(_FileName); } } public void LoadConfig(AppSyncConfig cfg) { var list = ReadFile(); cfg.Path = GetV(AppSyncConfig.AppSync_Path,list); cfg.Path = HostingEnvironment.MapPath(cfg.Path); cfg.Version =GetV(AppSyncConfig.AppSync_Version,list); cfg.Enabled = bool.Parse(GetV(AppSyncConfig.AppSync_Enabled,list)); cfg.BillPath = GetV(AppSyncConfig.AppSync_BillPath,list); cfg.BillPath = HostingEnvironment.MapPath(cfg.BillPath); } private string GetV(string key,List<Tuple<string,string>> list) { foreach (var item in list) { if (string.Compare(item.Item1, key,true) == 0) { return item.Item2; } } throw new Exception(string.Format("未找到Key:{0},对应的配置!",key)); } private List<Tuple<string,string>> ReadFile() { if (!File.Exists(FileName)) { throw new Exception(string.Format("未找到同步配置文件{0}!", _FileName)); } string[] lines = File.ReadAllLines(FileName); List<Tuple<string, string>> list = new List<Tuple<string, string>>(); foreach (string line in lines) { var paire= line.Split("=".ToArray(), StringSplitOptions.RemoveEmptyEntries); if (paire.Length != 2) { throw new Exception("配置文件的格式错误,需要是每行Name=Value的形式"); } list.Add(new Tuple<string,string>(paire[0].Trim(),paire[1].Trim())); } return list; } } #endregion }
注意:
1.本来版本配置文件放在Web.config中的,但是每次改完Web.config后网站就重新加载,搞的第一次都等老久,所以就做了文本配置文件放在根目录下。
2.服务器端提供一个bill.xml,可以用来配置同步时需要排除的文件或目录,当然以后可以考虑来控制数据库文件只在客户端不存在时下载。
数据结构
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Xml.Serialization; using System.Xml; namespace PDAJob.PDAService.Service { [Serializable] public class AppSyncItem { /// <summary> /// 文件名,包括扩展名 /// 相对同步文件存放目录的路径 /// 前门不要加"/" /// </summary> [XmlAttribute] public string FileName { get; set; } [XmlAttribute] public bool IsExclude { get; set; } [XmlAttribute] public string Version { get; set; } /// <summary> /// 是否目录 /// </summary> [XmlAttribute] public bool IsDir{get;set;} [XmlAttribute] public long FileSize { get; set; } } }
文件下载页面
namespace PDAJob.PDAService.Service { using System.IO; /// <summary> /// GetFile 的摘要说明 /// </summary> public class GetFile : IHttpHandler { public void ProcessRequest(HttpContext context) { string file=context.Request["f"]; if (string.IsNullOrWhiteSpace(file)) return; var mgr = new AppSyncMgr(); var path = Path.Combine(mgr.Config.Path, file); context.Response.ContentType = "application/octet-stream"; context.Response.Expires=-1; context.Response.TransmitFile(path); } public bool IsReusable { get { return false; } } } }
客户端部分
wince上的配置文件管理,貌似上面用不了app.config,所以只能自己写个了
using System; using System.Collections.Generic; using System.Text; namespace AppSyncCEClient { using System.Xml; using System.IO; using System.Xml.Serialization; using System.Reflection; public class ConfigMgr { public static readonly string _Path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().GetName().CodeBase); private static string _FileName = "AppSynCfg.xml"; private static string FileName { get { return string.Format(@"{0}\{1}", _Path, _FileName); } } private static List<Item> _Items = null; private static List<Item> Items { get { if (_Items == null) { lock (typeof(string)) { if (_Items == null) { ReadConfig(); } } } return _Items; } } private static void ReadConfig() { if (!File.Exists(FileName)) { _Items = new List<Item>(); return; } using (StreamReader sr = new StreamReader(FileName)) { XmlSerializer serializer = new XmlSerializer(typeof(List<Item>)); List<Item> list = serializer.Deserialize(sr) as List<Item>; if (list == null) { list = new List<Item>(); } _Items = list; sr.Close(); } } public static void SaveConfig() { string path = FileName; List<Item> list = Items; XmlSerializer serializer = new XmlSerializer(typeof(List<Item>)); using (StreamWriter sr = new StreamWriter(path)) { serializer.Serialize(sr, list); sr.Close(); } } public static string GetSetting(string key, string defValue) { foreach (Item item in Items) { if (string.Compare(item.Key, key, true) == 0) { return item.Value; } } return defValue; } public static void SetSetting(string key, string value) { bool isFind = false; foreach (Item item in Items) { if (string.Compare(item.Key, key, true) == 0) { item.Value=value; isFind = true; break; } } if (!isFind) { Items.Add(new Item(key, value)); } } public static void CreateConfig() { List<Item> list = new List<Item>(); list.Add(new Item("ScanSrvUrl", "http://192.168.1.95:6666/")); using (StreamWriter sw = new StreamWriter(FileName)) { XmlSerializer serializer = new XmlSerializer(typeof(List<Item>)); serializer.Serialize(sw, list); sw.Close(); }; } } public class Item { private string _key; private string _value; [XmlAttribute] public string Key { get { return _key; } set { _key = value; } } [XmlAttribute] public string Value { get { return _value; } set { _value = value; } } public Item() { } public Item(string key, string value) { this.Key = key; this.Value = value; } } }
UI&同步任务类
都放在一个文件里,vs2005,CE下没BackgroundWorker,线程类还没状态属性,只能肉搏了。
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; namespace AppSyncCEClient { using AppSync; using System.Reflection; using System.Collections; using System.IO; using System.Net; using System.Threading; using System.Diagnostics; public partial class frmMain : Form { public frmMain() { InitializeComponent(); } private AppSyncTask TaskMgr = null; private void frmMain_Load(object sender, EventArgs e) { timer1.Enabled = false; btnClose.Enabled = false; lblTip.Text = "程序启动中,请稍等..."; this.Show(); Application.DoEvents(); ServiceAppSync service = new AppSync.ServiceAppSync(); service.Url = ConfigMgr.GetSetting("SyncUrl", ""); string version = ConfigMgr.GetSetting("Version", "0.0"); TaskMgr = new AppSyncTask(service, version,this); TaskMgr.Do(); timer1.Enabled = true; } private void btnClose_Click(object sender, EventArgs e) { try { this.Enabled = false; ProcessStartInfo psInfo = new ProcessStartInfo(); psInfo.FileName = Path.Combine(ConfigMgr._Path, ConfigMgr.GetSetting("StartPath", "")); psInfo.UseShellExecute = true; Process.Start(psInfo); Thread.Sleep(1000); } catch { } finally { this.Close(); } } private void timer1_Tick(object sender, EventArgs e) { if (TaskMgr.IsComplete) { btnClose.Enabled = true; timer1.Enabled = false; } } private void frmMain_Closing(object sender, CancelEventArgs e) { if (!TaskMgr.IsComplete) { MessageBox.Show("任务执行期间不允许关闭窗体!"); e.Cancel = true; } } } public delegate void ShowProcessMsgHandle(int progress,string msg); /// <summary> /// 同步任务 /// </summary> public class AppSyncTask { Thread WorkerThread = null; private AppSync.ServiceAppSync Service = null; private string LocalVersion = string.Empty; public string Version = string.Empty; private frmMain UI = null; private bool IsRun = false; public AppSyncTask( ServiceAppSync service, string curVersion,frmMain form) { Service = service; LocalVersion = curVersion; UI = form; } public void Report(int progress, string msg) { if (UI.InvokeRequired) { UI.Invoke(new ShowProcessMsgHandle(Report), progress, msg); } else { UI.progressBar.Value = progress; UI.lblTip.Text = msg; } } public void Do() { try { IsRun = true; WorkerThread = new Thread(InternalDo); WorkerThread.Start(); Thread.Sleep(100); } catch (Exception ex) { MessageBox.Show(ex.Message); } } public bool IsComplete { get { return !IsRun; } } private void InternalDo() { //计数器 int success = 0; int error = 0; int b = 0; int t = 0; try { #region Report(0, "检测程序是否有可用更新...."); string version = Service.GetVersion(); Version = version; if (string.Compare(LocalVersion, version, true) >= 0) { Report(100, "当前版本已经是最新版本"); IsRun = false; return; } Report(5, "获取更新列表"); List<AppSyncItem> list = new List<AppSyncItem>(Service.GetList()); string msg = string.Format("有{0}个文件需要更新", list.Count); Report(10, msg); //创建本地目录 string root = ConfigMgr._Path; List<AppSyncItem> dirList = list.FindAll(FilterDir); dirList.Sort(CompareDir); foreach (AppSyncItem dir in dirList) { string path = Path.Combine(root, dir.FileName); if (!Directory.Exists(path)) Directory.CreateDirectory(path); } List<AppSyncItem> downList = list.FindAll(FilterFile); //更新文件 success = 0; error = 0; b = 90; t = downList.Count; #region 遍历下载文件 foreach (AppSyncItem file in downList) { try { string filename = Path.Combine(root, file.FileName); //根据文件大小使用不同的下载方式 FileReadData response = Service.GetFileData(file.FileName); if (response.Code == 0) { byte[] data = Convert.FromBase64String(response.DataB64); using (FileStream fs = File.OpenWrite(filename)) { fs.Position = 0; fs.Write(data, 0, data.Length); fs.Flush(); fs.Close(); } success++; msg = string.Format("{0}", file.FileName.Trim()); Report(funcPercent(success, error, t, b), msg); } else { error++; msg = string.Format("下载{0}错误,{1}", file.FileName, response.Msg); Report(funcPercent(success, error, t, b), msg); } } catch (System.Net.WebException) { error++; throw; } catch (Exception ex) { error++; msg = string.Format(ex.Message, file.FileName); Report(funcPercent(success, error, t, b), msg); continue; } } #endregion msg = string.Format("更新结束,成功{0},失败{1}.", success, error); Report(100, msg); if (error == 0) { ConfigMgr.SetSetting("Version", Version); ConfigMgr.SaveConfig(); } Thread.Sleep(100); IsRun = false; #endregion } catch (Exception ex) { Report(funcPercent(success, error, t, b), ex.Message); IsRun = false; } } #region 委托 /// <summary> /// 计算完成百分比 /// </summary> /// <param name="s">成功数</param> /// <param name="e">失败数</param> /// <param name="t">总数</param> /// <param name="b">基准百分比</param> /// <returns></returns> private int funcPercent(int s, int e, int t, int b) { if (t == 0) return 0; return (int)((decimal)(e + s) * b / (decimal)t); } /// <summary> /// 过滤目录 /// </summary> /// <param name="item"></param> /// <returns></returns> private bool FilterDir(AppSyncItem item) { return item.IsDir; } private bool FilterFile(AppSyncItem item) { return !item.IsDir; } private int CompareDir(AppSyncItem x, AppSyncItem y) { return x.FileName.Length - y.FileName.Length; } #endregion } public class ReportInfo { public string Msg = string.Empty; } }
另外关于vs2005 + wince 的一些注意,可以看下这里http://www.cnblogs.com/wdfrog/archive/2012/12/11/2812749.html