Adding autoupdate to application

Very often we will want our clients to be able to update automatically, especially if clients are run on large number of computers and / or in different locations.

To make our application automatically update itself, we need to use a simple but efficient pattern for automatic updates. The approach is exactly the same as described in Chapter 3, Tcl Standalone Binaries.

All that is needed is to perform the following steps:

  • Check if an updated version of the binary is available; if not, then download it
  • Write the binary as a temporary file with the specified filename
  • Run the temporary binary
  • As the temporary binary, overwrite the actual binary and exit
  • As the temporary binary, run the actual binary, which is now updated and exit
  • As the actual binary, remove the temporary binary and perform normal actions

In order to be able to tell when we are running a temporary binary, we should always be able to know both file names. As our temporary binary will always be prefixed with autoupdate-, our file names are similar to /path/to/client-cli-linux-x86 and /path/to/autoupdate-client-cli-linux-x86 respectively. This way a simple check can be used to determine whether the application should work normally or should overwrite the actual binary, restart it and exit.

As for determining whether our version is up to date or not, we'll use the MD5 checksum calculated when binaries were built. This and the corresponding .md5 files are enough to implement automatic updates.

Note

The complete code for automatic update is located in the 02autoupdate directory in the source code examples for this chapter.

Server-side

For implementing automatic updates, server-side needs to be able to return information about the latest version of the application, and return the latest binary for each platform.

We need to make two changes in the src-server/main.tcl file, compared to previous example. The first one is that we need to store the directory where the binaries are kept:

set csa::binariesdirectory [file join [pwd] binaries]

Also, we'll load the additional autoupdate.tcl script that will be responsible for providing the client with the binaries.

source [file join $starkit::topdir autoupdate.tcl]

The script itself is relatively small. All it does is serve requests for binaries. It does this by getting whatever filename was supplied, and combining that with the path to the binaries that was previously initialized. If a specified file is not found, the 404 error is returned and an error is logged.

proc csa::handleClientBinary {sock suffix} {
variable binariesdirectory
set filename [file join $binariesdirectory 
[file tail $suffix]]
if {[file exists $filename]} {
Httpd_ReturnFile $sock application/octet-stream 
$filename
} else {
log::warn "handleClientBinary: $filename not found"
Httpd_Error $sock 404
}
}

We'll also bind a particular prefix so that all requests to the client/binary will be handled by our command.

Url_PrefixInstall /client/binary csa::handleClientBinary

It works in such a way that the request to http://<host>:8981/client/binary/client-cli-linux-x86 will cause that file from the binaries subdirectory to be returned.

Note

The code mentioned in this section is located in the src-server/autoupdate.tcl file in the 02autoupdate directory in the source code examples for this chapter.

Client-side

A lot more logic needs to be embedded in the client, though.

As many operating systems do not allow overwriting a binary that is currently running, our overwrite process is a bit more tricky and described earlier in the chapter.

In the client's main.tcl file we'll introduce some small changes—first we'll add loading autoupdate.tcl file, we'll also schedule csa::checkAutoupdate command immediately, but csa::requestJobs later. The purpose is to wait to get the jobs before making sure our binary is up to date.

source [file join $starkit::topdir autoupdate.tcl]
after idle csa::checkAutoupdate
after 60000 csa::requestJobs

The autoupdate.tcl script begins with checking the name of the binary we are running as. If it starts with the autoupdate- prefix, this means that our application should overwrite the actual binary, run it and exit:

if {[regexp -nocase 
"autoupdate-(.*)"  [file tail [info nameofexecutable]] - binaryname]} {
# wait for parent process to exit
after 5000

We started by waiting 5 seconds for a parent process to exit. Then we determine target file name, which is the actual binary, such as /path/to/client-cli-linux-x86.

set dirname [file dirname [info nameofexecutable]]
set targetname [file join $dirname $binaryname]

Now we try to unmount the VFS of the executable, which is needed to copy the binary as a file from the Tcl level; otherwise Tcl will consider that path to be a directory.

Then we overwrite the target binary using our current binary, and try to change its permissions to being executable on Unix systems. Finally, we run the actual binary providing the same arguments that our autoupdate binary was run with.

catch {vfs::mk4::Unmount exe [info nameofexecutable]}
file copy -force [info nameofexecutable] $targetname
catch {file attributes $targetname -permissions 0755}
exec $targetname {*}$argv &
exit 0
} else {

If our binary is not running in the autoupdate mode, we check if the autoupdate-<binaryname> file exists. If it does, this means that we have been just started the end of the automatic update process. In this case we wait for five seconds for the autoupdate binary to exit, and try to remove it from the filesystem.

set dirname [file dirname [info nameofexecutable]]
set targetname [file join $dirname  "autoupdate-[file tail [info nameofexecutable]]" 
]
if {[file exists $targetname]} { after 5000 catch {file delete -force $targetname} } }

This is a complete implementation of handling automatic updates themselves.

After this, we need to create a procedure that will periodically check for updates of the binary file, and if there is a newer version then download and run it.

Also, the command will set csa::autoupdateInProgress variable that can be used by other parts of the application to stop performing tasks, if we are expecting to be restarted.

We'll start off by cancelling any scheduled checks and building URL to the binary.

