PDA

View Full Version : Add "Check for updates" feature



jiveaxe
12th January 2008, 09:39
Hi,
first of all sorry for this long post. I would like to add to my application the feature of the 3d title: check if there is a new version of the program or of the data files (mainly database) and eventually download and install the updates. I have searched the forum for a similar question but found only a discussion if integrate the "update" feature on the main program or in a separate one; all answers were for the latter. That topic solved one of the my problem. But now I have some other questions and since I don't never coded a internet application it was great if you can drive me to the right direction.

First of all I have divided the work in steps according to the phases of the "update" progress.

1) Check if there is an active internet connection: obviously the first step. What is the simplest way? A ping to the server is sufficient? Which qt4 method should I use?

2) What is the best way for version checking? My project is hosted by sourceforge. The program should check for two packages: the main program (less updated) and the data files (more updates). The program is distributed for linux (only source code, no binary) and windows. The data files are common for the two OSs.

2a) Should I add new packages to the project? (maybe myapp-update and myapp-data-update).

2b) Which compression method should I use for packaging the updates. In linux it's not a problem, what about windows? I don't want more dependencies other than qt4. Maybe a self extracting package?

3) If there is an update the main program calls the updater; this asks the user if download the updates.

3a) Which qt4 method gives the current OS?

3b) Which method I have to use for downloading?

3c) Where I have to download the files? In a tmp folder, obviously; in linux is /tmp but in windows it can change: which qt4 method gives the current system temp folder?

4) Once download is finished the updater asks if install the new files:

4a) In linux only the data files should be installed (the binary must be compiled): so they are installed in the application folder in $HOME. Ok.

4b) In windows: thanks to QApplication::applicationDirPath() I know where the application is installed; and there extract the binary. Where I have to put the data files? In the same directory is ok, or in the user profile subfolder? Which qt4 method gives the current user directory?


Am I forgetting something else? Any help will be very appreciated.

bender86
12th January 2008, 11:16
1) Check if there is an active internet connection: obviously the first step. What is the simplest way? A ping to the server is sufficient? Which qt4 method should I use?
Just try to connect. If it fails, then there is no active connection.


2b) Which compression method should I use for packaging the updates. In linux it's not a problem, what about windows? I don't want more dependencies other than qt4. Maybe a self extracting package?
Try looking to QByteArray::qCompress and QByteArray::qUncompress.


3b) Which method I have to use for downloading?
You can use QHttp or QFtp.


3c) Where I have to download the files? In a tmp folder, obviously; in linux is /tmp but in windows it can change: which qt4 method gives the current system temp folder?
You can use QTemporaryFile.



