使用Nginx和PySide6(PyQt)实现程序版本升级

  我们编写软件的时候,通常都会对软件进行迭代开发,通过程序升级来修复BUG或增加新功能。尽管软件升级的方式各异,但是它们的基本原理都是差不多的,即用新版本的程序文件替换旧版本的程序文件。那么如何实现程序的版本升级呢?

软件升级的一般步骤如下:

  1. 在服务器上发布升级文件,提供升级文件的下载服务。
  2. 软件自动(定时触发)或手动检索服务器,对比当前程序版本号和最新发布的程序版本号,判断当前程序是否需要升级。
  3. 如果发现新版本程序,则询问用户是否下载并更新程序。
  4. 当用户选择下载并更新程序后,启动升级程序并关闭当前主程序。
  5. 升级程序下载升级文件到本地,删除并替换旧程序。
  6. 关闭升级程序,启动新程序。

至此,程序升级完毕。

我用Nginx作为升级文件服务器,用PySide6做了一个升级演示程序。

1、首先修改nginx.conf,在http配置中添加文件下载配置。

    #download
    limit_conn_zone $binary_remote_addr zone=perip:1m;
    autoindex on;               # enable directory listing output
    autoindex_exact_size on;    # output file sizes rounded to kilobytes, megabytes, and gigabytes
    autoindex_localtime on;     # output local times in the directory
    limit_conn perip 1;  		# 每个ip的并发连接数
    limit_rate 1024k;			# 限制下载速度;

2、将升级文件拷贝到nginx存放页面的目录下。

使用Nginx和PySide6(PyQt)实现程序版本升级_第1张图片

其中升级文件的命名规则是文件名称+版本号,我们用浏览器测试一下。

使用Nginx和PySide6(PyQt)实现程序版本升级_第2张图片 

3、启动主程序,当前版本号为1.0.0

 使用Nginx和PySide6(PyQt)实现程序版本升级_第3张图片

4、点击“检查更新”按钮,查看服务器上的最新版本。

 使用Nginx和PySide6(PyQt)实现程序版本升级_第4张图片

5、点击“下载更新”按钮,启动升级程序。

 使用Nginx和PySide6(PyQt)实现程序版本升级_第5张图片

6、下载完成后,确认是否启动主程序。 

使用Nginx和PySide6(PyQt)实现程序版本升级_第6张图片

7、启动主程序,版本号已经变为1.0.2,升级完成。

使用Nginx和PySide6(PyQt)实现程序版本升级_第7张图片 

 使用Nginx和PySide6(PyQt)实现程序版本升级_第8张图片

主程序代码如下:

import os
import re
import sys
import time
import traceback
import urllib.request
from packaging.version import Version
from PySide6 import QtCore
from PySide6.QtCore import Slot, Signal, QThread
from PySide6.QtWidgets import QWidget, QVBoxLayout, QApplication, QStyleFactory, QPushButton, QLabel, QHBoxLayout, \
    QSpacerItem, QSizePolicy

APP_VERSION = '1.0.0'
APP_FILE_NAME = 'main.exe'
UPDATE_FILE_NAME = 'update.exe'
REQUEST_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36'