proc csa::checkAutoupdate {} {
variable hostname
variable autoupdateInProgress
after cancel csa::checkAutoupdate
set url "http://${hostname}:8981/client/binary/"
append url [file tail [info nameofexecutable]]

Next we'll try to download the<binary>.md5 file, which is the MD5 checksum of the actual binary, and is generated as part of the build process. csa::checkAutoupdateDone command will be run when the request has been completed. If initializing the download fails, we try again in one minute from now:

if {[catch {
http::geturl "$url.md5" -timeout 30000 
-command [list csa::checkAutoupdateDone $url]
}]} {
log::error "checkAutoupdate: Unable to send request"
# try again in 1 minute
after 60000 csa::checkAutoupdate
}
}

After request for the MD5 checksum has been completed, we start off by setting up a next check for one hour from now, which is the default interval for checking for updates.

proc csa::checkAutoupdateDone {url token} {
variable autoupdateInProgress
after 3600000 csa::checkAutoupdate

Next we check if the request has been successfully processed. Unlike many other examples so far, we also include support for error code 404 in this case—which means that the server does not have the appropriate file on its side.

if {[http::status $token] != "ok"} {
log::error "checkAutoupdateDone: Invalid HTTP status"
http::cleanup $token
return
} elseif {[http::ncode $token] == 404} {
log::warn "checkAutoupdateBinaryDone:
Server does not provide updates for this binary"
http::cleanup $token
return
} elseif {[http::ncode $token] != 200} {
log::error "checkAutoupdateDone: Invalid HTTP code"
http::cleanup $token
return
} else {

If there was no error processing the request, we retrieve the checksum from the request and compare it with our local binary's checksum.

set checksum [http::data $token]
if {$checksum != $::csa::applicationMD5} {

If the checksums are different, we need to update our binary. We start off by cancelling the next scheduled automatic update and initialize a request to download the actual binary, which will invoke the csa::checkAutoupdateBinaryDone command when it completes.

after cancel csa::checkAutoupdate
if {[catch {
http::geturl $url -timeout 900000 -binary 1 
-command csa::checkAutoupdateBinaryDone
}]} {

If an error has occurred, we set up the next attempt in one minute from now. We also log the error.

# an error occured;
# schedule next check in 1 minute from now
after 60000 csa::checkAutoupdate
log::error "checkAutoupdateDone: Unable to
download new binary"
} else {

If the download has started, we set the csa::autoupdateInProgress variable to true to indicate that an update is in progress.

# if download has started, we set this
# so no further requests for job are sent
set autoupdateInProgress true
}
}
}
}

When the download of the binary has finished, we start off by scheduling the next automatic update in case of problems. We also unset the csa::autoupdateInProgress variable for the same reason.

proc csa::checkAutoupdateBinaryDone {token} {
variable autoupdateInProgress
# just in case - schedule next check in 1 hour from now
# and clear the autoupdate flag
after 3600000 csa::checkAutoupdate
catch {unset autoupdateInProgress}
log::info "checkAutoupdateBinaryDone: Binary received"

The first thing we do is check whether the transfer has succeeded and no error was sent by the server—in case of any problems, we log them and exit the procedure. Next attempt will happen in an hour.

# check status & HTTP code; return if error occurred
if {[http::status $token] != "ok"} {
log::error "checkAutoupdateBinaryDone:
Invalid HTTP status"
http::cleanup $token
return
} elseif {[http::ncode $token] == 404} {
log::warn "checkAutoupdateBinaryDone:
Server does not provide updates for this binary"
http::cleanup $token
return
} elseif {[http::ncode $token] != 200} {
log::error "checkAutoupdateBinaryDone:
Invalid HTTP code [http::ncode $token]"
http::cleanup $token
return
} else {

In case the transfer succeeds, we create the autoupdate-<binaryname> filename as aufilename variable:

set aufilename [file join 
[file dirname [info nameofexecutable]] 
"autoupdate-[file tail [info nameofexecutable]]"]
log::debug "checkAutoupdateBinaryDone:
Writing to $aufilename"

We then try to write to that file and in case it fails we log an error.

if {[catch {
set fh [open $aufilename w]
fconfigure $fh -translation binary
puts -nonewline $fh [http::data $token]
close $fh
} error]} {
log::error "checkAutoupdateBinaryDone:
Error writing autoupdate binary: $error"
} else {

If we were able to create the autoupdate binary, we now change its attributes to be able to run it. We run it with the same arguments that our binary was run with and exit:

log::debug "checkAutoupdateBinaryDone:
Running $aufilename"
catch {file attributes 
$aufilename -permissions 0755}
exec $aufilename {*}$::argv &
exit 0
}
http::cleanup $token
}
}

Note

The code mentioned in this section is located in the src-client/autoupdate.tcl file in the 02autoupdate directory in the source code examples for this chapter.

In addition to the automatic updates themselves, the new example also checks for automatic updates in progress when requesting jobs. The csa::requestJobs command now starts with the following check:

proc csa::requestJobs {} {
variable hostname
variable autoupdateInProgress
after cancel csa::requestJobs
if {[info exists autoupdateInProgress]} {
log::info "requestJobs: Auto update in progress; 
retrying in 1 minute"
after 60000 csa::requestJobs
return
}

Note

The changes in the code in this section are located in the src-client/client.tcl file in the 02autoupdate directory, in the source code examples for this chapter.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.142.156.235