Web网络服务模块——程序自动升级

强制升级机制

使用某些软件时经常遇到“发现新版本,马上升级”的提示。对于一些程序,可以选择忽略提示,不进行升级。但有时,程序只给用户提供升级按钮,无法选择忽略提示的升级信息,这就是强制升级的机制。

强制升级主要出于两个方面的原因:1. 发现当前程序的主要bug,必须升级补丁,否则程序运行会出现问题;2. 程序某些关键功能模块需要进行更新,如果不升级,则软件的某些功能将不能使用,或者使用出错。

本系列笔记中books项目使用了强制自动升级的策略,我们也可以将其改为可选的升级策略。

实现强制升级机制需要以下3个方面的内容:

  1. 程序启动时要从网络读取程序新版本信息。
  2. 读出的新版本信息要与本地程序版本号进行对比,并根据版本对比结果判断是否需要升级,如果需要则进入下一步。
  3. 锁定其他程序按钮,只允许用户点击升级按钮。单击后程序进入升级程序。

系统实现

读取INI文件中的版本信息

books项目中使用INI文件记录程序的版本信息及相关的配置状态。更多关于INI文件的信息可参考INI配置文件的格式

读取版本信息包含两方面内容:1. 从网络中读取是否存在新版本程序,若有则读取新版本号;2. 读取本地版本信息。

从网络中读取新版本程序时,可以在指定的某处网站保存一个INI文件,该文件内部记录了新程序版本号,程序升级区域限制及最新程序下载地址等。例如GitHub—initxuan::books/updateversion.ini文件,其关键部分如下:

[update]
updateZone = ALL
updateVersion = 2.0

updateZone用于指定升级区域,不同区域用户可以根据自身情况选择是否允许升级,或者升级不同区域新版本程序,这样便于进行区域控制。此处可以通过配置参数来指定区域进行升级,如不同区域可通过设置区号进行区别,不同区域的区号用空格分隔;如果参数设置为“ALL”则表示所有区域均可升级。updateVersion用于指定当前最新程序版本号。应用程序会读取updateVersion的数值,只有当它比用户当前程序的版本号大时,升级程序才会启动。

当我们使用INI文件进行版本信息的记录时,需要将INI文件放置在某网站目录下,共程序下载后读取。

设计本地信息INI文件

在本地我们也需要使用INI文件来记录程序的相关信息包括当前程序的版本号、要从哪个网址下载升级程序等信息。例如GitHub—initxuan::books/config.ini,其关键部分如下:

[program]
userZone = 
username =
userID =
version = 1.0
installedDir = 

[update]
baseUrl = http://xxx.cn/release/
exeFileName = _update_setup_
versionFileName = updateversion.ini
updateLocalDir = update

[program]区块指定了程序相关的所有信息,包括用户的区域、用户名、用户ID、当前程序版本号等。[update]区块定义了从哪里下载升级INI信息文件,versionFileName指定了要下载的INI文件名,updateLocalDir指定了下载后的文件要放在哪个目录下(注:该值要作为参数传给上一篇笔记中记述的mydir.mkdir(LOCALUPDATEDIR)函数,在目录中创建新的update目录,并将升级的EXE文件下载到这里。)最后exeFileName指定了要下载的EXE文件名,按照程序规则,会在这个文件名前加上用户区域信息段,在文件名后加上版本号和“.exe”后缀,形成类似于“ALL_update_setup_2.0.exe”的文件名,然后在baseUrl指定的网址目录下下载这个文件。

读写本地目录下的INI文件

本地信息INI文件可以由程序设计者自由选择目录存放,按照惯例,这种信息文件一般放在两个位置:1. 应用程序所在目录。2. Windows常用的系统目录,如My Documents目录。books项目使用后者,将其放在My Documents下面的一个子目录,供程序读取。Qt程序提取Windows系统的目录My Documents时分为两步:1. 使用QStandardPaths::standardLocations(QStandardPaths::DocumentsLocation)得到目录My Documents的字符串列表;2. 从列表头部提取出目录的字符串。代码如下:

// 0.1寻找系统INI // my documents/x/config.ini
QStringList slist = QStandardPaths::standardLocations(QStandardPaths::DocumentsLocation);    // 第一步
QDir documentsDir = slist.at(0);    //第二步
QString configIni = "/x/config.ini";
QString configIniWhole = documentsDir.path() + configIni;

因为读出的目录My Documents尾部没有“/”,因此要在“/x/config.ini”头部加一个“/”,形成完整的目录文件地址。

使用Qt提取其他Windows系统目录时同样采用这两个函数,具体内容参见QStandardPaths Class | Qt Core 5.11

读出INI文件之后,就可以对INI文件进行读取解析了。Qt为读取INI文件提供了非常方便使用的类:QSettings,使用时仅需将INI文件名作为参数实例化该类,便可进行信息读取工作。参考代码如下:

// 0.2读入INI
QSettings configIniRead(configIniWhole, QSettings::IniFormat);
// 0.2.1 config
curUserName = configIniRead.value("/program/username").toString();
if(curUserName.isEmpty()) isLogin = false;
else isLogin = true;
curVersion = configIniRead.value("/program/version").toDouble();
curInstalledDir = configIniRead.value("/program/installedDir").toString();
QSettings configIniWrite(configIniWhole, QSettings::IniFormat);
configIniWrite.setValue("/program/installedDir", QDir::currentPath().trimmed());

//0.2.2 update
baseUrl = configIniRead.value("/update/baseUrl").toString();
exeFileName = configIniRead.value("/update/exeFileName").toString();
versionFileName = configIniRead.value("/update/versionFileName").toString();
updateDir = configIniRead.value("/update/updateLocalDir").toString();
isThereNewUpdate = false;    // 先设置为false,后面有比较版本号,如果大于当前版本,则设置为true
qDebug() << "---" << curVersion << "==" << curUserName << "==" << isLogin;

用户登录后就将用户信息记录在本地INI文件中,如果INI文件中没有用户信息,则表示用户未登录。程序先读取INI文件中用户信息,判断用户是否登录;然后读取升级相关地址、升级文件名、升级信息文件名等;最后设定isThereNewUpdate为false,等待后续如果有新版本程序,再将其设为true。

向INI文件写入信息可参考上述代码中:

QSettings configIniWrite(configIniWhole, QSettings::IniFormat);
configIniWrite.setValue("/program/installedDir", QDir::currentPath().trimmed());

通过定义另一个QSettings实例,使用函数setValue对指定的INI区块写入信息即可。

逻辑判断

接下来程序需要到指定网址下载升级的INI文件,下载的代码使用本项目即books项目中的下载类实现,然后进行读取分析,进行版本号比较的逻辑判断。

当某个功能被频繁使用时,应将该功能封装成类。

下载后的INI文件开始进行信息读取,进行版本比较,相关代码如下:

QString filename = updateDir + "/" + versionFileName;
QFile file(filename);
if(file.size() == 0){
    QMessageBox::information(this, "升级", "升级文件检测失败,请检查您的网络是否正常。");
}
else{
    // 打开文件,读版本号
    QSettings updateIniRead(filename, QSettings::IniFormat);
    QString updateZone = updateIniRead.value("/update/updateZone").toString();
    double updateVersion = updateIniRead.value("/update/updateVersion").toDouble();
    QString strUpdateVersion = updateIniRead.value("/update/updateVersion").toString();
    //qDebug() << "here" << updateVersion << updateZone << curUserZone;
    if(!curUserZone.isEmpty() && (updateZone.contains(curUserZone)) ||
      updateZone.contains("ALL") && (updateVersion > curVersion)){ // 同一区域,版本号不同
        isThereNewUpdate = true;    // 不同INI文件,升级后由升级程序直接复制my documents/config.ini
        QString tmpZone = updateZone.contains("ALL") ? "ALL" : curUserZone;
        exeFileName = tmpZone + tmpExeFileName + strUpdateVersion + ".exe";
    }
}
file.remove();