class MainWindow(QWidget):

    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)
        self.appName = None
        self.appVersion = None
        self.fileName = None
        self.fileSize = None
        self.datetime = None
        self.downloadURL = None

        self.resize(450, 100)
        self.setWindowTitle(f'主程序')

        self.verticalLayout = QVBoxLayout(self)

        self.versionLabel = QLabel(f'当前版本: {APP_VERSION}')
        self.verticalLayout.addWidget(self.versionLabel)

        self.tipLabel = QLabel(self)
        self.verticalLayout.addWidget(self.tipLabel)

        self.horizontalLayout = QHBoxLayout()

        self.horizontalSpacer = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
        self.horizontalLayout.addItem(self.horizontalSpacer)

        self.checkUpdateButton = QPushButton('检查更新')
        self.horizontalLayout.addWidget(self.checkUpdateButton)

        self.downloadUpdateButton = QPushButton('下载更新')
        self.downloadUpdateButton.setEnabled(False)
        self.horizontalLayout.addWidget(self.downloadUpdateButton)
        self.horizontalSpacer2 = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
        self.horizontalLayout.addItem(self.horizontalSpacer2)
        self.verticalLayout.addLayout(self.horizontalLayout)

        self.verticalLayout.setStretch(1, 1)

        self.searchThread = CheckUpdateThread(self)
        self.checkUpdateButton.clicked.connect(lambda: self.searchThread.start())
        self.downloadUpdateButton.clicked.connect(self.downloadUpdate)

    @Slot(str)
    def showCheckUpdateResult(self, appName, appVersion, fileName, datetime, fileSize, downloadURL):
        self.appName = appName
        self.appVersion = appVersion
        self.fileName = fileName
        self.datetime = datetime
        self.fileSize = fileSize
        self.downloadURL = downloadURL

        currentVersion = Version(APP_VERSION)
        latestVersion = Version(appVersion)
        if latestVersion > currentVersion:
            self.tipLabel.setText(f'发现新版本:{latestVersion}')
            self.downloadUpdateButton.setEnabled(True)
        else:
            self.tipLabel.setText(f'未发现新版本')
            self.downloadUpdateButton.setEnabled(False)

    def downloadUpdate(self):
        try:
            if os.path.exists(UPDATE_FILE_NAME):
                # cmd = f'{UPDATE_FILE_NAME} {APP_FILE_NAME} {APP_VERSION}'
                command = f'update.exe {self.appName} {self.appVersion} {self.fileName} {self.datetime} {self.fileSize} {self.downloadURL}'
                print(command)
                os.popen(command)
                sys.exit(0)
            else:
                self.tipLabel.setText('未发现升级程序')
        except Exception as e:
            print(e)
            print('error')

    def showTip(self, message):
        self.tipLabel.setText(message)


# 信号对象
class Communicate(QtCore.QObject):
    # 创建一个信号
    tipSignal = Signal(str)
    checkUpdateSignal = Signal(str, str, str, str, str, str)


class CheckUpdateThread(QThread):
    def __init__(self, parent=None):
        QThread.__init__(self, parent)
        self.signals = Communicate()
        self.signals.tipSignal.connect(parent.showTip)
        self.signals.checkUpdateSignal.connect(parent.showCheckUpdateResult)

    def run(self):
        self.signals.tipSignal.emit('正在检查更新...')
        appName = 'UpdateDemo'
        appVersion, fileName, datetime, fileSize, downloadURL = self.checkUpdate(appName)
        self.signals.checkUpdateSignal.emit(appName, appVersion, fileName, datetime, fileSize, downloadURL)
        # self.signals.tipSignal.emit('')

    def checkUpdate(self, appName):

        http_url = f'http://127.0.0.1/{appName}/'
        html = self.open_url(http_url)
        print(html)
        if html:
            file_list = re.findall(r'(.*) (.*) (.*)\r', html)

            latestVersion = None
            latestFileURL = None
            latestFileName = None
            latestFileSize = None
            latestDatetime = None
            for file in file_list:
                version = re.findall(f"{appName}(.+?).exe", file[0])[0]
                version = Version(version)
                if not latestVersion or version > latestVersion:
                    latestVersion = version
                    latestFileURL = http_url + file[0]
                    latestFileName = file[1].strip()
                    latestDatetime = str(time.mktime(time.strptime(file[2].strip(), "%d-%b-%Y  %H:%M")))
                    latestFileSize = file[3]
            return str(latestVersion), latestFileName, latestDatetime, latestFileSize, latestFileURL

    def open_url(self, url):
        if url:
            try:
                req = urllib.request.Request(url)
                req.add_header('User-Agent', REQUEST_USER_AGENT)
                response = urllib.request.urlopen(req)
                html = response.read().decode('utf-8')
                return html
            except Exception as e:
                self.signals.tipSignal.emit(str(e))
                traceback.print_exc()


