Merge branch 'master' of ssh://gitweb/git/desktop/standalone-updater

This commit is contained in:
Robert Knight 2011-08-30 11:22:41 +01:00
commit ef4dc40b52
27 changed files with 467 additions and 152 deletions

View file

@ -1,6 +1,7 @@
project(updater)
cmake_minimum_required(VERSION 2.6)
set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake/modules")
include_directories(external)
include_directories(external/TinyThread/source)

84
README
View file

@ -1,6 +1,6 @@
This tool is a component of an auto-update system. It is responsible for performing
the installation of an update after the necessary files have been downloaded
to a temporary directory.
This tool is a component of a cross-platform auto-update system.
It is responsible for performing the installation of an update after
the necessary files have been downloaded to a temporary directory.
This tool is responsible for:
@ -15,12 +15,12 @@ This tool is responsible for:
* Displaying a simple updater UI and re-launching the main application
once the update is installed.
The tool consists of a single small binary which only has a small number of external
dependencies that need to be present on the target system.
The tool consists of a single small binary which depends only on libraries
that are part of the base system.
The external dependencies of the updater binary are:
* The C++ runtime library (Linux, Mac),
* The C/C++ runtime libraries (Linux, Mac),
* pthreads (Linux, Mac),
* zlib (Linux, Mac)
* native UI library (Win32 API on Windows, Cocoa on Mac, GTK on Linux if available)
@ -31,3 +31,75 @@ containing the files for the update to a temporary directory. It then needs
to invoke the updater, specifying the installation directory, temporary package
directory and path to the update script file. The updater then installs the
update and restarts the application when done.
Building the Updater
====================
Create a new directory for the build and from that directory run:
cmake <path to source directory>
make
Customizing the Updater
=======================
To customize the application name, organization and messages displayed by the updater,
edit the AppInfo class and the icons in src/resources
Preparing an Update
===================
1. Create a directory containing your application's files,
laid out and with the same permissions as they would be when installed.
2. Create a config file specifying how the application's files should be
partitioned into packages - see tools/config-template.json
3. Use the tools/create-packages.rb script to create a file_list.xml file
and a set of package files required for updates.
4. Upload the file_list.xml file and packages to a server
After step 4 is done, you need to notify existing installs that an update
is available. The installed application then needs to download the
relevant packages, file_list.xml file and updater binary to a temporary
directory and invoke the updater.
Delta Updates
=============
The simplest possible auto-update implementation is for existing installs
to download a complete copy of the new version and install it. This is
appropriate if a full download and install will not take a long time for most users
(eg. if the application is small or they have a fast internet connection).
To reduce the download size, delta updates can be created which only include
the necessary files or components to update from the old to the new version.
The file_list.xml file format can be used to represent either a complete
install - in which every file that makes up the application is included,
or a delta update - in which case only new or updated files and packages
are included.
There are several ways in which this can be done:
Pre-computed Delta Updates
- For each release, create a full update plus delta updates from the
previous N releases. Users of recent releases will receive a small
delta update. Users of older releases will receive the full update.
Server-computed Delta Updates
- The server receives a request for an update from client version X and in response,
computes an update from version X to the current version Y, possibly
caching that information for future use. The client then receives the
delta file_list.xml file and downloads only the listed packages.
Applications such as Chrome and Firefox use a mixture of the above methods.
Client-computed Delta Updates
- The client downloads the file_list.xml file for the latest version and
computes a delta update file locally. It then downloads only the required
packages and invokes the updater, which installs only the changed or updated
files from those packages.
This is similar to Linux package management systems.

15
TODO
View file

@ -19,7 +19,7 @@ General Updater Tasks:
* Fix package dir cleanup failing on Win32 due to executable being in use [done]
* Test installing update if Microsoft Word with Mendeley plugin is active
* Write log file entries to an actual log file [partially done - needs to write to correct location]
* Write log file entries to an actual log file [Linux, Windows: done, Mac: TODO]
* Test updater on an old Windows system without Visual Studio installed and statically
link C++ runtime libraries if necessary
@ -34,6 +34,8 @@ Mendeley-specific Updater Tasks:
* Exclude Uninstall.exe from updates on Windows - see comments in utilities/autoupdate-setup/main.cpp in
the desktop source tree.
* Updater binary needs to be signed under Windows
Mendeley Desktop <= 1.0 auto-update system compatibility:
* Support for MD <= 1.0 updater command-line syntax [done]
@ -42,13 +44,16 @@ Mendeley Desktop <= 1.0 auto-update system compatibility:
Auto-update preparation tools:
* Tool to create .zip packages for a release and
upload them to S3
* Tool to generate backwards-compatible structure for XML file
* Tool to generate new structure for XML file
upload them to S3 [done]
* Tool to generate backwards-compatible structure for XML file [done]
* Tool to generate new structure for XML file [done]
Nice To Have
============
Update size:
* Support for applying binary patches (eg. with bspatch/bsdiff)
Telemetry:
* Call a project-specific URL to report successful/failed update installation
and starting of new app after update
@ -57,4 +62,6 @@ Source:
* Ensure no Mendeley branding in standalone project and publish code
Reliability:
* Create a lock to prevent Mendeley being started whilst updates are
in progress and to prevent multiple updates being run at once.
* Consider using file system transactions on Windows to make update installation atomic

