草庐IT

WPF简单自动更新(升级)程序+服务端

Alex1911 2023-03-28 原文

工作逻辑是用户启动主程序,主程序启动更新程序,更新程序立刻检查是否有已经下载好的更新包,如果有则立刻关闭主程序进行更新,如果没有则访问服务器查询更新包,并在后台静默下载,下载完成后等下一次主程序启动时更新

由于只是简单的更新程序,所以没有用数据库,客户端版本号以一个json文件保存,服务端则直接以压缩包的名称作为版本号

那么首先就要有一个服务端,我这里建了一个简单的Asp.Net Core WebApi程序,只有一个获取包列表和一个下载包的方法

Program 中要先添加允许访问的物理路径设置:

            string packagePath = Path.Combine(AppContext.BaseDirectory, "Packages");
            if (!Directory.Exists(packagePath))
                Directory.CreateDirectory(packagePath);

            app.UseStaticFiles(new StaticFileOptions
            {
                FileProvider = new PhysicalFileProvider(packagePath),  //添加允许访问的路径
                RequestPath = "/Packages"
            });

控制器方法:

        [HttpGet]
        public IActionResult GetPackages(long version)
        {
            _logger.LogInformation($"用户查询了一次{version}以后的更新包列表");

            string[] files = Directory.GetFiles(_path, "*.zip", SearchOption.TopDirectoryOnly);
            List<long> packages = new List<long>();
            for (int i = 0; i < files.Length; i++)
            {
                long serverVersion = Convert.ToInt64(Path.GetFileNameWithoutExtension(files[i]));  //懒得用数据库,直接用压缩包的文件名作为版本号,可自行改进
                if (serverVersion > version)
                    packages.Add(serverVersion);
            }
            packages.Sort();  //排序一下给回客户端,让客户端从旧到新下载(当然也可以返回到客户端后再排序)
            return new JsonResult(packages);
        }

        [HttpGet]
        public IActionResult Download(long version)
        {
            _logger.LogInformation($"一位用户请求下载{version}版本");

            string fileName = Path.Combine("Packages", version + ".zip");  //这里是绝对路径
            string fileFullName = Path.Combine(AppContext.BaseDirectory, fileName);
            if (!System.IO.File.Exists(fileFullName))
                return NotFound();  //如果客户端申请下载的更新包不存在,返回404

            return Redirect($"{HttpContext.Request.Scheme}://{HttpContext.Request.Host}/{fileName}");  //重定向到文件路径
        }

接下来是客户端那边更新程序的部分,由于健壮性判断有点多,所以只贴下载和替换关键部分,其余部分请下载附件查看
下载部分:

        /// <summary>
        /// 查找服务器上比当前版本新的包列表
        /// </summary>
        /// <param name="version">当前版本</param>
        private static async Task<long[]> GetNewPackages(long version)
        {
            var resp = await _client.GetAsync($"{Constant.Config.Host}UpdateService/GetPackages?version={version}");
            if (!resp.IsSuccessStatusCode)
                throw new HttpRequestException("查找更新失败", null, resp.StatusCode);

            var jsonPackageList = await resp.Content.ReadAsStringAsync();
            long[]? packages = JsonSerializer.Deserialize<long[]>(jsonPackageList);
            if (packages is null)
                throw new Exception("更新包名称不是数字");
            return packages;
        }

        /// <summary>
        /// 下载指定版本的包
        /// </summary>
        /// <param name="packages">包名列表</param>
        private static async Task DownloadPackage(long[] packages)
        {
            for (int i = 0; i < packages.Length; i++)
            {
                string packageName = Path.Combine(Constant.DownloadDirect, $"{packages[i]}.zip");
                var resp = await _client.GetAsync($"{Constant.Config.Host}UpdateService/Download?version={packages[i]}");
                if (!resp.IsSuccessStatusCode)
                    throw new HttpRequestException("下载更新失败", null, resp.StatusCode);

                using Stream stream = resp.Content.ReadAsStream();
                using FileStream fs = new FileStream(packageName, FileMode.OpenOrCreate, FileAccess.Write);
                stream.CopyTo(fs);
            }
        }