4b) In windows: thanks to QApplication::applicationDirPath() I know where the application is installed; and there extract the binary. Where I have to put the data files? In the same directory is ok, or in the user profile subfolder? Which qt4 method gives the current user directory?
User data should be in AppData directory. You can obtain it with SHGetSpecialFolderPath (http://msdn2.microsoft.com/en-us/library/bb762204(VS.85).aspx), or with %APPDATA% env variable.

jiveaxe
12th January 2008, 12:26
Just try to connect. If it fails, then there is no active connection.
Sorry, but what this mean? Have I to contact some server and evaluate the response?



Try looking to QByteArray::qCompress and QByteArray::qUncompress.
If I compress the file with zip should QByteArray::qUncompress uncompress it?



You can use QTemporaryFile.
Useful class. Thanks

Regards

bender86
12th January 2008, 18:46
Sorry, but what this mean? Have I to contact some server and evaluate the response?
Something like this:

...

QTcpSocket *socket = new QTcpSocket;
connect(socket,SIGNAL(connected()),this,SLOT(slotC onnected()));
connect(socket,SIGNAL(error(QAbstractSocket::Socke tError)),this,SLOT(slotError(QAbstractSocket::Sock etError)));
socket->connectToHost("updatehost",1212);

...

void ClassName::slotConnected() {
// Connected to server.
// Get update files.
}

void ClassName::slotError(QAbstractSocket::SocketError) {
// NOT connected to server.
QMessageBox::critical(0,QString(),tr("Can't connect to updates server!"));
}

I don't know how to do using QHttp or QFtp, but you should follow this way:
- Get update
- If ok, install update
- If error, tell user "Error!"



If I compress the file with zip should QByteArray::qUncompress uncompress it?
I don't think so. I think you should read zip file, find compressed data, then uncompress it.
Probably it is better to use some library.

elcuco
12th January 2008, 18:59
have in your site an xml file, and parse it. it will contain the latest version available.

ucntcme
12th January 2008, 22:38
I would go with the following:

Have an XML file on your site that the app downloads and parses to get the latest version number (and possibly date) along with a download URL.

Use QHttp to get it, and QXml to parse it (dom or sax, your choice). use QHttp or QFtp to retrieve it. As far as compression, QByteArray::qUncompress can deal with zipfiles but it takes extra work:


Note: If you want to use this function to uncompress external data compressed using zlib, you first need to prepend four bytes to the byte array that contain the expected length of the uncompressed data encoded in big-endian order (most significant byte first).

If you can not retrieve the download file, you would check for error as noted in a prior response and tell the user something like "Unable to retrieve update data. Please ensure your connection to the Internet is working and try again."

Using the XML file gives you additional options such as including a description of the update to allow the user to decide if they want to install the update (or data). You would retrieve and parse the XML file and could then display a "Release notes" based on the contents. Personally I don't like apps that have a "blind update"; I like to know what changed and preferably an option ignore that update, install it, or not install it right now. But that's me. ;)

jiveaxe
13th January 2008, 09:33
Thanks to all for the precious suggestions; now I can start working on.

Regards

jiveaxe
14th January 2008, 10:19
I have just started and now the first problem.

I got a segmentation fault with this code:


...
QTemporaryFile file;
if(!file.open()) {
...
}

QUrl url("http://localhost/mysite");
http = new QHttp;
http->setHost(url.host(), url.port(80));
http->get("/lastrelease.xml", &file);
http->close();

If I comment http->get no more error. The temporary file is created with this path: /tmp//qt_temp.xxxx . Is it normal the double '/' ?

Before somebody asks me, the local server is running and http://localhost/mysite/lastrelease.xml exists.

If I use QFile instead of QTemporaryFile:

QFile file("/tmp/lastrelease.xml");
if(!file.open(QIODevice::ReadWrite)){...}

I have no segmentation fault at all but the file created in /tmp is empty. In the console I have this line:

QIODevice::write: ReadOnly device

Where is the mistake?

Thanks

bender86
14th January 2008, 11:19
...
QTemporaryFile file;
if(!file.open()) {
...
}

QUrl url("http://localhost/mysite");
http = new QHttp;
http->setHost(url.host(), url.port(80));
http->get("/lastrelease.xml", &file);
http->close();

I think it should be:
http->setHost("localhost");
http->get("/mysite/lastrelease.xml");



The temporary file is created with this path: /tmp//qt_temp.xxxx . Is it normal the double '/' ?
Multiple / are ignored. You can check with ls /usr////src or similar.



QIODevice::write: ReadOnly device
Is /tmp writable? Try another directory (like $HOME).

jiveaxe
14th January 2008, 13:25
...
I think it should be:[CODE]http->setHost("localhost");
http->get("/mysite/lastrelease.xml");

It gives the same problem.

I have also tried to get some page from the remote site, but nothing changes. Using QTemporaryFile gives the segmentation fault; with QFile an empty output.

Try this:


QTemporaryFile file;
if(!file.open()) {
QMessageBox::critical(0,QString(),tr("Can't open the file!"));
}

QUrl url("http://www.bzip.org");
http = new QHttp;
http->setHost(url.host(), url.port(80));
http->get("/docs.html", &file);
http->close();

or this:


QFile file("/tmp/output.html");
if(!file.open(QIODevice::ReadWrite)) {
QMessageBox::critical(0,QString(),tr("Can't open the file!"));
}

