Python开发的GUI软件实现自动更新

上一篇文章中已经实现了打包以及版本管理的功能,软件已经可以发布到服务器上,只实现简单的版本检查,要实现自动下载并更新软件还有不少问题要处理。

实现方式

在启动程序的时候开启一条后台线程去检查更新,如果有更新就提示客户是否更新,软件默认是强制更新,不更新直接退出程序。

更新又分为手动更新和自动更新:

  1. 手动更新,自动打开浏览器跳转下载地址,需要手动去覆盖
  2. 自动更新,自动下载并更新软件,自动覆盖

检查更新

和上次文章的差不多,实现检查更新功能,需要在启动程序的时候开启一条后台线程去检查更新:

class Updater:

    def __init__(self, base_url: str):
        self.base_url = base_url

    def check_for_update(self, version):
        check_url = self.base_url + '/versions.json'
        res = requests.get(check_url).json()
        if len(res) and semver.compare(res[0]['version'], version) > 0:
            return res[0]

if __name__ == '__main__':
    app = tkinter.Tk()
    t = threading.Thread(target=lambda: check_for_update(app), name='update_thread')
    t.daemon = True  # 守护为True,设置True线程会随着进程一同关闭
    t.start()
    app.mainloop()

然后在此基础上实现手动和自动更新

手动更新

第一步先实现检查到新版本能手动下载文件并自己手动覆盖,保证如果自动更新有问题也能手动更新软件。

实现如下:

import threading
import webbrowser
from test import Updater
from versions import current_version
import tkinter
import tkinter.messagebox

def check_for_update(window):
    updater = Updater('https://xxxx/app')
    ver = updater.check_for_update(current_version)
    print(ver)
    if ver is not None:
        publish_notes = '\n'.join(ver.get('publishNotes', []))
        message = f'当前版本[{current_version}], 有新版本[{ver["version"]}],更新前请关闭相关打开文件.\n' \
                  f'更新内容:\n{publish_notes}\n请选择立即去下载更新[确定],暂不更新[取消]?'
        result = tkinter.messagebox.askokcancel(title='更新提示', message=message)
        if result:
            browser_update(ver, window)
        else:
            window.destroy()

def browser_update(ver, window):
    webbrowser.open(ver.get('updateUrl'))
    window.destroy()

if __name__ == '__main__':
    app = tkinter.Tk()
    t = threading.Thread(target=lambda: check_for_update(app), name='update_thread')
    t.daemon = True  # 守护为True,设置True线程会随着进程一同关闭
    t.start()
    app.mainloop()

检查结果如下:

image-20230708130517064

点击【确定】跳转到浏览器去下载,点击【取消】自动关闭程序

自动更新

自动更新有一定难度,因为通常情况下app.exe正在使用中,不能自己把自己更新掉。

常规做法是用另外一个Updater.exe来实现更新操作,我们目前打包的是单文件exe,再来一个exe不太现实,Python打包包含有Python的运行环境,软件包一般都很大,目前选择用开启一个bat文件来更新exe文件。

更新步骤

步骤如下:

  1. 使用requests下载zip文件到tmp目录
  2. 解压tmp目录下的zip文件
  3. 复制解压文件覆盖现有除app.exe之外的其他文件和文件夹
  4. 用子进程启动bat文件,然后关闭当前程序
  5. bat文件更新app.exe文件,启动新app.exe文件

更新工具

自动更新工具代码如下:

import os
import shutil
import textwrap
import threading
import zipfile

import requests
from tqdm.tk import tqdm
import tkinter