替换文件部分:

        /// <summary>
        /// 解压
        /// </summary>
        private async Task UnzipAllPackages()
        {
            _needUpdatePackages.Sort();
            progUpdate.Maximum = _needUpdatePackages.Count;
            for (int i = 0; i < _needUpdatePackages.Count; i++)
            {
                txbUpdate.Text = $"正在解压,第{i + 1}个,共{_needUpdatePackages.Count}个";
                progUpdate.Value = i;
                await Task.Delay(30);
                ZipFile.ExtractToDirectory(_needUpdatePackages[i], Constant.UnzipDirect, Encoding.GetEncoding("GB2312"), true);
            }
            progUpdate.Value = _needUpdatePackages.Count;

            if (long.TryParse(Path.GetFileNameWithoutExtension(_needUpdatePackages.Last()), out long lVersion))
            {
                _updateingVersion = lVersion;
            }
            else
            {
                MessageBox.Show("更新失败,请联系软件供应商。\r\n错误代码:0001", "错误");
                return;
            }

            await Task.Delay(30);
        }

        /// <summary>
        /// 替换文件
        /// </summary>
        /// <param name="files">文件列表</param>
        private async Task<bool> UpdateFiles(string[] files)
        {
            for (int i = 0; i < files.Length; i++)
            {
                txbUpdate.Text = $"正在更新文件,第{i + 1}个,共{files.Length}个";
                progUpdate.Value = i;

                if (files[i].Contains("自动更新程序"))
                {
                    continue;  //自己不能被替换,跳过自己,由主程序替换
                }

                string relativeFileName = files[i][Constant.UnzipDirect.Length..];
                if (relativeFileName[0] == '\\')
                    relativeFileName = relativeFileName[1..];
                string toName = Path.Combine(Environment.CurrentDirectory, relativeFileName);
                string toDir = Path.GetDirectoryName(toName)!;
                if (!Directory.Exists(toDir))
                    Directory.CreateDirectory(toDir);
                try
                {
                    File.Copy(files[i], toName, true);
                }
                catch
                {
                    MessageBox.Show($"更新失败,{toName.Substring(Environment.CurrentDirectory.Length)}无法被替换,即将回滚更新", "错误");
                    return false;
                }
                _coveredFiles.Add(toName);
                await Task.Delay(30);
            }
            progUpdate.Value = files.Length;
            await Task.Delay(30);
            return true;
        }

还有备份和更新失败后根据备份文件回滚的部分,由于和替换代码大量重叠就不贴出来了,附件的项目里面有,可直接编译运行

本文的主要目的也只是自己做个记录,以后有需要的时候可以直接拿过来改,肯定不如其他软件的更新程序这么完善,大佬们看一乐就好

附件:WPF自动更新程序.zip

