在 Windows Form 应用程序中下载更新






4.84/5 (21投票s)
在内网环境中从 Windows Form 应用程序下载更新。
引言
我曾在一家小型软件公司担任了近一年的顾问。这家软件公司为一家企业医疗机构开发大型医疗软件。该软件被设计为真正的三层模型。业务层(我们可以称之为中间层)是用 J2EE 编写的(使用了一些时髦的框架,如 Spring、Hibernate 等),并托管在 Web Logic servlet 容器中。客户端层使用 Windows Form API 用 C# 编写。这两个层之间的通信协议是 SOAP Web 服务。
部署过程是一个大问题,因为他们需要将客户端应用程序分发到超过 350 台机器(客户端终端)——尽管这是一种 XCopy 部署。嗯,这是桌面应用程序部署的一个众所周知的旧痛。但该软件处于维护模式,自然地,错误和新功能增强请求会定期出现,一些开发人员正在处理这些问题。
本质上,另一个旧的痛点是重新分发客户端终端机器之间的更新。我们已经知道许多应用程序会自动更新(例如,即时通讯应用程序,即 Yahoo、MSN、Skype 等)。因此,该公司只是在寻找这样的解决方案。
最终,当他们通知我这件事时,我在互联网上搜索,看看是否有人已经做了一些相关的工作。但我没有找到任何通用的解决方案(而是专有解决方案)。最后,我写了一个快速解决方案(我将在本文中讨论),以解决这个问题。我不是说我的方法是标准的/最好的(或任何其他形容词),但它对我们来说确实有效。
想法
当我开始记录此自动更新用例的工作流程时,我发现以下执行点
A. | 应用程序(假设应用程序在名为进程 A 的进程中运行)将有一个带文本“检查更新”的按钮,单击后,它将向服务器请求是否有任何可用更新。如果没有可用更新,则应用程序将从 G 点(下文提及)继续。 |
B. | 如果任何更新准备就绪,它将通知用户更新已准备好下载,并请求是否开始下载任务。如果响应为否定,则应用程序将从 G 点(下文提及)继续其工作。 |
C. | 如果用户同意更新,则应用程序将启动另一个进程(我们将其命名为进程 B)。 |
D. | 进程 B 将首先杀死进程 A。然后,它将从服务器下载所有文件(使用远程处理和二进制格式化程序——值得使用这些,因为我们处于内网环境中)。 |
E. | 进程 B 将重新启动进程 A(本质上是主应用程序)。 |
F. | 进程 B 将终止。 |
G. | 进程 A 将照常继续其任务。 |
我相信这足够简单。
现在,让我们开始思考实现。
分析复杂性并选择技术
我将任务分解成几个小部分,以分析复杂性可能出现在哪里。我发现与其他部分相比,只有通信部分具有挑战性。我决定使用 .NET Remoting,因为它在内网场景中具有令人满意的架构和性能。我需要感谢 .NET 框架具有酷炫的 XCopy 部署功能——这提供了一个干净简单的部署过程,并摆脱了旧的本机安装麻烦。我们的目标环境是企业内网,因此我将使用二进制格式化程序和 TCP/IP 通道——因为我认为这是内网环境中的最佳解决方案,因为没有防火墙。本质上,它可以为应用程序提供巨大的性能。
到目前为止一切顺利,现在我们可以开始实施解决方案了。
实现更新分发服务器
首先,我们将实现一个远程服务器,它将拥有我们应用程序的更新副本,所有终端将向该服务器询问/通信以获取更新,该服务器将相应地响应。如果有任何更新可用,它还将通过二进制格式向客户端终端提供更新的程序集。在实际场景中,应用程序应该构建为一个或多个程序集(本质上是 DLL),以便可以通过替换该应用程序的部分或全部程序集(DLL)来应用更新——而不是替换整个应用程序。但是,在本文中,为了演示目的,我将更新所有可执行文件(EXE)和程序集(DLL)。仅更新必要的程序集将提供更高的性能改进。
首先,我正在编写一个新项目——本质上,它是一个 Windows 窗体应用程序,它将在其中托管远程服务器。我将首先编写一个远程接口,它将在客户端和服务器之间扮演联系角色。
/// <summary>
/// A Contact that will be used by the
/// remote client to find a service
/// from the server.
/// </summary>
public interface IUpdateService
{
/// <summary>
/// Get all files
/// </summary>
/// <returns>
/// An array of <see cref="System.String"/>
/// contains the file names</returns>
string[] GetFiles();
/// <summary>
/// Get the current version of a file
/// </summary>
/// <param name="fileName">The file name</param>
/// <returns>The version that is currently available</returns>
string GetCurrentVersion(string fileName);
/// <summary>
/// Gets the entire file as binary
/// </summary>
/// <param name="fileName">The file name</param>
/// <returns>
/// An array of <see cref="Byte"/> containing the file content
/// </returns>
byte[] GetFile(string fileName);
}
您可能已经猜到,这就是我们的远程服务器将要实现的接口。现在我们将按如下方式实现此接口
/// <summary>
/// The update service
/// </summary>
public class UpdateService : MarshalByRefObject, IUpdateService
{
#region IUpdateService Members
/// <summary>
/// Get all files
/// </summary>
/// <returns></returns>
public string[] GetFiles()
{
Logger.LogMessage("Inside UpdateService::GetFiles()");
ArrayList collection = new ArrayList();
foreach(FileObject fileObject in ConfigInfo.Instance.FileObjects)
{
collection.Add(fileObject.FileInfo.Name);
}
return collection.ToArray(typeof(string)) as string[];
}
GetFiles
本质上从预定义的配置目录中读取可用的更新文件,并将其每个文件的名称暴露给其使用者。
/// <summary>
/// Get the current version of the file
/// </summary>
/// <param name="fileName"></param>
/// <returns></returns>
public string GetCurrentVersion(string fileName)
{
Logger.LogMessage("Inside UpdateService::GetCurrentVersion()");
foreach(FileObject fileObject in ConfigInfo.Instance.FileObjects)
{
if( fileObject.FileInfo.Name.Equals(fileName)
|| fileName.EndsWith(fileObject.FileInfo.Name))
{
return fileObject.Version;
}
}
throw
new ArgumentException("Given file is not found into the server.");
}
GetCurrentVersion
用服务器上可用的文件的最新版本号回复。我们将使用 .NET 程序集版本来跟踪版本号。对于非程序集文件(例如 XML、config 等),它将回复一个类似“NA
”的字符串。
/// <summary>
/// Gets the entire file
/// </summary>
/// <param name="fileName"></param>
/// <returns></returns>
public byte[] GetFile(string fileName)
{
Logger.LogMessage("Inside UpdateService::GetFile()");
foreach(FileObject fileObject
in ConfigInfo.Instance.FileObjects)
{
if( fileObject.FileInfo.Name.Equals(fileName)
|| fileName.EndsWith(fileObject.FileInfo.Name))
{
return GetBinaryContents(fileObject);
}
}
throw
new ArgumentException("Given file is not found into the server.");
}
现在,正如方法名所示,GetFile
简单地以二进制格式返回整个文件内容。该方法使用以下方法读取文件内容
/// <summary>
/// Gets the binary content of the entire file
/// </summary>
/// <param name="fileObject"></param>
private byte[] GetBinaryContents(FileObject fileObject)
{
byte[] block;
using(FileStream fileStream =
File.OpenRead(fileObject.FileInfo.FullName))
{
using(BinaryReader reader = new BinaryReader(fileStream))
{
block = reader.ReadBytes((int)fileStream.Length);
}
}
return block;
}
}
是时候向外界公开服务了。所以我要将服务托管在 Windows 窗体中。以下是公开服务的代码片段
/// <summary>
/// Starts the server onto a specific port
/// </summary>
private void StartServer()
{
// Display the log
Logger.LogMessage("Opening channel..");
// start listening
TcpServerChannel serverChannel = new TcpServerChannel(7444);
// now register the channel
Logger.LogMessage("Opening channel..completed.");
Logger.LogMessage("Registering channel..");
ChannelServices.RegisterChannel(serverChannel);
Logger.LogMessage("Registering channel..completed.");
Logger.LogMessage("Registering WKO Objects..");
// register/expose the wko objects
RemotingConfiguration.RegisterWellKnownServiceType(
typeof(UpdateService),"UpdateService",
WellKnownObjectMode.SingleCall);
Logger.LogMessage("Registering WKO Objects..completed.");
}
AppUpdateServer
项目现在已准备就绪。
现在是时候修改我们的主应用程序了,它最终将联系远程服务器并请求更新。为了演示这一点,我将编写一个名为 SampleApplication
的小型应用程序。它是一个 GUI 应用程序,假设它有一个名为“检查更新”的菜单。每当用户点击它时,应用程序将启动更新过程。
我们需要记住的一点是,该应用程序需要远程服务器的远程 URL 才能与远程服务器通信。因此,我们需要将其放入 SampleApplication
的配置文件中。现在,我在这里编写一个类,它将包含与更新相关的内容(只是为了将此代码与应用程序的其他业务代码分开),它将有一个名为“Update
”的方法,该方法将完成所有工作。
所以这是类的实现
/// <summary>
/// Utility that provides update functionalities
/// </summary>
public class UpdateUtil
{
/// <summary>
///
/// </summary>
private string remoteObjectUri = string.Empty;
/// <summary>
///
/// </summary>
private IUpdateService remoteService;
private IWin32Window owner;
private string applicationName;
/// <summary>
/// Creates a new instance
/// </summary>
public UpdateUtil(IWin32Window owner,string remoteObjectUri)
{
remoteService = null;
this.owner = owner;
this.remoteObjectUri = remoteObjectUri;
}
如您所见,该类需要远程对象 URI
和一个 win32
所有者(在本例中是示例应用程序)作为构造函数参数。
/// <summary>
/// Connect to the remote server
/// </summary>
/// <remarks>
/// Tries to create a WKO Instance
/// </remarks>
/// <returns>
/// <c>true</c> if the connection establishes
/// successfully, <c>false</c> otherwise.
/// </returns>
private bool ConnectRemoteServer()
{
try
{
remoteService =
Activator.GetObject( typeof(IUpdateService),
remoteObjectUri )
as IUpdateService;
}
catch(Exception remoteException )
{
System.Diagnostics.Trace.WriteLine(remoteException.Message);
}
return remoteService != null;
}
ConnectRemoteServer
简单地使用远程 URI 建立与服务器的连接。
/// <summary>
/// Determine if a new version of this
/// application is currently available
/// </summary>
/// <returns>
/// <c>true</c> if available, <c>false</c> otherwise
/// </returns>
private bool UpdateAvailable()
{
try
{
string assemblylocation
= Assembly.GetExecutingAssembly().CodeBase;
assemblylocation
= assemblylocation.Substring(
assemblylocation.LastIndexOf("/")+1);
applicationName = assemblylocation;
AssemblyName assemblyName
= Assembly.GetExecutingAssembly().GetName();
string localVersion
= assemblyName.Version.ToString();
string remoteVersion
= remoteService.GetCurrentVersion(applicationName);
return IsUpdateNecessary(localVersion,remoteVersion);
}
catch(Exception ex)
{
System.Diagnostics.Trace.WriteLine(ex.Message);
}
return false;
}
现在,此方法执行一个重要的任务,它询问服务器 SampleApplication.exe 在远程端有什么版本可用;如果任何更新可用,它会返回 true
给其调用者。UpdateAvailable
方法使用以下实用方法 IsUpdateNecessary
来确定可用版本号是否是当前版本号的更新。
/// <summary>
/// Is update needed?
/// </summary>
/// <param name="localVersion"></param>
/// <param name="remoteVersion"></param>
/// <returns></returns>
private bool IsUpdateNecessary(string localVersion,string remoteVersion)
{
try
{
long lcVersion = Convert.ToInt64( localVersion.Replace(".",""));
long rmVersion = Convert.ToInt64( remoteVersion.Replace(".",""));
return lcVersion < rmVersion ;
}
catch(Exception ex)
{
System.Diagnostics.Trace.WriteLine(ex.Message);
}
return false;
}
现在这是该类中唯一一个实际调用上述方法来执行 Update
启动的 public
方法。此方法内部执行了一些值得注意的操作。我现在将解释这些。首先,该方法只是检查是否有任何更新可用,如果是,则它 *不* 自己执行 Update
过程。它启动另一个进程 AppUpdate.exe 来完成这项工作。这背后的理论很简单;Update
过程将替换当前的 EXE。但是当 EXE 文件正在执行时,系统不允许我们替换它。所以我们必须启动另一个进程。
/// <summary>
/// Update the application executable
/// </summary>
public void Update()
{
if( !ConnectRemoteServer())
return ;// the remote connection was not okay
if( UpdateAvailable())
{
// lets checkout if any update version available or not
if( DialogResult.Yes ==
MessageBox.Show(owner,
"An update is available. \nWould you like to update now?",
"Sample Application Update",
MessageBoxButtons.YesNo,
MessageBoxIcon.Question))
{
string updateAppPath
= Path.Combine(AppDomain.CurrentDomain.BaseDirectory,
"AppUpdate.exe");
Process updateProcess = new Process();
updateProcess.StartInfo =
new ProcessStartInfo(updateAppPath,
Process.GetCurrentProcess().Id.ToString()
+ " "+remoteObjectUri);
updateProcess.Start();
}
}
}
}
现在,AppUpdate.exe 是我们完成任务唯一需要实现的东西。AppUpdate
本质上是另一个小的 Windows 窗体应用程序(我使用 Windows 窗体是因为我打算在更新过程中显示一个进度条窗口),它将执行实际的更新任务。此进程接受两个命令行参数。一个是 Sample Application 的进程 ID。另一个是远程服务器 URL。后者对我们来说很简单——我相信。我只是解释前者的目的。AppUpdate.exe 将替换 SampleApplication EXE,因此它将首先杀死进程(SampleApplication
在其中运行),然后它将更新应用程序 EXE 和 DLL,最后它将重新启动 SampleApplication
。
现在让我们关注 AppUpdate.exe。
AppUpdate
项目包含一个 Windows 窗体类,我在其中编写了 Update
相关内容。以下是代码片段
/// <summary>
/// Updates an application
/// </summary>
public class AppUpdate : System.Windows.Forms.Form
{
// .. removing .net fx declared variables for simplicity
private string applicationProcessID;
private string remoteObjectUri ;
/// <summary>
/// Creates a new instance
/// </summary>
public AppUpdate(string applicationProcessID,string remoteUrl)
{
// Required for Windows Form Designer support
InitializeComponent();
try
{
this.applicationProcessID = applicationProcessID;
this.remoteObjectUri = remoteUrl;
}
catch(Exception){}
}
该类的构造函数接受我刚才提到的两个参数,一个是 SampleApplication
的应用程序进程 ID,另一个是远程服务器 URI。
/// <summary>
/// Runs the update process
/// </summary>
public void RunUpdateProcess()
{
Process applicationProcess =
Process.GetProcessById(GetProcessID());
if( applicationProcess == null ) return;
targetApplicationFullpath =
applicationProcess.MainModule.FileName;
applicationProcess.Kill();
applicationProcess.WaitForExit();
if( !ConnectRemoteServer()) return ;
UpdateFiles();
}
RunUpdateProcess
是将执行所有任务的线索方法。当 Form
加载时,我们将调用此方法。
我的意思是,当进度窗口被激活时,我们将调用此方法。所以我们可以在这个类中为 Form_Load
编写一个事件处理程序,并可以从 Form_Load()
内部调用此方法。此方法杀死 SampleApplication
进程,然后调用另一个 private
方法 UpdateFiles
,其实现如下
/// <summary>
/// Updates the files from the remote server
/// </summary>
private void UpdateFiles()
{
string targetDir
= targetApplicationFullpath.Substring(0,
targetApplicationFullpath.LastIndexOf("\\"));
try
{
foreach( string file
in remoteService.GetFiles())
{
byte[] array =
remoteService.GetFile(file);
string fileName = Path.Combine(targetDir,file);
try
{
FileInfo finfo = new FileInfo(fileName);
if( finfo.Exists ) finfo.Delete();
using(FileStream outputFile
= new FileStream(fileName,
FileMode.CreateNew ,
FileAccess.Write ))
{
using(BinaryWriter writer =
new BinaryWriter(outputFile))
{
writer.Write(array,0,array.Length);
}
}
}
catch(Exception)
{
}
}
}
catch(Exception )
{
}
Process launch = new Process();
launch.StartInfo =
new ProcessStartInfo(targetApplicationFullpath);
launch.Start();
Application.Exit();
}
因此,我们可以看到此方法从远程服务器复制每个更新的文件,并用它们替换现有的旧版本。现在是时候运行此应用程序的窗口了——我们将在 Main
方法中执行此操作。
/// <summary>
/// Entry point of the application
/// </summary>
/// <param name="args"></param>
[STAThread()]
public static void Main(string [] args )
{
if( args.Length > 0 )
{
Application.Run(
new AppUpdate(args[0],args[1]));
}
}
}
Main
方法通过命令行参数接收进程 ID 和远程 URI,并在实例化 AppUpdate
类时提供这些参数。
这就是我们实现的所有内容。
审查和测试
为了测试这个解决方案,首先,我们必须启动服务器应用程序。在服务器机器上,应该有一个目录,其中包含所有更新的程序集以及所有 XML 文件和配置文件(根据需要)。服务器应用程序将读取所有文件版本号并暴露远程服务。然后我们将启动客户端应用程序(在本例中为 SampleApplication
)并单击菜单“检查更新”。然后 Update
过程将工作。
结论
再次强调,这是一个我编写的快速简单的解决方案。所以我没有理由声称它是一个标准或“经典之作”。但我相信这可以帮助用 .NET 编写并在内网场景中运行的桌面应用程序。最近,像 ClickOnce 和 XBAP 这样更经典的框架正在组织中用于此类解决方案。