只有当新版本大于当前版本的版本号,且用户已经登录、用户区域符合要求时,isThereNewUpdate才设置为true。根据上述信息得到下一步要下载的EXE文件名为exeFileName,然后根据这个文件名从网络中下载指定升级文件,开始升级。程序最后使用File.remove()将升级信息INI文件删除,防止非法用户窃取相关信息。

开始下载

如果获得了指定升级文件的相关信息,那么就允许用户单击“升级”按钮实现升级,这需要程序GUI界面的配合。如果无新版本升级信息,则“升级”按钮变灰;如果有升级版本,则“升级”按钮点亮,其他按钮变灰不可用。具体代码如下:

if(isThereNewUpdate){
    ui->lable_Update->setText("发现新版本,请单击升级程序按钮,否则无法正常使用该程序");
    ui->pushButton->setEnabled(false);
    ui->progressBar->hide();
}
else{
    ui->label_Update->setText("未发现新版本程序。");
    ui->pushButtonUpdate->setEnabled(false);
    ui->progressBar->hide();

ui->label_Update->setText控制一个Label模块,用于显示提示文字信息。ui->pushButton->setEnabled函数控制“升级”按钮,如果参数为false,则按钮灰;如果参数为true,则按钮点亮。ui->progressBar->hide()函数隐藏进度条。因为只有单击“升级”按钮开始正式升级程序时进度条才亮,并通过Qt网络模块connect函数与下载升级文件的百分比关联。代码如下:

void Dialog::on_pushButtonUpdate_clicked()
{
    // 显示进度条
    ui->progressBar->setValue(0);
    ui->progressBar->show();
    ui->label_Update->setText("正在下载升级程序,请稍后...");
    
    // 升级程序
    UpdateByNetwork *pUpdateExeFile = new UpdateByNetwork();
    pUpdateExeFile->setBaseAddress(baseUrl);
    pUpdateExeFile->setDownloadFileName(exeFileName);
    pUpdateExeFile->setlocalUpdateDir(updateDir);
    pUpdateExeFile->startDownload();
    connect(pUpdateExeFile->pReply, QNetworkReply::downloadProgress, this, myDownloadProgress);
    while(!pUpdateExeFile->isDownloaded())
    {
        QCoreApplication::processEvents();
        //QThread::currentThread()->msleep(300);
        //QThread::currentThread()->yieldCurrentThread();  // 不放弃当前线程,让它执行myFinished
    }

    QString exe = updateDir + "/" + exeFileName;
    QFile exeFile(exe);
    if(!exeFile.open(QIODevice::ReadOnly)){
        QMessageBox::information(this, "升级", "下载升级程序失败,请检查网络是否连通。");
        exeFile.close();
        delete pUpdateExeFile;
        return;
    }
    else if(exeFile.size() < 4096){
        if(exeFile.size() == 0)  // 未连接网络,下载的程序大小为0
            QMessageBox::information(this, "升级", "下载升级程序失败,请检查网络是否连通。");
        if(exeFile.size() < 4096)  // 连接了网络,但下载源错误,下载了2KB大小的文件(内容为tomcat错误代码)
            QMessageBox::information(this, "升级", "下载升级程序失败,下载源不存在。请联系客服。");
        exeFile.close();
        delete pUpdateExeFile;
        return;
    }
}

上述代码中循环while(!pUpdateExeFile->isDownloaded()){ }很重要,如果没有这段代码,程序将无法正常运行。常见的情况是升级程序还没下载完,主程序已经执行完,两者之间没有实现同步。

执行上述下载代码后,将升级EXE文件下载到指定目录,然后判断下载的EXE文件是否有效。常用的方法是通过下载文件的大小是否为0来判断下载是否成功。但有时在tomcat服务器下载文件时,如果文件不存在,将返回一个大小约为2KB的信息文件,因此需要补充判断else if(exeFile.size() < 4096),其中4096可以是任意大于2KB的值。

需要特别注意,有时根据网络传输情况,一个EXE文件还没有完全下载,Qt程序就给出完成下载的信息。整个过程完全合法,没有错误,但是下载的EXE文件不完整,无法使用。这是Qt本身的bug,如果要修改,则需要对封装的下载类代码进行补充:一种方法是利用多线程技术分块下载,在较差的环境下仍能以较快的速度进行下载;第二种方法是使用多次检验技术,反复判断Qt函数返回的数据是否完整、准确,如反复调用void myDownloadProgress(qint64 bytesReceived, qint64 bytesTotal)函数,确定获取的下载文件字节数是否正确。

启动进程外EXE文件完成升级

下载的升级程序是EXE格式的可执行文件,要完成升级还需要两个步骤:1. 启动这个EXE文件;2. 关闭当前运行的应用程序,等待文件覆盖,完成升级。代码如下:

// 已下载:执行升级程序,关闭当前程序
qDebug() << "EXE已下载";
delete pUpdateExeFile;
ui->label_Update->setText("升级程序下载完毕。");
ui->progressBar->hide();
ui->pushButtonUpdate->setEnabled(false);
QMessageBox::information(this, "升级", "下载升级程序成功,单击确定按钮开始升级。\n\n单击确定按钮后,将关闭当前程序并启动升级程序。");
QProcess::startDetached(updateDir + "/" + exeFileName);
// QProcess::startDetached("update/xjx_jxs_setup.exe");
qApp->quit();

升级程序下载完成后,首先有一些附加的工作需要处理,如隐藏升级进度条、Label字段显示提示信息等;然后会显示一个对话框,该对话框不仅是提示用户程序下载完毕,还可以使用户单击后,执行程序外的EXE升级文件以完成升级。

执行进程外的EXE进程使用Qt提供的QProcess::startDetached函数,该函数只需要将要执行的EXE文件名作为参数输入即可完成。startDetached在执行EXE进程后将与之解除关联,这正是本程序需要的功能。相关的函数还有QProcess::start等,可以通过参数配置使当前应用程序与执行的进程外程序保持进程间通信。

最后使用qApp->quit();退出当前应用程序,让升级程序覆盖旧文件,更新新文件。如果不退出当前应用,使升级程序接管升级过程也是可行的,但存在一定隐患,如:某些文件因为被当前应用程序锁住而无法被覆盖更新,会导致升级失败。这些内容主要取决于升级程序的能力,设计良好的升级程序可提示重新启动,完成这些被锁定文件的覆盖更更新。

Qt程序体系的完成基于事件驱动,任何一个Qt程序,无论大小都有一个事件驱动的函数驱动消息传递、事件运行。当我们打开任何一个Qt程序main.cpp文件都可以看到,return a.exec();函数就是一个事件驱动,它驱动这个程序依序执行。

当我们自定义类,且这个类要执行的是一个占用CPU较长的工作时,Qt主程序和这个类的子程序之间就需要有一个同步过程,即主程序要等待子程序执行完后再执行其他主程序代码。这就需要我们自定义一个事件驱动函数,使程序间实现同步,常用的为QCoreApplication::processEvents()函数。实际上,这个函数一般情况下可以实现事件驱动和保持同步的功能,但在外面加一个循环更能保证程序的有效性。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容

  • .bat脚本基本命令语法 目录 批处理的常见命令(未列举的命令还比较多,请查阅帮助信息) 1、REM 和 :: 2...
    庆庆庆庆庆阅读 8,006评论 1 19
  • Ubuntu的发音 Ubuntu,源于非洲祖鲁人和科萨人的语言,发作 oo-boon-too 的音。了解发音是有意...
    萤火虫de梦阅读 99,002评论 9 467
  • 个人学习批处理的初衷来源于实际工作;在某个迭代版本有个BS(安卓手游模拟器)大需求,从而在测试过程中就重复涉及到...
    Luckykailiu阅读 4,660评论 0 11
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,490评论 18 139
  • 谁能不留恋春夜的气息呢? 微凉而清新; 那遍野的草绿, 满山的花香, 似露珠挂在纯真的笑脸上。 梦中的人哪, 你在...
    雪夜花开阅读 178评论 2 9