运行时自更新程序的.NET实现
- 
面对的主要问题 
运行时自更新程序在实现上面对的主要问题上什么呢?
先说“更新”一词,“更新”有时也叫“升级”,“更新”程序的过程实际上就是以新的程序文件替换旧的程序文件的过程。在现实中,程序文件通常是由 .exe、.dll 以及其它的诸如xml,ini之类的用于配置的文件组成。在本文中是指.exe,.dll这些包含程序代码的文件。
“运行时更新”的主要问题是 .exe、.dll 文件在程序运行的时候会被系统锁定,因而进行文件替换的时候会被系统阻止。而如果把程序停止了再进行升级的话,而且无法由要升级的程序自身完成,得借助另一个程序完成,换句话说这不叫“运行时升级”了。本文里的“运行时升级”是指不中断正在运行的程序而同时进行程序文件的升级替换,升级完成后的新功能会在程序的下次运行后得以展现。
- 应用程序域(AppDomain)和影子拷贝(ShadowCopy)
在 .NET 中实现这点有着特别的便利,其中主要涉及到两个概念:应用程序域(AppDomain)、影子拷贝(ShadowCopy)。进程是用于在同一计算机上隔离不同的应用程序的,而在 .NET 的托管程序中,应用程序域(AppDomain)是比进程的范围更小的、更安全、用途更广泛的应用程序的隔离单元。这样在一个进程中,可以有多个不同的应用程序域,每个应用程序域可以独立运行一个应用程序而不会相互影响,某个应用程序的错误也不会影响其它的程序,并且每一个AppDomain中的应用程序都可以独立地加载和卸载。影子拷贝(ShadowCopy)是应用应用程序域(AppDomain)的一项特性。通常 AppDomain 加载应用程序时,公共语言运行库会将程序集文件锁定,因此只有卸载了程序集之后才能更新该文件。将 AppDomain 配置为影子拷贝(ShadowCopy)时,来自应用程序路径的程序集文件被复制到另一个位置并从该位置进行加载。这些程序集副本被锁定,但原始程序集文件将取消锁定并可以进行更新。本人的方案就是基于此实现的。
- 潜在的问题
似乎按照上面提到的应用程序域(AppDomain)和影子拷贝(ShadowCopy)的特性就可以直接达到目的了,还需要考虑什么吗?可惜的是,现实生活中,通往成功的道路通常都不是一帆风顺的。.NET托管程序中有一个默认程序域,它是.NET托管程序启动后的创建的第一个应用程序域,它与其它时候创建的应用程序域大部分没什么区别,但是却有一点影响到了本文主题的实现,那就是默认程序域是无法开启影子拷贝(ShadowCopy)特性的。那怎么办?那就在在默认程序域中创建一个新的AppDomain来加载实际需要运行的应用程序的程序集。没错,只能这样了,可是这个解决得并不彻底。运行在非默认程序域中的应用程序由于开启ShadowCopy特性而得以运行时更新了,可是从默认程序域创建并加载目标应用程序到新的AppDomain的这一个过程的逻辑还是无法运行时更新,可能通常情况下这一加载的逻辑并不需要升级,可谁又能保证这一点呢,因为我们都知道的一个道理是“真正不变的是变化”。为了彻底地解决这一问题,文本提出了以下的解决方案。
- 实现方案
方案中由以下的4个要素组成,并通过以恰当的方式组织这4个要素以实现本文的目标。4个要素如下:
- 主程序:是提供应用程序所有业务功能的程序,它要在持续运行的状态中被更新。
- 外壳程序:这是进程的真正入口,外壳程序启动后,在其默认程序域中创建新的应用程序域并配置影子拷贝加载主程序。
- 外壳加载dll类库:使用外壳程序进行加载主程序的逻辑被封装dll类库,主程序引用类库,外壳程序被作为资源嵌入到类库中。
- 更新程序:更新主程序的程序。更新的功能也可以是主程序的一部分,在此只是为了更明晰地表达而将更新功能独立出来。
实现的基本思路如下:外壳程序是以嵌入资源的方式包含在类库中。当主程序引用类库执行加载操作时,将根据当前的程序域中的属性中是否存在外壳程序设置的标识以确定当前是处于默认程序域中,还是已经由外壳程序加载。如果处于默认程序域,则将外壳程序释放到临时文件夹中启动,并将主程序的入口文件作为参数传递给外壳程序,外壳程序则根据参数,在一个新的应用程序域中加载主程序。主程序和外壳程序的执行流程图如下:

 外壳加载类库的实现