View file

@ -0,0 +1,21 @@
# Convert a binary data file into a C++
# source file for embedding into an application binary
#
# Currently only implemented for Unix. Requires the 'xxd'
# tool to be installed.
#
# INPUT_FILE : The name of the binary data file to be converted into a C++
# source file.
#
# CPP_FILE : The path of the C++ source file to be generated.
# See the documentation for xxd for information on
# the structure of the generated source file.
#
# INPUT_FILE_TARGET : The name of the target which generates INPUT_FILE
#
function (generate_cpp_resource_file INPUT_FILE CPP_FILE INPUT_FILE_TARGET)
add_custom_command(OUTPUT ${CPP_FILE}
COMMAND xxd -i ${INPUT_FILE} ${CPP_FILE}
DEPENDS ${INPUT_FILE_TARGET})
endfunction()

79
src/AppInfo.cpp Normal file
View file

@ -0,0 +1,79 @@
#include "AppInfo.h"
#include "FileUtils.h"
#include "Platform.h"
#include "StringUtils.h"
#include <iostream>
#ifdef PLATFORM_UNIX
#include <stdlib.h>
#include <pwd.h>
#endif
#ifdef PLATFORM_WINDOWS
#include <shlobj.h>
#endif
#ifdef PLATFORM_UNIX
std::string homeDir()
{
std::string dir = notNullString(getenv("HOME"));
if (!dir.empty())
{
return dir;
}
else
{
// note: if this process has been elevated with sudo,
// this will return the home directory of the root user
struct passwd* userData = getpwuid(getuid());
return notNullString(userData->pw_dir);
}
}
#endif
std::string appDataPath(const std::string& organizationName,
const std::string& appName)
{
#ifdef PLATFORM_LINUX
std::string xdgDataHome = notNullString(getenv("XDG_DATA_HOME"));
if (xdgDataHome.empty())
{
xdgDataHome = homeDir() + "/.local/share";
}
xdgDataHome += "/data/" + organizationName + '/' + appName;
return xdgDataHome;
#elif defined(PLATFORM_MAC)
// TODO - Mac implementation
#elif defined(PLATFORM_WINDOWS)
char buffer[MAX_PATH+1];
if (SHGetFolderPath(0, CSIDL_LOCAL_APPDATA, 0 /* hToken */, SHGFP_TYPE_CURRENT, buffer) == S_OK)
{
std::string path = FileUtils::toUnixPathSeparators(notNullString(buffer));
path += '/' + organizationName + '/' + appName;
return path;
}
else
{
return std::string();
}
#endif
}
std::string AppInfo::logFilePath()
{
return appDataPath(organizationName(),appName()) + '/' + "update-log.txt";
}
std::string AppInfo::updateErrorMessage(const std::string& details)
{
std::string result = "There was a problem installing the update:\n\n";
result += details;
result += "\n\nYou can try downloading and installing the latest version of "
"Mendeley Desktop from http://www.mendeley.com/download-mendeley-desktop";
return result;
}

View file

@ -2,11 +2,24 @@
#include <string>
/** This class provides project-specific updater properties,
* such as the name of the application being updated and
* the path to log details of the update install to.
*/
class AppInfo
{
public:
// Basic application information
static std::string name();
static std::string appName();
static std::string organizationName();
static std::string logFilePath();
/** Returns a message to display to the user in the event
* of a problem installing the update.
*/
static std::string updateErrorMessage(const std::string& details);
};
inline std::string AppInfo::name()
@ -19,3 +32,8 @@ inline std::string AppInfo::appName()
return "Mendeley Desktop";
}
inline std::string AppInfo::organizationName()
{
return "Mendeley Ltd.";
}

View file

