我们编写软件的时候,通常都会对软件进行迭代开发,通过程序升级来修复BUG或增加新功能。尽管软件升级的方式各异,但是它们的基本原理都是差不多的,即用新版本的程序文件替换旧版本的程序文件。那么如何实现程序的版本升级呢?
软件升级的一般步骤如下:
至此,程序升级完毕。
我用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存放页面的目录下。
其中升级文件的命名规则是文件名称+版本号,我们用浏览器测试一下。
3、启动主程序,当前版本号为1.0.0
4、点击“检查更新”按钮,查看服务器上的最新版本。
5、点击“下载更新”按钮,启动升级程序。
6、下载完成后,确认是否启动主程序。
7、启动主程序,版本号已经变为1.0.2,升级完成。
主程序代码如下:
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())
这里只考虑了升级主程序文件,升级功能相对简单,后续我再研究一下多文件多模块的升级。欢迎留言交流!