summaryrefslogtreecommitdiffstats
path: root/mmc_updater/src
diff options
context:
space:
mode:
authorAndrew <forkk@forkk.net>2013-12-09 12:04:05 -0600
committerAndrew <forkk@forkk.net>2013-12-09 12:04:05 -0600
commit7f52bed9e3f559adcbcf3f3c1c7ac2251964db8c (patch)
tree52097b57dc7d18ec4e35ef3a8e554af2d91545e8 /mmc_updater/src
parent9410dd042ea62224fa3e0eb3b525abbdf0c316ee (diff)
parent220e07aef4a34dca9f31ae0c5bb994e5c594125d (diff)
downloadMultiMC-7f52bed9e3f559adcbcf3f3c1c7ac2251964db8c.tar
MultiMC-7f52bed9e3f559adcbcf3f3c1c7ac2251964db8c.tar.gz
MultiMC-7f52bed9e3f559adcbcf3f3c1c7ac2251964db8c.tar.lz
MultiMC-7f52bed9e3f559adcbcf3f3c1c7ac2251964db8c.tar.xz
MultiMC-7f52bed9e3f559adcbcf3f3c1c7ac2251964db8c.zip
Merge branch 'feature_updater' into develop
Diffstat (limited to 'mmc_updater/src')
-rw-r--r--mmc_updater/src/AppInfo.cpp23
-rw-r--r--mmc_updater/src/AppInfo.h39
-rw-r--r--mmc_updater/src/CMakeLists.txt121
-rw-r--r--mmc_updater/src/DirIterator.cpp85
-rw-r--r--mmc_updater/src/DirIterator.h43
-rw-r--r--mmc_updater/src/FileUtils.cpp557
-rw-r--r--mmc_updater/src/FileUtils.h141
-rw-r--r--mmc_updater/src/Log.cpp65
-rw-r--r--mmc_updater/src/Log.h46
-rw-r--r--mmc_updater/src/MacBundle.cpp53
-rw-r--r--mmc_updater/src/MacBundle.h35
-rw-r--r--mmc_updater/src/Platform.h32
-rw-r--r--mmc_updater/src/ProcessUtils.cpp536
-rw-r--r--mmc_updater/src/ProcessUtils.h97
-rw-r--r--mmc_updater/src/StandardDirs.cpp63
-rw-r--r--mmc_updater/src/StandardDirs.h22
-rw-r--r--mmc_updater/src/StandardDirs.mm18
-rw-r--r--mmc_updater/src/StlSymbolsLeopard.cpp75
-rw-r--r--mmc_updater/src/StringUtils.h46
-rw-r--r--mmc_updater/src/UpdateDialog.cpp25
-rw-r--r--mmc_updater/src/UpdateDialog.h29
-rw-r--r--mmc_updater/src/UpdateDialogAscii.cpp70
-rw-r--r--mmc_updater/src/UpdateDialogAscii.h32
-rw-r--r--mmc_updater/src/UpdateDialogCocoa.h32
-rw-r--r--mmc_updater/src/UpdateDialogCocoa.mm194
-rw-r--r--mmc_updater/src/UpdateDialogGtk.cpp155
-rw-r--r--mmc_updater/src/UpdateDialogGtk.h42
-rw-r--r--mmc_updater/src/UpdateDialogGtkFactory.cpp59
-rw-r--r--mmc_updater/src/UpdateDialogGtkFactory.h13
-rw-r--r--mmc_updater/src/UpdateDialogWin32.cpp215
-rw-r--r--mmc_updater/src/UpdateDialogWin32.h39
-rw-r--r--mmc_updater/src/UpdateInstaller.cpp426
-rw-r--r--mmc_updater/src/UpdateInstaller.h72
-rw-r--r--mmc_updater/src/UpdateMessage.h42
-rw-r--r--mmc_updater/src/UpdateObserver.h15
-rw-r--r--mmc_updater/src/UpdateScript.cpp99
-rw-r--r--mmc_updater/src/UpdateScript.h85
-rw-r--r--mmc_updater/src/UpdaterOptions.cpp154
-rw-r--r--mmc_updater/src/UpdaterOptions.h28
-rw-r--r--mmc_updater/src/main.cpp203
-rw-r--r--mmc_updater/src/resources/Info.plist38
-rw-r--r--mmc_updater/src/resources/icon128.pngbin0 -> 3802 bytes
-rw-r--r--mmc_updater/src/resources/icon64.pngbin0 -> 2182 bytes
-rw-r--r--mmc_updater/src/resources/mac.icnsbin0 -> 43606 bytes
-rw-r--r--mmc_updater/src/resources/updater.icobin0 -> 82726 bytes
-rw-r--r--mmc_updater/src/resources/updater.rc30
-rw-r--r--mmc_updater/src/tests/CMakeLists.txt51
-rw-r--r--mmc_updater/src/tests/TestFileUtils.cpp50
-rw-r--r--mmc_updater/src/tests/TestFileUtils.h10
-rw-r--r--mmc_updater/src/tests/TestUpdateScript.cpp27
-rw-r--r--mmc_updater/src/tests/TestUpdateScript.h8
-rw-r--r--mmc_updater/src/tests/TestUpdaterOptions.cpp68
-rw-r--r--mmc_updater/src/tests/TestUpdaterOptions.h8
-rw-r--r--mmc_updater/src/tests/TestUtils.h108
-rw-r--r--mmc_updater/src/tests/file_list.xml52
-rw-r--r--mmc_updater/src/tests/new_app.cpp8
-rw-r--r--mmc_updater/src/tests/old_app.cpp7
-rwxr-xr-xmmc_updater/src/tests/test-update.rb218
-rw-r--r--mmc_updater/src/tests/v2_file_list.xml67
59 files changed, 4876 insertions, 0 deletions
diff --git a/mmc_updater/src/AppInfo.cpp b/mmc_updater/src/AppInfo.cpp
new file mode 100644
index 00000000..a5a9bb63
--- /dev/null
+++ b/mmc_updater/src/AppInfo.cpp
@@ -0,0 +1,23 @@
+#include "AppInfo.h"
+
+#include "FileUtils.h"
+#include "Platform.h"
+#include "StringUtils.h"
+#include "StandardDirs.h"
+
+#include <iostream>
+
+std::string AppInfo::logFilePath()
+{
+ return StandardDirs::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 "
+ "MultiMC from http://multimc.org/";
+ return result;
+}
+
diff --git a/mmc_updater/src/AppInfo.h b/mmc_updater/src/AppInfo.h
new file mode 100644
index 00000000..51d95886
--- /dev/null
+++ b/mmc_updater/src/AppInfo.h
@@ -0,0 +1,39 @@
+#pragma once
+
+#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()
+{
+ return "MultiMC Updater";
+}
+
+inline std::string AppInfo::appName()
+{
+ return "MultiMC";
+}
+
+inline std::string AppInfo::organizationName()
+{
+ return "MultiMC Contributors";
+}
+
diff --git a/mmc_updater/src/CMakeLists.txt b/mmc_updater/src/CMakeLists.txt
new file mode 100644
index 00000000..9b39bb83
--- /dev/null
+++ b/mmc_updater/src/CMakeLists.txt
@@ -0,0 +1,121 @@
+
+add_subdirectory(tests)
+
+find_package(Threads REQUIRED)
+include(GenerateCppResourceFile)
+
+set (UPDATER_SOURCES
+ AppInfo.cpp
+ AppInfo.h
+ DirIterator.cpp
+ DirIterator.h
+ FileUtils.cpp
+ FileUtils.h
+ Log.cpp
+ Log.h
+ ProcessUtils.cpp
+ ProcessUtils.h
+ StandardDirs.cpp
+ StandardDirs.h
+ UpdateDialog.cpp
+ UpdateInstaller.cpp
+ UpdateInstaller.h
+ UpdateScript.cpp
+ UpdateScript.h
+ UpdaterOptions.cpp
+ UpdaterOptions.h
+)
+
+add_definitions(-DTIXML_USE_STL)
+
+if (WIN32)
+ set(UPDATER_SOURCES ${UPDATER_SOURCES} UpdateDialogWin32.cpp UpdateDialogWin32.h)
+endif()
+
+if (UNIX)
+ set(UPDATER_SOURCES ${UPDATER_SOURCES} UpdateDialogAscii.cpp UpdateDialogAscii.h)
+ add_definitions(-Wall -Werror -Wconversion)
+if (APPLE)
+ set(MAC_DOCK_ICON_CPP_FILE ${CMAKE_CURRENT_BINARY_DIR}/mac_dock_icon.cpp)
+ set(MAC_INFO_PLIST_FILE ${CMAKE_CURRENT_BINARY_DIR}/mac_info_plist.cpp)
+ generate_cpp_resource_file(resource_macdockicon ${CMAKE_CURRENT_SOURCE_DIR}/resources/mac.icns ${MAC_DOCK_ICON_CPP_FILE})
+ generate_cpp_resource_file(resource_macplist ${CMAKE_CURRENT_SOURCE_DIR}/resources/Info.plist ${MAC_INFO_PLIST_FILE})
+ set(UPDATER_SOURCES ${UPDATER_SOURCES}
+ MacBundle.h
+ MacBundle.cpp
+ StandardDirs.mm
+ StlSymbolsLeopard.cpp
+ UpdateDialogCocoa.mm
+ UpdateDialogCocoa.h
+ mac_dock_icon.cpp
+ mac_info_plist.cpp
+ )
+else() # linuxes and other similar systems
+ find_package(GTK2 REQUIRED gtk)
+ include_directories(${GTK2_INCLUDE_DIRS})
+ add_library(updatergtk SHARED UpdateDialogGtk.cpp UpdateDialogGtk.h)
+ target_link_libraries(updatergtk ${GTK2_LIBRARIES})
+
+ # embed the GTK helper library into the updater binary.
+ # At runtime it will be extracted and loaded if the
+ # GTK libraries are available
+ get_property(GTK_UPDATER_LIB TARGET updatergtk PROPERTY LOCATION)
+ set(GTK_BIN_CPP_FILE ${CMAKE_CURRENT_BINARY_DIR}/libupdatergtk.cpp)
+ generate_cpp_resource_file(resource_updatergtk ${GTK_UPDATER_LIB} ${GTK_BIN_CPP_FILE})
+ add_dependencies(resource_updatergtk updatergtk)
+
+ set(UPDATER_SOURCES ${UPDATER_SOURCES} UpdateDialogGtkFactory.cpp UpdateDialogGtkFactory.h ${GTK_BIN_CPP_FILE})
+endif()
+endif()
+
+add_library(updatershared STATIC ${UPDATER_SOURCES})
+
+target_link_libraries(updatershared
+ anyoption
+ tinyxml
+)
+
+if (UNIX)
+ if (APPLE)
+ find_library(COCOA_LIBRARY Cocoa)
+ find_library(SECURITY_LIBRARY Security)
+ target_link_libraries(updatershared ${SECURITY_LIBRARY} ${COCOA_LIBRARY})
+ else()
+ add_dependencies(updatershared resource_updatergtk)
+ endif()
+ target_link_libraries(updatershared pthread dl)
+endif()
+
+if (WIN32)
+ set(EXE_FLAGS WIN32 resources/updater.rc)
+endif()
+
+add_executable(updater ${EXE_FLAGS} main.cpp)
+
+target_link_libraries(updater
+ updatershared
+)
+
+
+#### Updater Executable ####
+IF(WIN32)
+INSTALL(TARGETS updater
+ BUNDLE DESTINATION . COMPONENT Runtime
+ LIBRARY DESTINATION . COMPONENT Runtime
+ RUNTIME DESTINATION . COMPONENT Runtime
+)
+ENDIF()
+IF(UNIX)
+IF(APPLE)
+INSTALL(TARGETS updater
+ BUNDLE DESTINATION . COMPONENT Runtime
+ RUNTIME DESTINATION MultiMC.app/Contents/MacOS COMPONENT Runtime
+)
+ELSE()
+INSTALL(TARGETS updater
+ BUNDLE DESTINATION . COMPONENT Runtime
+ RUNTIME DESTINATION bin COMPONENT Runtime
+)
+ENDIF()
+ENDIF()
+
diff --git a/mmc_updater/src/DirIterator.cpp b/mmc_updater/src/DirIterator.cpp
new file mode 100644
index 00000000..a4604f05
--- /dev/null
+++ b/mmc_updater/src/DirIterator.cpp
@@ -0,0 +1,85 @@
+#include "DirIterator.h"
+
+#include "Log.h"
+#include "StringUtils.h"
+
+#ifdef PLATFORM_UNIX
+ #include <dirent.h>
+#endif
+
+#include <string.h>
+
+DirIterator::DirIterator(const char* path)
+{
+ m_path = path;
+
+#ifdef PLATFORM_UNIX
+ m_dir = opendir(path);
+ m_entry = 0;
+#else
+ // to list the contents of a directory, the first
+ // argument to FindFirstFile needs to be a wildcard
+ // of the form: C:\path\to\dir\*
+ std::string searchPath = m_path;
+ if (!endsWith(searchPath,"/"))
+ {
+ searchPath.append("/");
+ }
+ searchPath.append("*");
+ m_findHandle = FindFirstFile(searchPath.c_str(),&m_findData);
+ m_firstEntry = true;
+#endif
+}
+
+DirIterator::~DirIterator()
+{
+#ifdef PLATFORM_UNIX
+ closedir(m_dir);
+#else
+ FindClose(m_findHandle);
+#endif
+}
+
+bool DirIterator::next()
+{
+#ifdef PLATFORM_UNIX
+ m_entry = readdir(m_dir);
+ return m_entry != 0;
+#else
+ bool result;
+ if (m_firstEntry)
+ {
+ m_firstEntry = false;
+ return m_findHandle != INVALID_HANDLE_VALUE;
+ }
+ else
+ {
+ result = FindNextFile(m_findHandle,&m_findData);
+ }
+ return result;
+#endif
+}
+
+std::string DirIterator::fileName() const
+{
+#ifdef PLATFORM_UNIX
+ return m_entry->d_name;
+#else
+ return m_findData.cFileName;
+#endif
+}
+
+std::string DirIterator::filePath() const
+{
+ return m_path + '/' + fileName();
+}
+
+bool DirIterator::isDir() const
+{
+#ifdef PLATFORM_UNIX
+ return m_entry->d_type == DT_DIR;
+#else
+ return (m_findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0;
+#endif
+}
+
diff --git a/mmc_updater/src/DirIterator.h b/mmc_updater/src/DirIterator.h
new file mode 100644
index 00000000..f3fbb955
--- /dev/null
+++ b/mmc_updater/src/DirIterator.h
@@ -0,0 +1,43 @@
+#pragma once
+
+#include "Platform.h"
+
+#include <string>
+
+#ifdef PLATFORM_UNIX
+#include <dirent.h>
+#endif
+
+/** Simple class for iterating over the files in a directory
+ * and reporting their names and types.
+ */
+class DirIterator
+{
+ public:
+ DirIterator(const char* path);
+ ~DirIterator();
+
+ // iterate to the next entry in the directory
+ bool next();
+
+ // methods to return information about
+ // the current entry
+ std::string fileName() const;
+ std::string filePath() const;
+ bool isDir() const;
+
+ private:
+ std::string m_path;
+
+#ifdef PLATFORM_UNIX
+ DIR* m_dir;
+ dirent* m_entry;
+#endif
+
+#ifdef PLATFORM_WINDOWS
+ HANDLE m_findHandle;
+ WIN32_FIND_DATA m_findData;
+ bool m_firstEntry;
+#endif
+};
+
diff --git a/mmc_updater/src/FileUtils.cpp b/mmc_updater/src/FileUtils.cpp
new file mode 100644
index 00000000..10435e49
--- /dev/null
+++ b/mmc_updater/src/FileUtils.cpp
@@ -0,0 +1,557 @@
+#include "FileUtils.h"
+
+#include "DirIterator.h"
+#include "Log.h"
+#include "Platform.h"
+#include "StringUtils.h"
+
+#include <algorithm>
+#include <assert.h>
+#include <string.h>
+#include <fstream>
+#include <iostream>
+
+#ifdef PLATFORM_UNIX
+#include <unistd.h>
+#include <stdio.h>
+#include <fcntl.h>
+#include <sys/types.h>
+#include <sys/time.h>
+#include <sys/stat.h>
+#include <errno.h>
+#include <libgen.h>
+#endif
+
+FileUtils::IOException::IOException(const std::string& error)
+{
+ init(errno,error);
+}
+
+FileUtils::IOException::IOException(int errorCode, const std::string& error)
+{
+ init(errorCode,error);
+}
+
+void FileUtils::IOException::init(int errorCode, const std::string& error)
+{
+ m_error = error;
+
+#ifdef PLATFORM_UNIX
+ m_errorCode = errorCode;
+
+ if (m_errorCode > 0)
+ {
+ m_error += " details: " + std::string(strerror(m_errorCode));
+ }
+#endif
+
+#ifdef PLATFORM_WINDOWS
+ m_errorCode = 0;
+ m_error += " GetLastError returned: " + intToStr(GetLastError());
+#endif
+}
+
+FileUtils::IOException::~IOException() throw ()
+{
+}
+
+FileUtils::IOException::Type FileUtils::IOException::type() const
+{
+#ifdef PLATFORM_UNIX
+ switch (m_errorCode)
+ {
+ case 0:
+ return NoError;
+ case EROFS:
+ return ReadOnlyFileSystem;
+ case ENOSPC:
+ return DiskFull;
+ default:
+ return Unknown;
+ }
+#else
+ return Unknown;
+#endif
+}
+
+bool FileUtils::fileExists(const char* path) throw (IOException)
+{
+#ifdef PLATFORM_UNIX
+ struct stat fileInfo;
+ if (lstat(path,&fileInfo) != 0)
+ {
+ if (errno == ENOENT)
+ {
+ return false;
+ }
+ else
+ {
+ throw IOException("Error checking for file " + std::string(path));
+ }
+ }
+ return true;
+#else
+ DWORD result = GetFileAttributes(path);
+ if (result == INVALID_FILE_ATTRIBUTES)
+ {
+ return false;
+ }
+ return true;
+#endif
+}
+
+int FileUtils::fileMode(const char* path) throw (IOException)
+{
+#ifdef PLATFORM_UNIX
+ struct stat fileInfo;
+ if (stat(path,&fileInfo) != 0)
+ {
+ throw IOException("Error reading file permissions for " + std::string(path));
+ }
+ return fileInfo.st_mode;
+#else
+ // not implemented for Windows
+ return 0;
+#endif
+}
+
+void FileUtils::chmod(const char* path, int mode) throw (IOException)
+{
+#ifdef PLATFORM_UNIX
+ if (::chmod(path,static_cast<mode_t>(mode)) != 0)
+ {
+ throw IOException("Failed to set permissions on " + std::string(path) + " to " + intToStr(mode));
+ }
+#else
+ // TODO - Not implemented under Windows - all files
+ // get default permissions
+#endif
+}
+
+void FileUtils::moveFile(const char* src, const char* dest) throw (IOException)
+{
+#ifdef PLATFORM_UNIX
+ if (rename(src,dest) != 0)
+ {
+ throw IOException("Unable to rename " + std::string(src) + " to " + std::string(dest));
+ }
+#else
+ if (!MoveFile(src,dest))
+ {
+ throw IOException("Unable to rename " + std::string(src) + " to " + std::string(dest));
+ }
+#endif
+}
+
+void FileUtils::mkpath(const char* dir) throw (IOException)
+{
+ std::string currentPath;
+ std::istringstream stream(dir);
+ while (!stream.eof())
+ {
+ std::string segment;
+ std::getline(stream,segment,'/');
+ currentPath += segment;
+ if (!currentPath.empty() && !fileExists(currentPath.c_str()))
+ {
+ mkdir(currentPath.c_str());
+ }
+ currentPath += '/';
+ }
+}
+
+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)
+ {
+ throw IOException("Unable to create directory " + std::string(dir));
+ }
+#else
+ if (!CreateDirectory(dir,0 /* default security attributes */))
+ {
+ throw IOException("Unable to create directory " + std::string(dir));
+ }
+#endif
+}
+
+void FileUtils::rmdir(const char* dir) throw (IOException)
+{
+#ifdef PLATFORM_UNIX
+ if (::rmdir(dir) != 0)
+ {
+ throw IOException("Unable to remove directory " + std::string(dir));
+ }
+#else
+ if (!RemoveDirectory(dir))
+ {
+ throw IOException("Unable to remove directory " + std::string(dir));
+ }
+#endif
+}
+
+void FileUtils::createSymLink(const char* link, const char* target) throw (IOException)
+{
+#ifdef PLATFORM_UNIX
+ if (symlink(target,link) != 0)
+ {
+ throw IOException("Unable to create symlink " + std::string(link) + " to " + std::string(target));
+ }
+#else
+ // symlinks are not supported under Windows (at least, not universally.
+ // Windows Vista and later do actually support symlinks)
+ LOG(Warn,"Skipping symlink creation - not implemented in Windows");
+#endif
+}
+
+void FileUtils::removeFile(const char* src) throw (IOException)
+{
+#ifdef PLATFORM_UNIX
+ if (unlink(src) != 0)
+ {
+ if (errno != ENOENT)
+ {
+ throw IOException("Unable to remove file " + std::string(src));
+ }
+ }
+#else
+ if (!DeleteFile(src))
+ {
+ if (GetLastError() == ERROR_ACCESS_DENIED)
+ {
+ // if another process is using the file, try moving it to
+ // a temporary directory and then
+ // scheduling it for deletion on reboot
+ std::string tempDeletePathBase = tempPath();
+ tempDeletePathBase += '/';
+ tempDeletePathBase += fileName(src);
+
+ int suffix = 0;
+ std::string tempDeletePath = tempDeletePathBase;
+ while (fileExists(tempDeletePath.c_str()))
+ {
+ ++suffix;
+ tempDeletePath = tempDeletePathBase + '_' + intToStr(suffix);
+ }
+
+ LOG(Warn,"Unable to remove file " + std::string(src) + " - it may be in use. Moving to "
+ + tempDeletePath + " and scheduling delete on reboot.");
+ moveFile(src,tempDeletePath.c_str());
+ MoveFileEx(tempDeletePath.c_str(),0,MOVEFILE_DELAY_UNTIL_REBOOT);
+ }
+ else if (GetLastError() != ERROR_FILE_NOT_FOUND)
+ {
+ throw IOException("Unable to remove file " + std::string(src));
+ }
+ }
+#endif
+}
+
+std::string FileUtils::fileName(const char* path)
+{
+#ifdef PLATFORM_UNIX
+ char* pathCopy = strdup(path);
+ std::string basename = ::basename(pathCopy);
+ free(pathCopy);
+ return basename;
+#else
+ char baseName[MAX_PATH];
+ char extension[MAX_PATH];
+ _splitpath_s(path,
+ 0, /* drive */
+ 0, /* drive length */
+ 0, /* dir */
+ 0, /* dir length */
+ baseName,
+ MAX_PATH, /* baseName length */
+ extension,
+ MAX_PATH /* extension length */
+ );
+ return std::string(baseName) + std::string(extension);
+#endif
+}
+
+std::string FileUtils::dirname(const char* path)
+{
+#ifdef PLATFORM_UNIX
+ char* pathCopy = strdup(path);
+ std::string dirname = ::dirname(pathCopy);
+ free(pathCopy);
+ return dirname;
+#else
+ char drive[3];
+ char dir[MAX_PATH];
+
+ _splitpath_s(path,
+ drive, /* drive */
+ 3, /* drive length */
+ dir,
+ MAX_PATH, /* dir length */
+ 0, /* filename */
+ 0, /* filename length */
+ 0, /* extension */
+ 0 /* extension length */
+ );
+
+ std::string result;
+ if (drive[0])
+ {
+ result += std::string(drive);
+ }
+ result += dir;
+
+ return result;
+#endif
+}
+
+void FileUtils::touch(const char* path) throw (IOException)
+{
+#ifdef PLATFORM_UNIX
+ // see http://pubs.opengroup.org/onlinepubs/9699919799/utilities/touch.html
+ //
+ // we use utimes/futimes instead of utimensat/futimens for compatibility
+ // with older Linux and Mac
+
+ if (fileExists(path))
+ {
+ utimes(path,0 /* use current date/time */);
+ }
+ else
+ {
+ int fd = creat(path,S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
+ if (fd != -1)
+ {
+ futimes(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,
+ FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
+ 0,
+ CREATE_ALWAYS,
+ FILE_ATTRIBUTE_NORMAL,
+ 0);
+ if (result == INVALID_HANDLE_VALUE)
+ {
+ throw IOException("Unable to touch file " + std::string(path));
+ }
+ else
+ {
+ CloseHandle(result);
+ }
+#endif
+}
+
+void FileUtils::rmdirRecursive(const char* path) throw (IOException)
+{
+ // remove dir contents
+ DirIterator dir(path);
+ while (dir.next())
+ {
+ std::string name = dir.fileName();
+ if (name != "." && name != "..")
+ {
+ if (dir.isDir())
+ {
+ rmdir(dir.filePath().c_str());
+ }
+ else
+ {
+ removeFile(dir.filePath().c_str());
+ }
+ }
+ }
+
+ // remove the directory itself
+ rmdir(path);
+}
+
+std::string FileUtils::canonicalPath(const char* path)
+{
+#ifdef PLATFORM_UNIX
+ // on Linux and Mac OS 10.6, realpath() can allocate the required
+ // amount of memory automatically, however Mac OS 10.5 does not support
+ // this, so we used a fixed-sized buffer on all platforms
+ char canonicalPathBuffer[PATH_MAX+1];
+ if (realpath(path,canonicalPathBuffer) != 0)
+ {
+ return std::string(canonicalPathBuffer);
+ }
+ else
+ {
+ throw IOException("Error reading canonical path for " + std::string(path));
+ }
+#else
+ throw IOException("canonicalPath() not implemented");
+#endif
+}
+
+std::string FileUtils::toWindowsPathSeparators(const std::string& str)
+{
+ std::string result = str;
+ std::replace(result.begin(),result.end(),'/','\\');
+ return result;
+}
+
+std::string FileUtils::toUnixPathSeparators(const std::string& str)
+{
+ std::string result = str;
+ std::replace(result.begin(),result.end(),'\\','/');
+ return result;
+}
+
+std::string FileUtils::tempPath()
+{
+#ifdef PLATFORM_UNIX
+ std::string tmpDir(notNullString(getenv("TMPDIR")));
+ if (tmpDir.empty())
+ {
+ tmpDir = "/tmp";
+ }
+ return tmpDir;
+#else
+ char buffer[MAX_PATH+1];
+ GetTempPath(MAX_PATH+1,buffer);
+ return toUnixPathSeparators(buffer);
+#endif
+}
+
+bool startsWithDriveLetter(const char* path)
+{
+ return strlen(path) >= 2 &&
+ (isalpha(path[0])) &&
+ path[1] == ':';
+}
+
+bool FileUtils::isRelative(const char* path)
+{
+#ifdef PLATFORM_UNIX
+ return strlen(path) == 0 || path[0] != '/';
+#else
+ // on Windows, a path is relative if it does not start with:
+ // - '\\' (a UNC name)
+ // - '[Drive Letter]:\'
+ // - A single backslash
+ //
+ // the input path is assumed to have already been converted to use
+ // Unix-style path separators
+
+ std::string pathStr(path);
+
+ if ((!pathStr.empty() && pathStr.at(0) == '/') ||
+ (startsWith(pathStr,"//")) ||
+ (startsWithDriveLetter(pathStr.c_str())))
+ {
+ return false;
+ }
+ else
+ {
+ return true;
+ }
+#endif
+}
+
+void FileUtils::writeFile(const char* path, const char* data, int length) throw (IOException)
+{
+ std::ofstream stream(path,std::ios::binary | std::ios::trunc);
+ stream.write(data,length);
+}
+
+std::string FileUtils::readFile(const char* path) throw (IOException)
+{
+ std::ifstream inputFile(path, std::ios::in | std::ios::binary);
+ std::string content;
+ inputFile.seekg(0, std::ios::end);
+ content.resize(static_cast<unsigned int>(inputFile.tellg()));
+ inputFile.seekg(0, std::ios::beg);
+ inputFile.read(&content[0], static_cast<int>(content.size()));
+ return content;
+}
+
+void FileUtils::copyFile(const char* src, const char* dest) throw (IOException)
+{
+#ifdef PLATFORM_UNIX
+ std::ifstream inputFile(src,std::ios::binary);
+ std::ofstream outputFile(dest,std::ios::binary | std::ios::trunc);
+
+ if (!inputFile.good())
+ {
+ throw IOException("Failed to read file " + std::string(src));
+ }
+ if (!outputFile.good())
+ {
+ throw IOException("Failed to write file " + std::string(dest));
+ }
+
+ outputFile << inputFile.rdbuf();
+
+ if (inputFile.bad())
+ {
+ throw IOException("Error reading file " + std::string(src));
+ }
+ if (outputFile.bad())
+ {
+ throw IOException("Error writing file " + std::string(dest));
+ }
+
+ chmod(dest,fileMode(src));
+#else
+ if (!CopyFile(src,dest,FALSE))
+ {
+ throw IOException("Failed to copy " + std::string(src) + " to " + std::string(dest));
+ }
+#endif
+}
+
+std::string FileUtils::makeAbsolute(const char* path, const char* basePath)
+{
+ if (isRelative(path))
+ {
+ assert(!isRelative(basePath));
+ return std::string(basePath) + '/' + std::string(path);
+ }
+ else
+ {
+ return path;
+ }
+}
+
+void FileUtils::chdir(const char* path) throw (IOException)
+{
+#ifdef PLATFORM_UNIX
+ if (::chdir(path) != 0)
+ {
+ throw FileUtils::IOException("Unable to change directory");
+ }
+#else
+ if (!SetCurrentDirectory(path))
+ {
+ throw FileUtils::IOException("Unable to change directory");
+ }
+#endif
+}
+
+std::string FileUtils::getcwd() throw (IOException)
+{
+#ifdef PLATFORM_UNIX
+ char path[PATH_MAX];
+ if (!::getcwd(path,PATH_MAX))
+ {
+ throw FileUtils::IOException("Failed to get current directory");
+ }
+ return std::string(path);
+#else
+ char path[MAX_PATH];
+ if (GetCurrentDirectory(MAX_PATH,path) == 0)
+ {
+ throw FileUtils::IOException("Failed to get current directory");
+ }
+ return toUnixPathSeparators(std::string(path));
+#endif
+}
+
diff --git a/mmc_updater/src/FileUtils.h b/mmc_updater/src/FileUtils.h
new file mode 100644
index 00000000..cb5830ae
--- /dev/null
+++ b/mmc_updater/src/FileUtils.h
@@ -0,0 +1,141 @@
+#pragma once
+
+#include <exception>
+#include <string>
+
+#include "Platform.h"
+#include "StringUtils.h"
+
+
+/** A set of functions for performing common operations
+ * on files, throwing exceptions if an operation fails.
+ *
+ * Path arguments to FileUtils functions should use Unix-style path
+ * separators.
+ */
+class FileUtils
+{
+ public:
+ /** Base class for exceptions reported by
+ * FileUtils methods if an operation fails.
+ */
+ class IOException : public std::exception
+ {
+ public:
+ IOException(const std::string& error);
+ IOException(int errorCode, const std::string& error);
+
+ virtual ~IOException() throw ();
+
+ enum Type
+ {
+ NoError,
+ /** Unknown error type. Call what() to get the description
+ * provided by the OS.
+ */
+ Unknown,
+ ReadOnlyFileSystem,
+ DiskFull
+ };
+
+ virtual const char* what() const throw ()
+ {
+ return m_error.c_str();
+ }
+
+ Type type() const;
+
+ private:
+ void init(int errorCode, const std::string& error);
+
+ std::string m_error;
+ int m_errorCode;
+ };
+
+ /** Remove a file. Throws an exception if the file
+ * could not be removed.
+ *
+ * On Unix, a file can be removed even if it is in use if the user
+ * has the necessary permissions. removeFile() tries to simulate
+ * this behavior on Windows. If a file cannot be removed on Windows
+ * because it is in use it will be moved to a temporary directory and
+ * scheduled for deletion on the next restart.
+ */
+ static void removeFile(const char* src) throw (IOException);
+
+ /** Set the permissions of a file. @p permissions uses the standard
+ * Unix mode_t values.
+ */
+ static void chmod(const char* path, int permissions) throw (IOException);
+
+ /** Returns true if the file at @p path exists. If @p path is a symlink,
+ * returns true if the symlink itself exists, not the target.
+ */
+ static bool fileExists(const char* path) throw (IOException);
+
+ /** Returns the Unix mode flags of @p path. If @p path is a symlink,
+ * returns the mode flags of the target.
+ */
+ static int fileMode(const char* path) throw (IOException);
+
+ static void moveFile(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);
+ static void copyFile(const char* src, const char* dest) throw (IOException);
+
+ /** Create all the directories in @p path which do not yet exist.
+ * @p path may be relative or absolute.
+ */
+ static void mkpath(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.
+ * On Windows this includes the drive letter, if present in @p 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();
+
+ /** 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);
+
+ static std::string toWindowsPathSeparators(const std::string& str);
+
+ /** Returns true if the provided path is relative.
+ * Or false if absolute.
+ */
+ static bool isRelative(const char* path);
+
+ /** Converts @p path to an absolute path. If @p path is already absolute,
+ * just returns @p path, otherwise prefixes it with @p basePath to make it absolute.
+ *
+ * @p basePath should be absolute.
+ */
+ static std::string makeAbsolute(const char* path, const char* basePath);
+
+ static void writeFile(const char* path, const char* data, int length) throw (IOException);
+
+ static std::string readFile(const char* path) throw (IOException);
+
+ /** Changes the current working directory to @p path */
+ static void chdir(const char* path) throw (IOException);
+
+ /** Returns the current working directory of the application. */
+ static std::string getcwd() throw (IOException);
+};
+
diff --git a/mmc_updater/src/Log.cpp b/mmc_updater/src/Log.cpp
new file mode 100644
index 00000000..d4e5a214
--- /dev/null
+++ b/mmc_updater/src/Log.cpp
@@ -0,0 +1,65 @@
+#include "Log.h"
+
+#include "Platform.h"
+#include "StringUtils.h"
+#include "ProcessUtils.h"
+
+#include <string.h>
+#include <iostream>
+
+Log m_globalLog;
+
+Log* Log::instance()
+{
+ return &m_globalLog;
+}
+
+Log::Log()
+{
+}
+
+Log::~Log()
+{
+}
+
+void Log::open(const std::string& path)
+{
+ m_mutex.lock();
+ m_output.open(path.c_str(),std::ios_base::out | std::ios_base::app);
+ m_mutex.unlock();
+}
+
+void Log::writeToStream(std::ostream& stream, Type type, const char* text)
+{
+ // Multiple processes may be writing to the same log file during
+ // an update. No attempt is made to synchronize access to the file.
+ //
+ // Under Unix, appends to a single file on a local FS by multiple writers should be atomic
+ // provided that the length of 'text' is less than PIPE_BUF
+ //
+ switch (type)
+ {
+ case Info:
+ stream << "INFO ";
+ break;
+ case Warn:
+ stream << "WARN ";
+ break;
+ case Error:
+ stream << "ERROR ";
+ break;
+ }
+ stream << '(' << intToStr(ProcessUtils::currentProcessId()) << ") " << text << std::endl;
+}
+
+void Log::write(Type type, const char* text)
+{
+ m_mutex.lock();
+ writeToStream(std::cerr,type,text);
+ if (m_output.is_open())
+ {
+ writeToStream(m_output,type,text);
+ }
+ m_mutex.unlock();
+}
+
diff --git a/mmc_updater/src/Log.h b/mmc_updater/src/Log.h
new file mode 100644
index 00000000..cf6be832
--- /dev/null
+++ b/mmc_updater/src/Log.h
@@ -0,0 +1,46 @@
+#pragma once
+
+#include <string>
+#include <fstream>
+
+#include <thread>
+#include <mutex>
+
+class Log
+{
+ public:
+ enum Type
+ {
+ Info,
+ Warn,
+ Error
+ };
+
+ Log();
+ ~Log();
+
+ void open(const std::string& path);
+
+ /** Write @p text to the log. This method is thread-safe. */
+ void write(Type type, const std::string& text);
+ /** Write @p text to the log. This method is thread-safe. */
+ void write(Type type, const char* text);
+
+ static Log* instance();
+
+ private:
+ static void writeToStream(std::ostream& stream, Type type, const char* text);
+
+ std::mutex m_mutex;
+ std::ofstream m_output;
+};
+
+inline void Log::write(Type type, const std::string& text)
+{
+ write(type,text.c_str());
+}
+
+#define LOG(type,text) \
+ Log::instance()->write(Log::type,text)
+
+
diff --git a/mmc_updater/src/MacBundle.cpp b/mmc_updater/src/MacBundle.cpp
new file mode 100644
index 00000000..205869eb
--- /dev/null
+++ b/mmc_updater/src/MacBundle.cpp
@@ -0,0 +1,53 @@
+#include "MacBundle.h"
+
+#include "FileUtils.h"
+#include "Log.h"
+
+MacBundle::MacBundle(const std::string& path, const std::string& appName)
+: m_appName(appName)
+{
+ m_path = path + '/' + appName + ".app";
+}
+
+std::string MacBundle::bundlePath() const
+{
+ return m_path;
+}
+
+void MacBundle::create(const std::string& infoPlist,
+ const std::string& icon,
+ const std::string& exePath)
+{
+ try
+ {
+ // create the bundle directories
+ FileUtils::mkpath(m_path.c_str());
+
+ std::string contentDir = m_path + "/Contents";
+ std::string resourceDir = contentDir + "/Resources";
+ std::string binDir = contentDir + "/MacOS";
+
+ FileUtils::mkpath(resourceDir.c_str());
+ FileUtils::mkpath(binDir.c_str());
+
+ // create the Contents/Info.plist file
+ FileUtils::writeFile((contentDir + "/Info.plist").c_str(),infoPlist.c_str(),static_cast<int>(infoPlist.size()));
+
+ // save the icon to Contents/Resources/<appname>.icns
+ FileUtils::writeFile((resourceDir + '/' + m_appName + ".icns").c_str(),icon.c_str(),static_cast<int>(icon.size()));
+
+ // copy the app binary to Contents/MacOS/<appname>
+ m_exePath = binDir + '/' + m_appName;
+ FileUtils::copyFile(exePath.c_str(),m_exePath.c_str());
+ }
+ catch (const FileUtils::IOException& exception)
+ {
+ LOG(Error,"Unable to create app bundle. " + std::string(exception.what()));
+ }
+}
+
+std::string MacBundle::executablePath() const
+{
+ return m_exePath;
+}
+
diff --git a/mmc_updater/src/MacBundle.h b/mmc_updater/src/MacBundle.h
new file mode 100644
index 00000000..2b119d8f
--- /dev/null
+++ b/mmc_updater/src/MacBundle.h
@@ -0,0 +1,35 @@
+#pragma once
+
+#include <string>
+
+/** Class for creating minimal Mac app bundles. */
+class MacBundle
+{
+ public:
+ /** Create a MacBundle instance representing the bundle
+ * in <path>/<appName>.app
+ */
+ MacBundle(const std::string& path, const std::string& appName);
+
+ /** Create a simple Mac bundle.
+ *
+ * @param infoPlist The content of the Info.plist file
+ * @param icon The content of the app icon
+ * @param exePath The path of the file to use for the main app in the bundle.
+ */
+ void create(const std::string& infoPlist,
+ const std::string& icon,
+ const std::string& exePath);
+
+ /** Returns the path of the main executable within the Mac bundle. */
+ std::string executablePath() const;
+
+ /** Returns the path of the bundle */
+ std::string bundlePath() const;
+
+ private:
+ std::string m_path;
+ std::string m_appName;
+ std::string m_exePath;
+};
+
diff --git a/mmc_updater/src/Platform.h b/mmc_updater/src/Platform.h
new file mode 100644
index 00000000..6d9afdfb
--- /dev/null
+++ b/mmc_updater/src/Platform.h
@@ -0,0 +1,32 @@
+#pragma once
+
+// basic platform defines
+#ifdef __linux__
+ #define PLATFORM_LINUX
+#endif
+
+#ifdef WIN32
+ #define PLATFORM_WINDOWS
+ #define WIN32_LEAN_AND_MEAN
+ #include <windows.h>
+ #include <shellapi.h>
+
+ // disable warnings about exception specifications,
+ // which are not implemented in Visual C++
+ #pragma warning(disable:4290)
+#endif
+
+#ifdef __APPLE__
+ #define PLATFORM_MAC
+#endif
+
+#if defined(PLATFORM_LINUX) || defined(PLATFORM_MAC)
+ #define PLATFORM_UNIX
+#endif
+
+// platform-specific type aliases
+#if defined(PLATFORM_UNIX)
+ #define PLATFORM_PID pid_t
+#else
+ #define PLATFORM_PID DWORD
+#endif
diff --git a/mmc_updater/src/ProcessUtils.cpp b/mmc_updater/src/ProcessUtils.cpp
new file mode 100644
index 00000000..3b9ffac2
--- /dev/null
+++ b/mmc_updater/src/ProcessUtils.cpp
@@ -0,0 +1,536 @@
+#include "ProcessUtils.h"
+
+#include "FileUtils.h"
+#include "Platform.h"
+#include "StringUtils.h"
+#include "Log.h"
+
+#include <string.h>
+#include <vector>
+#include <iostream>
+
+#ifdef PLATFORM_WINDOWS
+#include <windows.h>
+#else
+#include <unistd.h>
+#include <stdlib.h>
+#include <sys/wait.h>
+#include <errno.h>
+#endif
+
+#ifdef PLATFORM_MAC
+#include <Security/Security.h>
+#include <mach-o/dyld.h>
+#endif
+
+PLATFORM_PID ProcessUtils::currentProcessId()
+{
+#ifdef PLATFORM_UNIX
+ return getpid();
+#else
+ return GetCurrentProcessId();
+#endif
+}
+
+int ProcessUtils::runSync(const std::string& executable,
+ const std::list<std::string>& args)
+{
+#ifdef PLATFORM_UNIX
+ return runSyncUnix(executable,args);
+#else
+ return runWindows(executable,args,RunSync);
+#endif
+}
+
+#ifdef PLATFORM_UNIX
+int ProcessUtils::runSyncUnix(const std::string& executable,
+ const std::list<std::string>& args)
+{
+ PLATFORM_PID pid = runAsyncUnix(executable,args);
+ int status = 0;
+ if (waitpid(pid,&status,0) != -1)
+ {
+ if (WIFEXITED(status))
+ {
+ return static_cast<char>(WEXITSTATUS(status));
+ }
+ else
+ {
+ LOG(Warn,"Child exited abnormally");
+ return -1;
+ }
+ }
+ else
+ {
+ LOG(Warn,"Failed to get exit status of child " + intToStr(pid));
+ return WaitFailed;
+ }
+}
+#endif
+
+void ProcessUtils::runAsync(const std::string& executable,
+ const std::list<std::string>& args)
+{
+#ifdef PLATFORM_WINDOWS
+ runWindows(executable,args,RunAsync);
+#elif defined(PLATFORM_UNIX)
+ runAsyncUnix(executable,args);
+#endif
+}
+
+int ProcessUtils::runElevated(const std::string& executable,
+ const std::list<std::string>& args,
+ const std::string& task)
+{
+#ifdef PLATFORM_WINDOWS
+ (void)task;
+ return runElevatedWindows(executable,args);
+#elif defined(PLATFORM_MAC)
+ (void)task;
+ return runElevatedMac(executable,args);
+#elif defined(PLATFORM_LINUX)
+ return runElevatedLinux(executable,args,task);
+#endif
+}
+
+bool ProcessUtils::waitForProcess(PLATFORM_PID pid)
+{
+#ifdef PLATFORM_UNIX
+ pid_t result = ::waitpid(pid, 0, 0);
+ if (result < 0)
+ {
+ LOG(Error,"waitpid() failed with error: " + std::string(strerror(errno)));
+ }
+ return result > 0;
+#elif defined(PLATFORM_WINDOWS)
+ HANDLE hProc;
+
+ if (!(hProc = OpenProcess(SYNCHRONIZE, FALSE, pid)))
+ {
+ LOG(Error,"Unable to get process handle for pid " + intToStr(pid) + " last error " + intToStr(GetLastError()));
+ return false;
+ }
+
+ DWORD dwRet = WaitForSingleObject(hProc, INFINITE);
+ CloseHandle(hProc);
+
+ if (dwRet == WAIT_FAILED)
+ {
+ LOG(Error,"WaitForSingleObject failed with error " + intToStr(GetLastError()));
+ }
+
+ return (dwRet == WAIT_OBJECT_0);
+#endif
+}
+
+#ifdef PLATFORM_LINUX
+int ProcessUtils::runElevatedLinux(const std::string& executable,
+ const std::list<std::string>& args,
+ const std::string& _task)
+{
+ std::string task(_task);
+ if (task.empty())
+ {
+ task = FileUtils::fileName(executable.c_str());
+ }
+
+ // try available graphical sudo instances until we find one that works.
+ // The different sudo front-ends have different behaviors with respect to error codes:
+ //
+ // - 'kdesudo': return 1 if the user enters the wrong password 3 times or if
+ // they cancel elevation
+ //
+ // - recent 'gksudo' versions: return 1 if the user enters the wrong password
+ // : return -1 if the user cancels elevation
+ //
+ // - older 'gksudo' versions : return 0 if the user cancels elevation
+
+ std::vector<std::string> sudos;
+
+ if (getenv("KDE_SESSION_VERSION"))
+ {
+ sudos.push_back("kdesudo");
+ }
+ sudos.push_back("gksudo");
+
+ for (unsigned int i=0; i < sudos.size(); i++)
+ {
+ const std::string& sudoBinary = sudos.at(i);
+
+ std::list<std::string> sudoArgs;
+ sudoArgs.push_back("-u");
+ sudoArgs.push_back("root");
+
+ if (sudoBinary == "kdesudo")
+ {
+ sudoArgs.push_back("-d");
+ sudoArgs.push_back("--comment");
+ std::string sudoMessage = task + " needs administrative privileges. Please enter your password.";
+ sudoArgs.push_back(sudoMessage);
+ }
+ else if (sudoBinary == "gksudo")
+ {
+ sudoArgs.push_back("--description");
+ sudoArgs.push_back(task);
+ }
+ else
+ {
+ sudoArgs.push_back(task);
+ }
+
+ sudoArgs.push_back("--");
+ sudoArgs.push_back(executable);
+ std::copy(args.begin(),args.end(),std::back_inserter(sudoArgs));
+
+ int result = ProcessUtils::runSync(sudoBinary,sudoArgs);
+
+ LOG(Info,"Tried to use sudo " + sudoBinary + " with response " + intToStr(result));
+
+ if (result != RunFailed)
+ {
+ return result;
+ break;
+ }
+ }
+ return RunElevatedFailed;
+}
+#endif
+
+#ifdef PLATFORM_MAC
+int ProcessUtils::runElevatedMac(const std::string& executable,
+ const std::list<std::string>& args)
+{
+ // request elevation using the Security Service.
+ //
+ // This only works when the application is being run directly
+ // from the Mac. Attempting to run the app via a remote SSH session
+ // (for example) will fail with an interaction-not-allowed error
+
+ OSStatus status;
+ AuthorizationRef authorizationRef;
+
+ status = AuthorizationCreate(
+ NULL,
+ kAuthorizationEmptyEnvironment,
+ kAuthorizationFlagDefaults,
+ &authorizationRef);
+
+ AuthorizationItem right = { kAuthorizationRightExecute, 0, NULL, 0 };
+ AuthorizationRights rights = { 1, &right };
+
+ AuthorizationFlags flags = kAuthorizationFlagDefaults |
+ kAuthorizationFlagInteractionAllowed |
+ kAuthorizationFlagPreAuthorize |
+ kAuthorizationFlagExtendRights;
+
+ if (status == errAuthorizationSuccess)
+ {
+ status = AuthorizationCopyRights(authorizationRef, &rights, NULL,
+ flags, NULL);
+
+ if (status == errAuthorizationSuccess)
+ {
+ char** argv;
+ argv = (char**) malloc(sizeof(char*) * args.size() + 1);
+
+ unsigned int i = 0;
+ for (std::list<std::string>::const_iterator iter = args.begin(); iter != args.end(); iter++)
+ {
+ argv[i] = strdup(iter->c_str());
+ ++i;
+ }
+ argv[i] = NULL;
+
+ FILE* pipe = NULL;
+
+ char* tool = strdup(executable.c_str());
+
+ status = AuthorizationExecuteWithPrivileges(authorizationRef, tool,
+ kAuthorizationFlagDefaults, argv, &pipe);
+
+ if (status == errAuthorizationSuccess)
+ {
+ // AuthorizationExecuteWithPrivileges does not provide a way to get the process ID
+ // of the child process.
+ //
+ // Discussions on Apple development forums suggest two approaches for working around this,
+ //
+ // - Modify the child process to sent its process ID back to the parent via
+ // the pipe passed to AuthorizationExecuteWithPrivileges.
+ //
+ // - Use the generic Unix wait() call.
+ //
+ // This code uses wait(), which is simpler, but suffers from the problem that wait() waits
+ // for any child process, not necessarily the specific process launched
+ // by AuthorizationExecuteWithPrivileges.
+ //
+ // Apple's documentation (see 'Authorization Services Programming Guide') suggests
+ // installing files in an installer as a legitimate use for
+ // AuthorizationExecuteWithPrivileges but in general strongly recommends
+ // not using this call and discusses a number of other alternatives
+ // for performing privileged operations,
+ // which we could consider in future.
+
+ int childStatus;
+ pid_t childPid = wait(&childStatus);
+
+ if (childStatus != 0)
+ {
+ LOG(Error,"elevated process failed with status " + intToStr(childStatus) + " pid "
+ + intToStr(childPid));
+ }
+ else
+ {
+ LOG(Info,"elevated process succeded with pid " + intToStr(childPid));
+ }
+
+ return childStatus;
+ }
+ else
+ {
+ LOG(Error,"failed to launch elevated process " + intToStr(status));
+ return RunElevatedFailed;
+ }
+
+ // If we want to know more information about what has happened:
+ // http://developer.apple.com/mac/library/documentation/Security/Reference/authorization_ref/Reference/reference.html#//apple_ref/doc/uid/TP30000826-CH4g-CJBEABHG
+ free(tool);
+ for (i = 0; i < args.size(); i++)
+ {
+ free(argv[i]);
+ }
+ }
+ else
+ {
+ LOG(Error,"failed to get rights to launch elevated process. status: " + intToStr(status));
+ return RunElevatedFailed;
+ }
+ }
+ else
+ {
+ return RunElevatedFailed;
+ }
+}
+#endif
+
+// convert a list of arguments in a space-separated string.
+// Arguments containing spaces are enclosed in quotes
+std::string quoteArgs(const std::list<std::string>& arguments)
+{
+ std::string quotedArgs;
+ for (std::list<std::string>::const_iterator iter = arguments.begin();
+ iter != arguments.end();
+ iter++)
+ {
+ std::string arg = *iter;
+
+ bool isQuoted = !arg.empty() &&
+ arg.at(0) == '"' &&
+ arg.at(arg.size()-1) == '"';
+
+ if (!isQuoted && arg.find(' ') != std::string::npos)
+ {
+ arg.insert(0,"\"");
+ arg.append("\"");
+ }
+ quotedArgs += arg;
+ quotedArgs += " ";
+ }
+ return quotedArgs;
+}
+
+#ifdef PLATFORM_WINDOWS
+int ProcessUtils::runElevatedWindows(const std::string& executable,
+ const std::list<std::string>& arguments)
+{
+ std::string args = quoteArgs(arguments);
+
+ SHELLEXECUTEINFO executeInfo;
+ ZeroMemory(&executeInfo,sizeof(executeInfo));
+ executeInfo.cbSize = sizeof(SHELLEXECUTEINFO);
+ executeInfo.fMask = SEE_MASK_NOCLOSEPROCESS;
+ // request UAC elevation
+ executeInfo.lpVerb = "runas";
+ executeInfo.lpFile = executable.c_str();
+ executeInfo.lpParameters = args.c_str();
+ executeInfo.nShow = SW_SHOWNORMAL;
+
+ LOG(Info,"Attempting to execute " + executable + " with administrator priviledges");
+ if (!ShellExecuteEx(&executeInfo))
+ {
+ LOG(Error,"Failed to start with admin priviledges using ShellExecuteEx()");
+ return RunElevatedFailed;
+ }
+
+ WaitForSingleObject(executeInfo.hProcess, INFINITE);
+
+ // this assumes the process succeeded - we need to check whether
+ // this is actually the case.
+ return 0;
+}
+#endif
+
+#ifdef PLATFORM_UNIX
+PLATFORM_PID ProcessUtils::runAsyncUnix(const std::string& executable,
+ const std::list<std::string>& args)
+{
+ pid_t child = fork();
+ if (child == 0)
+ {
+ // in child process
+ char** argBuffer = new char*[args.size() + 2];
+ argBuffer[0] = strdup(executable.c_str());
+ int i = 1;
+ for (std::list<std::string>::const_iterator iter = args.begin(); iter != args.end(); iter++)
+ {
+ argBuffer[i] = strdup(iter->c_str());
+ ++i;
+ }
+ argBuffer[i] = 0;
+
+ if (execvp(executable.c_str(),argBuffer) == -1)
+ {
+ LOG(Error,"error starting child: " + std::string(strerror(errno)));
+ exit(RunFailed);
+ }
+ }
+ else
+ {
+ LOG(Info,"Started child process " + intToStr(child));
+ }
+ return child;
+}
+#endif
+
+#ifdef PLATFORM_WINDOWS
+int ProcessUtils::runWindows(const std::string& _executable,
+ const std::list<std::string>& _args,
+ RunMode runMode)
+{
+ // most Windows API functions allow back and forward slashes to be
+ // used interchangeably. However, an application started with
+ // CreateProcess() may fail to find Side-by-Side library dependencies
+ // in the same directory as the executable if forward slashes are
+ // used as path separators, so convert the path to use back slashes here.
+ //
+ // This may be related to LoadLibrary() requiring backslashes instead
+ // of forward slashes.
+ std::string executable = FileUtils::toWindowsPathSeparators(_executable);
+
+ std::list<std::string> args(_args);
+ args.push_front(executable);
+ std::string commandLine = quoteArgs(args);
+
+ STARTUPINFO startupInfo;
+ ZeroMemory(&startupInfo,sizeof(startupInfo));
+ startupInfo.cb = sizeof(startupInfo);
+
+ PROCESS_INFORMATION processInfo;
+ ZeroMemory(&processInfo,sizeof(processInfo));
+
+ char* commandLineStr = strdup(commandLine.c_str());
+ bool result = CreateProcess(
+ executable.c_str(),
+ commandLineStr,
+ 0 /* process attributes */,
+ 0 /* thread attributes */,
+ false /* inherit handles */,
+ NORMAL_PRIORITY_CLASS /* creation flags */,
+ 0 /* environment */,
+ 0 /* current directory */,
+ &startupInfo /* startup info */,
+ &processInfo /* process information */
+ );
+
+ if (!result)
+ {
+ LOG(Error,"Failed to start child process. " + executable + " Last error: " + intToStr(GetLastError()));
+ return RunFailed;
+ }
+ else
+ {
+ if (runMode == RunSync)
+ {
+ if (WaitForSingleObject(processInfo.hProcess,INFINITE) == WAIT_OBJECT_0)
+ {
+ DWORD status = WaitFailed;
+ if (GetExitCodeProcess(processInfo.hProcess,&status) != 0)
+ {
+ LOG(Error,"Failed to get exit code for process");
+ }
+ return status;
+ }
+ else
+ {
+ LOG(Error,"Failed to wait for process to finish");
+ return WaitFailed;
+ }
+ }
+ else
+ {
+ // process is being run asynchronously - return zero as if it had
+ // succeeded
+ return 0;
+ }
+ }
+}
+#endif
+
+std::string ProcessUtils::currentProcessPath()
+{
+#ifdef PLATFORM_LINUX
+ std::string path = FileUtils::canonicalPath("/proc/self/exe");
+ LOG(Info,"Current process path " + path);
+ return path;
+#elif defined(PLATFORM_MAC)
+ uint32_t bufferSize = PATH_MAX;
+ char buffer[bufferSize];
+ _NSGetExecutablePath(buffer,&bufferSize);
+ return buffer;
+#else
+ char fileName[MAX_PATH];
+ GetModuleFileName(0 /* get path of current process */,fileName,MAX_PATH);
+ return fileName;
+#endif
+}
+
+#ifdef PLATFORM_WINDOWS
+void ProcessUtils::convertWindowsCommandLine(LPCWSTR commandLine, int& argc, char**& argv)
+{
+ argc = 0;
+ LPWSTR* argvUnicode = CommandLineToArgvW(commandLine,&argc);
+
+ argv = new char*[argc];
+ for (int i=0; i < argc; i++)
+ {
+ const int BUFFER_SIZE = 4096;
+ char buffer[BUFFER_SIZE];
+
+ int length = WideCharToMultiByte(CP_ACP,
+ 0 /* flags */,
+ argvUnicode[i],
+ -1, /* argvUnicode is null terminated */
+ buffer,
+ BUFFER_SIZE,
+ 0,
+ false);
+
+ // note: if WideCharToMultiByte() fails it will return zero,
+ // in which case we store a zero-length argument in argv
+ if (length == 0)
+ {
+ argv[i] = new char[1];
+ argv[i][0] = '\0';
+ }
+ else
+ {
+ // if the input string to WideCharToMultiByte is null-terminated,
+ // the output is also null-terminated
+ argv[i] = new char[length];
+ strncpy(argv[i],buffer,length);
+ }
+ }
+ LocalFree(argvUnicode);
+}
+#endif
+
diff --git a/mmc_updater/src/ProcessUtils.h b/mmc_updater/src/ProcessUtils.h
new file mode 100644
index 00000000..9fa10815
--- /dev/null
+++ b/mmc_updater/src/ProcessUtils.h
@@ -0,0 +1,97 @@
+#pragma once
+
+#include "Platform.h"
+
+#include <list>
+#include <string>
+
+/** A set of functions to get information about the current
+ * process and launch new processes.
+ */
+class ProcessUtils
+{
+ public:
+ enum Errors
+ {
+ /** Status code returned by runElevated() if launching
+ * the elevated process fails.
+ */
+ RunElevatedFailed = 255,
+ /** Status code returned by runSync() if the application
+ * cannot be started.
+ */
+ RunFailed = -8,
+ /** Status code returned by runSync() if waiting for
+ * the application to exit and reading its status code fails.
+ */
+ WaitFailed = -1
+ };
+
+ 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);
+
+ /** Run a process with administrative privileges and return the
+ * status code of the process, or 0 on Windows.
+ *
+ * Returns RunElevatedFailed if the elevated process could
+ * not be started.
+ */
+ static int runElevated(const std::string& executable,
+ const std::list<std::string>& args,
+ const std::string& task);
+
+ /** 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
+ /** Convert a unicode command line returned by GetCommandLineW()
+ * to a standard (argc,argv) pair. The resulting argv array and each
+ * element of argv must be freed using free()
+ */
+ static void convertWindowsCommandLine(LPCWSTR commandLine, int& argc, char**& argv);
+#endif
+
+ private:
+ enum RunMode
+ {
+ RunSync,
+ RunAsync
+ };
+ static int runElevatedLinux(const std::string& executable,
+ const std::list<std::string>& args,
+ const std::string& task);
+ static int runElevatedMac(const std::string& executable,
+ const std::list<std::string>& args);
+ static int runElevatedWindows(const std::string& executable,
+ const std::list<std::string>& args);
+
+ static PLATFORM_PID runAsyncUnix(const std::string& executable,
+ const std::list<std::string>& args);
+ static int runWindows(const std::string& executable,
+ const std::list<std::string>& args,
+ RunMode runMode);
+ static int runSyncUnix(const std::string& executable,
+ const std::list<std::string>& args);
+};
+
diff --git a/mmc_updater/src/StandardDirs.cpp b/mmc_updater/src/StandardDirs.cpp
new file mode 100644
index 00000000..72743d5e
--- /dev/null
+++ b/mmc_updater/src/StandardDirs.cpp
@@ -0,0 +1,63 @@
+#include "StandardDirs.h"
+
+#include "FileUtils.h"
+#include "StringUtils.h"
+
+#ifdef PLATFORM_UNIX
+ #include <stdlib.h>
+ #include <pwd.h>
+ #include <unistd.h>
+#endif
+
+#ifdef PLATFORM_WINDOWS
+#include <shlobj.h>
+#endif
+
+#ifdef PLATFORM_UNIX
+std::string StandardDirs::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 StandardDirs::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)
+ std::string path = applicationSupportFolderPath();
+ path += '/' + appName;
+ return path;
+#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
+}
diff --git a/mmc_updater/src/StandardDirs.h b/mmc_updater/src/StandardDirs.h
new file mode 100644
index 00000000..18526173
--- /dev/null
+++ b/mmc_updater/src/StandardDirs.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include "Platform.h"
+
+#include <string>
+
+class StandardDirs
+{
+ public:
+ static std::string appDataPath(const std::string& organizationName,
+ const std::string& appName);
+
+ private:
+#ifdef PLATFORM_UNIX
+ static std::string homeDir();
+#endif
+
+#ifdef PLATFORM_MAC
+ static std::string applicationSupportFolderPath();
+#endif
+};
+
diff --git a/mmc_updater/src/StandardDirs.mm b/mmc_updater/src/StandardDirs.mm
new file mode 100644
index 00000000..53eecd47
--- /dev/null
+++ b/mmc_updater/src/StandardDirs.mm
@@ -0,0 +1,18 @@
+#include <Foundation/Foundation.h>
+
+#include "StandardDirs.h"
+
+std::string StandardDirs::applicationSupportFolderPath()
+{
+ NSArray* paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory,
+ NSUserDomainMask,
+ true /* expand tildes */);
+
+ for (unsigned int i=0; i < [paths count]; i++)
+ {
+ NSString* path = [paths objectAtIndex:i];
+ return std::string([path UTF8String]);
+ }
+ return std::string();
+}
+
diff --git a/mmc_updater/src/StlSymbolsLeopard.cpp b/mmc_updater/src/StlSymbolsLeopard.cpp
new file mode 100644
index 00000000..d5f58ebf
--- /dev/null
+++ b/mmc_updater/src/StlSymbolsLeopard.cpp
@@ -0,0 +1,75 @@
+// Workarounds for iostream symbols that are referenced when building on OS X 10.7 but missing from
+// OS X 10.5's stdlibc++.dylib.
+//
+// In the <iostream> headers these are declared as extern templates but the symbols are not present under 10.5.
+// This file forces the compiler to instantiate the templates.
+//
+// see http://stackoverflow.com/questions/3484043/os-x-program-runs-on-dev-machine-crashing-horribly-on-others
+
+#include <iostream>
+
+_GLIBCXX_BEGIN_NAMESPACE(std)
+// From ostream_insert.h
+template ostream& __ostream_insert(ostream&, const char*, streamsize);
+
+#ifdef _GLIBCXX_USE_WCHAR_T
+template wostream& __ostream_insert(wostream&, const wchar_t*, streamsize);
+#endif
+
+// From ostream.tcc
+template ostream& ostream::_M_insert(long);
+template ostream& ostream::_M_insert(unsigned long);
+template ostream& ostream::_M_insert(bool);
+#ifdef _GLIBCXX_USE_LONG_LONG
+template ostream& ostream::_M_insert(long long);
+template ostream& ostream::_M_insert(unsigned long long);
+#endif
+template ostream& ostream::_M_insert(double);
+template ostream& ostream::_M_insert(long double);
+template ostream& ostream::_M_insert(const void*);
+
+#ifdef _GLIBCXX_USE_WCHAR_T
+template wostream& wostream::_M_insert(long);
+template wostream& wostream::_M_insert(unsigned long);
+template wostream& wostream::_M_insert(bool);
+#ifdef _GLIBCXX_USE_LONG_LONG
+template wostream& wostream::_M_insert(long long);
+template wostream& wostream::_M_insert(unsigned long long);
+#endif
+template wostream& wostream::_M_insert(double);
+template wostream& wostream::_M_insert(long double);
+template wostream& wostream::_M_insert(const void*);
+#endif
+
+// From istream.tcc
+template istream& istream::_M_extract(unsigned short&);
+template istream& istream::_M_extract(unsigned int&);
+template istream& istream::_M_extract(long&);
+template istream& istream::_M_extract(unsigned long&);
+template istream& istream::_M_extract(bool&);
+#ifdef _GLIBCXX_USE_LONG_LONG
+template istream& istream::_M_extract(long long&);
+template istream& istream::_M_extract(unsigned long long&);
+#endif
+template istream& istream::_M_extract(float&);
+template istream& istream::_M_extract(double&);
+template istream& istream::_M_extract(long double&);
+template istream& istream::_M_extract(void*&);
+
+#ifdef _GLIBCXX_USE_WCHAR_T
+template wistream& wistream::_M_extract(unsigned short&);
+template wistream& wistream::_M_extract(unsigned int&);
+template wistream& wistream::_M_extract(long&);
+template wistream& wistream::_M_extract(unsigned long&);
+template wistream& wistream::_M_extract(bool&);
+#ifdef _GLIBCXX_USE_LONG_LONG
+template wistream& wistream::_M_extract(long long&);
+template wistream& wistream::_M_extract(unsigned long long&);
+#endif
+template wistream& wistream::_M_extract(float&);
+template wistream& wistream::_M_extract(double&);
+template wistream& wistream::_M_extract(long double&);
+template wistream& wistream::_M_extract(void*&);
+#endif
+
+_GLIBCXX_END_NAMESPACE
diff --git a/mmc_updater/src/StringUtils.h b/mmc_updater/src/StringUtils.h
new file mode 100644
index 00000000..745b71c9
--- /dev/null
+++ b/mmc_updater/src/StringUtils.h
@@ -0,0 +1,46 @@
+#pragma once
+
+#include <string.h>
+#include <string>
+#include <sstream>
+#include <stdlib.h>
+
+template <class T>
+inline std::string intToStr(T i)
+{
+ std::stringstream stream;
+ stream << i;
+ return stream.str();
+}
+
+inline bool strToBool(const std::string& str)
+{
+ return str == "true" || atoi(str.c_str()) != 0;
+}
+
+/** Returns @p text if non-null or a pointer
+ * to an empty null-terminated string otherwise.
+ */
+inline const char* notNullString(const char* text)
+{
+ if (text)
+ {
+ return text;
+ }
+ else
+ {
+ return "";
+ }
+}
+
+inline bool endsWith(const std::string& str, const char* text)
+{
+ size_t length = strlen(text);
+ return str.find(text,str.size() - length) != std::string::npos;
+}
+
+inline bool startsWith(const std::string& str, const char* text)
+{
+ return str.find(text,0) == 0;
+}
+
diff --git a/mmc_updater/src/UpdateDialog.cpp b/mmc_updater/src/UpdateDialog.cpp
new file mode 100644
index 00000000..5f181a1c
--- /dev/null
+++ b/mmc_updater/src/UpdateDialog.cpp
@@ -0,0 +1,25 @@
+#include "UpdateDialog.h"
+
+UpdateDialog::UpdateDialog()
+: m_autoClose(false)
+{
+}
+
+void UpdateDialog::setAutoClose(bool autoClose)
+{
+ m_autoClose = autoClose;
+}
+
+bool UpdateDialog::autoClose() const
+{
+ return m_autoClose;
+}
+
+void UpdateDialog::updateFinished()
+{
+ if (m_autoClose)
+ {
+ quit();
+ }
+}
+
diff --git a/mmc_updater/src/UpdateDialog.h b/mmc_updater/src/UpdateDialog.h
new file mode 100644
index 00000000..d0782f8f
--- /dev/null
+++ b/mmc_updater/src/UpdateDialog.h
@@ -0,0 +1,29 @@
+#pragma once
+
+#include "UpdateObserver.h"
+
+/** Base class for the updater's UI, sub-classed
+ * by the different platform implementations.
+ */
+class UpdateDialog : public UpdateObserver
+{
+ public:
+ UpdateDialog();
+ virtual ~UpdateDialog() {};
+
+ /** Sets whether the updater should automatically
+ * exit once the update has been installed.
+ */
+ void setAutoClose(bool autoClose);
+ bool autoClose() const;
+
+ virtual void init(int argc, char** argv) = 0;
+ virtual void exec() = 0;
+ virtual void quit() = 0;
+
+ virtual void updateFinished();
+
+ private:
+ bool m_autoClose;
+};
+
diff --git a/mmc_updater/src/UpdateDialogAscii.cpp b/mmc_updater/src/UpdateDialogAscii.cpp
new file mode 100644
index 00000000..78eb7433
--- /dev/null
+++ b/mmc_updater/src/UpdateDialogAscii.cpp
@@ -0,0 +1,70 @@
+#include "UpdateDialogAscii.h"
+
+#include "AppInfo.h"
+#include "ProcessUtils.h"
+#include "StringUtils.h"
+
+const char* introMessage =
+ "%s (ASCII-art edition)\n"
+ "====================================\n"
+ "\n"
+ "We have a nice graphical interface for the %s, but unfortunately\n"
+ "we can't show it to you :(\n"
+ "\n"
+ "You can fix this by installing the GTK 2 libraries.\n\n"
+ "Installing Updates...\n";
+
+void UpdateDialogAscii::init(int /* argc */, char** /* argv */)
+{
+ const char* path = "/tmp/update-progress";
+ m_output.open(path);
+
+ char message[4096];
+ sprintf(message,introMessage,AppInfo::name().c_str());
+ m_output << message;
+
+ std::string command = "xterm";
+ std::list<std::string> args;
+ args.push_back("-hold");
+ args.push_back("-T");
+ args.push_back(AppInfo::name());
+ args.push_back("-e");
+ args.push_back("tail");
+ args.push_back("-n+1");
+ args.push_back("-f");
+ args.push_back(path);
+
+ ProcessUtils::runAsync(command,args);
+}
+
+void UpdateDialogAscii::updateError(const std::string& errorMessage)
+{
+ m_mutex.lock();
+ m_output << "\nThere was a problem installing the update: " << errorMessage << std::endl;
+ m_mutex.unlock();
+}
+
+void UpdateDialogAscii::updateProgress(int percentage)
+{
+ m_mutex.lock();
+ m_output << "Update Progress: " << intToStr(percentage) << '%' << std::endl;
+ m_mutex.unlock();
+}
+
+void UpdateDialogAscii::updateFinished()
+{
+ m_mutex.lock();
+ m_output << "\nUpdate Finished. You can now restart " << AppInfo::appName() << "." << std::endl;
+ m_mutex.unlock();
+
+ UpdateDialog::updateFinished();
+}
+
+void UpdateDialogAscii::quit()
+{
+}
+
+void UpdateDialogAscii::exec()
+{
+}
+
diff --git a/mmc_updater/src/UpdateDialogAscii.h b/mmc_updater/src/UpdateDialogAscii.h
new file mode 100644
index 00000000..138194c5
--- /dev/null
+++ b/mmc_updater/src/UpdateDialogAscii.h
@@ -0,0 +1,32 @@
+#pragma once
+
+#include "UpdateDialog.h"
+
+#include <fstream>
+#include <thread>
+#include <mutex>
+
+/** A fallback auto-update progress 'dialog' for use on
+ * Linux when the GTK UI cannot be loaded.
+ *
+ * The 'dialog' consists of an xterm tailing the contents
+ * of a file, into which progress messages are written.
+ */
+class UpdateDialogAscii : public UpdateDialog
+{
+ public:
+ // implements UpdateDialog
+ virtual void init(int argc, char** argv);
+ virtual void exec();
+ virtual void quit();
+
+ // implements UpdateObserver
+ virtual void updateError(const std::string& errorMessage);
+ virtual void updateProgress(int percentage);
+ virtual void updateFinished();
+
+ private:
+ std::mutex m_mutex;
+ std::ofstream m_output;
+};
+
diff --git a/mmc_updater/src/UpdateDialogCocoa.h b/mmc_updater/src/UpdateDialogCocoa.h
new file mode 100644
index 00000000..586fdc8e
--- /dev/null
+++ b/mmc_updater/src/UpdateDialogCocoa.h
@@ -0,0 +1,32 @@
+#pragma once
+
+#include "UpdateDialog.h"
+#include "UpdateObserver.h"
+
+class UpdateDialogPrivate;
+
+class UpdateDialogCocoa : public UpdateDialog
+{
+ public:
+ UpdateDialogCocoa();
+ ~UpdateDialogCocoa();
+
+ // implements UpdateDialog
+ virtual void init(int argc, char** argv);
+ virtual void exec();
+ virtual void quit();
+
+ // implements UpdateObserver
+ virtual void updateError(const std::string& errorMessage);
+ virtual void updateProgress(int percentage);
+ virtual void updateFinished();
+
+ static void* createAutoreleasePool();
+ static void releaseAutoreleasePool(void* data);
+
+ private:
+ void enableDockIcon();
+
+ UpdateDialogPrivate* d;
+};
+
diff --git a/mmc_updater/src/UpdateDialogCocoa.mm b/mmc_updater/src/UpdateDialogCocoa.mm
new file mode 100644
index 00000000..f24f3c4d
--- /dev/null
+++ b/mmc_updater/src/UpdateDialogCocoa.mm
@@ -0,0 +1,194 @@
+#include "UpdateDialogCocoa.h"
+
+#include <Cocoa/Cocoa.h>
+#include <Carbon/Carbon.h>
+
+#include "AppInfo.h"
+#include "Log.h"
+#include "StringUtils.h"
+
+@interface UpdateDialogDelegate : NSObject
+{
+ @public UpdateDialogPrivate* dialog;
+}
+- (void) finishClicked;
+- (void) reportUpdateError:(id)arg;
+- (void) reportUpdateProgress:(id)arg;
+- (void) reportUpdateFinished:(id)arg;
+@end
+
+class UpdateDialogPrivate
+{
+ public:
+ UpdateDialogPrivate()
+ : hadError(false)
+ {
+ }
+
+ UpdateDialogDelegate* delegate;
+ NSAutoreleasePool* pool;
+ NSWindow* window;
+ NSButton* finishButton;
+ NSTextField* progressLabel;
+ NSProgressIndicator* progressBar;
+ bool hadError;
+};
+
+@implementation UpdateDialogDelegate
+- (void) finishClicked
+{
+ [NSApp stop:self];
+}
+- (void) reportUpdateError: (id)arg
+{
+ dialog->hadError = true;
+
+ NSAlert* alert = [NSAlert
+ alertWithMessageText: @"Update Problem"
+ defaultButton: nil
+ alternateButton: nil
+ otherButton: nil
+ informativeTextWithFormat: @"There was a problem installing the update:\n\n%@", arg];
+ [alert runModal];
+}
+- (void) reportUpdateProgress: (id)arg
+{
+ int percentage = [arg intValue];
+ [dialog->progressBar setDoubleValue:(percentage/100.0)];
+}
+- (void) reportUpdateFinished: (id)arg
+{
+ NSMutableString* message = [[NSMutableString alloc] init];
+ if (!dialog->hadError)
+ {
+ [message appendString:@"Updates installed."];
+ }
+ else
+ {
+ [message appendString:@"Update failed."];
+ }
+
+ [message appendString:@" Click 'Finish' to restart the application."];
+ [dialog->progressLabel setTitleWithMnemonic:message];
+ [message release];
+}
+@end
+
+UpdateDialogCocoa::UpdateDialogCocoa()
+: d(new UpdateDialogPrivate)
+{
+ [NSApplication sharedApplication];
+ d->pool = [[NSAutoreleasePool alloc] init];
+}
+
+UpdateDialogCocoa::~UpdateDialogCocoa()
+{
+ [d->pool release];
+}
+
+void UpdateDialogCocoa::enableDockIcon()
+{
+ // convert the application to a foreground application and in
+ // the process, enable the dock icon
+
+ // the reverse transformation is not possible, according to
+ // http://stackoverflow.com/questions/2832961/is-it-possible-to-hide-the-dock-icon-programmatically
+ ProcessSerialNumber psn;
+ GetCurrentProcess(&psn);
+ TransformProcessType(&psn,kProcessTransformToForegroundApplication);
+}
+
+void UpdateDialogCocoa::init(int /* argc */, char** /* argv */)
+{
+ enableDockIcon();
+
+ // make the updater the active application. This does not
+ // happen automatically because the updater starts as a
+ // background application
+ [NSApp activateIgnoringOtherApps:YES];
+
+ d->delegate = [[UpdateDialogDelegate alloc] init];
+ d->delegate->dialog = d;
+
+ int width = 370;
+ int height = 100;
+
+ d->window = [[NSWindow alloc] initWithContentRect:NSMakeRect(200, 200, width, height)
+ styleMask:NSTitledWindowMask | NSMiniaturizableWindowMask
+ backing:NSBackingStoreBuffered defer:NO];
+ [d->window setTitle:[NSString stringWithUTF8String:AppInfo::name().c_str()]];
+
+ d->finishButton = [[NSButton alloc] init];
+ [d->finishButton setTitle:@"Finish"];
+ [d->finishButton setButtonType:NSMomentaryLightButton];
+ [d->finishButton setBezelStyle:NSRoundedBezelStyle];
+ [d->finishButton setTarget:d->delegate];
+ [d->finishButton setAction:@selector(finishClicked)];
+
+ d->progressBar = [[NSProgressIndicator alloc] init];
+ [d->progressBar setIndeterminate:false];
+ [d->progressBar setMinValue:0.0];
+ [d->progressBar setMaxValue:1.0];
+
+ d->progressLabel = [[NSTextField alloc] init];
+ [d->progressLabel setEditable:false];
+ [d->progressLabel setSelectable:false];
+ [d->progressLabel setTitleWithMnemonic:@"Installing Updates"];
+ [d->progressLabel setBezeled:false];
+ [d->progressLabel setDrawsBackground:false];
+
+ NSView* windowContent = [d->window contentView];
+ [windowContent addSubview:d->progressLabel];
+ [windowContent addSubview:d->progressBar];
+ [windowContent addSubview:d->finishButton];
+
+ [d->progressLabel setFrame:NSMakeRect(10,70,width - 10,20)];
+ [d->progressBar setFrame:NSMakeRect(10,40,width - 20,20)];
+ [d->finishButton setFrame:NSMakeRect(width - 85,5,80,30)];
+}
+
+void UpdateDialogCocoa::exec()
+{
+ [d->window makeKeyAndOrderFront:d->window];
+ [d->window center];
+ [NSApp run];
+}
+
+void UpdateDialogCocoa::updateError(const std::string& errorMessage)
+{
+ [d->delegate performSelectorOnMainThread:@selector(reportUpdateError:)
+ withObject:[NSString stringWithUTF8String:errorMessage.c_str()]
+ waitUntilDone:false];
+}
+
+void UpdateDialogCocoa::updateProgress(int percentage)
+{
+ [d->delegate performSelectorOnMainThread:@selector(reportUpdateProgress:)
+ withObject:[NSNumber numberWithInt:percentage]
+ waitUntilDone:false];
+}
+
+void UpdateDialogCocoa::updateFinished()
+{
+ [d->delegate performSelectorOnMainThread:@selector(reportUpdateFinished:)
+ withObject:nil
+ waitUntilDone:false];
+ UpdateDialog::updateFinished();
+}
+
+void* UpdateDialogCocoa::createAutoreleasePool()
+{
+ return [[NSAutoreleasePool alloc] init];
+}
+
+void UpdateDialogCocoa::releaseAutoreleasePool(void* arg)
+{
+ [(id)arg release];
+}
+
+void UpdateDialogCocoa::quit()
+{
+ [NSApp performSelectorOnMainThread:@selector(stop:) withObject:d->delegate waitUntilDone:false];
+}
+
+
diff --git a/mmc_updater/src/UpdateDialogGtk.cpp b/mmc_updater/src/UpdateDialogGtk.cpp
new file mode 100644
index 00000000..d91144f5
--- /dev/null
+++ b/mmc_updater/src/UpdateDialogGtk.cpp
@@ -0,0 +1,155 @@
+#include "UpdateDialogGtk.h"
+
+#include "AppInfo.h"
+#include "StringUtils.h"
+
+#include <glib.h>
+#include <gtk/gtk.h>
+
+UpdateDialogGtk* update_dialog_gtk_new()
+{
+ return new UpdateDialogGtk();
+}
+
+UpdateDialogGtk::UpdateDialogGtk()
+: m_hadError(false)
+{
+}
+
+void UpdateDialogGtk::init(int argc, char** argv)
+{
+ gtk_init(&argc,&argv);
+
+ m_window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
+ gtk_window_set_title(GTK_WINDOW(m_window),AppInfo::name().c_str());
+
+ 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);
+
+ m_finishButton = gtk_button_new_with_label("Finish");
+ gtk_widget_set_sensitive(m_finishButton,false);
+
+ 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);
+
+ gtk_container_add(GTK_CONTAINER(m_window),windowLayout);
+ gtk_container_set_border_width(GTK_CONTAINER(m_window),12);
+
+ gtk_box_pack_start(GTK_BOX(labelLayout),m_progressLabel,false,false,0);
+ gtk_box_pack_end(GTK_BOX(buttonLayout),m_finishButton,false,false,0);
+
+ gtk_box_pack_start(GTK_BOX(windowLayout),labelLayout,false,false,0);
+ gtk_box_pack_start(GTK_BOX(windowLayout),m_progressBar,false,false,0);
+ gtk_box_pack_start(GTK_BOX(windowLayout),buttonLayout,false,false,0);
+
+
+ gtk_widget_show(m_progressLabel);
+ gtk_widget_show(labelLayout);
+ gtk_widget_show(windowLayout);
+ gtk_widget_show(buttonLayout);
+ 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);
+}
+
+void UpdateDialogGtk::exec()
+{
+ gtk_main();
+}
+
+void UpdateDialogGtk::finish(GtkWidget* widget, gpointer _dialog)
+{
+ UpdateDialogGtk* dialog = static_cast<UpdateDialogGtk*>(_dialog);
+ dialog->quit();
+}
+
+void UpdateDialogGtk::quit()
+{
+ gtk_main_quit();
+}
+
+gboolean UpdateDialogGtk::notify(void* _message)
+{
+ UpdateMessage* message = static_cast<UpdateMessage*>(_message);
+ UpdateDialogGtk* dialog = static_cast<UpdateDialogGtk*>(message->receiver);
+ switch (message->type)
+ {
+ case UpdateMessage::UpdateProgress:
+ gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(dialog->m_progressBar),message->progress/100.0);
+ break;
+ case UpdateMessage::UpdateFailed:
+ {
+ dialog->m_hadError = true;
+ 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,
+ GTK_BUTTONS_CLOSE,
+ "%s",
+ errorMessage.c_str());
+ gtk_dialog_run (GTK_DIALOG (errorDialog));
+ gtk_widget_destroy (errorDialog);
+ gtk_widget_set_sensitive(dialog->m_finishButton,true);
+ }
+ break;
+ case UpdateMessage::UpdateFinished:
+ {
+ std::string message;
+ if (dialog->m_hadError)
+ {
+ message = "Update failed.";
+ }
+ else
+ {
+ message = "Update installed.";
+ }
+ message += " Click 'Finish' to restart the application.";
+ gtk_label_set_text(GTK_LABEL(dialog->m_progressLabel),message.c_str());
+ gtk_widget_set_sensitive(dialog->m_finishButton,true);
+ }
+ break;
+ }
+ delete message;
+
+ // do not invoke this function again
+ return false;
+}
+
+// callbacks during update installation
+void UpdateDialogGtk::updateError(const std::string& errorMessage)
+{
+ UpdateMessage* message = new UpdateMessage(this,UpdateMessage::UpdateFailed);
+ message->message = errorMessage;
+ g_idle_add(&UpdateDialogGtk::notify,message);
+}
+
+void UpdateDialogGtk::updateProgress(int percentage)
+{
+ UpdateMessage* message = new UpdateMessage(this,UpdateMessage::UpdateProgress);
+ message->progress = percentage;
+ g_idle_add(&UpdateDialogGtk::notify,message);
+}
+
+void UpdateDialogGtk::updateFinished()
+{
+ UpdateMessage* message = new UpdateMessage(this,UpdateMessage::UpdateFinished);
+ g_idle_add(&UpdateDialogGtk::notify,message);
+ UpdateDialog::updateFinished();
+}
+
+
diff --git a/mmc_updater/src/UpdateDialogGtk.h b/mmc_updater/src/UpdateDialogGtk.h
new file mode 100644
index 00000000..70e29c78
--- /dev/null
+++ b/mmc_updater/src/UpdateDialogGtk.h
@@ -0,0 +1,42 @@
+#pragma once
+
+#include "UpdateDialog.h"
+#include "UpdateMessage.h"
+#include "UpdateObserver.h"
+
+#include <gtk/gtk.h>
+
+class UpdateDialogGtk : public UpdateDialog
+{
+ public:
+ UpdateDialogGtk();
+
+ // implements UpdateDialog
+ virtual void init(int argc, char** argv);
+ virtual void exec();
+ virtual void quit();
+
+ // observer callbacks - these may be called
+ // from a background thread
+ virtual void updateError(const std::string& errorMessage);
+ virtual void updateProgress(int percentage);
+ virtual void updateFinished();
+
+ private:
+ static void finish(GtkWidget* widget, gpointer dialog);
+ static gboolean notify(void* message);
+
+ GtkWidget* m_window;
+ GtkWidget* m_progressLabel;
+ GtkWidget* m_finishButton;
+ GtkWidget* m_progressBar;
+ bool m_hadError;
+};
+
+// helper functions which allow the GTK dialog to be loaded dynamically
+// at runtime and used only if the GTK libraries are actually present
+extern "C" {
+ UpdateDialogGtk* update_dialog_gtk_new();
+}
+
+
diff --git a/mmc_updater/src/UpdateDialogGtkFactory.cpp b/mmc_updater/src/UpdateDialogGtkFactory.cpp
new file mode 100644
index 00000000..313da31a
--- /dev/null
+++ b/mmc_updater/src/UpdateDialogGtkFactory.cpp
@@ -0,0 +1,59 @@
+#include "UpdateDialogGtkFactory.h"
+
+#include "Log.h"
+#include "UpdateDialog.h"
+#include "StringUtils.h"
+
+#include <dlfcn.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+class UpdateDialogGtk;
+
+// GTK updater UI library embedded into
+// the updater binary
+extern unsigned char libupdatergtk_so[];
+extern unsigned int libupdatergtk_so_len;
+
+// pointers to helper functions in the GTK updater UI library
+UpdateDialogGtk* (*update_dialog_gtk_new)() = 0;
+
+bool extractFileFromBinary(const char* path, const void* buffer, size_t length)
+{
+ int fd = open(path,O_CREAT | O_WRONLY | O_TRUNC,0755);
+ size_t count = write(fd,buffer,length);
+ if (fd < 0 || count < length)
+ {
+ if (fd >= 0)
+ {
+ close(fd);
+ }
+ return false;
+ }
+ close(fd);
+ return true;
+}
+
+UpdateDialog* UpdateDialogGtkFactory::createDialog()
+{
+ const char* libPath = "/tmp/libupdatergtk.so";
+
+ if (!extractFileFromBinary(libPath,libupdatergtk_so,libupdatergtk_so_len))
+ {
+ LOG(Warn,"Failed to load the GTK UI library - " + std::string(strerror(errno)));
+ return 0;
+ }
+
+ void* gtkLib = dlopen(libPath,RTLD_LAZY);
+ if (!gtkLib)
+ {
+ LOG(Warn,"Failed to load the GTK UI - " + std::string(dlerror()));
+ return 0;
+ }
+ update_dialog_gtk_new = (UpdateDialogGtk* (*)()) dlsym(gtkLib,"update_dialog_gtk_new");
+ return reinterpret_cast<UpdateDialog*>(update_dialog_gtk_new());
+}
+
diff --git a/mmc_updater/src/UpdateDialogGtkFactory.h b/mmc_updater/src/UpdateDialogGtkFactory.h
new file mode 100644
index 00000000..1806c252
--- /dev/null
+++ b/mmc_updater/src/UpdateDialogGtkFactory.h
@@ -0,0 +1,13 @@
+#pragma once
+
+class UpdateDialog;
+
+/** Factory for loading the GTK version of the update dialog
+ * dynamically at runtime if the GTK libraries are available.
+ */
+class UpdateDialogGtkFactory
+{
+ public:
+ static UpdateDialog* createDialog();
+};
+
diff --git a/mmc_updater/src/UpdateDialogWin32.cpp b/mmc_updater/src/UpdateDialogWin32.cpp
new file mode 100644
index 00000000..bdc25437
--- /dev/null
+++ b/mmc_updater/src/UpdateDialogWin32.cpp
@@ -0,0 +1,215 @@
+#include "UpdateDialogWin32.h"
+
+#include "AppInfo.h"
+#include "Log.h"
+
+// enable themed controls
+// see http://msdn.microsoft.com/en-us/library/bb773175%28v=vs.85%29.aspx
+// for details
+#pragma comment(linker,"\"/manifestdependency:type='win32' \
+name='Microsoft.Windows.Common-Controls' version='6.0.0.0' \
+processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"")
+
+static const char* updateDialogClassName = "UPDATEDIALOG";
+
+static std::map<HWND,UpdateDialogWin32*> windowDialogMap;
+
+// enable the standard Windows font for a widget
+// (typically Tahoma or Segoe UI)
+void setDefaultFont(HWND window)
+{
+ SendMessage(window, WM_SETFONT,(WPARAM)GetStockObject(DEFAULT_GUI_FONT), MAKELPARAM(TRUE, 0));
+}
+
+LRESULT WINAPI updateDialogWindowProc(HWND window, UINT message, WPARAM wParam, LPARAM lParam)
+{
+ std::map<HWND,UpdateDialogWin32*>::const_iterator iter = windowDialogMap.find(window);
+ if (iter != windowDialogMap.end())
+ {
+ return iter->second->windowProc(window,message,wParam,lParam);
+ }
+ else
+ {
+ return DefWindowProc(window,message,wParam,lParam);
+ }
+};
+
+void registerWindowClass()
+{
+ WNDCLASSEX wcex;
+ ZeroMemory(&wcex,sizeof(WNDCLASSEX));
+
+ wcex.cbSize = sizeof(WNDCLASSEX);
+
+ HBRUSH background = CreateSolidBrush(GetSysColor(COLOR_3DFACE));
+
+ wcex.style = CS_HREDRAW | CS_VREDRAW;
+ wcex.lpfnWndProc = updateDialogWindowProc;
+ wcex.cbClsExtra = 0;
+ wcex.cbWndExtra = 0;
+ wcex.hIcon = LoadIcon(GetModuleHandle(0),"IDI_APPICON");
+ wcex.hCursor = LoadCursor(0,IDC_ARROW);
+ wcex.hbrBackground = (HBRUSH)background;
+ wcex.lpszMenuName = (LPCTSTR)0;
+ wcex.lpszClassName = updateDialogClassName;
+ wcex.hIconSm = 0;
+ wcex.hInstance = GetModuleHandle(0);
+
+ RegisterClassEx(&wcex);
+}
+
+UpdateDialogWin32::UpdateDialogWin32()
+: m_hadError(false)
+{
+ registerWindowClass();
+}
+
+UpdateDialogWin32::~UpdateDialogWin32()
+{
+ for (std::map<HWND,UpdateDialogWin32*>::iterator iter = windowDialogMap.begin();
+ iter != windowDialogMap.end();
+ iter++)
+ {
+ if (iter->second == this)
+ {
+ std::map<HWND,UpdateDialogWin32*>::iterator oldIter = iter;
+ ++iter;
+ windowDialogMap.erase(oldIter);
+ }
+ else
+ {
+ ++iter;
+ }
+ }
+}
+
+void UpdateDialogWin32::init(int /* argc */, char** /* argv */)
+{
+ int width = 300;
+ int height = 130;
+
+ DWORD style = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX;
+ m_window.CreateEx(0 /* dwExStyle */,
+ updateDialogClassName /* class name */,
+ AppInfo::name().c_str(),
+ style,
+ 0, 0, width, height,
+ 0 /* parent */, 0 /* menu */, 0 /* reserved */);
+ m_progressBar.Create(&m_window);
+ m_finishButton.Create(&m_window);
+ m_progressLabel.Create(&m_window);
+
+ installWindowProc(&m_window);
+ installWindowProc(&m_finishButton);
+
+ setDefaultFont(m_progressLabel);
+ setDefaultFont(m_finishButton);
+
+ m_progressBar.SetRange(0,100);
+ m_finishButton.SetWindowText("Finish");
+ m_finishButton.EnableWindow(false);
+ m_progressLabel.SetWindowText("Installing Updates");
+
+ m_window.SetWindowPos(0,0,0,width,height,0);
+ m_progressBar.SetWindowPos(0,10,40,width - 30,20,0);
+ m_progressLabel.SetWindowPos(0,10,15,width - 30,20,0);
+ m_finishButton.SetWindowPos(0,width-100,70,80,25,0);
+ m_window.CenterWindow();
+ m_window.ShowWindow();
+}
+
+void UpdateDialogWin32::exec()
+{
+ m_app.Run();
+}
+
+void UpdateDialogWin32::updateError(const std::string& errorMessage)
+{
+ UpdateMessage* message = new UpdateMessage(UpdateMessage::UpdateFailed);
+ message->message = errorMessage;
+ SendNotifyMessage(m_window.GetHwnd(),WM_USER,reinterpret_cast<WPARAM>(message),0);
+}
+
+void UpdateDialogWin32::updateProgress(int percentage)
+{
+ UpdateMessage* message = new UpdateMessage(UpdateMessage::UpdateProgress);
+ message->progress = percentage;
+ SendNotifyMessage(m_window.GetHwnd(),WM_USER,reinterpret_cast<WPARAM>(message),0);
+}
+
+void UpdateDialogWin32::updateFinished()
+{
+ UpdateMessage* message = new UpdateMessage(UpdateMessage::UpdateFinished);
+ SendNotifyMessage(m_window.GetHwnd(),WM_USER,reinterpret_cast<WPARAM>(message),0);
+ UpdateDialog::updateFinished();
+}
+
+void UpdateDialogWin32::quit()
+{
+ PostThreadMessage(GetWindowThreadProcessId(m_window.GetHwnd(), 0 /* process ID */), WM_QUIT, 0, 0);
+}
+
+LRESULT WINAPI UpdateDialogWin32::windowProc(HWND window, UINT message, WPARAM wParam, LPARAM lParam)
+{
+ switch (message)
+ {
+ case WM_CLOSE:
+ if (window == m_window.GetHwnd())
+ {
+ return 0;
+ }
+ break;
+ case WM_COMMAND:
+ {
+ if (reinterpret_cast<HWND>(lParam) == m_finishButton.GetHwnd())
+ {
+ quit();
+ }
+ }
+ break;
+ case WM_USER:
+ {
+ if (window == m_window.GetHwnd())
+ {
+ UpdateMessage* message = reinterpret_cast<UpdateMessage*>(wParam);
+ switch (message->type)
+ {
+ case UpdateMessage::UpdateFailed:
+ {
+ m_hadError = true;
+ std::string text = AppInfo::updateErrorMessage(message->message);
+ MessageBox(m_window.GetHwnd(),text.c_str(),"Update Problem",MB_OK);
+ }
+ break;
+ case UpdateMessage::UpdateProgress:
+ m_progressBar.SetPos(message->progress);
+ break;
+ case UpdateMessage::UpdateFinished:
+ {
+ std::string message;
+ m_finishButton.EnableWindow(true);
+ if (m_hadError)
+ {
+ message = "Update failed.";
+ }
+ else
+ {
+ message = "Updates installed.";
+ }
+ message += " Click 'Finish' to restart the application.";
+ m_progressLabel.SetWindowText(message.c_str());
+ }
+ break;
+ }
+ delete message;
+ }
+ }
+ break;
+ }
+ return DefWindowProc(window,message,wParam,lParam);
+}
+
+void UpdateDialogWin32::installWindowProc(CWnd* window)
+{
+ windowDialogMap[window->GetHwnd()] = this;
+}
diff --git a/mmc_updater/src/UpdateDialogWin32.h b/mmc_updater/src/UpdateDialogWin32.h
new file mode 100644
index 00000000..fe4208c8
--- /dev/null
+++ b/mmc_updater/src/UpdateDialogWin32.h
@@ -0,0 +1,39 @@
+#pragma once
+
+#include "Platform.h"
+#include "UpdateDialog.h"
+#include "UpdateMessage.h"
+
+#include "wincore.h"
+#include "controls.h"
+#include "stdcontrols.h"
+
+class UpdateDialogWin32 : public UpdateDialog
+{
+ public:
+ UpdateDialogWin32();
+ ~UpdateDialogWin32();
+
+ // implements UpdateDialog
+ virtual void init(int argc, char** argv);
+ virtual void exec();
+ virtual void quit();
+
+ // implements UpdateObserver
+ virtual void updateError(const std::string& errorMessage);
+ virtual void updateProgress(int percentage);
+ virtual void updateFinished();
+
+ LRESULT WINAPI windowProc(HWND window, UINT message, WPARAM wParam, LPARAM lParam);
+
+ private:
+ void installWindowProc(CWnd* window);
+
+ CWinApp m_app;
+ CWnd m_window;
+ CStatic m_progressLabel;
+ CProgressBar m_progressBar;
+ CButton m_finishButton;
+ bool m_hadError;
+};
+
diff --git a/mmc_updater/src/UpdateInstaller.cpp b/mmc_updater/src/UpdateInstaller.cpp
new file mode 100644
index 00000000..ced6ff39
--- /dev/null
+++ b/mmc_updater/src/UpdateInstaller.cpp
@@ -0,0 +1,426 @@
+#include "UpdateInstaller.h"
+
+#include "AppInfo.h"
+#include "FileUtils.h"
+#include "Log.h"
+#include "ProcessUtils.h"
+#include "UpdateObserver.h"
+
+UpdateInstaller::UpdateInstaller()
+: m_mode(Setup)
+, m_waitPid(0)
+, m_script(0)
+, m_observer(0)
+, m_forceElevated(false)
+, m_autoClose(false)
+{
+}
+
+void UpdateInstaller::setWaitPid(PLATFORM_PID pid)
+{
+ m_waitPid = pid;
+}
+
+void UpdateInstaller::setInstallDir(const std::string& path)
+{
+ m_installDir = path;
+}
+
+void UpdateInstaller::setPackageDir(const std::string& path)
+{
+ m_packageDir = path;
+}
+
+void UpdateInstaller::setBackupDir(const std::string& path)
+{
+ m_backupDir = path;
+}
+
+void UpdateInstaller::setMode(Mode mode)
+{
+ m_mode = mode;
+}
+
+void UpdateInstaller::setScript(UpdateScript* script)
+{
+ m_script = script;
+}
+
+void UpdateInstaller::setForceElevated(bool elevated)
+{
+ m_forceElevated = elevated;
+}
+
+void UpdateInstaller::setFinishCmd(const std::string& cmd)
+{
+ m_finishCmd = cmd;
+}
+
+std::list<std::string> UpdateInstaller::updaterArgs() const
+{
+ std::list<std::string> args;
+ args.push_back("--install-dir");
+ args.push_back(m_installDir);
+ args.push_back("--package-dir");
+ args.push_back(m_packageDir);
+ args.push_back("--script");
+ args.push_back(m_script->path());
+ if (m_autoClose)
+ {
+ args.push_back("--auto-close");
+ }
+ return args;
+}
+
+void UpdateInstaller::reportError(const std::string& error)
+{
+ if (m_observer)
+ {
+ m_observer->updateError(error);
+ m_observer->updateFinished();
+ }
+}
+
+std::string UpdateInstaller::friendlyErrorForError(const FileUtils::IOException& exception) const
+{
+ std::string friendlyError;
+
+ switch (exception.type())
+ {
+ case FileUtils::IOException::ReadOnlyFileSystem:
+#ifdef PLATFORM_MAC
+ friendlyError = AppInfo::appName() + " was started from a read-only location. "
+ "Copy it to the Applications folder on your Mac and run "
+ "it from there.";
+#else
+ friendlyError = AppInfo::appName() + " was started from a read-only location. "
+ "Re-install it to a location that can be updated and run it from there.";
+#endif
+ break;
+ case FileUtils::IOException::DiskFull:
+ friendlyError = "The disk is full. Please free up some space and try again.";
+ break;
+ default:
+ break;
+ }
+
+ return friendlyError;
+}
+
+void UpdateInstaller::run() throw ()
+{
+ if (!m_script || !m_script->isValid())
+ {
+ reportError("Unable to read update script");
+ return;
+ }
+ if (m_installDir.empty())
+ {
+ reportError("No installation directory specified");
+ return;
+ }
+
+ std::string updaterPath;
+ try
+ {
+ updaterPath = ProcessUtils::currentProcessPath();
+ }
+ catch (const FileUtils::IOException&)
+ {
+ LOG(Error,"error reading process path with mode " + intToStr(m_mode));
+ reportError("Unable to determine path of updater");
+ return;
+ }
+
+ if (m_mode == Setup)
+ {
+ 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");
+ args.push_back("main");
+ args.push_back("--wait");
+ args.push_back(intToStr(ProcessUtils::currentProcessId()));
+
+ int installStatus = 0;
+ if (m_forceElevated || !checkAccess())
+ {
+ LOG(Info,"Insufficient rights to install app to " + m_installDir + " requesting elevation");
+
+ // start a copy of the updater with admin rights
+ installStatus = ProcessUtils::runElevated(updaterPath,args,AppInfo::name());
+ }
+ else
+ {
+ LOG(Info,"Sufficient rights to install app - restarting with same permissions");
+ installStatus = ProcessUtils::runSync(updaterPath,args);
+ }
+
+ if (installStatus == 0)
+ {
+ LOG(Info,"Update install completed");
+ }
+ else
+ {
+ LOG(Error,"Update install failed with status " + intToStr(installStatus));
+ }
+
+ // restart the main application - this is currently done
+ // regardless of whether the installation succeeds or not
+ restartMainApp();
+
+ // clean up files created by the updater
+ cleanup();
+ }
+ else if (m_mode == Main)
+ {
+ LOG(Info,"Starting update installation");
+
+ // the detailed error string returned by the OS
+ std::string error;
+ // the message to present to the user. This may be the same
+ // as 'error' or may be different if a more helpful suggestion
+ // can be made for a particular problem
+ std::string friendlyError;
+
+ try
+ {
+ LOG(Info,"Installing new and updated files");
+ installFiles();
+
+ LOG(Info,"Uninstalling removed files");
+ uninstallFiles();
+
+ LOG(Info,"Removing backups");
+ removeBackups();
+
+ postInstallUpdate();
+ }
+ catch (const FileUtils::IOException& exception)
+ {
+ error = exception.what();
+ friendlyError = friendlyErrorForError(exception);
+ }
+ catch (const std::string& genericError)
+ {
+ error = genericError;
+ }
+
+ if (!error.empty())
+ {
+ LOG(Error,std::string("Error installing update ") + error);
+
+ try
+ {
+ revert();
+ }
+ catch (const FileUtils::IOException& exception)
+ {
+ LOG(Error,"Error reverting partial update " + std::string(exception.what()));
+ }
+
+ if (m_observer)
+ {
+ if (friendlyError.empty())
+ {
+ friendlyError = error;
+ }
+ m_observer->updateError(friendlyError);
+ }
+ }
+
+ if (m_observer)
+ {
+ m_observer->updateFinished();
+ }
+ }
+}
+
+void UpdateInstaller::cleanup()
+{
+ try
+ {
+ FileUtils::rmdirRecursive(m_packageDir.c_str());
+ }
+ catch (const FileUtils::IOException& ex)
+ {
+ LOG(Error,"Error cleaning up updater " + std::string(ex.what()));
+ }
+ LOG(Info,"Updater files removed");
+}
+
+void UpdateInstaller::revert()
+{
+ std::map<std::string,std::string>::const_iterator iter = m_backups.begin();
+ for (;iter != m_backups.end();iter++)
+ {
+ const std::string& installedFile = iter->first;
+ const std::string& backupFile = iter->second;
+
+ if (FileUtils::fileExists(installedFile.c_str()))
+ {
+ FileUtils::removeFile(installedFile.c_str());
+ }
+ FileUtils::moveFile(backupFile.c_str(),installedFile.c_str());
+ }
+}
+
+void UpdateInstaller::installFile(const UpdateScriptFile& file)
+{
+ std::string destPath = file.dest;
+ std::string target = file.linkTarget;
+
+ // backup the existing file if any
+ backupFile(destPath);
+
+ // create the target directory if it does not exist
+ std::string destDir = FileUtils::dirname(destPath.c_str());
+ if (!FileUtils::fileExists(destDir.c_str()))
+ {
+ FileUtils::mkpath(destDir.c_str());
+ }
+
+ std::string sourceFile = file.source;
+ if (!FileUtils::fileExists(sourceFile.c_str()))
+ {
+ throw "Source file does not exist: " + sourceFile;
+ }
+ FileUtils::copyFile(sourceFile.c_str(),destPath.c_str());
+
+ // set the permissions on the newly extracted file
+ FileUtils::chmod(destPath.c_str(),file.permissions);
+}
+
+void UpdateInstaller::installFiles()
+{
+ std::vector<UpdateScriptFile>::const_iterator iter = m_script->filesToInstall().begin();
+ int filesInstalled = 0;
+ for (;iter != m_script->filesToInstall().end();iter++)
+ {
+ installFile(*iter);
+ ++filesInstalled;
+ if (m_observer)
+ {
+ int toInstallCount = static_cast<int>(m_script->filesToInstall().size());
+ double percentage = ((1.0 * filesInstalled) / toInstallCount) * 100.0;
+ m_observer->updateProgress(static_cast<int>(percentage));
+ }
+ }
+}
+
+void UpdateInstaller::uninstallFiles()
+{
+ std::vector<std::string>::const_iterator iter = m_script->filesToUninstall().begin();
+ for (;iter != m_script->filesToUninstall().end();iter++)
+ {
+ std::string path = m_installDir + '/' + iter->c_str();
+ if (FileUtils::fileExists(path.c_str()))
+ {
+ FileUtils::removeFile(path.c_str());
+ }
+ else
+ {
+ LOG(Warn,"Unable to uninstall file " + path + " because it does not exist.");
+ }
+ }
+}
+
+void UpdateInstaller::backupFile(const std::string& path)
+{
+ if (!FileUtils::fileExists(path.c_str()))
+ {
+ // no existing file to backup
+ return;
+ }
+
+ std::string backupPath = path + ".bak";
+ FileUtils::removeFile(backupPath.c_str());
+ FileUtils::moveFile(path.c_str(), backupPath.c_str());
+ m_backups[path] = backupPath;
+}
+
+void UpdateInstaller::removeBackups()
+{
+ std::map<std::string,std::string>::const_iterator iter = m_backups.begin();
+ for (;iter != m_backups.end();iter++)
+ {
+ const std::string& backupFile = iter->second;
+ FileUtils::removeFile(backupFile.c_str());
+ }
+}
+
+bool UpdateInstaller::checkAccess()
+{
+ std::string testFile = m_installDir + "/update-installer-test-file";
+
+ try
+ {
+ FileUtils::removeFile(testFile.c_str());
+ }
+ catch (const FileUtils::IOException& error)
+ {
+ LOG(Info,"Removing existing access check file failed " + std::string(error.what()));
+ }
+
+ try
+ {
+ FileUtils::touch(testFile.c_str());
+ FileUtils::removeFile(testFile.c_str());
+ return true;
+ }
+ catch (const FileUtils::IOException& error)
+ {
+ LOG(Info,"checkAccess() failed " + std::string(error.what()));
+ return false;
+ }
+}
+
+void UpdateInstaller::setObserver(UpdateObserver* observer)
+{
+ m_observer = observer;
+}
+
+void UpdateInstaller::restartMainApp()
+{
+ try
+ {
+ std::string command = m_finishCmd;
+ std::list<std::string> args;
+
+ if (!command.empty())
+ {
+ LOG(Info,"Starting main application " + command);
+ ProcessUtils::runAsync(command,args);
+ }
+ else
+ {
+ LOG(Error,"No main binary specified");
+ }
+ }
+ catch (const std::exception& ex)
+ {
+ LOG(Error,"Unable to restart main app " + std::string(ex.what()));
+ }
+}
+
+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
+}
+
+void UpdateInstaller::setAutoClose(bool autoClose)
+{
+ m_autoClose = autoClose;
+}
+
diff --git a/mmc_updater/src/UpdateInstaller.h b/mmc_updater/src/UpdateInstaller.h
new file mode 100644
index 00000000..1eca0bc7
--- /dev/null
+++ b/mmc_updater/src/UpdateInstaller.h
@@ -0,0 +1,72 @@
+#pragma once
+
+#include "Platform.h"
+#include "FileUtils.h"
+#include "UpdateScript.h"
+
+#include <list>
+#include <string>
+#include <map>
+
+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:
+ enum Mode
+ {
+ Setup,
+ Main
+ };
+
+ UpdateInstaller();
+ void setInstallDir(const std::string& path);
+ void setPackageDir(const std::string& path);
+ void setBackupDir(const std::string& path);
+ void setMode(Mode mode);
+ void setScript(UpdateScript* script);
+ void setWaitPid(PLATFORM_PID pid);
+ void setForceElevated(bool elevated);
+ void setAutoClose(bool autoClose);
+ void setFinishCmd(const std::string& cmd);
+
+ void setObserver(UpdateObserver* observer);
+
+ void run() throw ();
+
+ void restartMainApp();
+
+ private:
+ void cleanup();
+ void revert();
+ void removeBackups();
+ bool checkAccess();
+
+ void installFiles();
+ void uninstallFiles();
+ 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;
+ std::string friendlyErrorForError(const FileUtils::IOException& ex) const;
+
+ Mode m_mode;
+ std::string m_installDir;
+ std::string m_packageDir;
+ std::string m_backupDir;
+ std::string m_finishCmd;
+ PLATFORM_PID m_waitPid;
+ UpdateScript* m_script;
+ UpdateObserver* m_observer;
+ std::map<std::string,std::string> m_backups;
+ bool m_forceElevated;
+ bool m_autoClose;
+};
+
diff --git a/mmc_updater/src/UpdateMessage.h b/mmc_updater/src/UpdateMessage.h
new file mode 100644
index 00000000..fee51ab8
--- /dev/null
+++ b/mmc_updater/src/UpdateMessage.h
@@ -0,0 +1,42 @@
+#pragma once
+
+#include <string>
+
+/** UpdateMessage stores information for a message
+ * about the status of update installation sent
+ * between threads.
+ */
+class UpdateMessage
+{
+ public:
+ enum Type
+ {
+ UpdateFailed,
+ UpdateProgress,
+ UpdateFinished
+ };
+
+ UpdateMessage(void* receiver, Type type)
+ {
+ init(receiver,type);
+ }
+
+ UpdateMessage(Type type)
+ {
+ 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;
+ }
+};
+
diff --git a/mmc_updater/src/UpdateObserver.h b/mmc_updater/src/UpdateObserver.h
new file mode 100644
index 00000000..84d47325
--- /dev/null
+++ b/mmc_updater/src/UpdateObserver.h
@@ -0,0 +1,15 @@
+#pragma once
+
+#include <string>
+
+/** Base class for observers of update installation status.
+ * See UpdateInstaller::setObserver()
+ */
+class UpdateObserver
+{
+ public:
+ virtual void updateError(const std::string& errorMessage) = 0;
+ virtual void updateProgress(int percentage) = 0;
+ virtual void updateFinished() = 0;
+};
+
diff --git a/mmc_updater/src/UpdateScript.cpp b/mmc_updater/src/UpdateScript.cpp
new file mode 100644
index 00000000..849a2217
--- /dev/null
+++ b/mmc_updater/src/UpdateScript.cpp
@@ -0,0 +1,99 @@
+#include "UpdateScript.h"
+
+#include "Log.h"
+#include "StringUtils.h"
+
+#include "tinyxml/tinyxml.h"
+
+std::string elementText(const TiXmlElement* element)
+{
+ if (!element)
+ {
+ return std::string();
+ }
+ return element->GetText();
+}
+
+UpdateScript::UpdateScript()
+{
+}
+
+void UpdateScript::parse(const std::string& path)
+{
+ m_path.clear();
+
+ TiXmlDocument document(path);
+ if (document.LoadFile())
+ {
+ m_path = path;
+
+ LOG(Info,"Loaded script from " + path);
+
+ const TiXmlElement* updateNode = document.RootElement();
+ parseUpdate(updateNode);
+ }
+ else
+ {
+ LOG(Error,"Unable to load script " + path);
+ }
+}
+
+bool UpdateScript::isValid() const
+{
+ return !m_path.empty();
+}
+
+void UpdateScript::parseUpdate(const TiXmlElement* updateNode)
+{
+ const TiXmlElement* installNode = updateNode->FirstChildElement("install");
+ if (installNode)
+ {
+ const TiXmlElement* installFileNode = installNode->FirstChildElement("file");
+ while (installFileNode)
+ {
+ m_filesToInstall.push_back(parseFile(installFileNode));
+ installFileNode = installFileNode->NextSiblingElement("file");
+ }
+ }
+
+ const TiXmlElement* uninstallNode = updateNode->FirstChildElement("uninstall");
+ if (uninstallNode)
+ {
+ const TiXmlElement* uninstallFileNode = uninstallNode->FirstChildElement("file");
+ while (uninstallFileNode)
+ {
+ m_filesToUninstall.push_back(uninstallFileNode->GetText());
+ uninstallFileNode = uninstallFileNode->NextSiblingElement("file");
+ }
+ }
+}
+
+UpdateScriptFile UpdateScript::parseFile(const TiXmlElement* element)
+{
+ UpdateScriptFile file;
+ // The name within the update files folder.
+ file.source = elementText(element->FirstChildElement("source"));
+ // The path to install to.
+ file.dest = elementText(element->FirstChildElement("dest"));
+
+ std::string modeString = elementText(element->FirstChildElement("mode"));
+ sscanf(modeString.c_str(),"%i",&file.permissions);
+
+ return file;
+}
+
+const std::vector<UpdateScriptFile>& UpdateScript::filesToInstall() const
+{
+ return m_filesToInstall;
+}
+
+const std::vector<std::string>& UpdateScript::filesToUninstall() const
+{
+ return m_filesToUninstall;
+}
+
+const std::string UpdateScript::path() const
+{
+ return m_path;
+}
+
diff --git a/mmc_updater/src/UpdateScript.h b/mmc_updater/src/UpdateScript.h
new file mode 100644
index 00000000..c825e35d
--- /dev/null
+++ b/mmc_updater/src/UpdateScript.h
@@ -0,0 +1,85 @@
+#pragma once
+
+#include <string>
+#include <vector>
+
+class TiXmlElement;
+
+/** Represents a package containing one or more
+ * files for an update.
+ */
+class UpdateScriptPackage
+{
+ public:
+ UpdateScriptPackage()
+ : size(0)
+ {}
+
+ std::string name;
+ std::string sha1;
+ std::string source;
+ int size;
+
+ bool operator==(const UpdateScriptPackage& other) const
+ {
+ return name == other.name &&
+ sha1 == other.sha1 &&
+ source == other.source &&
+ size == other.size;
+ }
+};
+
+/** Represents a file to be installed as part of an update. */
+class UpdateScriptFile
+{
+ public:
+ UpdateScriptFile()
+ : permissions(0)
+ {}
+
+ /// Path to copy from.
+ std::string source;
+ /// The path to copy to.
+ std::string dest;
+ std::string linkTarget;
+
+ /** The permissions for this file, specified
+ * using the standard Unix mode_t values.
+ */
+ int permissions;
+
+ bool operator==(const UpdateScriptFile& other) const
+ {
+ return source == other.source &&
+ dest == other.dest &&
+ permissions == other.permissions;
+ }
+};
+
+/** 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;
+ const std::string path() const;
+ const std::vector<UpdateScriptFile>& filesToInstall() const;
+ const std::vector<std::string>& filesToUninstall() const;
+
+ private:
+ void parseUpdate(const TiXmlElement* element);
+ UpdateScriptFile parseFile(const TiXmlElement* element);
+
+ std::string m_path;
+ std::vector<UpdateScriptFile> m_filesToInstall;
+ std::vector<std::string> m_filesToUninstall;
+};
+
diff --git a/mmc_updater/src/UpdaterOptions.cpp b/mmc_updater/src/UpdaterOptions.cpp
new file mode 100644
index 00000000..ae34562d
--- /dev/null
+++ b/mmc_updater/src/UpdaterOptions.cpp
@@ -0,0 +1,154 @@
+#include "UpdaterOptions.h"
+
+#include "Log.h"
+#include "AnyOption/anyoption.h"
+#include "FileUtils.h"
+#include "Platform.h"
+#include "StringUtils.h"
+
+#include <cstdlib>
+#include <iostream>
+
+UpdaterOptions::UpdaterOptions()
+: mode(UpdateInstaller::Setup)
+, waitPid(0)
+, showVersion(false)
+, forceElevated(false)
+, autoClose(false)
+{
+}
+
+UpdateInstaller::Mode stringToMode(const std::string& modeStr)
+{
+ if (modeStr == "main")
+ {
+ return UpdateInstaller::Main;
+ }
+ else
+ {
+ if (!modeStr.empty())
+ {
+ LOG(Error,"Unknown mode " + modeStr);
+ }
+ return UpdateInstaller::Setup;
+ }
+}
+
+void UpdaterOptions::parseOldFormatArg(const std::string& arg, std::string* key, std::string* value)
+{
+ size_t pos = arg.find('=');
+ if (pos != std::string::npos)
+ {
+ *key = arg.substr(0,pos);
+ *value = arg.substr(pos+1);
+ }
+}
+
+// this is a compatibility function to allow the updater binary
+// to be involved by legacy versions of Mendeley Desktop
+// which used a different syntax for the updater's command-line
+// arguments
+void UpdaterOptions::parseOldFormatArgs(int argc, char** argv)
+{
+ for (int i=0; i < argc; i++)
+ {
+ std::string key;
+ std::string value;
+
+ parseOldFormatArg(argv[i],&key,&value);
+
+ if (key == "CurrentDir")
+ {
+ // CurrentDir is the directory containing the main application
+ // binary. On Mac and Linux this differs from the root of
+ // the installation directory
+
+#ifdef PLATFORM_LINUX
+ // the main binary is in lib/mendeleydesktop/libexec,
+ // go up 3 levels
+ installDir = FileUtils::canonicalPath((value + "/../../../").c_str());
+#elif defined(PLATFORM_MAC)
+ // the main binary is in Contents/MacOS,
+ // go up 2 levels
+ installDir = FileUtils::canonicalPath((value + "/../../").c_str());
+#elif defined(PLATFORM_WINDOWS)
+ // the main binary is in the root of the install directory
+ installDir = value;
+#endif
+ }
+ else if (key == "TempDir")
+ {
+ packageDir = value;
+ }
+ else if (key == "UpdateScriptFileName")
+ {
+ scriptPath = value;
+ }
+ else if (key == "AppFileName")
+ {
+ // TODO - Store app file name
+ }
+ else if (key == "PID")
+ {
+ waitPid = static_cast<PLATFORM_PID>(atoll(value.c_str()));
+ }
+ else if (key == "--main")
+ {
+ mode = UpdateInstaller::Main;
+ }
+ }
+}
+
+void UpdaterOptions::parse(int argc, char** argv)
+{
+ AnyOption parser;
+ parser.setOption("install-dir");
+ parser.setOption("package-dir");
+ parser.setOption("finish-cmd");
+ parser.setOption("script");
+ parser.setOption("wait");
+ parser.setOption("mode");
+ parser.setFlag("version");
+ parser.setFlag("force-elevated");
+ parser.setFlag("auto-close");
+
+ parser.processCommandArgs(argc,argv);
+
+ if (parser.getValue("mode"))
+ {
+ mode = stringToMode(parser.getValue("mode"));
+ }
+ if (parser.getValue("install-dir"))
+ {
+ installDir = parser.getValue("install-dir");
+ }
+ if (parser.getValue("package-dir"))
+ {
+ packageDir = parser.getValue("package-dir");
+ }
+ if (parser.getValue("script"))
+ {
+ scriptPath = parser.getValue("script");
+ }
+ if (parser.getValue("wait"))
+ {
+ waitPid = static_cast<PLATFORM_PID>(atoll(parser.getValue("wait")));
+ }
+ if (parser.getValue("finish-cmd"))
+ {
+ finishCmd = parser.getValue("finish-cmd");
+ }
+
+ showVersion = parser.getFlag("version");
+ forceElevated = parser.getFlag("force-elevated");
+ autoClose = parser.getFlag("auto-close");
+
+ if (installDir.empty())
+ {
+ // if no --install-dir argument is present, try parsing
+ // the command-line arguments in the old format (which uses
+ // a list of 'Key=Value' args)
+ parseOldFormatArgs(argc,argv);
+ }
+}
+
diff --git a/mmc_updater/src/UpdaterOptions.h b/mmc_updater/src/UpdaterOptions.h
new file mode 100644
index 00000000..b4473a82
--- /dev/null
+++ b/mmc_updater/src/UpdaterOptions.h
@@ -0,0 +1,28 @@
+#pragma once
+
+#include "UpdateInstaller.h"
+
+/** Parses the command-line options to the updater binary. */
+class UpdaterOptions
+{
+ public:
+ UpdaterOptions();
+
+ void parse(int argc, char** argv);
+
+ UpdateInstaller::Mode mode;
+ std::string installDir;
+ std::string packageDir;
+ std::string scriptPath;
+ std::string finishCmd;
+ PLATFORM_PID waitPid;
+ std::string logFile;
+ bool showVersion;
+ bool forceElevated;
+ bool autoClose;
+
+ private:
+ void parseOldFormatArgs(int argc, char** argv);
+ static void parseOldFormatArg(const std::string& arg, std::string* key, std::string* value);
+};
+
diff --git a/mmc_updater/src/main.cpp b/mmc_updater/src/main.cpp
new file mode 100644
index 00000000..fb072ab5
--- /dev/null
+++ b/mmc_updater/src/main.cpp
@@ -0,0 +1,203 @@
+#include "AppInfo.h"
+#include "FileUtils.h"
+#include "Log.h"
+#include "Platform.h"
+#include "ProcessUtils.h"
+#include "StringUtils.h"
+#include "UpdateScript.h"
+#include "UpdaterOptions.h"
+
+#include <thread>
+
+#if defined(PLATFORM_LINUX)
+ #include "UpdateDialogGtkFactory.h"
+ #include "UpdateDialogAscii.h"
+#endif
+
+#if defined(PLATFORM_MAC)
+ #include "MacBundle.h"
+ #include "UpdateDialogCocoa.h"
+#endif
+
+#if defined(PLATFORM_WINDOWS)
+ #include "UpdateDialogWin32.h"
+#endif
+
+#include <iostream>
+#include <memory>
+
+#define UPDATER_VERSION "0.16"
+
+UpdateDialog* createUpdateDialog();
+
+void runUpdaterThread(void* arg)
+{
+#ifdef PLATFORM_MAC
+ // create an autorelease pool to free any temporary objects
+ // created by Cocoa whilst handling notifications from the UpdateInstaller
+ void* pool = UpdateDialogCocoa::createAutoreleasePool();
+#endif
+
+ try
+ {
+ UpdateInstaller* installer = static_cast<UpdateInstaller*>(arg);
+ installer->run();
+ }
+ catch (const std::exception& ex)
+ {
+ LOG(Error,"Unexpected exception " + std::string(ex.what()));
+ }
+
+#ifdef PLATFORM_MAC
+ UpdateDialogCocoa::releaseAutoreleasePool(pool);
+#endif
+}
+
+#ifdef PLATFORM_MAC
+extern unsigned char Info_plist[];
+extern unsigned int Info_plist_len;
+
+extern unsigned char mac_icns[];
+extern unsigned int mac_icns_len;
+
+bool unpackBundle(int argc, char** argv)
+{
+ MacBundle bundle(FileUtils::tempPath(),AppInfo::name());
+ std::string currentExePath = ProcessUtils::currentProcessPath();
+
+ if (currentExePath.find(bundle.bundlePath()) != std::string::npos)
+ {
+ // already running from a bundle
+ return false;
+ }
+ LOG(Info,"Creating bundle " + bundle.bundlePath());
+
+ // create a Mac app bundle
+ std::string plistContent(reinterpret_cast<const char*>(Info_plist),Info_plist_len);
+ std::string iconContent(reinterpret_cast<const char*>(mac_icns),mac_icns_len);
+ bundle.create(plistContent,iconContent,ProcessUtils::currentProcessPath());
+
+ std::list<std::string> args;
+ for (int i = 1; i < argc; i++)
+ {
+ args.push_back(argv[i]);
+ }
+ ProcessUtils::runSync(bundle.executablePath(),args);
+ return true;
+}
+#endif
+
+void setupConsole()
+{
+#ifdef PLATFORM_WINDOWS
+ // see http://stackoverflow.com/questions/587767/how-to-output-to-console-in-c-windows
+ // and http://www.libsdl.org/cgi/docwiki.cgi/FAQ_Console
+ AttachConsole(ATTACH_PARENT_PROCESS);
+ freopen( "CON", "w", stdout );
+ freopen( "CON", "w", stderr );
+#endif
+}
+
+int main(int argc, char** argv)
+{
+#ifdef PLATFORM_MAC
+ void* pool = UpdateDialogCocoa::createAutoreleasePool();
+#endif
+
+ Log::instance()->open(AppInfo::logFilePath());
+
+#ifdef PLATFORM_MAC
+ // when the updater is run for the first time, create a Mac app bundle
+ // and re-launch the application from the bundle. This permits
+ // setting up bundle properties (such as application icon)
+ if (unpackBundle(argc,argv))
+ {
+ return 0;
+ }
+#endif
+
+ UpdaterOptions options;
+ options.parse(argc,argv);
+ if (options.showVersion)
+ {
+ setupConsole();
+ std::cout << "Update installer version " << UPDATER_VERSION << std::endl;
+ return 0;
+ }
+
+ UpdateInstaller installer;
+ UpdateScript script;
+
+ if (!options.scriptPath.empty())
+ {
+ script.parse(FileUtils::makeAbsolute(options.scriptPath.c_str(),options.packageDir.c_str()));
+ }
+
+ LOG(Info,"started updater. install-dir: " + options.installDir
+ + ", package-dir: " + options.packageDir
+ + ", wait-pid: " + intToStr(options.waitPid)
+ + ", script-path: " + options.scriptPath
+ + ", mode: " + intToStr(options.mode)
+ + ", finish-cmd: " + options.finishCmd);
+
+ installer.setMode(options.mode);
+ installer.setInstallDir(options.installDir);
+ installer.setPackageDir(options.packageDir);
+ installer.setScript(&script);
+ installer.setWaitPid(options.waitPid);
+ installer.setForceElevated(options.forceElevated);
+ installer.setAutoClose(options.autoClose);
+ installer.setFinishCmd(options.finishCmd);
+
+ if (options.mode == UpdateInstaller::Main)
+ {
+ LOG(Info, "Showing updater UI - auto close? " + intToStr(options.autoClose));
+ std::auto_ptr<UpdateDialog> dialog(createUpdateDialog());
+ dialog->setAutoClose(options.autoClose);
+ dialog->init(argc, argv);
+ installer.setObserver(dialog.get());
+ std::thread updaterThread(runUpdaterThread, &installer);
+ dialog->exec();
+ updaterThread.join();
+ }
+ else
+ {
+ installer.run();
+ }
+
+#ifdef PLATFORM_MAC
+ UpdateDialogCocoa::releaseAutoreleasePool(pool);
+#endif
+
+ return 0;
+}
+
+UpdateDialog* createUpdateDialog()
+{
+#if defined(PLATFORM_WINDOWS)
+ return new UpdateDialogWin32();
+#elif defined(PLATFORM_MAC)
+ return new UpdateDialogCocoa();
+#elif defined(PLATFORM_LINUX)
+ UpdateDialog* dialog = UpdateDialogGtkFactory::createDialog();
+ if (!dialog)
+ {
+ dialog = new UpdateDialogAscii();
+ }
+ return dialog;
+#endif
+}
+
+#ifdef PLATFORM_WINDOWS
+// application entry point under Windows
+int CALLBACK WinMain(HINSTANCE hInstance,
+ HINSTANCE hPrevInstance,
+ LPSTR lpCmdLine,
+ int nCmdShow)
+{
+ int argc = 0;
+ char** argv;
+ ProcessUtils::convertWindowsCommandLine(GetCommandLineW(),argc,argv);
+ return main(argc,argv);
+}
+#endif
diff --git a/mmc_updater/src/resources/Info.plist b/mmc_updater/src/resources/Info.plist
new file mode 100644
index 00000000..93e97ccd
--- /dev/null
+++ b/mmc_updater/src/resources/Info.plist
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <!-- Note - The name of the application specified here must match the value
+ returned by AppInfo::name()
+ !-->
+ <key>CFBundleDevelopmentRegion</key>
+ <string>English</string>
+ <key>CFBundleExecutable</key>
+ <string>MultiMC Updater</string>
+ <key>CFBundleIconFile</key>
+ <string>MultiMC Updater.icns</string>
+ <key>CFBundleIdentifier</key>
+ <string>org.multimc.MultiMCUpdater</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundlePackageType</key>
+ <string>APPL</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>1.0</string>
+ <key>NSMainNibFile</key>
+ <string>MainMenu</string>
+ <key>NSPrincipalClass</key>
+ <string>NSApplication</string>
+ <key>LSMinimumSystemVersion</key>
+ <string>10.5</string>
+ <key>LSMinimumSystemVersionByArchitecture</key>
+ <dict>
+ <key>i386</key>
+ <string>10.5.0</string>
+ <key>x86_64</key>
+ <string>10.5.0</string>
+ </dict>
+</dict>
+</plist>
diff --git a/mmc_updater/src/resources/icon128.png b/mmc_updater/src/resources/icon128.png
new file mode 100644
index 00000000..324452aa
--- /dev/null
+++ b/mmc_updater/src/resources/icon128.png
Binary files differ
diff --git a/mmc_updater/src/resources/icon64.png b/mmc_updater/src/resources/icon64.png
new file mode 100644
index 00000000..5e3373e2
--- /dev/null
+++ b/mmc_updater/src/resources/icon64.png
Binary files differ
diff --git a/mmc_updater/src/resources/mac.icns b/mmc_updater/src/resources/mac.icns
new file mode 100644
index 00000000..7c8fa2ef
--- /dev/null
+++ b/mmc_updater/src/resources/mac.icns
Binary files differ
diff --git a/mmc_updater/src/resources/updater.ico b/mmc_updater/src/resources/updater.ico
new file mode 100644
index 00000000..b011bac9
--- /dev/null
+++ b/mmc_updater/src/resources/updater.ico
Binary files differ
diff --git a/mmc_updater/src/resources/updater.rc b/mmc_updater/src/resources/updater.rc
new file mode 100644
index 00000000..550970a8
--- /dev/null
+++ b/mmc_updater/src/resources/updater.rc
@@ -0,0 +1,30 @@
+IDI_APPICON ICON DISCARDABLE "updater.ico"
+
+1 VERSIONINFO
+FILEVERSION 0,0,1,0
+PRODUCTVERSION 1,0,1,0
+FILEFLAGSMASK 0X3FL
+FILEFLAGS 0X8L
+FILEOS 0X40004L
+FILETYPE 0X1
+FILESUBTYPE 0
+BEGIN
+ BLOCK "StringFileInfo"
+ BEGIN
+ BLOCK "000004b0"
+ BEGIN
+ VALUE "FileVersion", "0.0.1.0"
+ VALUE "ProductVersion", "1.0.1.0"
+ VALUE "OriginalFilename", "updater.exe"
+ VALUE "InternalName", "updater.exe"
+ VALUE "FileDescription", "Software Update Tool"
+ VALUE "CompanyName", "MultiMC Contributors"
+ VALUE "ProductName", "MultiMC Software Updater"
+ VALUE "PrivateBuild", "Built by BuildBot"
+ END
+ END
+ BLOCK "VarFileInfo"
+ BEGIN
+ VALUE "Translation", 0x0000, 0x04b0
+ END
+END \ No newline at end of file
diff --git a/mmc_updater/src/tests/CMakeLists.txt b/mmc_updater/src/tests/CMakeLists.txt
new file mode 100644
index 00000000..2af9b9c0
--- /dev/null
+++ b/mmc_updater/src/tests/CMakeLists.txt
@@ -0,0 +1,51 @@
+
+include_directories("${CMAKE_CURRENT_SOURCE_DIR}/..")
+
+if (APPLE)
+ set(HELPER_SHARED_SOURCES ../StlSymbolsLeopard.cpp)
+endif()
+
+# Create helper binaries for unit tests
+add_executable(oldapp
+ old_app.cpp
+ ${HELPER_SHARED_SOURCES}
+)
+add_executable(newapp
+ new_app.cpp
+ ${HELPER_SHARED_SOURCES}
+)
+
+# Install data files required by unit tests
+set(TEST_FILES
+ file_list.xml
+ v2_file_list.xml
+ test-update.rb
+)
+
+foreach(TEST_FILE ${TEST_FILES})
+ execute_process(
+ COMMAND
+ "${CMAKE_COMMAND}" -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/${TEST_FILE}" "${CMAKE_CURRENT_BINARY_DIR}"
+ )
+endforeach()
+
+# Add unit test binaries
+macro(ADD_UPDATER_TEST CLASS)
+ set(TEST_TARGET updater_${CLASS})
+ add_executable(${TEST_TARGET} ${CLASS}.cpp)
+ target_link_libraries(${TEST_TARGET} updatershared)
+ add_test(${TEST_TARGET} ${TEST_TARGET})
+ if (APPLE)
+ set_target_properties(${TEST_TARGET} PROPERTIES LINK_FLAGS "-framework Security -framework Cocoa")
+ endif()
+endmacro()
+
+add_updater_test(TestUpdateScript)
+add_updater_test(TestUpdaterOptions)
+add_updater_test(TestFileUtils)
+
+# Add updater that that performs a complete update install
+# and checks the result
+find_program(RUBY_BIN ruby)
+add_test(updater_TestUpdateInstall ${RUBY_BIN} test-update.rb)
+
diff --git a/mmc_updater/src/tests/TestFileUtils.cpp b/mmc_updater/src/tests/TestFileUtils.cpp
new file mode 100644
index 00000000..709acc5c
--- /dev/null
+++ b/mmc_updater/src/tests/TestFileUtils.cpp
@@ -0,0 +1,50 @@
+#include "TestFileUtils.h"
+
+#include "FileUtils.h"
+#include "TestUtils.h"
+
+void TestFileUtils::testDirName()
+{
+#ifdef PLATFORM_WINDOWS
+ std::string dirName = FileUtils::dirname("E:/Some Dir/App.exe");
+ TEST_COMPARE(dirName,"E:/Some Dir/");
+#endif
+}
+
+void TestFileUtils::testIsRelative()
+{
+#ifdef PLATFORM_WINDOWS
+ TEST_COMPARE(FileUtils::isRelative("temp"),true);
+ TEST_COMPARE(FileUtils::isRelative("D:/temp"),false);
+ TEST_COMPARE(FileUtils::isRelative("d:/temp"),false);
+#else
+ TEST_COMPARE(FileUtils::isRelative("/tmp"),false);
+ TEST_COMPARE(FileUtils::isRelative("tmp"),true);
+#endif
+}
+
+void TestFileUtils::testSymlinkFileExists()
+{
+#ifdef PLATFORM_UNIX
+ const char* linkName = "link-name";
+ FileUtils::removeFile(linkName);
+ FileUtils::createSymLink(linkName, "target-that-does-not-exist");
+ TEST_COMPARE(FileUtils::fileExists(linkName), true);
+#endif
+}
+
+void TestFileUtils::testStandardDirs()
+{
+ std::string tmpDir = FileUtils::tempPath();
+ TEST_COMPARE(FileUtils::fileExists(tmpDir.data()), true);
+}
+
+int main(int,char**)
+{
+ TestList<TestFileUtils> tests;
+ tests.addTest(&TestFileUtils::testDirName);
+ tests.addTest(&TestFileUtils::testIsRelative);
+ tests.addTest(&TestFileUtils::testSymlinkFileExists);
+ tests.addTest(&TestFileUtils::testStandardDirs);
+ return TestUtils::runTest(tests);
+}
diff --git a/mmc_updater/src/tests/TestFileUtils.h b/mmc_updater/src/tests/TestFileUtils.h
new file mode 100644
index 00000000..1a45164b
--- /dev/null
+++ b/mmc_updater/src/tests/TestFileUtils.h
@@ -0,0 +1,10 @@
+#pragma once
+
+class TestFileUtils
+{
+ public:
+ void testDirName();
+ void testIsRelative();
+ void testSymlinkFileExists();
+ void testStandardDirs();
+};
diff --git a/mmc_updater/src/tests/TestUpdateScript.cpp b/mmc_updater/src/tests/TestUpdateScript.cpp
new file mode 100644
index 00000000..30a7572a
--- /dev/null
+++ b/mmc_updater/src/tests/TestUpdateScript.cpp
@@ -0,0 +1,27 @@
+#include "TestUpdateScript.h"
+
+#include "TestUtils.h"
+#include "UpdateScript.h"
+
+#include <iostream>
+#include <algorithm>
+
+void TestUpdateScript::testV2Script()
+{
+ UpdateScript newFormat;
+ UpdateScript oldFormat;
+
+ newFormat.parse("file_list.xml");
+ oldFormat.parse("v2_file_list.xml");
+
+ TEST_COMPARE(newFormat.filesToInstall(),oldFormat.filesToInstall());
+ TEST_COMPARE(newFormat.filesToUninstall(),oldFormat.filesToUninstall());
+}
+
+int main(int,char**)
+{
+ TestList<TestUpdateScript> tests;
+ tests.addTest(&TestUpdateScript::testV2Script);
+ return TestUtils::runTest(tests);
+}
+
diff --git a/mmc_updater/src/tests/TestUpdateScript.h b/mmc_updater/src/tests/TestUpdateScript.h
new file mode 100644
index 00000000..513513d5
--- /dev/null
+++ b/mmc_updater/src/tests/TestUpdateScript.h
@@ -0,0 +1,8 @@
+#pragma once
+
+class TestUpdateScript
+{
+ public:
+ void testV2Script();
+};
+
diff --git a/mmc_updater/src/tests/TestUpdaterOptions.cpp b/mmc_updater/src/tests/TestUpdaterOptions.cpp
new file mode 100644
index 00000000..a4cb7d33
--- /dev/null
+++ b/mmc_updater/src/tests/TestUpdaterOptions.cpp
@@ -0,0 +1,68 @@
+#include "TestUpdaterOptions.h"
+
+#include "FileUtils.h"
+#include "Platform.h"
+#include "TestUtils.h"
+#include "UpdaterOptions.h"
+
+#include <string.h>
+#include <stdlib.h>
+
+void TestUpdaterOptions::testOldFormatArgs()
+{
+ const int argc = 6;
+ char* argv[argc];
+ argv[0] = strdup("updater");
+
+ std::string currentDir("CurrentDir=");
+ const char* appDir = 0;
+
+ // CurrentDir is the path to the directory containing the main
+ // Mendeley Desktop binary, on Linux and Mac this differs from
+ // the root of the install directory
+#ifdef PLATFORM_LINUX
+ appDir = "/tmp/path-to-app/lib/mendeleydesktop/libexec/";
+ FileUtils::mkpath(appDir);
+#elif defined(PLATFORM_MAC)
+ appDir = "/tmp/path-to-app/Contents/MacOS/";
+ FileUtils::mkpath(appDir);
+#elif defined(PLATFORM_WINDOWS)
+ appDir = "C:/path/to/app/";
+#endif
+ currentDir += appDir;
+
+ argv[1] = strdup(currentDir.c_str());
+ argv[2] = strdup("TempDir=/tmp/updater");
+ argv[3] = strdup("UpdateScriptFileName=/tmp/updater/file_list.xml");
+ argv[4] = strdup("AppFileName=/path/to/app/theapp");
+ argv[5] = strdup("PID=123456");
+
+ UpdaterOptions options;
+ options.parse(argc,argv);
+
+ TEST_COMPARE(options.mode,UpdateInstaller::Setup);
+#ifdef PLATFORM_LINUX
+ TEST_COMPARE(options.installDir,"/tmp/path-to-app");
+#elif defined(PLATFORM_MAC)
+ // /tmp is a symlink to /private/tmp on Mac
+ TEST_COMPARE(options.installDir,"/private/tmp/path-to-app");
+#else
+ TEST_COMPARE(options.installDir,"C:/path/to/app/");
+#endif
+ TEST_COMPARE(options.packageDir,"/tmp/updater");
+ TEST_COMPARE(options.scriptPath,"/tmp/updater/file_list.xml");
+ TEST_COMPARE(options.waitPid,123456);
+
+ for (int i=0; i < argc; i++)
+ {
+ free(argv[i]);
+ }
+}
+
+int main(int,char**)
+{
+ TestList<TestUpdaterOptions> tests;
+ tests.addTest(&TestUpdaterOptions::testOldFormatArgs);
+ return TestUtils::runTest(tests);
+}
+
diff --git a/mmc_updater/src/tests/TestUpdaterOptions.h b/mmc_updater/src/tests/TestUpdaterOptions.h
new file mode 100644
index 00000000..5ed102c1
--- /dev/null
+++ b/mmc_updater/src/tests/TestUpdaterOptions.h
@@ -0,0 +1,8 @@
+#pragma once
+
+class TestUpdaterOptions
+{
+ public:
+ void testOldFormatArgs();
+};
+
diff --git a/mmc_updater/src/tests/TestUtils.h b/mmc_updater/src/tests/TestUtils.h
new file mode 100644
index 00000000..68d97da5
--- /dev/null
+++ b/mmc_updater/src/tests/TestUtils.h
@@ -0,0 +1,108 @@
+#pragma once
+
+#include <iostream>
+#include <functional>
+#include <string>
+#include <vector>
+
+template <class T>
+class TestList
+{
+ public:
+ void addTest(void (T::*test)())
+ {
+ m_tests.push_back(std::mem_fun(test));
+ }
+
+ int size() const
+ {
+ return m_tests.size();
+ }
+
+ void runTest(T* testInstance, int i)
+ {
+ m_tests.at(i)(testInstance);
+ }
+
+ private:
+ std::vector<std::mem_fun_t<void,T> > m_tests;
+};
+
+class TestUtils
+{
+ public:
+ template <class X, class Y>
+ static void compare(const X& x, const Y& y, const char* xString, const char* yString)
+ {
+ if (x != y)
+ {
+ throw "Actual and expected values differ. "
+ "Actual: " + toString(x,xString) +
+ " Expected: " + toString(y,yString);
+ }
+ }
+
+ template <typename T>
+ static std::string toString(T value, const char* context)
+ {
+ return "Unprintable: " + std::string(context);
+ }
+
+ template <class T>
+ static int runTest(class TestList<T>& tests) throw ()
+ {
+ std::string errorText;
+ try
+ {
+ T testInstance;
+ for (int i=0; i < tests.size(); i++)
+ {
+ tests.runTest(&testInstance,i);
+ }
+ }
+ catch (const std::exception& ex)
+ {
+ errorText = ex.what();
+ }
+ catch (const std::string& error)
+ {
+ errorText = error;
+ }
+ catch (...)
+ {
+ errorText = "Unknown exception";
+ }
+
+ if (errorText.empty())
+ {
+ std::cout << "Test passed" << std::endl;
+ return 0;
+ }
+ else
+ {
+ std::cout << "Test failed: " << errorText << std::endl;
+ return 1;
+ }
+ }
+};
+
+template <>
+inline std::string TestUtils::toString(const std::string& value, const char*)
+{
+ return value;
+}
+template <>
+inline std::string TestUtils::toString(std::string value, const char*)
+{
+ return value;
+}
+template <>
+inline std::string TestUtils::toString(const char* value, const char*)
+{
+ return value;
+}
+
+#define TEST_COMPARE(x,y) \
+ TestUtils::compare(x,y,#x,#y);
+
+
diff --git a/mmc_updater/src/tests/file_list.xml b/mmc_updater/src/tests/file_list.xml
new file mode 100644
index 00000000..dff4b54f
--- /dev/null
+++ b/mmc_updater/src/tests/file_list.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0"?>
+<update version="3">
+ <targetVersion>2.0</targetVersion>
+ <platform>Test</platform>
+ <dependencies>
+ <!-- The new updater is standalone and has no dependencies,
+ except for standard system libraries.
+ !-->
+ </dependencies>
+ <packages>
+ <package>
+ <name>app-pkg</name>
+ <hash>$APP_PACKAGE_HASH</hash>
+ <size>$APP_PACKAGE_SIZE</size>
+ <source>http://some/dummy/URL</source>
+ </package>
+ </packages>
+ <install>
+ <file>
+ <name>$APP_FILENAME</name>
+ <hash>$UPDATED_APP_HASH</hash>
+ <size>$UPDATED_APP_SIZE</size>
+ <permissions>0755</permissions>
+ <package>app-pkg</package>
+ <is-main-binary>true</is-main-binary>
+ </file>
+ <file>
+ <name>$UPDATER_FILENAME</name>
+ <hash>$UPDATER_HASH</hash>
+ <size>$UPDATER_SIZE</size>
+ <permissions>0755</permissions>
+ </file>
+ <!-- Test symlink !-->
+ <file>
+ <name>test-dir/app-symlink</name>
+ <target>../app</target>
+ </file>
+ <!-- Test file in new directory !-->
+ <file>
+ <name>new-dir/new-dir2/new-file.txt</name>
+ <hash>$TEST_FILENAME</hash>
+ <size>$TEST_SIZE</size>
+ <package>app-pkg</package>
+ <permissions>0644</permissions>
+ </file>
+ </install>
+ <uninstall>
+ <!-- TODO - List some files to uninstall here !-->
+ <file>file-to-uninstall.txt</file>
+ <file>symlink-to-file-to-uninstall.txt</file>
+ </uninstall>
+</update>
diff --git a/mmc_updater/src/tests/new_app.cpp b/mmc_updater/src/tests/new_app.cpp
new file mode 100644
index 00000000..7fad1380
--- /dev/null
+++ b/mmc_updater/src/tests/new_app.cpp
@@ -0,0 +1,8 @@
+#include <iostream>
+
+int main(int,char**)
+{
+ std::cout << "new app starting" << std::endl;
+ return 0;
+}
+
diff --git a/mmc_updater/src/tests/old_app.cpp b/mmc_updater/src/tests/old_app.cpp
new file mode 100644
index 00000000..476a3405
--- /dev/null
+++ b/mmc_updater/src/tests/old_app.cpp
@@ -0,0 +1,7 @@
+#include <iostream>
+
+int main(int,char**)
+{
+ std::cout << "old app starting" << std::endl;
+ return 0;
+}
diff --git a/mmc_updater/src/tests/test-update.rb b/mmc_updater/src/tests/test-update.rb
new file mode 100755
index 00000000..82965cf4
--- /dev/null
+++ b/mmc_updater/src/tests/test-update.rb
@@ -0,0 +1,218 @@
+#!/usr/bin/ruby
+
+require 'fileutils.rb'
+require 'find'
+require 'rbconfig'
+require 'optparse'
+
+# Install directory - this contains a space to check
+# for correct escaping of paths when passing comamnd
+# line arguments under Windows
+INSTALL_DIR = File.expand_path("install dir/")
+PACKAGE_DIR = File.expand_path("package-dir/")
+PACKAGE_SRC_DIR = File.expand_path("package-src-dir/")
+IS_WINDOWS = RbConfig::CONFIG['host_os'] =~ /mswin|mingw/
+
+if IS_WINDOWS
+ OLDAPP_NAME = "oldapp.exe"
+ NEWAPP_NAME = "newapp.exe"
+ APP_NAME = "app.exe"
+ UPDATER_NAME = "updater.exe"
+ ZIP_TOOL = File.expand_path("../zip-tool.exe")
+else
+ OLDAPP_NAME = "oldapp"
+ NEWAPP_NAME = "newapp"
+ APP_NAME = "app"
+ UPDATER_NAME = "updater"
+ ZIP_TOOL = File.expand_path("../zip-tool")
+end
+
+file_list_vars = {
+ "APP_FILENAME" => APP_NAME,
+ "UPDATER_FILENAME" => UPDATER_NAME
+}
+
+def replace_vars(src_file,dest_file,vars)
+ content = File.read(src_file)
+ vars.each do |key,value|
+ content.gsub! "$#{key}",value
+ end
+ File.open(dest_file,'w') do |file|
+ file.print content
+ end
+end
+
+# Returns true if |src_file| and |dest_file| have the same contents, type
+# and permissions or false otherwise
+def compare_files(src_file, dest_file)
+ if File.ftype(src_file) != File.ftype(dest_file)
+ $stderr.puts "Type of file #{src_file} and #{dest_file} differ"
+ return false
+ end
+
+ if File.file?(src_file) && !FileUtils.identical?(src_file, dest_file)
+ $stderr.puts "Contents of file #{src_file} and #{dest_file} differ"
+ return false
+ end
+
+ src_stat = File.stat(src_file)
+ dest_stat = File.stat(dest_file)
+
+ if src_stat.mode != dest_stat.mode
+ $stderr.puts "Permissions of #{src_file} and #{dest_file} differ"
+ return false
+ end
+
+ return true
+end
+
+# Compares the contents of two directories and returns a map of (file path => change type)
+# for files and directories which differ between the two
+def compare_dirs(src_dir, dest_dir)
+ src_dir += '/' if !src_dir.end_with?('/')
+ dest_dir += '/' if !dest_dir.end_with?('/')
+
+ src_file_map = {}
+ Find.find(src_dir) do |src_file|
+ src_file = src_file[src_dir.length..-1]
+ src_file_map[src_file] = nil
+ end
+
+ change_map = {}
+ Find.find(dest_dir) do |dest_file|
+ dest_file = dest_file[dest_dir.length..-1]
+
+ if !src_file_map.include?(dest_file)
+ change_map[dest_file] = :deleted
+ elsif !compare_files("#{src_dir}/#{dest_file}", "#{dest_dir}/#{dest_file}")
+ change_map[dest_file] = :updated
+ end
+
+ src_file_map.delete(dest_file)
+ end
+
+ src_file_map.each do |file|
+ change_map[file] = :added
+ end
+
+ return change_map
+end
+
+def create_test_file(name, content)
+ File.open(name, 'w') do |file|
+ file.puts content
+ end
+ return name
+end
+
+force_elevation = false
+run_in_debugger = false
+
+OptionParser.new do |parser|
+ parser.on("-f","--force-elevated","Force the updater to elevate itself") do
+ force_elevation = true
+ end
+ parser.on("-d","--debug","Run the updater under GDB") do
+ run_in_debugger = true
+ end
+end.parse!
+
+# copy 'src' to 'dest', preserving the attributes
+# of 'src'
+def copy_file(src, dest)
+ FileUtils.cp src, dest, :preserve => true
+end
+
+# Remove the install and package dirs if they
+# already exist
+FileUtils.rm_rf(INSTALL_DIR)
+FileUtils.rm_rf(PACKAGE_DIR)
+FileUtils.rm_rf(PACKAGE_SRC_DIR)
+
+# Create the install directory with the old app
+Dir.mkdir(INSTALL_DIR)
+copy_file OLDAPP_NAME, "#{INSTALL_DIR}/#{APP_NAME}"
+
+# Create a dummy file to uninstall
+uninstall_test_file = create_test_file("#{INSTALL_DIR}/file-to-uninstall.txt", "this file should be removed after the update")
+uninstall_test_symlink = if not IS_WINDOWS
+ FileUtils.ln_s("#{INSTALL_DIR}/file-to-uninstall.txt", "#{INSTALL_DIR}/symlink-to-file-to-uninstall.txt")
+else
+ create_test_file("#{INSTALL_DIR}/symlink-to-file-to-uninstall.txt", "dummy file. this is a symlink on Unix")
+end
+
+# Populate package source dir with files to install
+Dir.mkdir(PACKAGE_SRC_DIR)
+nested_dir_path = "#{PACKAGE_SRC_DIR}/new-dir/new-dir2"
+FileUtils.mkdir_p(nested_dir_path)
+FileUtils::chmod 0755, "#{PACKAGE_SRC_DIR}/new-dir"
+FileUtils::chmod 0755, "#{PACKAGE_SRC_DIR}/new-dir/new-dir2"
+nested_dir_test_file = "#{nested_dir_path}/new-file.txt"
+File.open(nested_dir_test_file,'w') do |file|
+ file.puts "this is a new file in a new nested dir"
+end
+FileUtils::chmod 0644, nested_dir_test_file
+copy_file NEWAPP_NAME, "#{PACKAGE_SRC_DIR}/#{APP_NAME}"
+FileUtils::chmod 0755, "#{PACKAGE_SRC_DIR}/#{APP_NAME}"
+
+# Create .zip packages from source files
+Dir.mkdir(PACKAGE_DIR)
+Dir.chdir(PACKAGE_SRC_DIR) do
+ if !system("#{ZIP_TOOL} #{PACKAGE_DIR}/app-pkg.zip .")
+ raise "Unable to create update package"
+ end
+end
+
+# Copy the install script and updater to the target
+# directory
+replace_vars("file_list.xml","#{PACKAGE_DIR}/file_list.xml",file_list_vars)
+copy_file "../#{UPDATER_NAME}", "#{PACKAGE_DIR}/#{UPDATER_NAME}"
+
+# Run the updater using the new syntax
+#
+# Run the application from the install directory to
+# make sure that it looks in the correct directory for
+# the file_list.xml file and packages
+#
+install_path = File.expand_path(INSTALL_DIR)
+Dir.chdir(INSTALL_DIR) do
+ flags = "--force-elevated" if force_elevation
+ debug_flags = "gdb --args" if run_in_debugger
+ cmd = "#{debug_flags} #{PACKAGE_DIR}/#{UPDATER_NAME} #{flags} --install-dir \"#{install_path}\" --package-dir \"#{PACKAGE_DIR}\" --script file_list.xml --auto-close"
+ puts "Running '#{cmd}'"
+ system(cmd)
+end
+
+# TODO - Correctly wait until updater has finished
+sleep(1)
+
+# Check that the app was updated
+app_path = "#{INSTALL_DIR}/#{APP_NAME}"
+output = `"#{app_path}"`
+if (output.strip != "new app starting")
+ throw "Updated app produced unexpected output: #{output}"
+end
+
+# Check that the packaged dir and install dir match
+dir_diff = compare_dirs(PACKAGE_SRC_DIR, INSTALL_DIR)
+ignored_files = ["test-dir", "test-dir/app-symlink", UPDATER_NAME]
+have_unexpected_change = false
+dir_diff.each do |path, change_type|
+ if !ignored_files.include?(path)
+ case change_type
+ when :added
+ $stderr.puts "File #{path} was not installed"
+ when :changed
+ $stderr.puts "File #{path} differs between install and package dir"
+ when :deleted
+ $stderr.puts "File #{path} was not uninstalled"
+ end
+ have_unexpected_change = true
+ end
+end
+
+if have_unexpected_change
+ throw "Unexpected differences between packaging and update dir"
+end
+
+puts "Test passed"
diff --git a/mmc_updater/src/tests/v2_file_list.xml b/mmc_updater/src/tests/v2_file_list.xml
new file mode 100644
index 00000000..202e5bbe
--- /dev/null
+++ b/mmc_updater/src/tests/v2_file_list.xml
@@ -0,0 +1,67 @@
+<?xml version="1.0"?>
+
+<!-- The v2-compatible attribute lets the update script parser
+ know that it is dealing with a script structured for backwards
+ compatibility with the MD <= 1.0 updater.
+!-->
+<update version="3" v2-compatible="true">
+ <targetVersion>2.0</targetVersion>
+ <platform>Test</platform>
+ <dependencies>
+ <!-- The new updater is standalone and has no dependencies,
+ except for standard system libraries and itself.
+ !-->
+ </dependencies>
+ <packages>
+ <package>
+ <name>app-pkg</name>
+ <hash>$APP_PACKAGE_HASH</hash>
+ <size>$APP_PACKAGE_SIZE</size>
+ <source>http://some/dummy/URL</source>
+ </package>
+ </packages>
+
+ <!-- For compatibility with the update download in MD <= 1.0,
+ an <install> section lists the packages to download and
+ the real list of files to install is in the <install-v3>
+ section. !-->
+ <install>
+ <!-- A duplicate of the <packages> section should appear here,
+ except that each package is listed using the same structure
+ as files in the install-v3/files section.
+ !-->
+ </install>
+ <install-v3>
+ <file>
+ <name>$APP_FILENAME</name>
+ <hash>$UPDATED_APP_HASH</hash>
+ <size>$UPDATED_APP_SIZE</size>
+ <permissions>0755</permissions>
+ <package>app-pkg</package>
+ <is-main-binary>true</is-main-binary>
+ </file>
+ <file>
+ <name>$UPDATER_FILENAME</name>
+ <hash>$UPDATER_HASH</hash>
+ <size>$UPDATER_SIZE</size>
+ <permissions>0755</permissions>
+ </file>
+ <!-- Test symlink !-->
+ <file>
+ <name>test-dir/app-symlink</name>
+ <target>../app</target>
+ </file>
+ <file>
+ <name>new-dir/new-dir2/new-file.txt</name>
+ <hash>$TEST_FILENAME</hash>
+ <size>$TEST_SIZE</size>
+ <package>app-pkg</package>
+ <permissions>0644</permissions>
+ </file>
+ </install-v3>
+ <uninstall>
+ <!-- TODO - List some files to uninstall here !-->
+ <file>file-to-uninstall.txt</file>
+ <file>symlink-to-file-to-uninstall.txt</file>
+ </uninstall>
+</update>