diff options
author | Petr Mrázek <peterix@gmail.com> | 2013-12-02 00:55:24 +0100 |
---|---|---|
committer | Petr Mrázek <peterix@gmail.com> | 2013-12-02 00:55:24 +0100 |
commit | 6aa9bd0f77dcb5128167fae62e32aa5252fe85c6 (patch) | |
tree | 632994a61888929af9289927d338bd19a2b3f32c /mmc_updater/src | |
parent | 613699b3626aea750093ab7eaaeccaa28c0e87c6 (diff) | |
download | MultiMC-6aa9bd0f77dcb5128167fae62e32aa5252fe85c6.tar MultiMC-6aa9bd0f77dcb5128167fae62e32aa5252fe85c6.tar.gz MultiMC-6aa9bd0f77dcb5128167fae62e32aa5252fe85c6.tar.lz MultiMC-6aa9bd0f77dcb5128167fae62e32aa5252fe85c6.tar.xz MultiMC-6aa9bd0f77dcb5128167fae62e32aa5252fe85c6.zip |
Renew the updater branch
Now with some actual consensus on what the updater will do!
Diffstat (limited to 'mmc_updater/src')
59 files changed, 4906 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..18072b38 --- /dev/null +++ b/mmc_updater/src/Platform.h @@ -0,0 +1,30 @@ +#pragma once + +// basic platform defines +#ifdef __linux__ + #define PLATFORM_LINUX +#endif + +#ifdef WIN32 + #define PLATFORM_WINDOWS + #include <windows.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
\ No newline at end of file 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..23e1a4ca --- /dev/null +++ b/mmc_updater/src/UpdateInstaller.cpp @@ -0,0 +1,439 @@ +#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; +} + +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 = m_installDir + '/' + file.path; + 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()); + } + + if (target.empty()) + { + std::string sourceFile = m_packageDir + '/' + FileUtils::fileName(file.path.c_str()); + 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); + } + else + { + // create the symlink + FileUtils::createSymLink(destPath.c_str(),target.c_str()); + } +} + +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; + std::list<std::string> args; + + for (std::vector<UpdateScriptFile>::const_iterator iter = m_script->filesToInstall().begin(); + iter != m_script->filesToInstall().end(); + iter++) + { + if (iter->isMainBinary) + { + command = m_installDir + '/' + iter->path; + } + } + + if (!command.empty()) + { + LOG(Info,"Starting main application " + command); + ProcessUtils::runAsync(command,args); + } + else + { + LOG(Error,"No main binary specified in update script"); + } + } + 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..5dfa263e --- /dev/null +++ b/mmc_updater/src/UpdateInstaller.h @@ -0,0 +1,70 @@ +#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 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; + 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..606163dd --- /dev/null +++ b/mmc_updater/src/UpdateScript.cpp @@ -0,0 +1,98 @@ +#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; + file.path = elementText(element->FirstChildElement("name")); + + std::string modeString = elementText(element->FirstChildElement("permissions")); + sscanf(modeString.c_str(),"%i",&file.permissions); + + file.linkTarget = elementText(element->FirstChildElement("target")); + file.isMainBinary = strToBool(elementText(element->FirstChildElement("is-main-binary"))); + 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..fec463f3 --- /dev/null +++ b/mmc_updater/src/UpdateScript.h @@ -0,0 +1,86 @@ +#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) + , isMainBinary(false) + {} + + std::string path; + std::string linkTarget; + + /** The permissions for this file, specified + * using the standard Unix mode_t values. + */ + int permissions; + + bool isMainBinary; + + bool operator==(const UpdateScriptFile& other) const + { + return path == other.path && + permissions == other.permissions && + linkTarget == other.linkTarget && + isMainBinary == other.isMainBinary; + } +}; + +/** 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..1ea820d2 --- /dev/null +++ b/mmc_updater/src/UpdaterOptions.cpp @@ -0,0 +1,156 @@ +#include "UpdaterOptions.h" + +#include "Log.h" +#include "AnyOption/anyoption.h" +#include "FileUtils.h" +#include "Platform.h" +#include "StringUtils.h" + +#include <cstdlib> +#include <iostream> + +#ifdef PLATFORM_WINDOWS +long long atoll(const char* string) +{ + return _atoi64(string); +} +#endif + +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("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"))); + } + + 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..a8496d9f --- /dev/null +++ b/mmc_updater/src/UpdaterOptions.h @@ -0,0 +1,27 @@ +#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; + 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..e23cf16d --- /dev/null +++ b/mmc_updater/src/main.cpp @@ -0,0 +1,201 @@ +#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)); + + 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); + + 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 Binary files differnew file mode 100644 index 00000000..324452aa --- /dev/null +++ b/mmc_updater/src/resources/icon128.png diff --git a/mmc_updater/src/resources/icon64.png b/mmc_updater/src/resources/icon64.png Binary files differnew file mode 100644 index 00000000..5e3373e2 --- /dev/null +++ b/mmc_updater/src/resources/icon64.png diff --git a/mmc_updater/src/resources/mac.icns b/mmc_updater/src/resources/mac.icns Binary files differnew file mode 100644 index 00000000..7c8fa2ef --- /dev/null +++ b/mmc_updater/src/resources/mac.icns diff --git a/mmc_updater/src/resources/updater.ico b/mmc_updater/src/resources/updater.ico Binary files differnew file mode 100644 index 00000000..b011bac9 --- /dev/null +++ b/mmc_updater/src/resources/updater.ico 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..9e8c1392 --- /dev/null +++ b/mmc_updater/src/tests/TestUpdateScript.cpp @@ -0,0 +1,48 @@ +#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()); +} + +void TestUpdateScript::testPermissions() +{ + UpdateScript script; + script.parse("file_list.xml"); + + for (std::vector<UpdateScriptFile>::const_iterator iter = script.filesToInstall().begin(); + iter != script.filesToInstall().end(); + iter++) + { + if (iter->isMainBinary) + { + TEST_COMPARE(iter->permissions,0755); + } + if (!iter->linkTarget.empty()) + { + TEST_COMPARE(iter->permissions,0); + } + } +} + +int main(int,char**) +{ + TestList<TestUpdateScript> tests; + tests.addTest(&TestUpdateScript::testV2Script); + tests.addTest(&TestUpdateScript::testPermissions); + 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..fd0994fe --- /dev/null +++ b/mmc_updater/src/tests/TestUpdateScript.h @@ -0,0 +1,9 @@ +#pragma once + +class TestUpdateScript +{ + public: + void testV2Script(); + void testPermissions(); +}; + 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> |