GUI software developed by Python realizes automatic update

Original link: https://fugary.com/?p=523

In the previous article, the packaging and version management functions have been realized. The software can be released to the server, and only a simple version check is implemented. There are still many problems to be solved to realize automatic download and update of the software.

Method to realize

When the program is started, a background thread is opened to check for updates. If there is an update, the customer will be prompted whether to update. The software defaults to a forced update, and the program will be exited without updating.

Updates are further divided into manual updates and automatic updates:

  1. Manual update, automatically open the browser to jump to the download address, need to manually override
  2. Auto update, auto download and update software, auto overwrite

Check for updates

Similar to the previous article, to implement the function of checking for updates, you need to start a background thread to check for updates when starting the program:

 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()

Then implement manual and automatic updates on this basis

manual update

The first step is to check that the new version can be manually downloaded and overwritten by yourself, so that if there is a problem with the automatic update, the software can also be updated manually.

The implementation is as follows:

 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()

The inspection results are as follows:

image-20230708130517064

Click [OK] to jump to the browser to download, click [Cancel] to automatically close the program

automatic update

It is difficult to update automatically, because usually app.exe is in use and cannot update itself.

The conventional method is to use another Updater.exe to implement the update operation. We currently package a single-file exe , and it is not realistic to add another exe . Python package contains Python operating environment. The software package is generally very large. Currently, we choose to use the open A bat file to update the exe file.

update steps

Proceed as follows:

  1. Use requests to download the zip file to the tmp directory
  2. Unzip the zip文件in the tmp directory
  3. Copy the decompressed file to overwrite existing files and folders except app.exe
  4. Start the bat file with a subprocess, then close the current program
  5. Update app.exe file with bat file and start the new app.exe file

update tool

The automatic update tool code is as follows:

 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() # 更新文件

call tool update

Call AutoUpdater to realize automatic update while retaining the logic of manual update. The update code is as follows:

 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()

Check the results, automatic downloads also include manual downloads:

image-20230708133745016

The update uses tqdm to display the tkinter progress bar, which is convenient to see the download progress.

The automatically generated bat file is as follows:

 @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

The ping command is used to delay the execution of the next step for a few seconds.

It has been tested to be able to use this method to automatically update.

This article is transferred from: https://fugary.com/?p=523
This site is only for collection, and the copyright belongs to the original author.