esky: keep frozen apps fresh
Esky is an auto-update framework for frozen Python applications. It provides a simple API through which apps can find, fetch and install updates, and a bootstrapping mechanism that keeps the app safe in the face of failed or partial updates.
Esky is currently capable of freezing apps with py2exe, py2app, cxfreeze and bbfreeze. Adding support for other freezer programs should be straightforward; patches will be gratefully accepted.
The main interface is the 'Esky' class, which represents a frozen app. An Esky must be given the path to the top-level directory of the frozen app, and a 'VersionFinder' object that it will use to search for updates. Typical usage for an app automatically updating itself would look something like this:
if hasattr(sys,"frozen"): app = esky.Esky(sys.executable,"http://example.com/downloads/") app.auto_update()
A simple default VersionFinder is provided that hits a specified URL to get a list of available versions. More sophisticated implementations will likely be added in the future, and you're encouraged to develop a custom VersionFinder subclass to meet your specific needs.
The real trick is freezing your app in a format sutiable for use with esky. You'll almost certainly want to use the "bdist_esky" distutils command, and should consult its docstring for full details; the following is an example of a simple setup.py script using esky:
from esky import bdist_esky from distutils.core import setup setup(name="appname", version="1.2.3", scripts=["appname/script1.py","appname/gui/script2.pyw"], options={"bdist_esky":{"includes":["mylib"]}}, )
Invoking this setup script would create an esky for "appname" version 1.2.3:
#> python setup.py bdist_esky ... ... #> ls dist/ appname-1.2.3.linux-i686.zip #>
The contents of this zipfile can be extracted to the filesystem to give a fully working application. If made available online then it can also be found, downloaded and used as an upgrade by older versions of the application.
When you find you need to move beyond the simple logic of Esky.auto_update() (e.g. to show feedback in the GUI) then the following properties and methods are available on the Esky class:
app.version: the current best available version.
- app.active_version: the currently-executing version, or None
- if the esky isn't for the current app.
- app.find_update(): find the best available update, or None
- if no updates are available.
app.fetch_version(v): fetch the specified version into local storage.
app.install_version(v): install and activate the specified version.
- app.uninstall_version(v): (try to) uninstall the specified version; will
- fail if the version is currently in use.
- app.cleanup(): (try to) clean up various partly-installed
- or old versions lying around the app dir.
- app.reinitialize(): re-initialize internal state after changing
- the installed version.
If updating an application that is not writable by normal users, esky has the ability to gain root privileges through the use of a helper program. The following methods control this behaviour:
app.has_root(): check whether esky currently has root privs.
app.get_root(): escalate to root privs by spawning helper app.
app.drop_root(): kill helper app and drop root privileges
When properly installed, the on-disk layout of an app managed by esky looks like this:
prog.exe - esky bootstrapping executable appdata/ - container for all the esky magic appname-X.Y.platform/ - specific version of the application prog.exe - executable(s) as produced by freezer module library.zip - pure-python frozen modules pythonXY.dll - python DLL esky-files/ - esky control files bootstrap/ - files not yet moved into bootstrapping env bootstrap-manifest.txt - list of files expected in bootstrap env lockfile.txt - lockfile to block removal of in-use versions ...other deps... updates/ - work area for fetching/unpacking updates
This is also the layout of the zipfiles produced by bdist_esky. The "appname-X.Y" directory is simply a frozen app directory with some extra control information generated by esky.
- To install a new version "appname-X.Z", esky performs the following steps:
- extract it into a temporary directory under "updates"
- move all bootstrapping files into "appname-X.Z.platm/esky/bootstrap"
- atomically rename it into the main directory as "appname-X.Z.platform"
- move contents of "appname-X.Z.platform/esky/bootstrap" into the main dir
- remove the "appname-X.Z.platform/esky/bootstrap" directory
- To uninstall an existing version "appname-X.Y", esky does the following
- remove files used by only that version from the bootstrap env
- rename its "bootstrap-manifest.txt" file to "bootstrap-manifest-old.txt"
Where such facilities are provided by the operating system, this process is performed within a filesystem transaction. Nevertheless, the esky bootstrapping executable is able to detect and recover from a failed update should such an unfortunate situation arise.
To clean up after failed or partial updates, applications should periodically call the "cleanup" method on their esky. This removes uninstalled versions and generally tries to tidy up in the main application directory.