65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.84/5 (21投票s)

2007年6月7日

CPOL

9分钟阅读

viewsIcon

131191

downloadIcon

2885

在内网环境中从 Windows Form 应用程序下载更新。

Screenshot - 2.jpg

引言

我曾在一家小型软件公司担任了近一年的顾问。这家软件公司为一家企业医疗机构开发大型医疗软件。该软件被设计为真正的三层模型。业务层(我们可以称之为中间层)是用 J2EE 编写的(使用了一些时髦的框架,如 SpringHibernate 等),并托管在 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 加载时,我们将调用此方法。

Screenshot - 3.jpg

我的意思是,当进度窗口被激活时,我们将调用此方法。所以我们可以在这个类中为 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 编写并在内网场景中运行的桌面应用程序。最近,像 ClickOnceXBAP 这样更经典的框架正在组织中用于此类解决方案。

© . All rights reserved.