class AutoUpdater:

    def __init__(self, download_url, **kwargs):
        self.download_url = download_url
        self.tmp_path = kwargs.get('tmp_path', 'tmp')
        self.file_name = kwargs.get('file_name')
        if not os.path.exists(self.tmp_path):
            os.mkdir(self.tmp_path)
        if not self.file_name:
            self.file_name = self.download_url.split('/')[-1]

    def download_file(self):
        th = threading.Thread(target=self.do_download_file)
        th.start()
        th.join()

    def do_download_file(self):
        download_file = os.path.join(self.tmp_path, self.file_name)
        if not os.path.exists(download_file):
            print(f'downloading file: {self.download_url}')
            response = requests.get(self.download_url, stream=True)
            chunk_size = 1024  # 每次下载的数据大小
            content_size = int(response.headers['content-length'])  # 下载文件总大小
            tq = tqdm(iterable=response.iter_content(chunk_size=chunk_size), tk_parent=None,
                      desc=f'下载文件:{self.file_name}',
                      leave=False, total=content_size, unit='B', unit_scale=True)
            with open(download_file, 'wb') as file:
                for data in tq:
                    tq.update(len(data))
                    file.write(data)
                    file.flush()
            tq.close()
            print(f'downloaded file: {self.file_name}')

    def extract_files(self):
        download_file = os.path.join(self.tmp_path, self.file_name)
        extract_dir = self.get_extract_dir()
        if not os.path.exists(extract_dir):
            os.mkdir(extract_dir)
        with zipfile.ZipFile(download_file) as zf:
            zf.extractall(path=extract_dir)  # 解压目录
        print(f'extract file: {self.file_name}')

    def get_extract_dir(self):
        download_file = os.path.join(self.tmp_path, self.file_name)
        return download_file[:download_file.rfind('.')]

    def replace_files(self):
        self.do_replace_files()

    def check_files(self):
        # 校验文件完整性,暂未实现
        pass

    def do_replace_files(self):
        extract_dir = self.get_extract_dir()
        for file in os.listdir(extract_dir):
            if not file.endswith('app.exe'):
                try:
                    print(f'替换文件:{file}')
                    self.copy_files(os.path.join(extract_dir, file), '.')
                except BaseException as e:
                    if os.path.isdir(file):
                        tkinter.messagebox.showwarning(title='错误', message=f'文件夹[{file}]有文件正在使用,更新失败,请关闭文件后重试')
                    else:
                        tkinter.messagebox.showwarning(title='错误', message=f'文件[{file}]正在使用,更新失败,请关闭文件后重试')
                    print(f'替换文件错误{e}')
                    raise e

    @staticmethod
    def copy_files(src_file, dest_dir):
        file_name = src_file.split(os.sep)[-1]
        if os.path.isfile(src_file):
            shutil.copyfile(src_file, os.path.join(dest_dir, file_name))
        else:
            dest_path = os.path.join(dest_dir, file_name)
            if os.path.exists(dest_path):
                shutil.rmtree(dest_path)
            shutil.copytree(src_file, os.path.join(dest_dir, file_name))

    def make_updater_bat(self):
        app_name = 'app.exe'
        app_file = os.path.join(self.get_extract_dir(), app_name)
        with open('updater.bat', 'w', encoding='utf8') as updater:
            updater.write(textwrap.dedent(f'''\
            @echo off
            echo 正在更新[{app_name}]最新版本,请勿关闭窗口...
            ping -n 2 127.0.0.1 > nul
            echo 正在复制[{app_file}],请勿关闭窗口...
            del app.exe
            copy {app_file} . /Y
            echo 更新完成,等待自动启动{app_name}...
            ping -n 5 127.0.0.1 > nul
            start app.exe
            exit
            '''))
            updater.flush()

    def auto_update(self):
        self.download_file()  # 下载更新文件
        self.check_files()  # 校验文件,暂未实现
        self.extract_files()  # 解压文件
        self.make_updater_bat()  # 生成替换脚本文件
        self.replace_files()  # 更新文件

调用工具更新

调用AutoUpdater实现自动更新,同时保留手动更新的逻辑,更新代码如下:

import subprocess
import threading
import webbrowser

from autoupdates import AutoUpdater
from test import Updater
from versions import current_version
import tkinter
import tkinter.messagebox

def check_for_update(window):
    updater = Updater('https://xxxx/app')
    ver = updater.check_for_update(current_version)
    if ver is not None:
        publish_notes = '\n'.join(ver.get('publishNotes', []))
        message = f'当前版本[{current_version}], 有新版本[{ver["version"]}],更新前请关闭相关打开文件.\n' \
                  f'更新内容:\n{publish_notes}\n请选择立即自动更新[是],手动下载更新[否],暂不更新[取消]?'
        force_update = ver.get('forceUpdate', False)
        result = tkinter.messagebox.askyesnocancel(title='更新提示', message=message)
        if result is not None:
            if result:
                print(f'自动更新版本[{current_version}]->[{ver["version"]}]')
                auto_update(ver, window)
            else:
                print(f'浏览器更新版本[{current_version}]->[{ver["version"]}]')
                browser_update(ver, window)
        elif force_update:
            window.destroy()

def browser_update(ver, window):
    webbrowser.open(ver.get('updateUrl'))
    window.destroy()

def auto_update(ver, window):
    window.withdraw()
    updater = AutoUpdater(ver.get('updateUrl'))
    try:
        updater.auto_update()
        # 使用bat文件更新exe文件,其他文件用python覆盖
        subprocess.Popen(f'updater.bat')
    finally:
        print('关闭主窗口.')
        window.destroy()

if __name__ == '__main__':
    app = tkinter.Tk()
    t = threading.Thread(target=lambda: check_for_update(app), name='update_thread')
    t.daemon = True  # 守护为True,设置True线程会随着进程一同关闭
    t.start()
    app.mainloop()

检查结果,自动下载也包含手动下载:

image-20230708133745016

更新使用tqdm展示tkinter进度条,方便看到下载进度。

自动产生的bat文件如下:

@echo off
echo 正在更新[app.exe]最新版本,请勿关闭窗口...
ping -n 2 127.0.0.1 > nul
echo 正在复制[tmp\xxxx\app.exe],请勿关闭窗口...
del app.exe
copy tmp\xxxx\app.exe . /Y
echo 更新完成,等待自动启动app.exe...
ping -n 5 127.0.0.1 > nul
start app.exe
exit

里面使用ping命令实现延迟几秒执行下一步操作。

目前已经测试过能够使用这种方式自动更新。

评论

  1. 屠夫哥哥
    3月前
    2024-4-23 17:57:55

    老大 自动更行小窗口不居中 一般咋处理啊,捣鼓了半天,又换回了手动更新, 无赖没解决掉心有不甘的,又起来捣鼓

    • gary
      博主
      屠夫哥哥
      3月前
      2024-4-25 9:52:14
      def center_window(win, w, h):
          # 获取屏幕 宽、高
          ws = win.winfo_screenwidth()
          hs = win.winfo_screenheight()
          # 计算 x, y 位置
          x = (ws/2) - (w/2)
          y = (hs/2) - (h/2)
          win.geometry('%dx%d+%d+%d' % (w, h, x, y))
      
      #center_window(tq._tk_window, 300, 100)
  2. 111
    已编辑
    9月前
    2023-11-02 11:25:39

    一直提示程序占用 没办法更新自己本身的exe

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