有关WPF简单自动更新(升级)程序+服务端的更多相关文章

  1. ruby-on-rails - 使用 Ruby on Rails 进行自动化测试 - 最佳实践 - 2

    很好奇,就使用ruby​​onrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提

  2. ruby - 使用 ruby​​ 和 savon 的 SOAP 服务 - 2

    我正在尝试使用ruby​​和Savon来使用网络服务。测试服务为http://www.webservicex.net/WS/WSDetails.aspx?WSID=9&CATID=2require'rubygems'require'savon'client=Savon::Client.new"http://www.webservicex.net/stockquote.asmx?WSDL"client.get_quotedo|soap|soap.body={:symbol=>"AAPL"}end返回SOAP异常。检查soap信封,在我看来soap请求没有正确的命名空间。任何人都可以建议我

  3. ruby - 在 Ruby 程序执行时阻止 Windows 7 PC 进入休眠状态 - 2

    我需要在客户计算机上运行Ruby应用程序。通常需要几天才能完成(复制大备份文件)。问题是如果启用sleep,它会中断应用程序。否则,计算机将持续运行数周,直到我下次访问为止。有什么方法可以防止执行期间休眠并让Windows在执行后休眠吗?欢迎任何疯狂的想法;-) 最佳答案 Here建议使用SetThreadExecutionStateWinAPI函数,使应用程序能够通知系统它正在使用中,从而防止系统在应用程序运行时进入休眠状态或关闭显示。像这样的东西:require'Win32API'ES_AWAYMODE_REQUIRED=0x0

  4. ruby-on-rails - 如何验证 update_all 是否实际在 Rails 中更新 - 2

    给定这段代码defcreate@upgrades=User.update_all(["role=?","upgraded"],:id=>params[:upgrade])redirect_toadmin_upgrades_path,:notice=>"Successfullyupgradeduser."end我如何在该操作中实际验证它们是否已保存或未重定向到适当的页面和消息? 最佳答案 在Rails3中,update_all不返回任何有意义的信息,除了已更新的记录数(这可能取决于您的DBMS是否返回该信息)。http://ar.ru

  5. ruby - 具有身份验证的私有(private) Ruby Gem 服务器 - 2

    我想安装一个带有一些身份验证的私有(private)Rubygem服务器。我希望能够使用公共(public)Ubuntu服务器托管内部gem。我读到了http://docs.rubygems.org/read/chapter/18.但是那个没有身份验证-如我所见。然后我读到了https://github.com/cwninja/geminabox.但是当我使用基本身份验证(他们在他们的Wiki中有)时,它会提示从我的服务器获取源。所以。如何制作带有身份验证的私有(private)Rubygem服务器?这是不可能的吗?谢谢。编辑:Geminabox问题。我尝试“捆绑”以安装新的gem..

  6. ruby - 通过 rvm 升级 ruby​​gems 的问题 - 2

    尝试通过RVM将RubyGems升级到版本1.8.10并出现此错误:$rvmrubygemslatestRemovingoldRubygemsfiles...Installingrubygems-1.8.10forruby-1.9.2-p180...ERROR:Errorrunning'GEM_PATH="/Users/foo/.rvm/gems/ruby-1.9.2-p180:/Users/foo/.rvm/gems/ruby-1.9.2-p180@global:/Users/foo/.rvm/gems/ruby-1.9.2-p180:/Users/foo/.rvm/gems/rub

  7. ruby - 如何指定 Rack 处理程序 - 2

    Rackup通过Rack的默认处理程序成功运行任何Rack应用程序。例如:classRackAppdefcall(environment)['200',{'Content-Type'=>'text/html'},["Helloworld"]]endendrunRackApp.new但是当最后一行更改为使用Rack的内置CGI处理程序时,rackup给出“NoMethodErrorat/undefinedmethod`call'fornil:NilClass”:Rack::Handler::CGI.runRackApp.newRack的其他内置处理程序也提出了同样的反对意见。例如Rack

  8. ruby - 在 Ruby 中编写命令行实用程序 - 2

    我想用ruby​​编写一个小的命令行实用程序并将其作为gem分发。我知道安装后,Guard、Sass和Thor等某些gem可以从命令行自行运行。为了让gem像二进制文件一样可用,我需要在我的gemspec中指定什么。 最佳答案 Gem::Specification.newdo|s|...s.executable='name_of_executable'...endhttp://docs.rubygems.org/read/chapter/20 关于ruby-在Ruby中编写命令行实用程序

  9. ruby-on-rails - Rails 应用程序之间的通信 - 2

    我构建了两个需要相互通信和发送文件的Rails应用程序。例如,一个Rails应用程序会发送请求以查看其他应用程序数据库中的表。然后另一个应用程序将呈现该表的json并将其发回。我还希望一个应用程序将存储在其公共(public)目录中的文本文件发送到另一个应用程序的公共(public)目录。我从来没有做过这样的事情,所以我什至不知道从哪里开始。任何帮助,将不胜感激。谢谢! 最佳答案 无论Rails是什么,几乎所有Web应用程序都有您的要求,大多数现代Web应用程序都需要相互通信。但是有一个小小的理解需要你坚持下去,网站不应直接访问彼此

  10. ruby - 无法运行 Rails 2.x 应用程序 - 2

    我尝试运行2.x应用程序。我使用rvm并为此应用程序设置其他版本的ruby​​:$rvmuseree-1.8.7-head我尝试运行服务器,然后出现很多错误:$script/serverNOTE:Gem.source_indexisdeprecated,useSpecification.Itwillberemovedonorafter2011-11-01.Gem.source_indexcalledfrom/Users/serg/rails_projects_terminal/work_proj/spohelp/config/../vendor/rails/railties/lib/r

随机推荐