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:
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.
The complete code for automatic update is located in the 02autoupdate
directory in the source code examples for this chapter.
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.
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.
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 } }
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 }
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.
3.142.156.235