pyinstaller打包的python exe程序实现自动更新
点击上方↑↑↑蓝字[协议分析与还原]关注我们
近期在为一系列python工具exe程序加自动更新功能,方便管理维护及分发。
近期沉迷于python的开发,积累了一系列python工具,需要部署到其它机器上,部署的过程都是拷贝,拷贝一次也就罢了,一碰到程序没写好,需要升级,就很麻烦了,得再拷贝一次,说不定还拷错了,烦死个人。
于是决定做个能够匹配我工具升级需求的自动更新工具,每台待安装电脑的第一次手动安装完整程序,之后每次程序启动的时候自动升级,从服务器上下载升级包,覆盖旧程序,完成升级。
由于我的python工具的特殊性,是由pyinstaller打包成exe的py文件以及一批网页文件组成,因此升级包需要综合考虑,不能顾头不顾腚。
01
—
自动更新框架
毫无疑问,我们的整个程序的更新应该是C/S架构,服务端存放安装包和版本更新控制服务,客户端每次启动时与服务器通信检查更新,如果有更新则用更新替换现有版本,之后进入工具主流程。
为了客户端的灵活性,我们选择客户端启动更新与客户端主体功能分离,二者独立实现,在启动更新部分实现自动更新功能并完成主体功能的启动,主体功能使用单独的进程实现,这样自动更新就只需要关心更新部分,相对稳定,而工具主体部分,则可随意根据项目调整。
大概就是这样:
框架基本上就是这样了,一个服务端程序,一个启动程序,一个主体程序。服务端程序另外开发,我直接用php开发了个简单的服务接口,校验请求参数及返回程序升级包即可,启动程序负责更新启动部分,主体程序负责工具主体部分,二者用python开发,用脚本统一打包。
02
—
启动程序
启动程序负责与服务器联系,进行自动更新,自动更新完毕启动主体程序。
启动程序大概功能实现如下:
def main(process_name):
controlstop_process(process_name) # 停止exe
current_dir = os.getcwd()
local_exe_path = os.path.join(current_dir, process_name)
update_exe(local_exe_path) # 更新exe及相关文件,删除重新下载
startexe(local_exe_path) # 启动新exe
首先停止已启动的主体程序,咱要专注更新。
def controlstop_process(process_name):
current_dir = os.getcwd()
for proc in psutil.process_iter(['name']):
if proc.info['name'] == process_name:
proc.terminate()
print(f"进程 {process_name} 已终止")
return
接下来进行更新过程,检查更新并进行更新,更新主体程序exe及相关文件,为了更好地实现,直接删除当前目录下的这些内容。检查更新过程中,获取主体程序exe的版本参数到服务器比对,需要更新则传回文件内容。
def update_exe(local_exe_path):
ret, tmppath = checkupdate(local_exe_path)
if ret and len(tmppath) > 0:
current_dir = os.getcwd()
delete_all_but_startmain()
tmpdatapath = os.path.join(tmppath, 'dist')
shutil.copytree(tmpdatapath, current_dir, dirs_exist_ok=True)
shutil.rmtree(tmppath)#删除临时目录
下载的更新内容zip,我们在这里直接写临时目录然后解压,返回解压后路径供上面的更新函数使用。
respdata = ret.content
with tempfile.NamedTemporaryFile(delete=False) as temp_file:
temp_file.write(respdata)
temp_file_path = temp_file.name
# 创建临时子目录
temp_subdir = tempfile.mkdtemp()
# 解压到临时子目录
with zipfile.ZipFile(temp_file_path, 'r') as zip_ref:
for file_info in zip_ref.infolist():
file_info.filename=file_info.filename.encode('cp437').decode('gbk')
zip_ref.extract(file_info,temp_subdir)
# zip_ref.extractall(temp_subdir)
# 删除临时文件
os.remove(temp_file_path)
# shutil.rmtree(temp_subdir)
return True, temp_subdir
最后当主体程序更新完毕,就是启动主体程序了。
03
—
打包exe脚本
主体程序和启动程序的python代码是放在一起的,为方便打包,当然要用一个脚本来统一搞更合适,何况还有些网页相关文件需要一起打包。因为启动程序后续不更新,为方便管理,除第一次外,我们将主体程序和其它网页相关文件放一个压缩包打包发布,而启动程序单独发布。
这个过程中,exe的生成当然使用pyinstaller了,不过,新版本pyinstaller无法加key,有点小小的遗憾,如果需要稍微加点形同虚设的防护,估计得用老版本的pyinstaller,如果想真正防护起来,防止被逆向破解,要么用cython,要么换一种真正的编程语言了。
一定要记住,python是脚本语言。
一个简单的bat脚本就是这样:
@echo off
chcp 65001
if exist "dist\haha.exe" (
del "dist\haha.exe"
)
if exist "dist\startmain.exe" (
del "dist\startmain.exe"
)
pyinstaller --version-file=version.txt -F -n "haha.exe" main.py
xcopy "haha.html" dist /Y
::设置变量
set source_folder=css
set target_folder=dist\css
::检查目标文件夹是否存在,不存在则创建
if not exist "%target_folder%" (
md "%target_folder%"
)
::复制文件夹
xcopy "%source_folder%" "%target_folder%" /S /E /Y
::设置变量
set source_folder=js
set target_folder=dist\js
::检查目标文件夹是否存在,不存在则创建
if not exist "%target_folder%" (
md "%target_folder%"
)
::复制文件夹
xcopy "%source_folder%" "%target_folder%" /S /E /Y
::设置变量
set source_dir=dist\*
set zip_file=dist\haha.zip
::检查源目录是否存在
if not exist "%source_dir%" (
echo 源目录不存在!
pause
exit /b
)
::创建zip文件
"zip.exe" -r %zip_file% "%source_dir%"
echo "打包完成!"
::startmain不打包
pyinstaller --version-file=version.txt -F -n "startmain.exe" startmain.py
打包得到的haha.zip,就是最终放到服务器上去的升级包了,而startmain.exe则是启动程序,记住,这个程序里面会删当前目录下的其它文件,所以,一定要把它放单独文件夹,防止删库跑路,当然,这是一个码农的得道之路,也不是不能做。
04
—
服务端程序
要问世界上最好的语言是什么,毫无疑问,当然是PHP了,大家请大声说出来:PHP是世界上最好的语言。
我们的服务端程序,当然要用PHP来写,先想好怎么实现。
我们有一个版本管理文件,里面有需要维护的升级包的信息,包括应用名称、版本、大小、哈希、存储路径、升级包名称、是否强制更新,当然,支持管理多个工具的版本。
版本管理文件长这样:
{
"apps": [
{
"app_id": "haha",
"version": "1.2.3",
"reset": "1",//1 强制更新,0 正常更新
"hash": "e10adc3949ba59abbe56e057f20f883e", // 文件哈希,根据实际情况选择算法
"size": 123456, // 文件大小,单位字节
"path": "updates/",
"update_file": "haha_1.2.3.zip" // 更新文件
},
{
"app_id": "hehe",
"version": "1.2.3",
"reset": "0",
"hash": "e10adc3949ba59abbe56e057f20f883e", // 文件哈希,根据实际情况选择算法
"size": 123456, // 文件大小,单位字节
"path": "updates/",
"update_file": "hehe_1.2.3.zip" // 更新文件
}
]
}
之后,就是我们的PHP大佬出马了,利用这个文件来进行版本的比对,决定是否给客户端更新:
<?php
$appId=$_POST['app'];
$version= $_POST['version'];
// 版本信息文件路径
$versionFile = 'version.txt';
$contents = file_get_contents($versionFile);
$pattern = '/\/\/.*\n/';
$contents = preg_replace($pattern, '', $contents);
// 读取当前版本信息
$versionInfo = json_decode($contents, true);
// 查找对应App的信息
foreach ($versionInfo['apps'] as $app) {
if ($app['app_id'] === $appId) {
// 找到对应App,进行版本比较
if ( "0"!==$app['reset'] or $version!==$app['version'] ) {
// 版本不一致,返回更新信息
$filePath = $app['path'] . $app['update_file'];
if (file_exists($filePath)) {
header('Content-Type: application/zip'); // 根据文件类型设置
header('Content-Disposition: attachment; filename="' . $appInfo['update_file'] . '"');
header('Content-Length: ' . filesize($filePath));
readfile($filePath);
exit;
} else {
// 文件不存在
http_response_code(404);
echo '更新文件不存在';
exit;
}
} else {
// 版本一致,无需更新
http_response_code(200);
echo '版本一致,无需更新';
exit;
}
}
}
http_response_code(200);
echo '没有app,无需更新';
?>
短短几行,PHP就完成了我们需要的功能,棒不棒。
05
—
JAVA在诋毁同行
一位PHP程序员去面试,面试官问:“你为什么选择PHP?”
程序员答:“因为PHP是世界上最好的语言,它能让我用最少的代码写出最多的bug!”
新的规则,及时收推文要先给公号星标
别忘了星标一下,不然就错过了
长按进行关注,时刻进行交流。
作者:多姿多彩