QUrl url("http://www.bzip.org");
http = new QHttp;
http->setHost(url.host(), url.port(80));
http->get("/docs.html", &file);
http->close();



Is /tmp writable? Try another directory (like $HOME).

Of course /tmp is writable.

Regards

bender86
14th January 2008, 16:04
You have to close the QFile after writing into.
You can catch QHttp::dataReadProgress(int done, int total). If done == total, then download is complete, and you can close file (you will need an event loop, so call QCoreApplication::exec()).
Check also QHttp example (http://doc.trolltech.com/4.3/network-http.html).

For QTemporaryFile, keep in mind that if you create it on stack, it will be destroyed when gone out of scope (usually after a }). If you pass his address to http.get("/file",&tempFile), QHttp will try to write to a invalid address.

jiveaxe
14th January 2008, 17:08
Check also QHttp example (http://doc.trolltech.com/4.3/network-http.html).

I give it a try.


For QTemporaryFile, keep in mind that if you create it on stack, it will be destroyed when gone out of scope (usually after a }). If you pass his address to http.get("/file",&tempFile), QHttp will try to write to a invalid address.

As I wrote in my previous code QTemporaryFile should be in the same scope of http.

Bye

bender86
14th January 2008, 17:56
As I wrote in my previous code QTemporaryFile should be in the same scope of http.
QHttp::get() is asynchronous. If you do something like this:

void f() {
QTemporaryFile temp;
temp.open();
QHttp *http = new QHttp;
http->setHost("localhost");
http->get("/file",&file);
}
QHttp::get returns immediately, before actually download file. http is a heap allocated object, so it survives at and of f(), but file not, and is destroyed. When http obtains file from webserver, tries to write to an invalid location (because file is no longer existant).
You should declare both QHttp and QTemporaryFile as pointers and create their instances with new, and catch QHttp signal to know when http request is finished. However QHttp example explains well.

jiveaxe
15th January 2008, 11:02
Finally I have made a working code; if It may help somebody else here it is:


MainWindow::MainWindow()
{
...
http = new QHttp(this);
connect(http, SIGNAL(requestFinished(int, bool)), this, SLOT(httpRequestFinished(int, bool)));
connect(http, SIGNAL(responseHeaderReceived(const QHttpResponseHeader &)), this, SLOT(readResponseHeader(const QHttpResponseHeader &)));

file = 0;
downloadFile();
}

MainWindow::~MainWindow()
{
if(file) {
delete file;
file = 0;
}
}

void MainWindow::downloadFile()
{
if(file) {
delete file;
file = 0;
}

QUrl url("http://localhost/sito/lastrelease.xml");

file = new QTemporaryFile;
if (!file->open()) {
statusBar()->showMessage(tr("Impossible to save the file %1: %2.").arg(file->fileName()).arg(file->errorString()));
delete file;
file = 0;
return;
}

http->setHost(url.host(), url.port(80));

httpRequestAborted = false;
httpGetId = http->get(url.path(), file);
}

void MainWindow::httpRequestFinished(int requestId, bool error)
{
if (requestId != httpGetId)
return;
if (httpRequestAborted) {
if(file) {
file->close();
file->remove();
}
return;
}

if (requestId != httpGetId)
return;

file->close();

if (error) {
file->remove();
statusBar()->showMessage(tr("Download failed: %1.").arg(http->errorString()));
} else {
statusBar()->showMessage(tr("lastrelease downloaded"));
}
}

void MainWindow::readResponseHeader(const QHttpResponseHeader &responseHeader)
{
if (responseHeader.statusCode() != 200) {
statusBar()->showMessage(tr("Download failed: %1.").arg(responseHeader.reasonPhrase()));
httpRequestAborted = true;
http->abort();
return;
}
}

Also the xml part of the work is ok. I have to decide where store the current versions of application/database: for application probably is better in the code for simple retrieving; for database I don't know if is better to store it in a table or in the name of the db file (db-20070115 for example). Perhaps the first solution is simpler. What do you say?

Thanks