if __name__ == "__main__":
    app = QApplication([])
    app.setStyle(QStyleFactory.create('Fusion'))
    window = MainWindow()
    window.show()
    sys.exit(app.exec())

 升级程序代码如下:

import os
import sys
import time
import requests
from contextlib import closing
from PySide6 import QtCore
from PySide6.QtCore import QThread, Slot, Signal
from PySide6.QtWidgets import QWidget, QVBoxLayout, QApplication, QStyleFactory, QLineEdit, QProgressBar, \
    QLabel, QFormLayout, QMessageBox

APP_FILE_NAME = 'main.exe'
REQUEST_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36'


class MainWindow(QWidget):

    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)
        self.appName = None
        self.appVersion = None
        self.fileName = None
        self.datetime = None
        self.fileSize = None
        self.downloadURL = None

        self.resize(550, 250)
        self.setWindowTitle(f'升级程序')

        self.verticalLayout = QVBoxLayout(self)

        self.formLayout = QFormLayout()

        self.appNameLabel = QLabel('应用名称')
        self.formLayout.setWidget(0, QFormLayout.LabelRole, self.appNameLabel)

        self.appNameLineEdit = QLineEdit(self)
        self.appNameLineEdit.setEnabled(False)
        self.formLayout.setWidget(0, QFormLayout.FieldRole, self.appNameLineEdit)

        self.appVersionLabel = QLabel('应用版本')
        self.formLayout.setWidget(1, QFormLayout.LabelRole, self.appVersionLabel)

        self.appVersionLineEdit = QLineEdit(self)
        self.appVersionLineEdit.setEnabled(False)
        self.formLayout.setWidget(1, QFormLayout.FieldRole, self.appVersionLineEdit)

        self.fileNameLabel = QLabel('文件名称')
        self.formLayout.setWidget(2, QFormLayout.LabelRole, self.fileNameLabel)

        self.fileNameLineEdit = QLineEdit(self)
        self.fileNameLineEdit.setEnabled(False)
        self.formLayout.setWidget(2, QFormLayout.FieldRole, self.fileNameLineEdit)

        self.fileSizeLabel = QLabel('文件大小')
        self.formLayout.setWidget(3, QFormLayout.LabelRole, self.fileSizeLabel)

        self.fileSizeLineEdit = QLineEdit(self)
        self.fileSizeLineEdit.setEnabled(False)
        self.formLayout.setWidget(3, QFormLayout.FieldRole, self.fileSizeLineEdit)

        self.datetimeLabel = QLabel('发布时间')
        self.formLayout.setWidget(4, QFormLayout.LabelRole, self.datetimeLabel)

        self.datetimeLineEdit = QLineEdit(self)
        self.datetimeLineEdit.setEnabled(False)
        self.formLayout.setWidget(4, QFormLayout.FieldRole, self.datetimeLineEdit)

        self.downloadURLLabel = QLabel('下载地址')
        self.formLayout.setWidget(5, QFormLayout.LabelRole, self.downloadURLLabel)

        self.downloadURLLineEdit = QLineEdit(self)
        self.downloadURLLineEdit.setEnabled(False)
        self.formLayout.setWidget(5, QFormLayout.FieldRole, self.downloadURLLineEdit)

        self.verticalLayout.addLayout(self.formLayout)

        self.progressBar = QProgressBar(self)
        self.progressBar.setRange(0, 100)
        self.progressBar.setValue(0)
        self.progressBar.setTextVisible(True)
        self.progressBar.setFormat(' 下载进度:%p%')
        self.progressBar.setStyleSheet("QProgressBar{"
                                       "height:20px;"
                                       "text-align:center;"
                                       "border: 1px solid #DDDCDC;"
                                       "background: #F1F1F1;"
                                       "}")

        self.verticalLayout.addWidget(self.progressBar)

        self.tipLabel = QLabel(self)
        self.verticalLayout.addWidget(self.tipLabel)

        self.updateThread = DownloadThread(self)
        self.updateThread.start()

    def setUpdateInfo(self, info):
        self.appName = info[0]
        self.appVersion = info[1]
        self.fileName = info[2]
        self.datetime = info[3]
        self.fileSize = info[4]
        self.downloadURL = info[5]

        self.appNameLineEdit.setText(self.appName)
        self.appVersionLineEdit.setText(self.appVersion)
        self.fileNameLineEdit.setText(self.fileName)
        self.datetimeLineEdit.setText(time.strftime("%Y-%m-%d %H:%M", time.localtime(float(self.datetime))))
        self.fileSizeLineEdit.setText(f'{self.fileSize} 字节')
        self.downloadURLLineEdit.setText(self.downloadURL)

    @Slot(int)
    def downloadUpdate(self, value):
        self.progressBar.setValue(value)

        if value == 100:
            msgBox = QMessageBox(QMessageBox.Icon.Question, "确认", "是否重新启动主程序?",
                                 QMessageBox.StandardButton.NoButton, self)
            msgBox.addButton("确定", QMessageBox.ButtonRole.AcceptRole)
            msgBox.addButton("取消", QMessageBox.ButtonRole.RejectRole)

            if msgBox.exec() == 0:
                try:
                    os.popen(r'main.exe')
                except Exception as e:
                    print(e)

            sys.exit(0)

    @Slot(str)
    def showTip(self, message):
        self.tipLabel.setText(message)


