強制升級機制
使用某些軟件時經(jīng)常遇到“發(fā)現(xiàn)新版本昆雀,馬上升級”的提示辱志。對于一些程序,可以選擇忽略提示狞膘,不進行升級揩懒。但有時,程序只給用戶提供升級按鈕挽封,無法選擇忽略提示的升級信息已球,這就是強制升級的機制。
強制升級主要出于兩個方面的原因:1. 發(fā)現(xiàn)當(dāng)前程序的主要bug辅愿,必須升級補丁智亮,否則程序運行會出現(xiàn)問題;2. 程序某些關(guān)鍵功能模塊需要進行更新点待,如果不升級阔蛉,則軟件的某些功能將不能使用,或者使用出錯癞埠。
本系列筆記中books項目使用了強制自動升級的策略状原,我們也可以將其改為可選的升級策略。
實現(xiàn)強制升級機制需要以下3個方面的內(nèi)容:
- 程序啟動時要從網(wǎng)絡(luò)讀取程序新版本信息苗踪。
- 讀出的新版本信息要與本地程序版本號進行對比颠区,并根據(jù)版本對比結(jié)果判斷是否需要升級,如果需要則進入下一步通铲。
- 鎖定其他程序按鈕毕莱,只允許用戶點擊升級按鈕。單擊后程序進入升級程序颅夺。
系統(tǒng)實現(xiàn)
讀取INI文件中的版本信息
books項目中使用INI文件記錄程序的版本信息及相關(guān)的配置狀態(tài)央串。更多關(guān)于INI文件的信息可參考INI配置文件的格式。
讀取版本信息包含兩方面內(nèi)容:1. 從網(wǎng)絡(luò)中讀取是否存在新版本程序碗啄,若有則讀取新版本號质和;2. 讀取本地版本信息。
從網(wǎng)絡(luò)中讀取新版本程序時稚字,可以在指定的某處網(wǎng)站保存一個INI文件饲宿,該文件內(nèi)部記錄了新程序版本號,程序升級區(qū)域限制及最新程序下載地址等胆描。例如GitHub—initxuan::books/updateversion.ini文件瘫想,其關(guān)鍵部分如下:
[update]
updateZone = ALL
updateVersion = 2.0
updateZone用于指定升級區(qū)域,不同區(qū)域用戶可以根據(jù)自身情況選擇是否允許升級昌讲,或者升級不同區(qū)域新版本程序国夜,這樣便于進行區(qū)域控制。此處可以通過配置參數(shù)來指定區(qū)域進行升級短绸,如不同區(qū)域可通過設(shè)置區(qū)號進行區(qū)別车吹,不同區(qū)域的區(qū)號用空格分隔筹裕;如果參數(shù)設(shè)置為“ALL”則表示所有區(qū)域均可升級。updateVersion用于指定當(dāng)前最新程序版本號窄驹。應(yīng)用程序會讀取updateVersion的數(shù)值朝卒,只有當(dāng)它比用戶當(dāng)前程序的版本號大時,升級程序才會啟動乐埠。
當(dāng)我們使用INI文件進行版本信息的記錄時抗斤,需要將INI文件放置在某網(wǎng)站目錄下,共程序下載后讀取丈咐。
設(shè)計本地信息INI文件
在本地我們也需要使用INI文件來記錄程序的相關(guān)信息包括當(dāng)前程序的版本號瑞眼、要從哪個網(wǎng)址下載升級程序等信息。例如GitHub—initxuan::books/config.ini棵逊,其關(guān)鍵部分如下:
[program]
userZone =
username =
userID =
version = 1.0
installedDir =
[update]
baseUrl = http://xxx.cn/release/
exeFileName = _update_setup_
versionFileName = updateversion.ini
updateLocalDir = update
[program]區(qū)塊指定了程序相關(guān)的所有信息伤疙,包括用戶的區(qū)域、用戶名歹河、用戶ID掩浙、當(dāng)前程序版本號等。[update]區(qū)塊定義了從哪里下載升級INI信息文件秸歧,versionFileName指定了要下載的INI文件名厨姚,updateLocalDir指定了下載后的文件要放在哪個目錄下(注:該值要作為參數(shù)傳給上一篇筆記中記述的mydir.mkdir(LOCALUPDATEDIR)
函數(shù),在目錄中創(chuàng)建新的update目錄键菱,并將升級的EXE文件下載到這里谬墙。)最后exeFileName指定了要下載的EXE文件名,按照程序規(guī)則经备,會在這個文件名前加上用戶區(qū)域信息段拭抬,在文件名后加上版本號和“.exe”后綴,形成類似于“ALL_update_setup_2.0.exe”的文件名侵蒙,然后在baseUrl指定的網(wǎng)址目錄下下載這個文件造虎。
讀寫本地目錄下的INI文件
本地信息INI文件可以由程序設(shè)計者自由選擇目錄存放,按照慣例纷闺,這種信息文件一般放在兩個位置:1. 應(yīng)用程序所在目錄算凿。2. Windows常用的系統(tǒng)目錄,如My Documents目錄犁功。books項目使用后者氓轰,將其放在My Documents下面的一個子目錄,供程序讀取浸卦。Qt程序提取Windows系統(tǒng)的目錄My Documents時分為兩步:1. 使用QStandardPaths::standardLocations(QStandardPaths::DocumentsLocation)
得到目錄My Documents的字符串列表署鸡;2. 從列表頭部提取出目錄的字符串。代碼如下:
// 0.1尋找系統(tǒng)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系統(tǒng)目錄時同樣采用這兩個函數(shù),具體內(nèi)容參見QStandardPaths Class | Qt Core 5.11
讀出INI文件之后撒穷,就可以對INI文件進行讀取解析了匣椰。Qt為讀取INI文件提供了非常方便使用的類:QSettings裆熙,使用時僅需將INI文件名作為參數(shù)實例化該類端礼,便可進行信息讀取工作。參考代碼如下:
// 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; // 先設(shè)置為false入录,后面有比較版本號蛤奥,如果大于當(dāng)前版本,則設(shè)置為true
qDebug() << "---" << curVersion << "==" << curUserName << "==" << isLogin;
用戶登錄后就將用戶信息記錄在本地INI文件中僚稿,如果INI文件中沒有用戶信息凡桥,則表示用戶未登錄。程序先讀取INI文件中用戶信息蚀同,判斷用戶是否登錄缅刽;然后讀取升級相關(guān)地址、升級文件名蠢络、升級信息文件名等衰猛;最后設(shè)定isThereNewUpdate為false,等待后續(xù)如果有新版本程序刹孔,再將其設(shè)為true啡省。
向INI文件寫入信息可參考上述代碼中:
QSettings configIniWrite(configIniWhole, QSettings::IniFormat);
configIniWrite.setValue("/program/installedDir", QDir::currentPath().trimmed());
通過定義另一個QSettings實例,使用函數(shù)setValue對指定的INI區(qū)塊寫入信息即可髓霞。
邏輯判斷
接下來程序需要到指定網(wǎng)址下載升級的INI文件卦睹,下載的代碼使用本項目即books項目中的下載類實現(xiàn),然后進行讀取分析方库,進行版本號比較的邏輯判斷结序。
當(dāng)某個功能被頻繁使用時,應(yīng)將該功能封裝成類纵潦。
下載后的INI文件開始進行信息讀取徐鹤,進行版本比較,相關(guān)代碼如下:
QString filename = updateDir + "/" + versionFileName;
QFile file(filename);
if(file.size() == 0){
QMessageBox::information(this, "升級", "升級文件檢測失敗酪穿,請檢查您的網(wǎng)絡(luò)是否正常凳干。");
}
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)){ // 同一區(qū)域被济,版本號不同
isThereNewUpdate = true; // 不同INI文件救赐,升級后由升級程序直接復(fù)制my documents/config.ini
QString tmpZone = updateZone.contains("ALL") ? "ALL" : curUserZone;
exeFileName = tmpZone + tmpExeFileName + strUpdateVersion + ".exe";
}
}
file.remove();
只有當(dāng)新版本大于當(dāng)前版本的版本號,且用戶已經(jīng)登錄、用戶區(qū)域符合要求時经磅,isThereNewUpdate才設(shè)置為true泌绣。根據(jù)上述信息得到下一步要下載的EXE文件名為exeFileName,然后根據(jù)這個文件名從網(wǎng)絡(luò)中下載指定升級文件预厌,開始升級阿迈。程序最后使用File.remove()將升級信息INI文件刪除,防止非法用戶竊取相關(guān)信息轧叽。
開始下載
如果獲得了指定升級文件的相關(guān)信息苗沧,那么就允許用戶單擊“升級”按鈕實現(xiàn)升級,這需要程序GUI界面的配合炭晒。如果無新版本升級信息待逞,則“升級”按鈕變灰;如果有升級版本网严,則“升級”按鈕點亮识樱,其他按鈕變灰不可用。具體代碼如下:
if(isThereNewUpdate){
ui->lable_Update->setText("發(fā)現(xiàn)新版本震束,請單擊升級程序按鈕怜庸,否則無法正常使用該程序");
ui->pushButton->setEnabled(false);
ui->progressBar->hide();
}
else{
ui->label_Update->setText("未發(fā)現(xiàn)新版本程序垢村。");
ui->pushButtonUpdate->setEnabled(false);
ui->progressBar->hide();
ui->label_Update->setText
控制一個Label模塊割疾,用于顯示提示文字信息。ui->pushButton->setEnabled
函數(shù)控制“升級”按鈕肝断,如果參數(shù)為false杈曲,則按鈕灰;如果參數(shù)為true胸懈,則按鈕點亮担扑。ui->progressBar->hide()
函數(shù)隱藏進度條。因為只有單擊“升級”按鈕開始正式升級程序時進度條才亮趣钱,并通過Qt網(wǎng)絡(luò)模塊connect函數(shù)與下載升級文件的百分比關(guān)聯(lián)涌献。代碼如下:
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(); // 不放棄當(dāng)前線程首有,讓它執(zhí)行myFinished
}
QString exe = updateDir + "/" + exeFileName;
QFile exeFile(exe);
if(!exeFile.open(QIODevice::ReadOnly)){
QMessageBox::information(this, "升級", "下載升級程序失敗燕垃,請檢查網(wǎng)絡(luò)是否連通。");
exeFile.close();
delete pUpdateExeFile;
return;
}
else if(exeFile.size() < 4096){
if(exeFile.size() == 0) // 未連接網(wǎng)絡(luò)井联,下載的程序大小為0
QMessageBox::information(this, "升級", "下載升級程序失敗卜壕,請檢查網(wǎng)絡(luò)是否連通。");
if(exeFile.size() < 4096) // 連接了網(wǎng)絡(luò)烙常,但下載源錯誤轴捎,下載了2KB大小的文件(內(nèi)容為tomcat錯誤代碼)
QMessageBox::information(this, "升級", "下載升級程序失敗,下載源不存在。請聯(lián)系客服侦副。");
exeFile.close();
delete pUpdateExeFile;
return;
}
}
上述代碼中循環(huán)while(!pUpdateExeFile->isDownloaded()){ }
很重要侦锯,如果沒有這段代碼,程序?qū)o法正常運行秦驯。常見的情況是升級程序還沒下載完尺碰,主程序已經(jīng)執(zhí)行完,兩者之間沒有實現(xiàn)同步译隘。
執(zhí)行上述下載代碼后亲桥,將升級EXE文件下載到指定目錄,然后判斷下載的EXE文件是否有效细燎。常用的方法是通過下載文件的大小是否為0來判斷下載是否成功两曼。但有時在tomcat服務(wù)器下載文件時皂甘,如果文件不存在玻驻,將返回一個大小約為2KB的信息文件,因此需要補充判斷else if(exeFile.size() < 4096)
偿枕,其中4096可以是任意大于2KB的值璧瞬。
需要特別注意,有時根據(jù)網(wǎng)絡(luò)傳輸情況渐夸,一個EXE文件還沒有完全下載嗤锉,Qt程序就給出完成下載的信息。整個過程完全合法墓塌,沒有錯誤瘟忱,但是下載的EXE文件不完整,無法使用苫幢。這是Qt本身的bug访诱,如果要修改,則需要對封裝的下載類代碼進行補充:一種方法是利用多線程技術(shù)分塊下載韩肝,在較差的環(huán)境下仍能以較快的速度進行下載触菜;第二種方法是使用多次檢驗技術(shù),反復(fù)判斷Qt函數(shù)返回的數(shù)據(jù)是否完整哀峻、準(zhǔn)確涡相,如反復(fù)調(diào)用void myDownloadProgress(qint64 bytesReceived, qint64 bytesTotal)
函數(shù),確定獲取的下載文件字節(jié)數(shù)是否正確剩蟀。
啟動進程外EXE文件完成升級
下載的升級程序是EXE格式的可執(zhí)行文件催蝗,要完成升級還需要兩個步驟:1. 啟動這個EXE文件;2. 關(guān)閉當(dāng)前運行的應(yīng)用程序育特,等待文件覆蓋丙号,完成升級。代碼如下:
// 已下載:執(zhí)行升級程序,關(guān)閉當(dāng)前程序
qDebug() << "EXE已下載";
delete pUpdateExeFile;
ui->label_Update->setText("升級程序下載完畢槽袄。");
ui->progressBar->hide();
ui->pushButtonUpdate->setEnabled(false);
QMessageBox::information(this, "升級", "下載升級程序成功烙无,單擊確定按鈕開始升級。\n\n單擊確定按鈕后遍尺,將關(guān)閉當(dāng)前程序并啟動升級程序截酷。");
QProcess::startDetached(updateDir + "/" + exeFileName);
// QProcess::startDetached("update/xjx_jxs_setup.exe");
qApp->quit();
升級程序下載完成后,首先有一些附加的工作需要處理乾戏,如隱藏升級進度條迂苛、Label字段顯示提示信息等;然后會顯示一個對話框鼓择,該對話框不僅是提示用戶程序下載完畢三幻,還可以使用戶單擊后,執(zhí)行程序外的EXE升級文件以完成升級呐能。
執(zhí)行進程外的EXE進程使用Qt提供的QProcess::startDetached函數(shù)念搬,該函數(shù)只需要將要執(zhí)行的EXE文件名作為參數(shù)輸入即可完成。startDetached在執(zhí)行EXE進程后將與之解除關(guān)聯(lián)摆出,這正是本程序需要的功能朗徊。相關(guān)的函數(shù)還有QProcess::start等,可以通過參數(shù)配置使當(dāng)前應(yīng)用程序與執(zhí)行的進程外程序保持進程間通信偎漫。
最后使用qApp->quit();
退出當(dāng)前應(yīng)用程序爷恳,讓升級程序覆蓋舊文件,更新新文件象踊。如果不退出當(dāng)前應(yīng)用温亲,使升級程序接管升級過程也是可行的,但存在一定隱患杯矩,如:某些文件因為被當(dāng)前應(yīng)用程序鎖住而無法被覆蓋更新栈虚,會導(dǎo)致升級失敗。這些內(nèi)容主要取決于升級程序的能力菊碟,設(shè)計良好的升級程序可提示重新啟動节芥,完成這些被鎖定文件的覆蓋更更新。
Qt程序體系的完成基于事件驅(qū)動逆害,任何一個Qt程序头镊,無論大小都有一個事件驅(qū)動的函數(shù)驅(qū)動消息傳遞、事件運行魄幕。當(dāng)我們打開任何一個Qt程序main.cpp文件都可以看到相艇,
return a.exec();
函數(shù)就是一個事件驅(qū)動,它驅(qū)動這個程序依序執(zhí)行纯陨。當(dāng)我們自定義類坛芽,且這個類要執(zhí)行的是一個占用CPU較長的工作時留储,Qt主程序和這個類的子程序之間就需要有一個同步過程,即主程序要等待子程序執(zhí)行完后再執(zhí)行其他主程序代碼咙轩。這就需要我們自定義一個事件驅(qū)動函數(shù)获讳,使程序間實現(xiàn)同步,常用的為
QCoreApplication::processEvents()
函數(shù)活喊。實際上丐膝,這個函數(shù)一般情況下可以實現(xiàn)事件驅(qū)動和保持同步的功能,但在外面加一個循環(huán)更能保證程序的有效性钾菊。