jiveaxe
16th January 2008, 08:26
Hi,
I have choosen to store database version on one table of the db itself. Now I have to work on compressed archive for distributing the updates. I have found an interesting article on qtnode (http://qtnode.net/wiki?title=Main_Page) about Self-Extracting Installer (http://qtnode.net/wiki?title=Self-Extracting_Installer). It seems what I was looking for but I haven't well understand the usage. My update package should contain a sqlite file (appdb for example) and a folder containing some images. Can I store all of this as they are or Self-Extracting_Installer can stores only files and not folders? In the latter case after extracting the qcompress archive I should do something like this:


cp appdb QDir::homePath() + QDir::separator() + ".appname";
cp *.png QDir::homePath() + QDir::separator() + ".appname/images";

Am I right?

One more question: which extension should have the final package?

Thanks

EDIT:
Ok, ok; with some trying I have understand the usage. The files must be appended to stub (which is an executable): so the final package is an executable (and in windows should have .exe exention). Regards paths, when a file is stored, it is stored even the path given during compression, so


qcompress images/image1.png update-20080116
qcompress appdb update-20080116

gives during decompression the following files:


appdb
images/image1.png

Than I can move all together


cp -r * QDir::homePath + QDir::separator + ".appname/";

Regards

jiveaxe
16th January 2008, 17:04
I diverted on QuaZIP (http://quazip.sourceforge.net/) a qt widget for accessing zip files.

Bye

jiveaxe
21st January 2008, 19:04
I have a new problem with an undesidered requestFinished signal. For clearness I list here the steps of the code section:


1: Click download button

2: http->get(...);

(when requestFinished(...) signal is emitted)

3: QMessageBox: Need to quit MainApp before install

(if OK)

4: quit MainApp

5: install new files

6: QMessageBox: now restart MainApp and quit updater

(if OK)

7: updater closed and MainApp restarted

If I follow the steps without pause the code works good but if I don't click any button on the QMessageBox at step 6 after 10-15 seconds is emitted a new requestFinished signal and the QMessageBox of the step 3 pops up while that at step 6 is still running.

Here the code:


void Updater::httpRequestFinished(int requestId, bool error)
{
if (requestId != httpGetId)
return;
if (httpRequestAborted) {
if (file) {
file->close();
file->remove();
delete file;
file = 0;
}

progressDialog->hide();
return;
}

progressDialog->hide();
file->close();

if (error) {
file->remove();
QMessageBox::information(this, tr("HTTP"),
trUtf8("Download failed: %1.")
.arg(http->errorString()));
}
}

(httpRequestFinished is connected with requestFinished)
I tryed using http->close() and http->clearPendingRequests() at the end of httpRequestFinished() but the signal is still emitted.

Hoping I was clear enough, someone can tell me what I have missed?

Thanks

will49
18th September 2008, 22:01
Did you ever find a solution for this double "requestFinished" signal being sent? I'm having the same problem. I'm closing the temporary file I use to download the page when I get the first "requestFinished" signal, but then sometimes I get another "requestFinished" signal with the same id and error=0.

This causes the application to crash - I presume because I'm deleting the temporary file used in the Qhttp get().

I'm using Qt 4.3.4 on Windows.

It also seems that if I don't display a dialog to the user when I get the "requestFinished" signal then this doesn't happen. So maybe Qt is sending another signal if the first one is blocked by the dialog being shown?

will49
19th September 2008, 00:46
FYI - I got around this by not displaying the dialog box immediately. Instead I used a timer to delay it. I found that it would only happen on the non-Debug build and only if the dialog is displayed as soon as I get the "requestFinished" signal. Also it has nothing to do with closing the temporary file used in the Qhttp get().

Weird.....

jiveaxe
19th September 2008, 08:54
You should find an aswer in this my other discussion:

http://www.qtcentre.org/forum/f-qt-programming-2/t-some-errors-if-i-dont-click-on-qmessagebox-button-11437.html

Let me know if it works.

Regards

will49
19th September 2008, 16:39
OK great. Thanks. I ended up figuring out by using a timer, but the second solution using Qt::QueuedConnection is more elegant.