@ -2,6 +2,7 @@
add_subdirectory(tests)
find_package(Threads REQUIRED)
include(GenerateCppResourceFile)
if (UNIX)
add_definitions(-Wall -Werror -Wconversion)
@ -23,8 +24,9 @@ endif()
add_definitions(-DTIXML_USE_STL)
set (SOURCES
AppInfo.cpp
DirIterator.cpp
FileOps.cpp
FileUtils.cpp
Log.cpp
ProcessUtils.cpp
UpdateInstaller.cpp
@ -42,7 +44,7 @@ endif()
set (HEADERS
AppInfo.h
DirIterator.h
FileOps.h
FileUtils.h
Log.h
ProcessUtils.h
UpdateInstaller.h
@ -59,13 +61,11 @@ if (ENABLE_GTK)
# embed the GTK helper library into the updater binary.
# At runtime it will be extracted and loaded if the
# GTK libraries are available
set(GTK_UPDATER_LIB libupdatergtk.so)
set(GTK_BIN_FILE ${CMAKE_CURRENT_BINARY_DIR}/libupdatergtk.cpp)
add_custom_command(OUTPUT ${GTK_BIN_FILE}
COMMAND xxd -i ${GTK_UPDATER_LIB} ${GTK_BIN_FILE}
DEPENDS updatergtk)
set(SOURCES ${SOURCES} UpdateDialogGtkWrapper.cpp ${GTK_BIN_FILE})
set(GTK_BIN_CPP_FILE ${CMAKE_CURRENT_BINARY_DIR}/libupdatergtk.cpp)
generate_cpp_resource_file(${GTK_UPDATER_LIB} ${GTK_BIN_CPP_FILE} updatergtk)
set(SOURCES ${SOURCES} UpdateDialogGtkWrapper.cpp ${GTK_BIN_CPP_FILE})
set(HEADERS ${HEADERS} UpdateDialogGtkWrapper.h)
endif()

View file

@ -8,6 +8,9 @@
#include <dirent.h>
#endif
/** Simple class for iterating over the files in a directory
* and reporting their names and types.
*/
class DirIterator
{
public:

View file

@ -1,4 +1,4 @@
#include "FileOps.h"
#include "FileUtils.h"
#include "DirIterator.h"
#include "Log.h"
@ -20,7 +20,7 @@
#include <libgen.h>
#endif
FileOps::IOException::IOException(const std::string& error)
FileUtils::IOException::IOException(const std::string& error)
: m_errno(0)
{
m_error = error;
@ -39,11 +39,11 @@ FileOps::IOException::IOException(const std::string& error)
#endif
}
FileOps::IOException::~IOException() throw ()
FileUtils::IOException::~IOException() throw ()
{
}
bool FileOps::fileExists(const char* path) throw (IOException)
bool FileUtils::fileExists(const char* path) throw (IOException)
{
#ifdef PLATFORM_UNIX
struct stat fileInfo;
@ -69,7 +69,7 @@ bool FileOps::fileExists(const char* path) throw (IOException)
#endif
}
void FileOps::setQtPermissions(const char* path, int qtPermissions) throw (IOException)
void FileUtils::setQtPermissions(const char* path, int qtPermissions) throw (IOException)
{
#ifdef PLATFORM_UNIX
int mode = toUnixPermissions(qtPermissions);
@ -83,7 +83,7 @@ void FileOps::setQtPermissions(const char* path, int qtPermissions) throw (IOExc
#endif
}
void FileOps::moveFile(const char* src, const char* dest) throw (IOException)
void FileUtils::moveFile(const char* src, const char* dest) throw (IOException)
{
#ifdef PLATFORM_UNIX
if (rename(src,dest) != 0)
@ -98,7 +98,7 @@ void FileOps::moveFile(const char* src, const char* dest) throw (IOException)
#endif
}
void FileOps::extractFromZip(const char* zipFilePath, const char* src, const char* dest) throw (IOException)
void FileUtils::extractFromZip(const char* zipFilePath, const char* src, const char* dest) throw (IOException)
{
unzFile zipFile = unzOpen(zipFilePath);
int result = unzLocateFile(zipFile,src,0);
@ -138,7 +138,7 @@ void FileOps::extractFromZip(const char* zipFilePath, const char* src, const cha
unzClose(zipFile);
}
void FileOps::mkdir(const char* dir) throw (IOException)
void FileUtils::mkdir(const char* dir) throw (IOException)
{
#ifdef PLATFORM_UNIX
if (::mkdir(dir,S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH) != 0)
@ -153,7 +153,7 @@ void FileOps::mkdir(const char* dir) throw (IOException)
#endif
}
void FileOps::rmdir(const char* dir) throw (IOException)
void FileUtils::rmdir(const char* dir) throw (IOException)
{
#ifdef PLATFORM_UNIX
if (::rmdir(dir) != 0)
@ -168,7 +168,7 @@ void FileOps::rmdir(const char* dir) throw (IOException)
#endif
}
void FileOps::createSymLink(const char* link, const char* target) throw (IOException)
void FileUtils::createSymLink(const char* link, const char* target) throw (IOException)
{
#ifdef PLATFORM_UNIX
if (symlink(target,link) != 0)
@ -182,7 +182,7 @@ void FileOps::createSymLink(const char* link, const char* target) throw (IOExcep
#endif
}
void FileOps::removeFile(const char* src) throw (IOException)
void FileUtils::removeFile(const char* src) throw (IOException)
{
#ifdef PLATFORM_UNIX
if (unlink(src) != 0)
@ -225,7 +225,7 @@ void FileOps::removeFile(const char* src) throw (IOException)
#endif
}
std::string FileOps::fileName(const char* path)
std::string FileUtils::fileName(const char* path)
{
#ifdef PLATFORM_UNIX
char* pathCopy = strdup(path);
@ -240,7 +240,7 @@ std::string FileOps::fileName(const char* path)
#endif
}
std::string FileOps::dirname(const char* path)
std::string FileUtils::dirname(const char* path)
{
#ifdef PLATFORM_UNIX
char* pathCopy = strdup(path);
@ -254,18 +254,26 @@ std::string FileOps::dirname(const char* path)
#endif
}
void FileOps::touch(const char* path) throw (IOException)
void FileUtils::touch(const char* path) throw (IOException)
{
#ifdef PLATFORM_UNIX
// see http://pubs.opengroup.org/onlinepubs/9699919799/utilities/touch.html
int fd = creat(path,S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
if (fd != -1)
if (fileExists(path))
{
close(fd);
utimensat(AT_FDCWD,path,0 /* use current date/time */,0);
}
else
{
throw IOException("Unable to touch file " + std::string(path));
int fd = creat(path,S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
if (fd != -1)
{
futimens(fd,0 /* use current date/time */);
close(fd);
}
else
{
throw IOException("Unable to touch file " + std::string(path));
}
}
#else
HANDLE result = CreateFile(path,GENERIC_WRITE,
@ -285,7 +293,7 @@ void FileOps::touch(const char* path) throw (IOException)
#endif
}
void FileOps::rmdirRecursive(const char* path) throw (IOException)
void FileUtils::rmdirRecursive(const char* path) throw (IOException)
{
// remove dir contents
DirIterator dir(path);
@ -309,7 +317,7 @@ void FileOps::rmdirRecursive(const char* path) throw (IOException)
rmdir(path);
}
std::string FileOps::canonicalPath(const char* path)
std::string FileUtils::canonicalPath(const char* path)
{
#ifdef PLATFORM_UNIX
// on Linux and Mac OS 10.6, realpath() can allocate the required
@ -339,7 +347,7 @@ void addFlag(InFlags inFlags, int testBit, OutFlags& outFlags, int setBit)
}
#ifdef PLATFORM_UNIX
int FileOps::toUnixPermissions(int qtPermissions)
int FileUtils::toUnixPermissions(int qtPermissions)
{
mode_t result = 0;
addFlag(qtPermissions,ReadUser,result,S_IRUSR);
@ -355,7 +363,7 @@ int FileOps::toUnixPermissions(int qtPermissions)
}
#endif
std::string FileOps::toUnixPathSeparators(const std::string& str)
std::string FileUtils::toUnixPathSeparators(const std::string& str)
{
std::string result = str;
for (size_t i=0; i < result.size(); i++)
@ -368,7 +376,7 @@ std::string FileOps::toUnixPathSeparators(const std::string& str)
return result;
}
std::string FileOps::tempPath()
std::string FileUtils::tempPath()
{
#ifdef PLATFORM_UNIX
return "/tmp";

View file

@ -5,9 +5,15 @@
#include "StringUtils.h"
class FileOps
/** A set of functions for performing common operations
* on files, throwing exceptions if an operation fails.
*/
class FileUtils
{
public:
/** Base class for exceptions reported by
* FileUtils methods if an operation fails.
*/
class IOException : public std::exception
{
public:
@ -25,6 +31,7 @@ class FileOps
int m_errno;
};
/** Reproduction of Qt's QFile::Permission enum. */
enum QtFilePermission
{
ReadOwner = 0x4000,
@ -52,25 +59,45 @@ class FileOps
*/
static void removeFile(const char* src) throw (IOException);
/** Set the permissions of a file using a combination of flags
* from the QtFilePermission enum.
*/
static void setQtPermissions(const char* path, int permissions) throw (IOException);
static bool fileExists(const char* path) throw (IOException);
static void moveFile(const char* src, const char* dest) throw (IOException);
static void extractFromZip(const char* zipFile, const char* src, const char* dest) throw (IOException);
static void mkdir(const char* dir) throw (IOException);
static void rmdir(const char* dir) throw (IOException);
static void createSymLink(const char* link, const char* target) throw (IOException);
static void touch(const char* path) throw (IOException);
/** Returns the file name part of a file path, including the extension. */
static std::string fileName(const char* path);
/** Returns the directory part of a file path. */
static std::string dirname(const char* path);
/** Remove a directory and all of its contents. */
static void rmdirRecursive(const char* dir) throw (IOException);
/** Return the full, absolute path to a file, resolving any
* symlinks and removing redundant sections.
*/
static std::string canonicalPath(const char* path);
/** Returns the path to a directory for storing temporary files. */
static std::string tempPath();
/** Extract the file @p src from the zip archive @p zipFile and
* write it to @p dest.
*/
static void extractFromZip(const char* zipFile, const char* src, const char* dest) throw (IOException);
/** Returns a copy of the path 'str' with Windows-style '\'
* dir separators converted to Unix-style '/' separators
*/
static std::string toUnixPathSeparators(const std::string& str);
private:
static int toUnixPermissions(int qtPermissions);
// returns a copy of the path 'str' with Windows-style '\'
// dir separators converted to Unix-style '/' separators
static std::string toUnixPathSeparators(const std::string& str);
};

View file

@ -2,40 +2,13 @@
#include "Platform.h"
#include "StringUtils.h"
#include "ProcessUtils.h"
#include <string.h>
#include <iostream>
#ifdef PLATFORM_UNIX
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#endif
Log m_globalLog;
#ifdef PLATFORM_UNIX
pid_t currentProcessId = 0;
pid_t processId()
{
if (currentProcessId == 0)
{
currentProcessId = getpid();
}
return currentProcessId;
}
#else
DWORD currentProcessId = 0;
DWORD processId()
{
if (currentProcessId == 0)
{
currentProcessId = GetCurrentProcessId();
}
return currentProcessId;
}
#endif
Log* Log::instance()
{
return &m_globalLog;
@ -68,7 +41,7 @@ void Log::writeToStream(std::ostream& stream, Type type, const char* text)
stream << "ERROR ";
break;
}
stream << '(' << intToStr(processId()) << ") " << text << std::endl;
stream << '(' << intToStr(ProcessUtils::currentProcessId()) << ") " << text << std::endl;
}
void Log::write(Type type, const char* text)
@ -80,9 +53,3 @@ void Log::write(Type type, const char* text)
}
}
std::string Log::defaultPath()
{
std::string path = "update-log.txt";
return path;
}

View file

@ -22,7 +22,6 @@ class Log
void write(Type type, const char* text);
static Log* instance();
static std::string defaultPath();
private:
static void writeToStream(std::ostream& stream, Type type, const char* text);

View file

@ -1,6 +1,6 @@
#include "ProcessUtils.h"
#include "FileOps.h"
#include "FileUtils.h"
#include "Platform.h"
#include "StringUtils.h"
#include "Log.h"
@ -108,7 +108,7 @@ bool ProcessUtils::waitForProcess(PLATFORM_PID pid)
int ProcessUtils::runElevatedLinux(const std::string& executable,
const std::list<std::string>& args)
{
std::string sudoMessage = FileOps::fileName(executable.c_str()) + " needs administrative privileges. Please enter your password.";
std::string sudoMessage = FileUtils::fileName(executable.c_str()) + " needs administrative privileges. Please enter your password.";
std::vector<std::string> sudos;
sudos.push_back("kdesudo");
@ -421,7 +421,7 @@ int ProcessUtils::runWindows(const std::string& executable,
std::string ProcessUtils::currentProcessPath()
{
#ifdef PLATFORM_LINUX
std::string path = FileOps::canonicalPath("/proc/self/exe");
std::string path = FileUtils::canonicalPath("/proc/self/exe");
LOG(Info,"Current process path " + path);
return path;
#elif defined(PLATFORM_MAC)
@ -451,7 +451,7 @@ void ProcessUtils::convertWindowsCommandLine(LPCWSTR commandLine, int& argc, cha
int length = WideCharToMultiByte(CP_ACP,
0 /* flags */,
argvUnicode[i],
-1, /* argvUnicode is null terminated*/
-1, /* argvUnicode is null terminated */
buffer,
BUFFER_SIZE,
0,

View file

@ -5,6 +5,9 @@
#include <list>
#include <string>
/** A set of functions to get information about the current
* process and launch new processes.
*/
class ProcessUtils
{
public:
@ -18,11 +21,22 @@ class ProcessUtils
static PLATFORM_PID currentProcessId();
/** Returns the absolute path to the main binary for
* the current process.
*/
static std::string currentProcessPath();
/** Start a process and wait for it to finish before
* returning its exit code.
*
* Returns -1 if the process cannot be started.
*/
static int runSync(const std::string& executable,
const std::list<std::string>& args);
/** Start a process and return without waiting for
* it to finish.
*/
static void runAsync(const std::string& executable,
const std::list<std::string>& args);
@ -35,6 +49,10 @@ class ProcessUtils
static int runElevated(const std::string& executable,
const std::list<std::string>& args);
/** Wait for a process to exit.
* Returns true if the process was found and has exited or false
* otherwise.
*/
static bool waitForProcess(PLATFORM_PID pid);
#ifdef PLATFORM_WINDOWS

View file

@ -44,9 +44,9 @@ void UpdateDialogGtk::init(int argc, char** argv)
m_window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
gtk_window_set_title(GTK_WINDOW(m_window),AppInfo::name().c_str());
gtk_window_set_resizable(GTK_WINDOW(m_window),false);
m_progressLabel = gtk_label_new("Installing Updates");
GtkWidget* windowLayout = gtk_vbox_new(FALSE,3);
GtkWidget* buttonLayout = gtk_hbox_new(FALSE,3);
GtkWidget* labelLayout = gtk_hbox_new(FALSE,3);
@ -56,6 +56,12 @@ void UpdateDialogGtk::init(int argc, char** argv)
m_progressBar = gtk_progress_bar_new();
// give the dialog a sensible default size by setting a minimum
// width on the progress bar. This is used instead of setting
// a default size for the dialog since gtk_window_set_default_size()
// is ignored when a dialog is marked as non-resizable
gtk_widget_set_usize(m_progressBar,350,-1);
gtk_signal_connect(GTK_OBJECT(m_finishButton),"clicked",
GTK_SIGNAL_FUNC(UpdateDialogGtk::finish),this);
@ -77,7 +83,9 @@ void UpdateDialogGtk::init(int argc, char** argv)
gtk_widget_show(m_finishButton);
gtk_widget_show(m_progressBar);
gtk_window_set_resizable(GTK_WINDOW(m_window),false);
gtk_window_set_position(GTK_WINDOW(m_window),GTK_WIN_POS_CENTER);
gtk_widget_show(m_window);
}
@ -103,7 +111,7 @@ gboolean UpdateDialogGtk::notify(void* _message)
case UpdateMessage::UpdateFailed:
{
dialog->m_hadError = true;
std::string errorMessage = "There was a problem installing the update:\n\n" + message->message;
std::string errorMessage = AppInfo::updateErrorMessage(message->message);
GtkWidget* errorDialog = gtk_message_dialog_new (GTK_WINDOW(dialog->m_window),
GTK_DIALOG_DESTROY_WITH_PARENT,
GTK_MESSAGE_ERROR,

View file

@ -91,7 +91,7 @@ void UpdateDialogWin32::init()
DWORD style = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX;
m_window.CreateEx(0 /* dwExStyle */,
updateDialogClassName /* class name */,
AppInfo::name(),
AppInfo::name().c_str(),
style,
0, 0, width, height,
0 /* parent */, 0 /* menu */, 0 /* reserved */);
@ -171,8 +171,7 @@ LRESULT WINAPI UpdateDialogWin32::windowProc(HWND window, UINT message, WPARAM w
case UpdateMessage::UpdateFailed:
{
m_hadError = true;
std::string text = "There was a problem installing the update:\n\n" +
message->message;
std::string text = AppInfo::updateErrorMessage(message->message);
MessageBox(m_window.GetHwnd(),text.c_str(),"Update Problem",MB_OK);
}
break;

View file

@ -1,6 +1,6 @@
#include "UpdateInstaller.h"
#include "FileOps.h"
#include "FileUtils.h"
#include "Log.h"
#include "ProcessUtils.h"
#include "UpdateObserver.h"
@ -60,6 +60,7 @@ void UpdateInstaller::reportError(const std::string& error)
if (m_observer)
{
m_observer->updateError(error);
m_observer->updateFinished();
}
}
@ -81,16 +82,20 @@ void UpdateInstaller::run() throw ()
{
updaterPath = ProcessUtils::currentProcessPath();
}
catch (const FileOps::IOException& ex)
catch (const FileUtils::IOException& ex)
{
LOG(Error,"error reading process path with mode " + intToStr(m_mode));
reportError("Unable to determine path of updater");
return;
}
if (m_mode == Setup)
{
LOG(Info,"Waiting for main app process to finish");
ProcessUtils::waitForProcess(m_waitPid);
if (m_waitPid != 0)
{
LOG(Info,"Waiting for main app process to finish");
ProcessUtils::waitForProcess(m_waitPid);
}
std::list<std::string> args = updaterArgs();
args.push_back("--mode");
@ -143,8 +148,10 @@ void UpdateInstaller::run() throw ()
LOG(Info,"Removing backups");
removeBackups();
postInstallUpdate();
}
catch (const FileOps::IOException& exception)
catch (const FileUtils::IOException& exception)
{
error = exception.what();
}
@ -174,9 +181,9 @@ void UpdateInstaller::cleanup()
{
try
{
FileOps::rmdirRecursive(m_packageDir.c_str());
FileUtils::rmdirRecursive(m_packageDir.c_str());
}
catch (const FileOps::IOException& ex)
catch (const FileUtils::IOException& ex)
{
LOG(Error,"Error cleaning up updater " + std::string(ex.what()));
}
@ -191,11 +198,11 @@ void UpdateInstaller::revert()
const std::string& installedFile = iter->first;
const std::string& backupFile = iter->second;
if (FileOps::fileExists(installedFile.c_str()))
if (FileUtils::fileExists(installedFile.c_str()))
{
FileOps::removeFile(installedFile.c_str());
FileUtils::removeFile(installedFile.c_str());
}
FileOps::moveFile(backupFile.c_str(),installedFile.c_str());
FileUtils::moveFile(backupFile.c_str(),installedFile.c_str());
}
}
@ -208,32 +215,32 @@ void UpdateInstaller::installFile(const UpdateScriptFile& file)
backupFile(destPath);
// create the target directory if it does not exist
std::string destDir = FileOps::dirname(destPath.c_str());
if (!FileOps::fileExists(destDir.c_str()))
std::string destDir = FileUtils::dirname(destPath.c_str());
if (!FileUtils::fileExists(destDir.c_str()))
{
FileOps::mkdir(destDir.c_str());
FileUtils::mkdir(destDir.c_str());
}
if (target.empty())
{
// locate the package containing the file
std::string packageFile = m_packageDir + '/' + file.package + ".zip";
if (!FileOps::fileExists(packageFile.c_str()))
if (!FileUtils::fileExists(packageFile.c_str()))
{
throw "Package file does not exist: " + packageFile;
}
// extract the file from the package and copy it to
// the destination
FileOps::extractFromZip(packageFile.c_str(),file.path.c_str(),destPath.c_str());
FileUtils::extractFromZip(packageFile.c_str(),file.path.c_str(),destPath.c_str());
// set the permissions on the newly extracted file
FileOps::setQtPermissions(destPath.c_str(),file.permissions);
FileUtils::setQtPermissions(destPath.c_str(),file.permissions);
}
else
{
// create the symlink
FileOps::createSymLink(destPath.c_str(),target.c_str());
FileUtils::createSymLink(destPath.c_str(),target.c_str());
}
}
@ -259,9 +266,9 @@ void UpdateInstaller::uninstallFiles()
for (;iter != m_script->filesToUninstall().end();iter++)
{
std::string path = m_installDir + '/' + iter->c_str();
if (FileOps::fileExists(path.c_str()))
if (FileUtils::fileExists(path.c_str()))
{
FileOps::removeFile(path.c_str());
FileUtils::removeFile(path.c_str());
}
else
{
@ -272,15 +279,15 @@ void UpdateInstaller::uninstallFiles()
void UpdateInstaller::backupFile(const std::string& path)
{
if (!FileOps::fileExists(path.c_str()))
if (!FileUtils::fileExists(path.c_str()))
{
// no existing file to backup
return;
}
std::string backupPath = path + ".bak";
FileOps::removeFile(backupPath.c_str());
FileOps::moveFile(path.c_str(), backupPath.c_str());
FileUtils::removeFile(backupPath.c_str());
FileUtils::moveFile(path.c_str(), backupPath.c_str());
m_backups[path] = backupPath;
}
@ -290,7 +297,7 @@ void UpdateInstaller::removeBackups()
for (;iter != m_backups.end();iter++)
{
const std::string& backupFile = iter->second;
FileOps::removeFile(backupFile.c_str());
FileUtils::removeFile(backupFile.c_str());
}
}
@ -300,20 +307,20 @@ bool UpdateInstaller::checkAccess()
try
{
FileOps::removeFile(testFile.c_str());
FileUtils::removeFile(testFile.c_str());
}
catch (const FileOps::IOException& error)
catch (const FileUtils::IOException& error)
{
LOG(Info,"Removing existing access check file failed " + std::string(error.what()));
}
try
{
FileOps::touch(testFile.c_str());
FileOps::removeFile(testFile.c_str());
FileUtils::touch(testFile.c_str());
FileUtils::removeFile(testFile.c_str());
return true;
}
catch (const FileOps::IOException& error)
catch (const FileUtils::IOException& error)
{
LOG(Info,"checkAccess() failed " + std::string(error.what()));
return false;
@ -351,3 +358,15 @@ void UpdateInstaller::restartMainApp()
}
}
void UpdateInstaller::postInstallUpdate()
{
// perform post-install actions
#ifdef PLATFORM_MAC
// touch the application's bundle directory so that
// OS X' Launch Services notices any changes in the application's
// Info.plist file.
FileUtils::touch(m_installDir.c_str());
#endif
}

View file

@ -9,6 +9,11 @@
class UpdateObserver;
/** Central class responsible for installing updates,
* launching an elevated copy of the updater if required
* and restarting the main application once the update
* is installed.
*/
class UpdateInstaller
{
public:
@ -43,6 +48,7 @@ class UpdateInstaller
void installFile(const UpdateScriptFile& file);
void backupFile(const std::string& path);
void reportError(const std::string& error);
void postInstallUpdate();
std::list<std::string> updaterArgs() const;

View file

@ -2,6 +2,10 @@
#include <string>
/** UpdateMessage stores information for a message
* about the status of update installation sent
* between threads.
*/
class UpdateMessage
{
public:
@ -22,16 +26,17 @@ class UpdateMessage
init(0,type);
}
void* receiver;
Type type;
std::string message;
int progress;
private:
void init(void* receiver, Type type)
{
this->progress = 0;
this->receiver = receiver;
this->type = type;
}
void* receiver;
Type type;
std::string message;
int progress;
};

View file

@ -2,6 +2,9 @@
#include <string>
/** Base class for observers of update installation status.
* See UpdateInstaller::setObserver()
*/
class UpdateObserver
{
public:

View file

@ -5,6 +5,9 @@
class TiXmlElement;
/** Represents a package containing one or more
* files for an update.
*/
class UpdateScriptPackage
{
public:
@ -26,6 +29,7 @@ class UpdateScriptPackage
}
};
/** Represents a file to be installed as part of an update. */
class UpdateScriptFile
{
public:
@ -50,11 +54,17 @@ class UpdateScriptFile
}
};
/** Stores information about the packages and files included
* in an update, parsed from an XML file.
*/
class UpdateScript
{
public:
UpdateScript();
/** Initialize this UpdateScript with the script stored
* in the XML file at @p path.
*/
void parse(const std::string& path);
bool isValid() const;

View file

@ -2,6 +2,7 @@
#include "UpdateInstaller.h"
/** Parses the command-line options to the updater binary. */
class UpdaterOptions
{
public:

View file

@ -1,3 +1,4 @@
#include "AppInfo.h"
#include "Log.h"
#include "Platform.h"
#include "ProcessUtils.h"
@ -51,7 +52,7 @@ void runUpdaterThread(void* arg)
int main(int argc, char** argv)
{
Log::instance()->open(Log::defaultPath());
Log::instance()->open(AppInfo::logFilePath());
UpdaterOptions options;
options.parse(argc,argv);

View file

@ -3,12 +3,19 @@ This directory contains a set of tools for preparing auto-updates.
* create-packages.rb
Given a directory containing the set of files that make up a release,
laid out as they are when installed and a JSON file mapping files
to packages, this tool generates a set of packages for the release
laid out as they are when installed and a JSON package config file,
this tool generates a set of packages for the release
and a file_list.xml listing all the files that make up the release.
* single-package-map.json
* config-template.json
This is the simplest possible package map, where all files for
This is a template for the config file that specifies:
- How to partition the files that make up an installed application
into packages.
- The path of the main binary to run after the update completes
- The name of the updater binary to download as part of the update
This is the simplest possible package configuration, where all files for
a release are placed in a single package. This means that the whole
package will need to be downloaded to install the update.

View file

@ -0,0 +1,11 @@
{
"packages" : {
"app" : [
".*"
]
},
"updater-binary" : "updater",
"main-binary" : "myapp"
}

View file

@ -1,5 +1,6 @@
#!/usr/bin/ruby
require 'fileutils'
require 'rubygems'
require 'find'
require 'json'
@ -8,10 +9,12 @@ require 'optparse'
# syntax:
#
# create-packages.rb <input directory> <package map> <output directory>
# create-packages.rb <input directory> <config file> <output directory>
#
# Takes the set of files that make up a release and splits them up into
# a set of .zip packages
# a set of .zip packages along with a file_list.xml file listing all
# the files in the release and mapping them to their respective
# packages.
#
# Outputs:
#
@ -40,8 +43,8 @@ class UpdateScriptFile
# flags from the QFile::Permission enum in Qt
# size - The size of the file in bytes
# package - The name of the package containing this file
attr_reader :path,:hash,:permissions,:size,:package,:target
attr_writer :path,:hash,:permissions,:size,:package,:target
attr_reader :path,:hash,:permissions,:size,:package,:target,:is_main_binary
attr_writer :path,:hash,:permissions,:size,:package,:target,:is_main_binary
end
# Utility method - convert a hash map to an REXML element
@ -76,12 +79,23 @@ def strip_prefix(string,prefix)
end
class UpdateScriptGenerator
def initialize(input_dir,output_dir, file_list, package_file_map)
# input_dir - The directory containing files that make up the install
# output_dir - The directory containing the generated packages
# package_config - The PackageConfig specifying the file -> package map
# for the application and other config options
# file_list - A list of all files in 'input_dir' which make up the install
# package_file_map - A map of (package name -> [paths of files in this package])
def initialize(input_dir, output_dir, package_config, file_list, package_file_map)
@config = package_config
# List of files to install in this version
@files_to_install = []
file_list.each do |path|
file = UpdateScriptFile.new
file.path = strip_prefix(path,input_dir)
file.is_main_binary = (file.path == package_config.main_binary)
if (File.symlink?(path))
file.target = File.readlink(path)
@ -112,7 +126,7 @@ class UpdateScriptGenerator
end
end
def toXML()
def to_xml()
doc = REXML::Document.new
update_elem = REXML::Element.new("update")
doc.add_element update_elem
@ -128,7 +142,7 @@ class UpdateScriptGenerator
def deps_to_xml()
deps_elem = REXML::Element.new("dependencies")
deps = ["updater.exe"]
deps = @config.updater_binary
deps.each do |dependency|
dep_elem = REXML::Element.new("file")
dep_elem.text = dependency
@ -166,6 +180,11 @@ class UpdateScriptGenerator
attributes["hash"] = file.hash
attributes["package"] = file.package
end
if (file.is_main_binary)
attributes["is-main-binary"] = "true"
end
hash_to_xml(file_elem,attributes)
end
return install_elem
@ -225,16 +244,21 @@ class UpdateScriptGenerator
end
end
class PackageMap
class PackageConfig
attr_reader :main_binary, :updater_binary
def initialize(map_file)
@rule_map = {}
map_json = JSON.parse(File.read(map_file))
map_json.each do |package,rules|
config_json = JSON.parse(File.read(map_file))
config_json["packages"].each do |package,rules|
rules.each do |rule|
rule_regex = Regexp.new(rule)
@rule_map[rule_regex] = package
end
end
@main_binary = config_json["main-binary"]
@updater_binary = config_json["updater-binary"]
end
def package_for_file(file)
@ -259,6 +283,8 @@ input_dir = ARGV[0]
package_map_file = ARGV[1]
output_dir = ARGV[2]
FileUtils.mkpath(output_dir)
# get the details of each input file
input_file_list = []
Find.find(input_dir) do |path|
@ -269,14 +295,14 @@ end
# map each input file to a corresponding package
# read the package map
package_map = PackageMap.new(package_map_file)
package_config = PackageConfig.new(package_map_file)
# map of package name -> array of files
package_file_map = {}
input_file_list.each do |file|
next if File.symlink?(file)
package = package_map.package_for_file(file)
package = package_config.package_for_file(file)
if (!package)
raise "Unable to find package for file #{file}"
end
@ -295,18 +321,22 @@ package_file_map.each do |package,files|
quoted_file_list = quoted_files.join(" ")
output_path = File.expand_path(output_dir)
output_file = "#{output_path}/#{package}.zip"
File.unlink(output_file) if File.exist?(output_file)
Dir.chdir(input_dir) do
#if (!system("zip #{output_path}/#{package}.zip #{quoted_file_list}"))
# raise "Failed to generate package #{package}"
# end
if (!system("zip #{output_path}/#{package}.zip #{quoted_file_list}"))
raise "Failed to generate package #{package}"
end
end
end
# output the file_list.xml file
update_script = UpdateScriptGenerator.new(input_dir,output_dir,input_file_list,package_file_map)
update_script = UpdateScriptGenerator.new(input_dir,output_dir,package_config,input_file_list,package_file_map)
output_xml_file = "#{output_dir}/file_list.unformatted.xml"
File.open(output_xml_file,'w') do |file|
file.write update_script.toXML()
file.write update_script.to_xml()
end
# xmllint generates more readable formatted XML than REXML, so write unformatted

View file

@ -1,5 +0,0 @@
{
"app" : [
".*"
]
}