# 信号对象
class Communicate(QtCore.QObject):
    # 创建一个信号
    tipSignal = Signal(str)
    downloadUpdateSignal = Signal(int)


class DownloadThread(QThread):
    def __init__(self, parent=None):
        QThread.__init__(self, parent)
        self.parent = parent
        self.signals = Communicate()
        self.signals.tipSignal.connect(parent.showTip)
        self.signals.downloadUpdateSignal.connect(parent.downloadUpdate)

    def run(self):
        self.signals.tipSignal.emit('正在下载更新文件...')
        file_path = f'{self.parent.fileName}'  # 文件路径
        with closing(requests.get(self.parent.downloadURL, headers={"User-Agent": REQUEST_USER_AGENT},
                                  stream=True)) as response:
            chunk_size = 1024  # 单次请求最大值
            content_size = int(response.headers['content-length'])  # 内容体总大小
            data_count = 0
            with open(file_path, "wb") as file:
                for data in response.iter_content(chunk_size=chunk_size):
                    file.write(data)
                    data_count = data_count + len(data)
                    now_jd = (data_count / content_size) * 100
                    # print("\r 文件下载进度:%d%%(%d/%d) - %s" % (now_jd, data_count, content_size, file_path), end=" ")
                    self.signals.tipSignal.emit(f'文件已下载:({data_count}字节/{content_size}字节) {round(now_jd, 2)}%')
                    self.signals.downloadUpdateSignal.emit(now_jd)

        if os.path.exists(file_path):
            if os.path.exists(APP_FILE_NAME):
                os.remove(APP_FILE_NAME)
            os.rename(file_path, APP_FILE_NAME)
            self.signals.tipSignal.emit('更新文件下载完毕,重新启动程序后生效。')
        else:
            self.signals.tipSignal.emit('更新文件失败')


if __name__ == "__main__":

    argv = sys.argv[1:]
    if not argv:
        print('参数错误')
    else:
        app = QApplication([])
        app.setStyle(QStyleFactory.create('Fusion'))
        window = MainWindow()
        window.setUpdateInfo(argv)
        window.show()
        sys.exit(app.exec())

这里只考虑了升级主程序文件,升级功能相对简单,后续我再研究一下多文件多模块的升级。欢迎留言交流!

你可能感兴趣的:(Python,nginx,pyqt,服务器)