外壳加载类库的实现
1 /// <summary>
2 /// 启动程序;
3 /// 如果当前不是处于 Shell 程序加载运行的状态,方法将执行 Shell 释放,启动等操作,
4 /// 如果当前已经处于 Shell 程序加载的状态下,则启动主程序;
5 /// </summary>
6 /// <param name="entryMethod">主程序的入口方法的回调委托</param>
7 /// <param name="args">入口参数</param>
8 public void Start(EntryMethodCallback entryMethod, string[] args)
9 {
10 bool shellRunning = (AppDomain.CurrentDomain.GetData(WAITHANDLE_NAME) != null);
11 if (shellRunning)
12 {
13 //当前是外壳程序加载运行,进入主程序;
14 PrepareToRunMainApp(entryMethod, args);
15 }
16 else
17 {
18 //释放 ShellRunningSign,并启动外壳程序;
19 ReleaseAndRunShell();
20 }
21 }
 主程序入口的实现
主程序入口的实现
1 namespace MainApplication
2 {
3 static class Program
4 {
5 /// <summary>
6 /// 应用程序的主入口点。
7 /// </summary>
8 [STAThread]
9 static void Main()
10 {
11 ShellStartInfo shellStartInfo = new ShellStartInfo();
12 //主程序的入口程序集;
13 shellStartInfo.MainAppExePath = Assembly.GetExecutingAssembly().Location;
14 //主程序的START同步信号量的名称,对于主程序的每次进入而言,这必须是常量;
15 shellStartInfo.MainStartSign = "BAA96D9D4D33";
16 //外壳程序与主程序同步的超时时长;
17 shellStartInfo.Timeout = 2000;
18
19 //外壳加载对象;调用 Start 方法时需指定主程序的入口方法的委托(EntryMethodCallback)和入口参数;
20 ShellLoader shellLoader = new ShellLoader(shellStartInfo);
21 shellLoader.Start(delegate(string[] args)
22 {
23 Application.EnableVisualStyles();
24 Application.SetCompatibleTextRenderingDefault(false);
25 Application.Run(new FrmMain());
26 },
27 null);
28 }
29 }
30 }
 外壳程序的实现
外壳程序的实现
1 namespace Shell
2 {
3 /// <summary>
4 /// 外壳程序;
5 /// </summary>
6 class Program
7 {
8 /// <summary>
9 /// 外壳的入口参数顺序:
10 /// 1、主程序入口程序集路径;
11 /// 2、主程序的 START 同步信号的名称;
12 /// 3、同步的超时时长,整数,单位是毫秒;
13 /// 4、主程序退出同步对象 ManualResetEvent 在 AppDomain 属性中的名称;
14 /// </summary>
15 /// <param name="args"></param>
16 static void Main(string[] args)
17 {
18 if (args.Length < 4)
19 {
20 return;
21 }
22 string mainAppPath = args[0];
23 string mainStartSign = args[1];
24 int timeout;
25 if (!int.TryParse(args[2], out timeout))
26 {
27 //无效的 timeout 参数;
28 return;
29 }
30 string waithandleName = args[3];
31
32 //检测主程序在释放了 Shell 之后是否已经退出;
33 Mutex mtxMainStart = new Mutex(false, mainStartSign);
34 bool mainAppQuit = mtxMainStart.WaitOne(timeout);
35 mtxMainStart.ReleaseMutex();
36 if (!mainAppQuit)
37 {
38 //主程序退出超时,外壳程序退出;
39 return;
40 }
41
42 //创建新的程序域加载主程序;
43 AppDomainSetup ads = AppDomain.CurrentDomain.SetupInformation;
44 ads.ShadowCopyFiles = "TRUE";//配置为影子拷贝;
45 string mainAppFolder = Path.GetDirectoryName(mainAppPath);
46 ads.ApplicationBase = mainAppFolder;
47
48 AppDomain newDomain = AppDomain.CreateDomain("MAIN_APP_DOMAIN", AppDomain.CurrentDomain.Evidence, ads);
49
50 ManualResetEvent mre = new ManualResetEvent(false);
51 newDomain.SetData(waithandleName, mre);
52
53 Thread thrd = new Thread(delegate()
54 {
55 newDomain.ExecuteAssembly(mainAppPath);
56 });
57 thrd.Start();
58
59 //等待主程序退出后结束外壳程序;
60 mre.WaitOne();
61 }
62 }
63 }
64
附件:源码
 
                     
                    
                 
                    
                
 

 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号